diff options
author | 2024-12-20 07:38:06 +0800 | |
---|---|---|
committer | 2024-12-20 13:39:09 +0800 | |
commit | 688b57b8bcbe55d2d5bee5f1a4b813572eb4f505 (patch) | |
tree | 0f4b758622f93d31351ffdbee0e54d8d056704c9 | |
parent | 22c82b50571ea4d24da89dd378dda8e1d6da17a8 (diff) |
[Catalyst] Support AND/OR combination for permissions
Bug: 374115149
Flag: EXEMPT new class
Test: unit
Change-Id: I9e74d12e0fb9e60f0251ae7560a0d26877982534
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 +} |