Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 8df8b77e authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Update policy to revoke permission when purpose validation fails." into main

parents 9082c0a6 a2d794d9
Loading
Loading
Loading
Loading
+41 −11
Original line number Diff line number Diff line
@@ -986,9 +986,12 @@ class AppIdPermissionPolicy : SchemePolicy() {
            return
        }
        if (permission.isNormal) {
            val wasGranted = oldFlags.hasBits(PermissionFlags.INSTALL_GRANTED)
            if (!wasGranted) {
                val wasRevoked = oldFlags.hasBits(PermissionFlags.INSTALL_REVOKED)
            var newFlags: Int
            val wasInstallGranted = oldFlags.hasBits(PermissionFlags.INSTALL_GRANTED)
            val wasInstallRevoked = oldFlags.hasBits(PermissionFlags.INSTALL_REVOKED)
            if (wasInstallGranted || !wasInstallRevoked) {
                newFlags = PermissionFlags.INSTALL_GRANTED
            } else {
                val isRequestedByInstalledPackage =
                    installedPackageState != null &&
                        permissionName in
@@ -1002,9 +1005,8 @@ class AppIdPermissionPolicy : SchemePolicy() {
                // If this is an existing, non-system package,
                // then we can't add any new permissions to it.
                // Except if this is a permission that was added to the platform
                var newFlags =
                newFlags =
                    if (
                        !wasRevoked ||
                        isRequestedByInstalledPackage ||
                            isRequestedBySystemPackage ||
                            isCompatibilityPermission
@@ -1013,13 +1015,25 @@ class AppIdPermissionPolicy : SchemePolicy() {
                    } else {
                        PermissionFlags.INSTALL_REVOKED
                    }
            }
            // Starting from Android 17, an app requesting permission which requires purpose must
            // declare at least one valid purpose in its manifest before it can be granted. Note
            // that a flag state may have INSTALL_GRANTED and PURPOSE_REVOKED bits set, in which
            // case the permission will not be granted.
            if (Flags.purposeDeclarationEnabled() && permission.requiresPurpose) {
                val hasValidPurpose =
                    requestingPackageStates.anyIndexed { _, it ->
                        hasValidPurposeForPackage(it.androidPackage!!, permission)
                    }
                if (!hasValidPurpose) {
                    newFlags = newFlags or PermissionFlags.PURPOSE_REVOKED
                }
            }
            if (permission.isAppOp) {
                newFlags =
                        newFlags or
                            (oldFlags and (PermissionFlags.ROLE or PermissionFlags.USER_SET))
                    newFlags or (oldFlags and (PermissionFlags.ROLE or PermissionFlags.USER_SET))
            }
            setPermissionFlags(appId, userId, permissionName, newFlags)
            }
        } else if (permission.isSignature || permission.isInternal) {
            val wasProtectionGranted = oldFlags.hasBits(PermissionFlags.PROTECTION_GRANTED)
            var newFlags =
@@ -1307,6 +1321,22 @@ class AppIdPermissionPolicy : SchemePolicy() {
        return false
    }

    // TODO(b/422817717) - Handle app-compat scenarios by using versioning info at the purpose level
    private fun hasValidPurposeForPackage(
        androidPackage: AndroidPackage,
        permission: Permission,
    ): Boolean {
        // TODO(b/419394842) - Use < Android C build version code when available.
        if (androidPackage.targetSdkVersion <= Build.VERSION_CODES.BAKLAVA) {
            // Purpose declaration is not supported on older target versions. Bypass check.
            return true
        }
        val purposes = androidPackage.usesPermissionMapping[permission.name]?.purposes
        // There must be at least one valid purpose defined as the parsing logic would otherwise
        // force set the permission to not require purpose, making this code unreachable.
        return purposes?.any { it in permission.validPurposes } ?: false
    }

    private fun MutateStateScope.shouldGrantPermissionBySignature(
        packageState: PackageState,
        permission: Permission,
+6 −0
Original line number Diff line number Diff line
@@ -146,6 +146,12 @@ data class Permission(
    inline val knownCerts: Set<String>
        get() = permissionInfo.knownCerts

    inline val requiresPurpose: Boolean
        get() = permissionInfo.requiresPurpose

    inline val validPurposes: Set<String>
        get() = permissionInfo.validPurposes

    inline val hasGids: Boolean
        get() = gids.isNotEmpty()

+11 −0
Original line number Diff line number Diff line
@@ -294,6 +294,12 @@ object PermissionFlags {
     */
    const val USER_SELECTED = 1 shl 23

    /**
     * Permission flag for a normal permission that is revoked at install-time due to failing
     * purpose validation check.
     */
    const val PURPOSE_REVOKED = 1 shl 24

    /** Mask for all permission flags. */
    const val MASK_ALL = 0.inv()

@@ -328,6 +334,9 @@ object PermissionFlags {
    const val MASK_RESTRICTED = RESTRICTION_REVOKED or SOFT_RESTRICTED

    fun isPermissionGranted(flags: Int): Boolean {
        if (flags.hasBits(PURPOSE_REVOKED)) {
            return false
        }
        if (flags.hasBits(INSTALL_GRANTED)) {
            return true
        }
@@ -497,6 +506,7 @@ object PermissionFlags {
        if (apiFlags.hasBits(PackageManager.FLAG_PERMISSION_SELECTED_LOCATION_ACCURACY)) {
            flags = flags or USER_SELECTED
        }
        flags = flags or (oldFlags and PURPOSE_REVOKED)
        return flags
    }

@@ -526,6 +536,7 @@ object PermissionFlags {
            ONE_TIME -> "ONE_TIME"
            HIBERNATION -> "HIBERNATION"
            USER_SELECTED -> "USER_SELECTED"
            PURPOSE_REVOKED -> "PURPOSE_REVOKED"
            else -> "0x${flag.toUInt().toString(16).uppercase()}"
        }

+1 −1
Original line number Diff line number Diff line
@@ -52,7 +52,7 @@ class AppIdPermissionPolicyPermissionStatesTest : BasePermissionPolicyTest() {

    @Test
    fun testEvaluatePermissionState_normalPermissionAlreadyGranted_remainsUnchanged() {
        val oldFlags = PermissionFlags.INSTALL_GRANTED or PermissionFlags.INSTALL_REVOKED
        val oldFlags = PermissionFlags.INSTALL_GRANTED
        testEvaluatePermissionState(oldFlags, PermissionInfo.PROTECTION_NORMAL) {}

        val actualFlags = getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_NAME_0)
+251 −0
Original line number Diff line number Diff line
@@ -18,6 +18,10 @@ package com.android.server.permission.test

import android.content.pm.PermissionGroupInfo
import android.content.pm.PermissionInfo
import android.os.Build
import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
import android.permission.flags.Flags
import com.android.server.permission.access.GetStateScope
import com.android.server.permission.access.immutable.* // ktlint-disable no-wildcard-imports
import com.android.server.permission.access.permission.AppIdPermissionPolicy
@@ -64,6 +68,253 @@ class AppIdPermissionPolicyTest : BasePermissionPolicyTest() {
            .isEqualTo(expectedNewFlags)
    }

    @Test
    @EnableFlags(Flags.FLAG_PURPOSE_DECLARATION_ENABLED)
    fun testOnPackageAdded_requestsPermissionWithNoPurpose_permissionRevoked() {
        testOnPackageAddedForPurposeDeclaration(
            BUILD_VERSION_CODES_C,
            emptySet()
        )

        val actualFlags = getPermissionFlags(APP_ID_0, USER_ID_0, PERMISSION_NAME_0)
        val expectedNewFlags = PermissionFlags.INSTALL_GRANTED or PermissionFlags.PURPOSE_REVOKED

        assertWithMessage(
            "After onPackageAdded() is called for appId $APP_ID_0 that requests a" +
                " purpose-required permission with no purpose the actual permission flags" +
                " ($actualFlags) should be install granted + purpose revoked ($expectedNewFlags)"
        )
            .that(actualFlags)
            .isEqualTo(expectedNewFlags)

        assertThat(PermissionFlags.isPermissionGranted(actualFlags)).isFalse()
    }

    @Test
    @DisableFlags(Flags.FLAG_PURPOSE_DECLARATION_ENABLED)
    fun testOnPackageAdded_requestsPermissionWithNoPurposeAndFlagDisabled_permissionGranted() {
        testOnPackageAddedForPurposeDeclaration(
            BUILD_VERSION_CODES_C,
            emptySet()
        )

        val actualFlags = getPermissionFlags(APP_ID_0, USER_ID_0, PERMISSION_NAME_0)
        val expectedNewFlags = PermissionFlags.INSTALL_GRANTED

        assertWithMessage(
            "With purpose declaration flag disabled, after onPackageAdded() is called for appId" +
                " $APP_ID_0 that requests a purpose-required permission with no purpose, the" +
                " actual permission flags ($actualFlags) should be granted ($expectedNewFlags)"
        )
            .that(actualFlags)
            .isEqualTo(expectedNewFlags)

        assertThat(PermissionFlags.isPermissionGranted(actualFlags)).isTrue()
    }

    @Test
    @EnableFlags(Flags.FLAG_PURPOSE_DECLARATION_ENABLED)
    fun testOnPackageAdded_requestsPermissionWithInvalidPurpose_permissionRevoked() {
        testOnPackageAddedForPurposeDeclaration(
            BUILD_VERSION_CODES_C,
            setOf(INVALID_PURPOSE)
        )

        val actualFlags = getPermissionFlags(APP_ID_0, USER_ID_0, PERMISSION_NAME_0)
        val expectedNewFlags = PermissionFlags.INSTALL_GRANTED or PermissionFlags.PURPOSE_REVOKED

        assertWithMessage(
            "After onPackageAdded() is called for appId $APP_ID_0 that requests a" +
                " purpose-required permission with invalid purpose, the actual permission flags" +
                " ($actualFlags) should be install granted + purpose revoked ($expectedNewFlags)"
        )
            .that(actualFlags)
            .isEqualTo(expectedNewFlags)

        assertThat(PermissionFlags.isPermissionGranted(actualFlags)).isFalse()
    }

    @Test
    @EnableFlags(Flags.FLAG_PURPOSE_DECLARATION_ENABLED)
    fun testOnPackageAdded_requestsPermissionWithValidAndInvalidPurpose_permissionGranted() {
        testOnPackageAddedForPurposeDeclaration(
            BUILD_VERSION_CODES_C,
            // At least one valid purpose is sufficient for the permission to be granted.
            setOf(VALID_PURPOSE, INVALID_PURPOSE),
            oldFlags = PermissionFlags.INSTALL_REVOKED or PermissionFlags.PURPOSE_REVOKED
        )

        val actualFlags = getPermissionFlags(APP_ID_0, USER_ID_0, PERMISSION_NAME_0)
        val expectedNewFlags = PermissionFlags.INSTALL_GRANTED

        assertWithMessage(
            "After onPackageAdded() is called for appId $APP_ID_0 that requests a" +
                " purpose-required permission with valid and invalid purpose, the actual" +
                " permission flags ($actualFlags) should be granted ($expectedNewFlags)"
        )
            .that(actualFlags)
            .isEqualTo(expectedNewFlags)

        assertThat(PermissionFlags.isPermissionGranted(actualFlags)).isTrue()
    }

    @Test
    @EnableFlags(Flags.FLAG_PURPOSE_DECLARATION_ENABLED)
    fun testOnPackageAdded_requestsPermissionWithValidPurpose_permissionGranted() {
        testOnPackageAddedForPurposeDeclaration(
            BUILD_VERSION_CODES_C,
            setOf(VALID_PURPOSE),
            oldFlags = PermissionFlags.INSTALL_REVOKED or PermissionFlags.PURPOSE_REVOKED
        )

        val actualFlags = getPermissionFlags(APP_ID_0, USER_ID_0, PERMISSION_NAME_0)
        val expectedNewFlags = PermissionFlags.INSTALL_GRANTED

        assertWithMessage(
            "After onPackageAdded() is called for appId $APP_ID_0 that requests a" +
                " purpose-required permission with valid purpose, the actual permission flags" +
                " ($actualFlags) should be granted ($expectedNewFlags)"
        )
            .that(actualFlags)
            .isEqualTo(expectedNewFlags)

        assertThat(PermissionFlags.isPermissionGranted(actualFlags)).isTrue()
    }

    @Test
    @EnableFlags(Flags.FLAG_PURPOSE_DECLARATION_ENABLED)
    fun testOnPackageAdded_requestsPermissionWithNoPurposeTargetingOldSdk_permissionGranted() {
        testOnPackageAddedForPurposeDeclaration(
            Build.VERSION_CODES.BAKLAVA,
            emptySet()
        )

        val actualFlags = getPermissionFlags(APP_ID_0, USER_ID_0, PERMISSION_NAME_0)
        val expectedNewFlags = PermissionFlags.INSTALL_GRANTED

        assertWithMessage(
            "After onPackageAdded() is called for appId $APP_ID_0 that requests a" +
                " purpose-required permission with no purpose on an older SDK than Android C, the" +
                " actual permission flags ($actualFlags) should be granted ($expectedNewFlags)"
        )
            .that(actualFlags)
            .isEqualTo(expectedNewFlags)

        assertThat(PermissionFlags.isPermissionGranted(actualFlags)).isTrue()
    }

    @Test
    @EnableFlags(Flags.FLAG_PURPOSE_DECLARATION_ENABLED)
    fun testOnPackageAdded_requestsPermissionWithValidPurposeInAnotherPackage_permissionGranted() {
        // Set package1 with valid purpose, so it should be sufficient for any other package
        // sharing the same UID to pass the purpose validation check.
        val sharedUidPackage1 =
            mockPackageState(
                APP_ID_0,
                mockAndroidPackage(
                    PACKAGE_NAME_1,
                    targetSdkVersion = BUILD_VERSION_CODES_C,
                    requestedPermissions = setOf(PERMISSION_NAME_0),
                    usesPermissionMapping =
                        mapOf(
                            PERMISSION_NAME_0 to
                                mockParsedUsesPermission(
                                    PERMISSION_NAME_0,
                                    purposes = setOf(VALID_PURPOSE)
                                )
                        )
                )
            )
        addPackageState(sharedUidPackage1)

        testOnPackageAddedForPurposeDeclaration(
            BUILD_VERSION_CODES_C,
            emptySet()
        )

        val actualFlags = getPermissionFlags(APP_ID_0, USER_ID_0, PERMISSION_NAME_0)
        val expectedNewFlags = PermissionFlags.INSTALL_GRANTED

        assertWithMessage(
            "After onPackageAdded() is called for package $PACKAGE_NAME_0 / appId $APP_ID_0" +
                " that requests a purpose-required permission with no purpose, the actual" +
                " permission flags ($actualFlags) should be granted ($expectedNewFlags)" +
                " due to valid purpose declared package in $PACKAGE_NAME_1 / appId $APP_ID_0"
        )
            .that(actualFlags)
            .isEqualTo(expectedNewFlags)
    }

    @Test
    @EnableFlags(Flags.FLAG_PURPOSE_DECLARATION_ENABLED)
    fun testOnPackageAdded_requestsPermissionWithNoPurposeAndAnotherPackageTargetingOldSdk_permissionGranted() {
        // Set package1 targeting old SDK, so it should be sufficient for any other package
        // sharing the same UID to pass the purpose validation check.
        val sharedUidPackage1 =
            mockPackageState(
                APP_ID_0,
                mockAndroidPackage(
                    PACKAGE_NAME_1,
                    targetSdkVersion = Build.VERSION_CODES.BAKLAVA,
                    requestedPermissions = setOf(PERMISSION_NAME_0),
                    usesPermissionMapping =
                        mapOf(PERMISSION_NAME_0 to mockParsedUsesPermission(PERMISSION_NAME_0))
                )
            )
        addPackageState(sharedUidPackage1)

        testOnPackageAddedForPurposeDeclaration(
            BUILD_VERSION_CODES_C,
            emptySet()
        )

        val actualFlags = getPermissionFlags(APP_ID_0, USER_ID_0, PERMISSION_NAME_0)
        val expectedNewFlags = PermissionFlags.INSTALL_GRANTED

        assertWithMessage(
            "After onPackageAdded() is called for package $PACKAGE_NAME_0 / appId $APP_ID_0 that" +
                " requests a purpose-required permission with no purpose, the actual permission" +
                " flags ($actualFlags) should be granted ($expectedNewFlags) due to" +
                " $PACKAGE_NAME_1 / appId $APP_ID_0 which targets an old SDK version."
        )
            .that(actualFlags)
            .isEqualTo(expectedNewFlags)

        assertThat(PermissionFlags.isPermissionGranted(actualFlags)).isTrue()
    }

    private fun testOnPackageAddedForPurposeDeclaration(
        targetSdkVersion: Int,
        purposes: Set<String>,
        oldFlags: Int = PermissionFlags.INSTALL_GRANTED,
    ) {
        val addedPackage =
            mockPackageState(
                APP_ID_0,
                mockAndroidPackage(
                    PACKAGE_NAME_0,
                    targetSdkVersion = targetSdkVersion,
                    requestedPermissions = setOf(PERMISSION_NAME_0),
                    usesPermissionMapping =
                        mapOf(
                            PERMISSION_NAME_0 to
                                mockParsedUsesPermission(PERMISSION_NAME_0, purposes = purposes)
                        )
                )
            )
        val platformPackage = mockPackageState(PLATFORM_APP_ID, mockAndroidPackage(PLATFORM_PACKAGE_NAME))
        addPackageState(platformPackage)
        addPackageState(addedPackage)
        addPermission(defaultPermissionWithValidPurpose)
        setPermissionFlags(APP_ID_0, USER_ID_0, PERMISSION_NAME_0, oldFlags)

        mutateState {
            with(appIdPermissionPolicy) {
                onPackageAdded(addedPackage)
            }
        }
    }

    @Test
    fun testOnStorageVolumeMounted_nonSystemAppAfterNonSystemUpdate_remainsRevoked() {
        val permissionOwnerPackageState = mockPackageState(APP_ID_0, mockSimpleAndroidPackage())
Loading