summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Jacky Wang <jiannan@google.com> 2024-12-20 07:38:06 +0800
committer Jacky Wang <jiannan@google.com> 2024-12-20 13:39:09 +0800
commit688b57b8bcbe55d2d5bee5f1a4b813572eb4f505 (patch)
tree0f4b758622f93d31351ffdbee0e54d8d056704c9
parent22c82b50571ea4d24da89dd378dda8e1d6da17a8 (diff)
[Catalyst] Support AND/OR combination for permissions
Bug: 374115149 Flag: EXEMPT new class Test: unit Change-Id: I9e74d12e0fb9e60f0251ae7560a0d26877982534
-rw-r--r--packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/Permissions.kt162
-rw-r--r--packages/SettingsLib/DataStore/tests/src/com/android/settingslib/datastore/PermissionsTest.kt327
2 files changed, 489 insertions, 0 deletions
diff --git a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/Permissions.kt b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/Permissions.kt
new file mode 100644
index 000000000000..871642054aef
--- /dev/null
+++ b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/Permissions.kt
@@ -0,0 +1,162 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.datastore
+
+import android.content.Context
+import android.content.pm.PackageManager.PERMISSION_GRANTED
+
+/**
+ * Class to manage permissions, which supports a combination of AND / OR.
+ *
+ * Samples:
+ * - `Permissions.EMPTY`: no permission is required
+ * - `Permissions.allOf(p1, p2) or p3 or Permissions.allOf(p4, p5)`
+ * - `Permissions.anyOf(p1, p2) and p3 and Permissions.anyOf(p4, p5)`
+ * - `Permissions.allOf(p1, p2) or (Permissions.allOf(p3, p4) and p5)`: ALWAYS add `()` explicitly
+ * when and/or operators are used at the same time.
+ */
+sealed class Permissions(vararg permissions: Any) {
+ internal val permissions = mutableSetOf(*permissions)
+
+ val size: Int
+ get() = permissions.size
+
+ override fun hashCode() = permissions.hashCode()
+
+ override fun equals(other: Any?) =
+ other is Permissions &&
+ permissions == other.permissions &&
+ (permissions.size == 1 || javaClass == other.javaClass)
+
+ abstract fun check(context: Context, pid: Int, uid: Int): Boolean
+
+ internal fun addForAnd(permission: Any): Permissions =
+ when {
+ // ensure empty permissions will never been modified
+ permissions.isEmpty() -> (permission as? Permissions) ?: AllOfPermissions(permission)
+ permission is Permissions && permission.permissions.isEmpty() -> this
+ this is AllOfPermissions -> apply { and(permission) }
+ permission is AllOfPermissions -> permission.also { it.and(this) }
+ // anyOf(p1) and p2 => allOf(p1, p2)
+ permissions.size == 1 && this is AnyOfPermissions && permission is String ->
+ AllOfPermissions(permissions.first(), permission)
+ // anyOf(p1) and anyOf(p2) => allOf(p1, p2)
+ permissions.size == 1 &&
+ permission is AnyOfPermissions &&
+ permission.permissions.size == 1 ->
+ AllOfPermissions(permissions.first(), permission.permissions.first())
+ else -> AllOfPermissions(this, permission)
+ }
+
+ internal fun addForOr(permission: Any): Permissions =
+ when {
+ // ensure empty permissions will never been modified
+ permissions.isEmpty() -> (permission as? Permissions) ?: AnyOfPermissions(permission)
+ permission is Permissions && permission.permissions.isEmpty() -> this
+ this is AnyOfPermissions -> apply { or(permission) }
+ permission is AnyOfPermissions -> permission.also { it.or(this) }
+ // allOf(p1) or p2 => anyOf(p1, p2)
+ permissions.size == 1 && this is AllOfPermissions && permission is String ->
+ AnyOfPermissions(permissions.first(), permission)
+ // allOf(p1) or allOf(p2) => anyOf(p1, p2)
+ permissions.size == 1 &&
+ permission is AllOfPermissions &&
+ permission.permissions.size == 1 ->
+ AnyOfPermissions(permissions.first(), permission.permissions.first())
+ else -> AnyOfPermissions(this, permission)
+ }
+
+ protected fun Any.check(context: Context, pid: Int, uid: Int) =
+ when (this) {
+ is String -> context.checkPermission(this, pid, uid) == PERMISSION_GRANTED
+ else -> (this as Permissions).check(context, pid, uid)
+ }
+
+ fun forEach(action: (Any) -> Unit) {
+ for (permission in permissions) action(permission)
+ }
+
+ companion object {
+ /** Returns [Permissions] that requires all of the permissions. */
+ fun allOf(vararg permissions: String): Permissions =
+ if (permissions.isEmpty()) EMPTY else AllOfPermissions(*permissions)
+
+ /** Returns [Permissions] that requires any of the permissions. */
+ fun anyOf(vararg permissions: String): Permissions =
+ if (permissions.isEmpty()) EMPTY else AnyOfPermissions(*permissions)
+
+ /** No permission required. */
+ val EMPTY: Permissions = AllOfPermissions()
+ }
+}
+
+class AllOfPermissions internal constructor(vararg permissions: Any) : Permissions(*permissions) {
+
+ override fun toString() = permissions.joinToString(prefix = "allOf(", postfix = ")")
+
+ override fun check(context: Context, pid: Int, uid: Int): Boolean {
+ // use for-loop explicitly instead of "all" extension for empty permissions
+ for (permission in permissions) {
+ if (!permission.check(context, pid, uid)) return false
+ }
+ return true
+ }
+
+ internal fun and(permission: Any) {
+ when {
+ // in-place merge to reduce the hierarchy
+ permission is AllOfPermissions -> permissions.addAll(permission.permissions)
+ // allOf(...) and anyOf(p) => allOf(..., p)
+ permission is AnyOfPermissions && permission.permissions.size == 1 ->
+ permissions.add(permission.permissions.first())
+
+ else -> permissions.add(permission)
+ }
+ }
+}
+
+class AnyOfPermissions internal constructor(vararg permissions: Any) : Permissions(*permissions) {
+
+ override fun toString() = permissions.joinToString(prefix = "anyOf(", postfix = ")")
+
+ override fun check(context: Context, pid: Int, uid: Int): Boolean {
+ // use for-loop explicitly instead of "any" extension for empty permissions
+ for (permission in permissions) {
+ if (permission.check(context, pid, uid)) return true
+ }
+ return permissions.isEmpty()
+ }
+
+ internal fun or(permission: Any) {
+ when {
+ // in-place merge to reduce the hierarchy
+ permission is AnyOfPermissions -> permissions.addAll(permission.permissions)
+ // anyOf(...) or allOf(p) => anyOf(..., p)
+ permission is AllOfPermissions && permission.permissions.size == 1 ->
+ permissions.add(permission.permissions.first())
+ else -> permissions.add(permission)
+ }
+ }
+}
+
+infix fun Permissions.and(permission: String): Permissions = addForAnd(permission)
+
+infix fun Permissions.and(permissions: Permissions): Permissions = addForAnd(permissions)
+
+infix fun Permissions.or(permission: String): Permissions = addForOr(permission)
+
+infix fun Permissions.or(permissions: Permissions): Permissions = addForOr(permissions)
diff --git a/packages/SettingsLib/DataStore/tests/src/com/android/settingslib/datastore/PermissionsTest.kt b/packages/SettingsLib/DataStore/tests/src/com/android/settingslib/datastore/PermissionsTest.kt
new file mode 100644
index 000000000000..5641f0d82e57
--- /dev/null
+++ b/packages/SettingsLib/DataStore/tests/src/com/android/settingslib/datastore/PermissionsTest.kt
@@ -0,0 +1,327 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.datastore
+
+import android.content.Context
+import android.content.ContextWrapper
+import android.content.pm.PackageManager.PERMISSION_DENIED
+import android.content.pm.PackageManager.PERMISSION_GRANTED
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.settingslib.datastore.Permissions.Companion.EMPTY
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class PermissionsTest {
+ private val context: Context = ApplicationProvider.getApplicationContext()
+
+ @Test
+ fun empty() {
+ assertThat(Permissions.allOf()).isSameInstanceAs(EMPTY)
+ assertThat(Permissions.anyOf()).isSameInstanceAs(EMPTY)
+ assertThat(EMPTY.check(context, 0, 0)).isTrue()
+ assertThat((EMPTY and "a").permissions).containsExactly("a")
+ assertThat((EMPTY or "a").permissions).containsExactly("a")
+ }
+
+ @Test
+ fun allOf_op_empty() {
+ val allOf = Permissions.allOf("a")
+ assertThat(allOf and EMPTY).isSameInstanceAs(allOf)
+ assertThat(EMPTY and allOf).isSameInstanceAs(allOf)
+ assertThat(EMPTY or allOf).isSameInstanceAs(allOf)
+ assertThat(allOf or EMPTY).isSameInstanceAs(allOf)
+ }
+
+ @Test
+ fun anyOf_op_empty() {
+ val anyOf = Permissions.anyOf("a")
+ assertThat(anyOf and EMPTY).isSameInstanceAs(anyOf)
+ assertThat(EMPTY and anyOf).isSameInstanceAs(anyOf)
+ assertThat(EMPTY or anyOf).isSameInstanceAs(anyOf)
+ assertThat(anyOf or EMPTY).isSameInstanceAs(anyOf)
+ }
+
+ @Test
+ fun allOf1_and_allOf1() {
+ val allOf = Permissions.allOf("a")
+ assertThat(allOf and Permissions.allOf("b")).isSameInstanceAs(allOf)
+ assertThat(allOf.permissions).containsExactly("a", "b")
+ }
+
+ @Test
+ fun allOf1_or_allOf1() {
+ val merged = Permissions.allOf("a") or Permissions.allOf("b")
+ assertThat(merged.permissions).containsExactly("a", "b")
+ assertThat(merged).isInstanceOf(AnyOfPermissions::class.java)
+ }
+
+ @Test
+ fun allOf1_and_anyOf1() {
+ val allOf = Permissions.allOf("a")
+ val anyOf = Permissions.anyOf("b")
+ assertThat(allOf and anyOf).isSameInstanceAs(allOf)
+ assertThat(allOf.permissions).containsExactly("a", "b")
+ }
+
+ @Test
+ fun allOf1_or_anyOf1() {
+ val allOf = Permissions.allOf("a")
+ val anyOf = Permissions.anyOf("b")
+ assertThat(allOf or anyOf).isSameInstanceAs(anyOf)
+ assertThat(anyOf.permissions).containsExactly("a", "b")
+ }
+
+ @Test
+ fun anyOf1_and_allOf1() {
+ val anyOf = Permissions.anyOf("a")
+ val allOf = Permissions.allOf("b")
+ assertThat(anyOf and allOf).isSameInstanceAs(allOf)
+ assertThat(allOf.permissions).containsExactly("a", "b")
+ }
+
+ @Test
+ fun anyOf1_or_allOf1() {
+ val anyOf = Permissions.anyOf("a")
+ val allOf = Permissions.allOf("b")
+ assertThat(anyOf or allOf).isSameInstanceAs(anyOf)
+ assertThat(anyOf.permissions).containsExactly("a", "b")
+ }
+
+ @Test
+ fun anyOf1_and_anyOf1() {
+ val merged = Permissions.anyOf("a") and Permissions.anyOf("b")
+ assertThat(merged.permissions).containsExactly("a", "b")
+ assertThat(merged).isInstanceOf(AllOfPermissions::class.java)
+ }
+
+ @Test
+ fun anyOf1_or_anyOf1() {
+ val anyOf = Permissions.anyOf("a")
+ assertThat(anyOf or Permissions.anyOf("b")).isSameInstanceAs(anyOf)
+ assertThat(anyOf.permissions).containsExactly("a", "b")
+ }
+
+ @Test
+ fun allOf1_and_anyOf2() {
+ val allOf = Permissions.allOf("a")
+ val anyOf = Permissions.anyOf("a", "b")
+ assertThat(allOf and anyOf).isSameInstanceAs(allOf)
+ assertThat(allOf.permissions).containsExactly("a", anyOf)
+ }
+
+ @Test
+ fun allOf1_or_anyOf2() {
+ val allOf = Permissions.allOf("a")
+ val anyOf = Permissions.anyOf("a", "b")
+ assertThat(allOf and anyOf).isSameInstanceAs(allOf)
+ assertThat(allOf.permissions).containsExactly("a", anyOf)
+ }
+
+ @Test
+ fun allOf2_and_anyOf1() {
+ val allOf = Permissions.allOf("a", "b")
+ val anyOf = Permissions.anyOf("c")
+ assertThat(allOf and anyOf).isSameInstanceAs(allOf)
+ assertThat(allOf.permissions).containsExactly("a", "b", "c")
+ }
+
+ @Test
+ fun allOf2_or_anyOf1() {
+ val allOf = Permissions.allOf("a", "b")
+ val anyOf = Permissions.anyOf("b")
+ assertThat(allOf or anyOf).isSameInstanceAs(anyOf)
+ assertThat(anyOf.permissions).containsExactly("b", allOf)
+ }
+
+ @Test
+ fun anyOf1_and_allOf2() {
+ val anyOf = Permissions.anyOf("c")
+ val allOf = Permissions.allOf("a", "b")
+ assertThat(anyOf and allOf).isSameInstanceAs(allOf)
+ assertThat(allOf.permissions).containsExactly("a", "b", "c")
+ }
+
+ @Test
+ fun anyOf1_or_allOf2() {
+ val anyOf = Permissions.anyOf("a")
+ val allOf = Permissions.allOf("a", "b")
+ assertThat(anyOf or allOf).isSameInstanceAs(anyOf)
+ assertThat(anyOf.permissions).containsExactly("a", allOf)
+ }
+
+ @Test
+ fun anyOf2_and_allOf1() {
+ val anyOf = Permissions.anyOf("a", "b")
+ val allOf = Permissions.allOf("a")
+ assertThat(anyOf and allOf).isSameInstanceAs(allOf)
+ assertThat(allOf.permissions).containsExactly("a", anyOf)
+ }
+
+ @Test
+ fun anyOf2_or_allOf1() {
+ val anyOf = Permissions.anyOf("a", "b")
+ val allOf = Permissions.allOf("c")
+ assertThat(anyOf or allOf).isSameInstanceAs(anyOf)
+ assertThat(anyOf.permissions).containsExactly("a", "b", "c")
+ }
+
+ @Test
+ fun allOf2_and_allOf2() {
+ val allOf = Permissions.allOf("a", "b")
+ assertThat(allOf and Permissions.allOf("a", "c")).isSameInstanceAs(allOf)
+ assertThat(allOf.permissions).containsExactly("a", "b", "c")
+ }
+
+ @Test
+ fun allOf2_or_allOf2() {
+ val allOf1 = Permissions.allOf("a", "b")
+ val allOf2 = Permissions.allOf("a", "c")
+ assertThat((allOf1 or allOf2).permissions).containsExactly(allOf1, allOf2)
+ }
+
+ @Test
+ fun allOf2_and_anyOf2() {
+ val allOf = Permissions.allOf("a", "b")
+ val anyOf = Permissions.anyOf("a", "c")
+ assertThat(allOf and anyOf).isSameInstanceAs(allOf)
+ assertThat(allOf.permissions).containsExactly("a", "b", anyOf)
+ }
+
+ @Test
+ fun allOf2_or_anyOf2() {
+ val allOf = Permissions.allOf("a", "b")
+ val anyOf = Permissions.anyOf("a", "c")
+ assertThat(allOf or anyOf).isSameInstanceAs(anyOf)
+ assertThat(anyOf.permissions).containsExactly("a", "c", allOf)
+ }
+
+ @Test
+ fun anyOf2_and_allOf2() {
+ val anyOf = Permissions.anyOf("a", "b")
+ val allOf = Permissions.allOf("a", "c")
+ assertThat(anyOf and allOf).isSameInstanceAs(allOf)
+ assertThat(allOf.permissions).containsExactly("a", "c", anyOf)
+ }
+
+ @Test
+ fun anyOf2_or_allOf2() {
+ val anyOf = Permissions.anyOf("a", "b")
+ val allOf = Permissions.allOf("a", "c")
+ assertThat(anyOf or allOf).isSameInstanceAs(anyOf)
+ assertThat(anyOf.permissions).containsExactly("a", "b", allOf)
+ }
+
+ @Test
+ fun anyOf2_and_anyOf2() {
+ val anyOf1 = Permissions.anyOf("a", "b")
+ val anyOf2 = Permissions.anyOf("a", "c")
+ assertThat((anyOf1 and anyOf2).permissions).containsExactly(anyOf1, anyOf2)
+ }
+
+ @Test
+ fun anyOf2_or_anyOf2() {
+ val anyOf = Permissions.anyOf("a", "b")
+ assertThat(anyOf or Permissions.anyOf("a", "c")).isSameInstanceAs(anyOf)
+ assertThat(anyOf.permissions).containsExactly("a", "b", "c")
+ }
+
+ @Test
+ fun check_allOf() {
+ val permissions = Permissions.allOf("a", "b")
+ assertThat(permissions.check(PermissionsContext(setOf()), 0, 0)).isFalse()
+ assertThat(permissions.check(PermissionsContext(setOf("a")), 0, 0)).isFalse()
+ assertThat(permissions.check(PermissionsContext(setOf("b")), 0, 0)).isFalse()
+ assertThat(permissions.check(PermissionsContext(setOf("a", "b")), 0, 0)).isTrue()
+ }
+
+ @Test
+ fun check_allOf_mixed() {
+ val permissions = Permissions.allOf("a", "b") or "c"
+ assertThat(permissions.permissions).hasSize(2)
+ assertThat(permissions.check(PermissionsContext(setOf()), 0, 0)).isFalse()
+ assertThat(permissions.check(PermissionsContext(setOf("a")), 0, 0)).isFalse()
+ assertThat(permissions.check(PermissionsContext(setOf("b")), 0, 0)).isFalse()
+ assertThat(permissions.check(PermissionsContext(setOf("a", "b")), 0, 0)).isTrue()
+ assertThat(permissions.check(PermissionsContext(setOf("c")), 0, 0)).isTrue()
+ assertThat(permissions.check(PermissionsContext(setOf("a", "c")), 0, 0)).isTrue()
+ assertThat(permissions.check(PermissionsContext(setOf("b", "c")), 0, 0)).isTrue()
+ assertThat(permissions.check(PermissionsContext(setOf("a", "b", "c")), 0, 0)).isTrue()
+ }
+
+ @Test
+ fun check_anyOf() {
+ val permissions = Permissions.anyOf("a", "b")
+ assertThat(permissions.check(PermissionsContext(setOf()), 0, 0)).isFalse()
+ assertThat(permissions.check(PermissionsContext(setOf("a")), 0, 0)).isTrue()
+ assertThat(permissions.check(PermissionsContext(setOf("b")), 0, 0)).isTrue()
+ assertThat(permissions.check(PermissionsContext(setOf("a", "b")), 0, 0)).isTrue()
+ }
+
+ @Test
+ fun check_anyOf_mixed() {
+ val permissions = Permissions.anyOf("a", "b") and "c"
+ assertThat(permissions.permissions).hasSize(2)
+ assertThat(permissions.check(PermissionsContext(setOf()), 0, 0)).isFalse()
+ assertThat(permissions.check(PermissionsContext(setOf("a")), 0, 0)).isFalse()
+ assertThat(permissions.check(PermissionsContext(setOf("b")), 0, 0)).isFalse()
+ assertThat(permissions.check(PermissionsContext(setOf("a", "b")), 0, 0)).isFalse()
+ assertThat(permissions.check(PermissionsContext(setOf("c")), 0, 0)).isFalse()
+ assertThat(permissions.check(PermissionsContext(setOf("a", "c")), 0, 0)).isTrue()
+ assertThat(permissions.check(PermissionsContext(setOf("b", "c")), 0, 0)).isTrue()
+ assertThat(permissions.check(PermissionsContext(setOf("a", "b", "c")), 0, 0)).isTrue()
+ }
+
+ @Test
+ fun equals() {
+ assertThat(Permissions.allOf("a")).isEqualTo(Permissions.allOf("a"))
+ assertThat(Permissions.allOf("a")).isEqualTo(Permissions.anyOf("a"))
+ assertThat(Permissions.anyOf("a")).isEqualTo(Permissions.allOf("a"))
+ assertThat(Permissions.anyOf("a")).isEqualTo(Permissions.anyOf("a"))
+
+ assertThat(Permissions.anyOf("a") and "b").isEqualTo(Permissions.allOf("a", "b"))
+ assertThat(Permissions.allOf("a") or "b").isEqualTo(Permissions.anyOf("a", "b"))
+ assertThat(Permissions.allOf("a") and "a").isEqualTo(Permissions.allOf("a"))
+ assertThat(Permissions.anyOf("a") or "a").isEqualTo(Permissions.anyOf("a"))
+
+ assertThat(Permissions.allOf("a", "c") and Permissions.allOf("c", "b"))
+ .isEqualTo(Permissions.allOf("a", "b", "c"))
+ assertThat(Permissions.anyOf("a", "c") or Permissions.anyOf("c", "b"))
+ .isEqualTo(Permissions.anyOf("a", "b", "c"))
+
+ assertThat(Permissions.allOf("a", "c") and Permissions.allOf("c", "b"))
+ .isEqualTo(Permissions.allOf("b", "c") and Permissions.allOf("c", "a"))
+ assertThat(Permissions.anyOf("a", "c") or Permissions.anyOf("c", "b"))
+ .isEqualTo(Permissions.anyOf("b", "c") or Permissions.anyOf("c", "a"))
+ }
+
+ @Test
+ fun notEquals() {
+ assertThat(Permissions.allOf("a")).isNotEqualTo(Permissions.allOf("a", "b"))
+ assertThat(Permissions.anyOf("a")).isNotEqualTo(Permissions.anyOf("a", "b"))
+ assertThat(Permissions.allOf("a", "b")).isNotEqualTo(Permissions.anyOf("a", "b"))
+ }
+}
+
+private class PermissionsContext(private val granted: Set<String>) :
+ ContextWrapper(ApplicationProvider.getApplicationContext()) {
+
+ override fun checkPermission(permission: String, pid: Int, uid: Int) =
+ if (permission in granted) PERMISSION_GRANTED else PERMISSION_DENIED
+}