diff options
Diffstat (limited to 'PermissionController/role-controller')
10 files changed, 429 insertions, 257 deletions
diff --git a/PermissionController/role-controller/Android.bp b/PermissionController/role-controller/Android.bp index 166823b08..9f217660a 100644 --- a/PermissionController/role-controller/Android.bp +++ b/PermissionController/role-controller/Android.bp @@ -17,6 +17,9 @@ package { default_applicable_licenses: ["Android-Apache-2.0"], } +// Any place that role-controller is added as a dependency must also include +// "com.android.permission.flags-aconfig-java" or +// "com.android.permission.flags-aconfig-java-export", java_library { name: "role-controller", srcs: [ @@ -24,13 +27,17 @@ java_library { ], libs: [ "androidx.annotation_annotation", + "com.android.permission.flags-aconfig-java", + "framework-annotations-lib", ], static_libs: [ "modules-utils-build_system", "android.app.appfunctions.exported-flags-aconfig-java", "android.companion.virtualdevice.flags-aconfig-java-export", + "android.content.pm.flags-aconfig-java-export", "android.permission.flags-aconfig-java-export", "android.os.flags-aconfig-java-export", + "device_policy_aconfig_flags_java_export", ], apex_available: [ "com.android.permission", diff --git a/PermissionController/role-controller/java/com/android/role/controller/behavior/ReservedForTestingProfileGroupExclusivityRoleBehavior.java b/PermissionController/role-controller/java/com/android/role/controller/behavior/ReservedForTestingProfileGroupExclusivityRoleBehavior.java new file mode 100644 index 000000000..a9be00806 --- /dev/null +++ b/PermissionController/role-controller/java/com/android/role/controller/behavior/ReservedForTestingProfileGroupExclusivityRoleBehavior.java @@ -0,0 +1,58 @@ +/* + * 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.role.controller.behavior; + +import android.app.role.RoleManager; +import android.content.Context; +import android.os.UserHandle; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.role.controller.model.Role; +import com.android.role.controller.model.RoleBehavior; +import com.android.role.controller.util.RoleFlags; +import com.android.role.controller.util.UserUtils; + +import java.util.List; + +public class ReservedForTestingProfileGroupExclusivityRoleBehavior implements RoleBehavior { + @Nullable + @Override + public List<String> getDefaultHoldersAsUser(@NonNull Role role, @NonNull UserHandle user, + @NonNull Context context) { + if (RoleFlags.isProfileGroupExclusivityAvailable()) { + Context userContext = UserUtils.getUserContext(context, user); + RoleManager roleManager = userContext.getSystemService(RoleManager.class); + return roleManager.getDefaultHoldersForTest(role.getName()); + } else { + return null; + } + } + + @Override + public boolean isVisibleAsUser(@NonNull Role role, @NonNull UserHandle user, + @NonNull Context context) { + if (RoleFlags.isProfileGroupExclusivityAvailable()) { + Context userContext = UserUtils.getUserContext(context, user); + RoleManager roleManager = userContext.getSystemService(RoleManager.class); + return roleManager.isRoleVisibleForTest(role.getName()); + } else { + return false; + } + } +} diff --git a/PermissionController/role-controller/java/com/android/role/controller/model/AppOp.java b/PermissionController/role-controller/java/com/android/role/controller/model/AppOp.java index 1c71e0c99..56c4944a0 100644 --- a/PermissionController/role-controller/java/com/android/role/controller/model/AppOp.java +++ b/PermissionController/role-controller/java/com/android/role/controller/model/AppOp.java @@ -23,6 +23,7 @@ import android.os.UserHandle; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import com.android.modules.utils.build.SdkLevel; import com.android.role.controller.util.PackageUtils; @@ -130,7 +131,8 @@ public class AppOp { return Permissions.setAppOpUidModeAsUser(packageName, mName, defaultMode, user, context); } - boolean isAvailableByFeatureFlagAndSdkVersion() { + @VisibleForTesting + public boolean isAvailableByFeatureFlagAndSdkVersion() { if (mFeatureFlag != null && !mFeatureFlag.get()) { return false; } diff --git a/PermissionController/role-controller/java/com/android/role/controller/model/Permissions.java b/PermissionController/role-controller/java/com/android/role/controller/model/Permissions.java index e788fdce1..820ff3d4e 100644 --- a/PermissionController/role-controller/java/com/android/role/controller/model/Permissions.java +++ b/PermissionController/role-controller/java/com/android/role/controller/model/Permissions.java @@ -51,11 +51,13 @@ public class Permissions { private static final boolean DEBUG = false; + private static final Object sPermissionInfoLock = new Object(); + private static final ArrayMap<String, Boolean> sIsRuntimePermission = new ArrayMap<>(); + private static final ArrayMap<String, Boolean> sIsRestrictedPermission = new ArrayMap<>(); + + private static final Object sForegroundBackgroundPermissionMappingsLock = new Object(); private static ArrayMap<String, String> sForegroundToBackgroundPermission; private static ArrayMap<String, List<String>> sBackgroundToForegroundPermissions; - private static final Object sForegroundBackgroundPermissionMappingsLock = new Object(); - - private static final ArrayMap<String, Boolean> sRestrictedPermissions = new ArrayMap<>(); /** * Filter a list of permissions based on their SDK versions. @@ -86,8 +88,9 @@ public class Permissions { * * @param packageName the package name of the application to be granted permissions to * @param permissions the list of permissions to be granted - * @param overrideDisabledSystemPackage whether to ignore the permissions of a disabled system - * package (if this package is an updated system package) + * @param ignoreDisabledSystemPackage whether to ignore the requested permissions of a disabled + * system package (if this package is an updated system + * package) when granting runtime permissions * @param overrideUserSetAndFixed whether to override user set and fixed flags on the permission * @param setGrantedByRole whether the permissions will be granted as granted-by-role * @param setGrantedByDefault whether the permissions will be granted as granted-by-default @@ -101,7 +104,7 @@ public class Permissions { * PackageInfo, java.util.Set, boolean, boolean, int) */ public static boolean grantAsUser(@NonNull String packageName, - @NonNull List<String> permissions, boolean overrideDisabledSystemPackage, + @NonNull List<String> permissions, boolean ignoreDisabledSystemPackage, boolean overrideUserSetAndFixed, boolean setGrantedByRole, boolean setGrantedByDefault, boolean setSystemFixed, @NonNull UserHandle user, @NonNull Context context) { if (setGrantedByRole == setGrantedByDefault) { @@ -145,15 +148,17 @@ public class Permissions { // choice to grant this app the permissions needed to function. For all other // apps, (default grants on first boot and user creation) we don't grant default // permissions if the version on the system image does not declare them. - if (!overrideDisabledSystemPackage && isUpdatedSystemApp(packageInfo)) { + if (!ignoreDisabledSystemPackage && isUpdatedSystemApp(packageInfo)) { PackageInfo disabledSystemPackageInfo = getFactoryPackageInfoAsUser(packageName, user, context); if (disabledSystemPackageInfo != null) { - if (ArrayUtils.isEmpty(disabledSystemPackageInfo.requestedPermissions)) { - return false; + for (int i = permissionsToGrant.size() - 1; i >= 0; i--) { + String permission = permissionsToGrant.valueAt(i); + if (isRuntimePermission(permission, context) && !ArrayUtils.contains( + disabledSystemPackageInfo.requestedPermissions, permission)) { + permissionsToGrant.removeAt(i); + } } - CollectionUtils.retainAll(permissionsToGrant, - disabledSystemPackageInfo.requestedPermissions); if (permissionsToGrant.isEmpty()) { return false; } @@ -258,7 +263,8 @@ public class Permissions { if (!wasPermissionOrAppOpGranted) { // If we've granted a permission which wasn't granted, it's no longer user set or fixed. newMask |= PackageManager.FLAG_PERMISSION_USER_FIXED - | PackageManager.FLAG_PERMISSION_USER_SET; + | PackageManager.FLAG_PERMISSION_USER_SET + | PackageManager.FLAG_PERMISSION_ONE_TIME; } // If a component gets a permission for being the default handler A and also default handler // B, we grant the weaker grant form. This only applies to default permission grant. @@ -629,7 +635,8 @@ public class Permissions { } if (!overrideUserSetAndFixed) { fixedFlags |= PackageManager.FLAG_PERMISSION_USER_FIXED - | PackageManager.FLAG_PERMISSION_USER_SET; + | PackageManager.FLAG_PERMISSION_USER_SET + | PackageManager.FLAG_PERMISSION_ONE_TIME; } return (flags & fixedFlags) != 0; } @@ -701,6 +708,50 @@ public class Permissions { return true; } + private static boolean isRuntimePermission(@NonNull String permission, + @NonNull Context context) { + synchronized (sPermissionInfoLock) { + Boolean isRuntimePermission = sIsRuntimePermission.get(permission); + if (isRuntimePermission != null) { + return isRuntimePermission; + } + fetchPermissionInfoLocked(permission, context); + return sIsRuntimePermission.get(permission); + } + } + + private static boolean isRestrictedPermission(@NonNull String permission, + @NonNull Context context) { + synchronized (sPermissionInfoLock) { + Boolean isRestrictedPermission = sIsRestrictedPermission.get(permission); + if (isRestrictedPermission != null) { + return isRestrictedPermission; + } + fetchPermissionInfoLocked(permission, context); + return sIsRestrictedPermission.get(permission); + } + } + + private static void fetchPermissionInfoLocked(@NonNull String permission, + @NonNull Context context) { + PackageManager packageManager = context.getPackageManager(); + PermissionInfo permissionInfo = null; + try { + permissionInfo = packageManager.getPermissionInfo(permission, 0); + } catch (PackageManager.NameNotFoundException e) { + Log.e(LOG_TAG, "Cannot get PermissionInfo for permission: " + permission); + } + + // Don't expect that to be a transient error, so we can still cache the failed information. + boolean isRuntimePermission = permissionInfo != null + && permissionInfo.getProtection() == PermissionInfo.PROTECTION_DANGEROUS; + boolean isRestrictedPermission = permissionInfo != null + && (permissionInfo.flags & (PermissionInfo.FLAG_SOFT_RESTRICTED + | PermissionInfo.FLAG_HARD_RESTRICTED)) != 0; + sIsRuntimePermission.put(permission, isRuntimePermission); + sIsRestrictedPermission.put(permission, isRestrictedPermission); + } + private static boolean isForegroundPermission(@NonNull String permission, @NonNull Context context) { ensureForegroundBackgroundPermissionMappings(context); @@ -731,40 +782,13 @@ public class Permissions { synchronized (sForegroundBackgroundPermissionMappingsLock) { if (sForegroundToBackgroundPermission == null && sBackgroundToForegroundPermissions == null) { - createForegroundBackgroundPermissionMappings(context); + createForegroundBackgroundPermissionMappingsLocked(context); } } } - private static boolean isRestrictedPermission(@NonNull String permission, + private static void createForegroundBackgroundPermissionMappingsLocked( @NonNull Context context) { - synchronized (sRestrictedPermissions) { - if (sRestrictedPermissions.containsKey(permission)) { - return sRestrictedPermissions.get(permission); - } - } - - PackageManager packageManager = context.getPackageManager(); - PermissionInfo permissionInfo = null; - try { - permissionInfo = packageManager.getPermissionInfo(permission, 0); - } catch (PackageManager.NameNotFoundException e) { - Log.e(LOG_TAG, "Cannot get PermissionInfo for permission: " + permission); - } - - // Don't expect that to be a transient error, so we can still cache the failed information. - boolean isRestrictedPermission = permissionInfo != null - && (permissionInfo.flags & (PermissionInfo.FLAG_SOFT_RESTRICTED - | PermissionInfo.FLAG_HARD_RESTRICTED)) != 0; - - synchronized (sRestrictedPermissions) { - sRestrictedPermissions.put(permission, isRestrictedPermission); - } - - return isRestrictedPermission; - } - - private static void createForegroundBackgroundPermissionMappings(@NonNull Context context) { List<String> permissions = new ArrayList<>(); sBackgroundToForegroundPermissions = new ArrayMap<>(); diff --git a/PermissionController/role-controller/java/com/android/role/controller/model/Role.java b/PermissionController/role-controller/java/com/android/role/controller/model/Role.java index 04fd615e1..02fa0d455 100644 --- a/PermissionController/role-controller/java/com/android/role/controller/model/Role.java +++ b/PermissionController/role-controller/java/com/android/role/controller/model/Role.java @@ -37,17 +37,23 @@ import android.text.TextUtils; import android.util.ArrayMap; import android.util.ArraySet; import android.util.Log; +import android.util.SparseBooleanArray; +import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.StringRes; +import androidx.annotation.VisibleForTesting; import com.android.modules.utils.build.SdkLevel; import com.android.role.controller.util.CollectionUtils; import com.android.role.controller.util.PackageUtils; +import com.android.role.controller.util.RoleFlags; import com.android.role.controller.util.RoleManagerCompat; import com.android.role.controller.util.UserUtils; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -82,6 +88,36 @@ public class Role { private static final String CERTIFICATE_SEPARATOR = ":"; + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + EXCLUSIVITY_NONE, + EXCLUSIVITY_USER, + EXCLUSIVITY_PROFILE_GROUP + }) + public @interface Exclusivity {} + + /** + * Does not enforce any exclusivity, which means multiple apps may hold this role in a user. + */ + public static final int EXCLUSIVITY_NONE = 0; + + /** Enforces exclusivity within one user. */ + public static final int EXCLUSIVITY_USER = 1; + + /** + * Enforces exclusivity across all users (including profile users) in the same profile group. + */ + public static final int EXCLUSIVITY_PROFILE_GROUP = 2; + + /** Set of valid exclusivity values. */ + private static final SparseBooleanArray sExclusivityValues = new SparseBooleanArray(); + static { + sExclusivityValues.put(EXCLUSIVITY_NONE, true); + sExclusivityValues.put(EXCLUSIVITY_USER, true); + sExclusivityValues.put(EXCLUSIVITY_PROFILE_GROUP, + RoleFlags.isProfileGroupExclusivityAvailable()); + } + /** * The name of this role. Must be unique. */ @@ -109,9 +145,10 @@ public class Role { private final int mDescriptionResource; /** - * Whether this role is exclusive, i.e. allows at most one holder. + * The exclusivity of this role, i.e. whether this role allows multiple holders, or allows at + * most one holder within a user or a profile group. */ - private final boolean mExclusive; + private final int mExclusivity; /** * Whether this role should fall back to the default holder. @@ -185,8 +222,8 @@ public class Role { /** * Whether the UI for this role will show the "None" item. Only valid if this role is - * {@link #mExclusive exclusive}, and {@link #getFallbackHolder(Context)} should also return - * empty to allow actually selecting "None". + * {@link #isExclusive()}, and {@link #getFallbackHolder(Context)} should + * also return empty to allow actually selecting "None". */ private final boolean mShowNone; @@ -240,14 +277,14 @@ public class Role { public Role(@NonNull String name, boolean allowBypassingQualification, @Nullable RoleBehavior behavior, @Nullable String defaultHoldersResourceName, - @StringRes int descriptionResource, boolean exclusive, boolean fallBackToDefaultHolder, - @Nullable Supplier<Boolean> featureFlag, @StringRes int labelResource, - int maxSdkVersion, int minSdkVersion, boolean onlyGrantWhenAdded, - boolean overrideUserWhenGranting, @StringRes int requestDescriptionResource, - @StringRes int requestTitleResource, boolean requestable, - @StringRes int searchKeywordsResource, @StringRes int shortLabelResource, - boolean showNone, boolean statik, boolean systemOnly, boolean visible, - @NonNull List<RequiredComponent> requiredComponents, + @StringRes int descriptionResource, @Exclusivity int exclusivity, + boolean fallBackToDefaultHolder, @Nullable Supplier<Boolean> featureFlag, + @StringRes int labelResource, int maxSdkVersion, int minSdkVersion, + boolean onlyGrantWhenAdded, boolean overrideUserWhenGranting, + @StringRes int requestDescriptionResource, @StringRes int requestTitleResource, + boolean requestable, @StringRes int searchKeywordsResource, + @StringRes int shortLabelResource, boolean showNone, boolean statik, boolean systemOnly, + boolean visible, @NonNull List<RequiredComponent> requiredComponents, @NonNull List<Permission> permissions, @NonNull List<Permission> appOpPermissions, @NonNull List<AppOp> appOps, @NonNull List<PreferredActivity> preferredActivities, @Nullable String uiBehaviorName) { @@ -256,7 +293,7 @@ public class Role { mBehavior = behavior; mDefaultHoldersResourceName = defaultHoldersResourceName; mDescriptionResource = descriptionResource; - mExclusive = exclusive; + mExclusivity = exclusivity; mFallBackToDefaultHolder = fallBackToDefaultHolder; mFeatureFlag = featureFlag; mLabelResource = labelResource; @@ -297,7 +334,29 @@ public class Role { } public boolean isExclusive() { - return mExclusive; + return getExclusivity() != EXCLUSIVITY_NONE; + } + + @Exclusivity + public int getExclusivity() { + if (com.android.permission.flags.Flags.crossUserRoleEnabled() && mBehavior != null) { + Integer exclusivity = mBehavior.getExclusivity(); + if (exclusivity != null) { + if (!sExclusivityValues.get(exclusivity)) { + throw new IllegalArgumentException("Invalid exclusivity: " + exclusivity); + } + if (mShowNone && exclusivity == EXCLUSIVITY_NONE) { + throw new IllegalArgumentException( + "Role cannot be non-exclusive when showNone is true: " + exclusivity); + } + if (!mPreferredActivities.isEmpty() && exclusivity == EXCLUSIVITY_PROFILE_GROUP) { + throw new IllegalArgumentException( + "Role cannot have preferred activities when exclusivity is profileGroup"); + } + return exclusivity; + } + } + return mExclusivity; } @Nullable @@ -413,8 +472,25 @@ public class Role { if (!isAvailableByFeatureFlagAndSdkVersion()) { return false; } + + if (getExclusivity() == EXCLUSIVITY_PROFILE_GROUP + && UserUtils.isPrivateProfile(user, context)) { + return false; + } + if (mBehavior != null) { - return mBehavior.isAvailableAsUser(this, user, context); + boolean isAvailableAsUser = mBehavior.isAvailableAsUser(this, user, context); + // Ensure that cross-user role is only available if also available for + // the profile-group's full user + if (isAvailableAsUser && getExclusivity() == EXCLUSIVITY_PROFILE_GROUP) { + UserHandle profileParent = UserUtils.getProfileParentOrSelf(user, context); + if (!Objects.equals(profileParent, user) + && !mBehavior.isAvailableAsUser(this, profileParent, context)) { + throw new IllegalArgumentException("Role is not available for profile parent: " + + profileParent.getIdentifier()); + } + } + return isAvailableAsUser; } return true; } @@ -424,7 +500,8 @@ public class Role { * * @return whether this role is available based on SDK version */ - boolean isAvailableByFeatureFlagAndSdkVersion() { + @VisibleForTesting + public boolean isAvailableByFeatureFlagAndSdkVersion() { if (mFeatureFlag != null && !mFeatureFlag.get()) { return false; } @@ -449,6 +526,12 @@ public class Role { @NonNull public List<String> getDefaultHoldersAsUser(@NonNull UserHandle user, @NonNull Context context) { + // Do not allow default role holder for non-active user if the role is exclusive to profile + // group + if (isNonActiveUserForProfileGroupExclusiveRole(user, context)) { + return Collections.emptyList(); + } + if (mBehavior != null) { List<String> defaultHolders = mBehavior.getDefaultHoldersAsUser(this, user, context); if (defaultHolders != null) { @@ -560,6 +643,10 @@ public class Role { if (!RoleManagerCompat.isRoleFallbackEnabledAsUser(this, user, context)) { return null; } + // Do not fall back for non-active user if the role is exclusive to profile group + if (isNonActiveUserForProfileGroupExclusiveRole(user, context)) { + return null; + } if (mFallBackToDefaultHolder) { return CollectionUtils.firstOrNull(getDefaultHoldersAsUser(user, context)); } @@ -569,6 +656,17 @@ public class Role { return null; } + private boolean isNonActiveUserForProfileGroupExclusiveRole(@NonNull UserHandle user, + @NonNull Context context) { + if (RoleFlags.isProfileGroupExclusivityAvailable() + && getExclusivity() == Role.EXCLUSIVITY_PROFILE_GROUP) { + Context userContext = UserUtils.getUserContext(context, user); + RoleManager userRoleManager = userContext.getSystemService(RoleManager.class); + return !Objects.equals(userRoleManager.getActiveUserForRole(mName), user); + } + return false; + } + /** * Check whether this role is allowed to bypass qualification, if enabled globally. * @@ -1039,7 +1137,7 @@ public class Role { */ @Nullable public Intent getRestrictionIntentAsUser(@NonNull UserHandle user, @NonNull Context context) { - if (SdkLevel.isAtLeastU() && mExclusive) { + if (SdkLevel.isAtLeastU() && isExclusive()) { UserManager userManager = context.getSystemService(UserManager.class); if (userManager.hasUserRestrictionForUser(UserManager.DISALLOW_CONFIG_DEFAULT_APPS, user)) { @@ -1102,7 +1200,7 @@ public class Role { + ", mBehavior=" + mBehavior + ", mDefaultHoldersResourceName=" + mDefaultHoldersResourceName + ", mDescriptionResource=" + mDescriptionResource - + ", mExclusive=" + mExclusive + + ", mExclusivity=" + mExclusivity + ", mFallBackToDefaultHolder=" + mFallBackToDefaultHolder + ", mFeatureFlag=" + mFeatureFlag + ", mLabelResource=" + mLabelResource diff --git a/PermissionController/role-controller/java/com/android/role/controller/model/RoleBehavior.java b/PermissionController/role-controller/java/com/android/role/controller/model/RoleBehavior.java index 3849a50e3..86ca8e2ce 100644 --- a/PermissionController/role-controller/java/com/android/role/controller/model/RoleBehavior.java +++ b/PermissionController/role-controller/java/com/android/role/controller/model/RoleBehavior.java @@ -32,6 +32,14 @@ import java.util.List; public interface RoleBehavior { /** + * @see Role#getExclusivity() + */ + @Nullable + default Integer getExclusivity() { + return null; + } + + /** * @see Role#onRoleAddedAsUser(UserHandle, Context) */ default void onRoleAddedAsUser(@NonNull Role role, @NonNull UserHandle user, diff --git a/PermissionController/role-controller/java/com/android/role/controller/model/RoleParser.java b/PermissionController/role-controller/java/com/android/role/controller/model/RoleParser.java index f1a275daf..4b05554e3 100644 --- a/PermissionController/role-controller/java/com/android/role/controller/model/RoleParser.java +++ b/PermissionController/role-controller/java/com/android/role/controller/model/RoleParser.java @@ -19,12 +19,9 @@ package com.android.role.controller.model; import android.app.AppOpsManager; import android.content.Context; import android.content.Intent; -import android.content.pm.PackageManager; -import android.content.pm.PermissionInfo; import android.content.res.Resources; import android.content.res.XmlResourceParser; import android.os.Build; -import android.os.Process; import android.permission.flags.Flags; import android.util.ArrayMap; import android.util.Log; @@ -92,6 +89,7 @@ public class RoleParser { private static final String ATTRIBUTE_DEFAULT_HOLDERS = "defaultHolders"; private static final String ATTRIBUTE_DESCRIPTION = "description"; private static final String ATTRIBUTE_EXCLUSIVE = "exclusive"; + private static final String ATTRIBUTE_EXCLUSIVITY = "exclusivity"; private static final String ATTRIBUTE_FALL_BACK_TO_DEFAULT_HOLDER = "fallBackToDefaultHolder"; private static final String ATTRIBUTE_FEATURE_FLAG = "featureFlag"; private static final String ATTRIBUTE_LABEL = "label"; @@ -138,6 +136,10 @@ public class RoleParser { sModeNameToMode.put(MODE_NAME_FOREGROUND, AppOpsManager.MODE_FOREGROUND); } + private static final String EXCLUSIVITY_NONE = "none"; + private static final String EXCLUSIVITY_USER = "user"; + private static final String EXCLUSIVITY_PROFILE_GROUP = "profileGroup"; + private static final Supplier<Boolean> sFeatureFlagFallback = () -> false; private static final ArrayMap<Class<?>, Class<?>> sPrimitiveToWrapperClass = new ArrayMap<>(); @@ -156,16 +158,16 @@ public class RoleParser { @NonNull private final Context mContext; - private final boolean mValidationEnabled; + private final boolean mThrowOnError; public RoleParser(@NonNull Context context) { this(context, false); } @VisibleForTesting - public RoleParser(@NonNull Context context, boolean validationEnabled) { + public RoleParser(@NonNull Context context, boolean throwOnError) { mContext = context; - mValidationEnabled = validationEnabled; + mThrowOnError = throwOnError; } /** @@ -175,18 +177,21 @@ public class RoleParser { */ @NonNull public ArrayMap<String, Role> parse() { + Pair<ArrayMap<String, PermissionSet>, ArrayMap<String, Role>> xml = parseRolesXml(); + if (xml == null) { + return new ArrayMap<>(); + } + return xml.second; + } + + @Nullable + @VisibleForTesting + public Pair<ArrayMap<String, PermissionSet>, ArrayMap<String, Role>> parseRolesXml() { try (XmlResourceParser parser = getRolesXml()) { - Pair<ArrayMap<String, PermissionSet>, ArrayMap<String, Role>> xml = parseXml(parser); - if (xml == null) { - return new ArrayMap<>(); - } - ArrayMap<String, PermissionSet> permissionSets = xml.first; - ArrayMap<String, Role> roles = xml.second; - validateResult(permissionSets, roles); - return roles; + return parseXml(parser); } catch (XmlPullParserException | IOException e) { throwOrLogMessage("Unable to parse roles.xml", e); - return new ArrayMap<>(); + return null; } } @@ -413,13 +418,45 @@ public class RoleParser { shortLabelResource = 0; } - Boolean exclusive = requireAttributeBooleanValue(parser, ATTRIBUTE_EXCLUSIVE, true, - TAG_ROLE); - if (exclusive == null) { - skipCurrentTag(parser); - return null; + int exclusivity; + if (com.android.permission.flags.Flags.crossUserRoleEnabled()) { + String exclusivityName = requireAttributeValue(parser, ATTRIBUTE_EXCLUSIVITY, TAG_ROLE); + if (exclusivityName == null) { + skipCurrentTag(parser); + return null; + } + switch (exclusivityName) { + case EXCLUSIVITY_NONE: + exclusivity = Role.EXCLUSIVITY_NONE; + break; + case EXCLUSIVITY_USER: + exclusivity = Role.EXCLUSIVITY_USER; + break; + case EXCLUSIVITY_PROFILE_GROUP: + // TODO(b/372743073): change to isAtLeastB once available + // EXCLUSIVITY_PROFILE behavior only available for B+ + // fallback to default of EXCLUSIVITY_USER + exclusivity = SdkLevel.isAtLeastV() + ? Role.EXCLUSIVITY_PROFILE_GROUP + : Role.EXCLUSIVITY_USER; + break; + default: + throwOrLogMessage("Invalid value for \"exclusivity\" on <role>: " + name + + ", exclusivity: " + exclusivityName); + skipCurrentTag(parser); + return null; + } + } else { + Boolean exclusive = + requireAttributeBooleanValue(parser, ATTRIBUTE_EXCLUSIVE, true, TAG_ROLE); + if (exclusive == null) { + skipCurrentTag(parser); + return null; + } + exclusivity = exclusive ? Role.EXCLUSIVITY_USER : Role.EXCLUSIVITY_NONE; } + boolean fallBackToDefaultHolder = getAttributeBooleanValue(parser, ATTRIBUTE_FALL_BACK_TO_DEFAULT_HOLDER, false); @@ -470,7 +507,7 @@ public class RoleParser { 0); boolean showNone = getAttributeBooleanValue(parser, ATTRIBUTE_SHOW_NONE, false); - if (showNone && !exclusive) { + if (showNone && exclusivity == Role.EXCLUSIVITY_NONE) { throwOrLogMessage("showNone=\"true\" is invalid for a non-exclusive role: " + name); skipCurrentTag(parser); return null; @@ -543,6 +580,12 @@ public class RoleParser { skipCurrentTag(parser); continue; } + if (exclusivity == Role.EXCLUSIVITY_PROFILE_GROUP) { + throwOrLogMessage("<preferred-activities> is not supported for a" + + " profile-group-exclusive role: " + name); + skipCurrentTag(parser); + continue; + } preferredActivities = parsePreferredActivities(parser); break; default: @@ -567,12 +610,12 @@ public class RoleParser { preferredActivities = Collections.emptyList(); } return new Role(name, allowBypassingQualification, behavior, defaultHoldersResourceName, - descriptionResource, exclusive, fallBackToDefaultHolder, featureFlag, labelResource, - maxSdkVersion, minSdkVersion, onlyGrantWhenAdded, overrideUserWhenGranting, - requestDescriptionResource, requestTitleResource, requestable, - searchKeywordsResource, shortLabelResource, showNone, statik, systemOnly, visible, - requiredComponents, permissions, appOpPermissions, appOps, preferredActivities, - uiBehaviorName); + descriptionResource, exclusivity, fallBackToDefaultHolder, featureFlag, + labelResource, maxSdkVersion, minSdkVersion, onlyGrantWhenAdded, + overrideUserWhenGranting, requestDescriptionResource, requestTitleResource, + requestable, searchKeywordsResource, shortLabelResource, showNone, statik, + systemOnly, visible, requiredComponents, permissions, appOpPermissions, appOps, + preferredActivities, uiBehaviorName); } @NonNull @@ -624,7 +667,7 @@ public class RoleParser { int queryFlags = getAttributeIntValue(parser, ATTRIBUTE_QUERY_FLAGS, 0); IntentFilterData intentFilterData = null; List<RequiredMetaData> metaData = new ArrayList<>(); - List<String> validationMetaDataNames = mValidationEnabled ? new ArrayList<>() : null; + List<String> validationMetaDataNames = mThrowOnError ? new ArrayList<>() : null; int type; int depth; @@ -651,7 +694,7 @@ public class RoleParser { if (metaDataName == null) { continue; } - if (mValidationEnabled) { + if (mThrowOnError) { validateNoDuplicateElement(metaDataName, validationMetaDataNames, "meta data"); } @@ -668,7 +711,7 @@ public class RoleParser { RequiredMetaData requiredMetaData = new RequiredMetaData(metaDataName, metaDataValue, metaDataProhibited); metaData.add(requiredMetaData); - if (mValidationEnabled) { + if (mThrowOnError) { validationMetaDataNames.add(metaDataName); } break; @@ -1204,7 +1247,7 @@ public class RoleParser { } private void throwOrLogMessage(String message) { - if (mValidationEnabled) { + if (mThrowOnError) { throw new IllegalArgumentException(message); } else { Log.wtf(LOG_TAG, message); @@ -1212,7 +1255,7 @@ public class RoleParser { } private void throwOrLogMessage(String message, Throwable cause) { - if (mValidationEnabled) { + if (mThrowOnError) { throw new IllegalArgumentException(message, cause); } else { Log.wtf(LOG_TAG, message, cause); @@ -1222,168 +1265,4 @@ public class RoleParser { private void throwOrLogForUnknownTag(@NonNull XmlResourceParser parser) { throwOrLogMessage("Unknown tag: " + parser.getName()); } - - /** - * Validates the permission names with {@code PackageManager} and ensures that all app ops with - * a permission in {@code AppOpsManager} have declared that permission in its role and ensures - * that all preferred activities are listed in the required components. - */ - private void validateResult(@NonNull ArrayMap<String, PermissionSet> permissionSets, - @NonNull ArrayMap<String, Role> roles) { - if (!mValidationEnabled) { - return; - } - - int permissionSetsSize = permissionSets.size(); - for (int permissionSetsIndex = 0; permissionSetsIndex < permissionSetsSize; - permissionSetsIndex++) { - PermissionSet permissionSet = permissionSets.valueAt(permissionSetsIndex); - - List<Permission> permissions = permissionSet.getPermissions(); - int permissionsSize = permissions.size(); - for (int permissionsIndex = 0; permissionsIndex < permissionsSize; permissionsIndex++) { - Permission permission = permissions.get(permissionsIndex); - - validatePermission(permission); - } - } - - int rolesSize = roles.size(); - for (int rolesIndex = 0; rolesIndex < rolesSize; rolesIndex++) { - Role role = roles.valueAt(rolesIndex); - - if (!role.isAvailableByFeatureFlagAndSdkVersion()) { - continue; - } - - List<RequiredComponent> requiredComponents = role.getRequiredComponents(); - int requiredComponentsSize = requiredComponents.size(); - for (int requiredComponentsIndex = 0; requiredComponentsIndex < requiredComponentsSize; - requiredComponentsIndex++) { - RequiredComponent requiredComponent = requiredComponents.get( - requiredComponentsIndex); - - String permission = requiredComponent.getPermission(); - if (permission != null) { - validatePermission(permission); - } - } - - List<Permission> permissions = role.getPermissions(); - int permissionsSize = permissions.size(); - for (int i = 0; i < permissionsSize; i++) { - Permission permission = permissions.get(i); - - validatePermission(permission); - } - - List<AppOp> appOps = role.getAppOps(); - int appOpsSize = appOps.size(); - for (int i = 0; i < appOpsSize; i++) { - AppOp appOp = appOps.get(i); - - validateAppOp(appOp); - } - - List<Permission> appOpPermissions = role.getAppOpPermissions(); - int appOpPermissionsSize = appOpPermissions.size(); - for (int i = 0; i < appOpPermissionsSize; i++) { - validateAppOpPermission(appOpPermissions.get(i)); - } - - List<PreferredActivity> preferredActivities = role.getPreferredActivities(); - int preferredActivitiesSize = preferredActivities.size(); - for (int preferredActivitiesIndex = 0; - preferredActivitiesIndex < preferredActivitiesSize; - preferredActivitiesIndex++) { - PreferredActivity preferredActivity = preferredActivities.get( - preferredActivitiesIndex); - - if (!role.getRequiredComponents().contains(preferredActivity.getActivity())) { - throw new IllegalArgumentException("<activity> of <preferred-activity> not" - + " required in <required-components>, role: " + role.getName() - + ", preferred activity: " + preferredActivity); - } - } - } - } - - private void validatePermission(@NonNull Permission permission) { - if (!permission.isAvailableAsUser(Process.myUserHandle(), mContext)) { - return; - } - validatePermission(permission.getName(), true); - } - - private void validatePermission(@NonNull String permission) { - validatePermission(permission, false); - } - - private void validatePermission(@NonNull String permission, boolean enforceIsRuntimeOrRole) { - PackageManager packageManager = mContext.getPackageManager(); - boolean isAutomotive = packageManager.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE); - // Skip validation for car permissions which may not be available on all build targets. - if (!isAutomotive && permission.startsWith("android.car")) { - return; - } - - PermissionInfo permissionInfo; - try { - permissionInfo = packageManager.getPermissionInfo(permission, 0); - } catch (PackageManager.NameNotFoundException e) { - throw new IllegalArgumentException("Unknown permission: " + permission, e); - } - - if (enforceIsRuntimeOrRole) { - if (!(permissionInfo.getProtection() == PermissionInfo.PROTECTION_DANGEROUS - || (permissionInfo.getProtectionFlags() & PermissionInfo.PROTECTION_FLAG_ROLE) - == PermissionInfo.PROTECTION_FLAG_ROLE)) { - throw new IllegalArgumentException( - "Permission is not a runtime or role permission: " + permission); - } - } - } - - private void validateAppOpPermission(@NonNull Permission appOpPermission) { - if (!appOpPermission.isAvailableAsUser(Process.myUserHandle(), mContext)) { - return; - } - validateAppOpPermission(appOpPermission.getName()); - } - - private void validateAppOpPermission(@NonNull String appOpPermission) { - PackageManager packageManager = mContext.getPackageManager(); - PermissionInfo permissionInfo; - try { - permissionInfo = packageManager.getPermissionInfo(appOpPermission, 0); - } catch (PackageManager.NameNotFoundException e) { - throw new IllegalArgumentException("Unknown app op permission: " + appOpPermission, e); - } - if ((permissionInfo.getProtectionFlags() & PermissionInfo.PROTECTION_FLAG_APPOP) - != PermissionInfo.PROTECTION_FLAG_APPOP) { - throw new IllegalArgumentException("Permission is not an app op permission: " - + appOpPermission); - } - } - - private void validateAppOp(@NonNull AppOp appOp) { - if (!appOp.isAvailableByFeatureFlagAndSdkVersion()) { - return; - } - // This throws IllegalArgumentException if app op is unknown. - String permission = AppOpsManager.opToPermission(appOp.getName()); - if (permission != null) { - PackageManager packageManager = mContext.getPackageManager(); - PermissionInfo permissionInfo; - try { - permissionInfo = packageManager.getPermissionInfo(permission, 0); - } catch (PackageManager.NameNotFoundException e) { - throw new RuntimeException(e); - } - if (permissionInfo.getProtection() == PermissionInfo.PROTECTION_DANGEROUS) { - throw new IllegalArgumentException("App op has an associated runtime permission: " - + appOp.getName()); - } - } - } } diff --git a/PermissionController/role-controller/java/com/android/role/controller/service/RoleControllerServiceImpl.java b/PermissionController/role-controller/java/com/android/role/controller/service/RoleControllerServiceImpl.java index bc7562c11..d00fd47af 100644 --- a/PermissionController/role-controller/java/com/android/role/controller/service/RoleControllerServiceImpl.java +++ b/PermissionController/role-controller/java/com/android/role/controller/service/RoleControllerServiceImpl.java @@ -16,6 +16,7 @@ package com.android.role.controller.service; +import android.annotation.UserIdInt; import android.app.role.RoleControllerService; import android.app.role.RoleManager; import android.content.Context; @@ -34,6 +35,7 @@ import com.android.role.controller.model.Roles; import com.android.role.controller.util.CollectionUtils; import com.android.role.controller.util.LegacyRoleFallbackEnabledUtils; import com.android.role.controller.util.PackageUtils; +import com.android.role.controller.util.RoleFlags; import com.android.role.controller.util.UserUtils; import java.util.ArrayList; @@ -49,11 +51,21 @@ public class RoleControllerServiceImpl extends RoleControllerService { private static final boolean DEBUG = false; + public static volatile SetActiveUserForRoleMethod sSetActiveUserForRoleMethod; private UserHandle mUser; private Context mContext; private RoleManager mUserRoleManager; + /** Method for setting active user from role controller */ + public interface SetActiveUserForRoleMethod { + /** + * Sets user as active for the given role. + * @see RoleManager#setActiveUserForRole(String, UserHandle, int) + */ + void setActiveUserForRole(@NonNull String roleName, @UserIdInt int userId, int flags); + } + public RoleControllerServiceImpl() {} public RoleControllerServiceImpl(@NonNull UserHandle user, @NonNull Context context) { @@ -121,6 +133,19 @@ public class RoleControllerServiceImpl extends RoleControllerService { String roleName = role.getName(); + if (RoleFlags.isProfileGroupExclusivityAvailable() + && role.getExclusivity() == Role.EXCLUSIVITY_PROFILE_GROUP) { + if (mUserRoleManager.getActiveUserForRole(roleName) == null) { + UserHandle profileParent = UserUtils.getProfileParentOrSelf(mUser, mContext); + if (Objects.equals(mUser, profileParent)) { + Log.i(LOG_TAG, "No active user for role: " + roleName + ", setting " + + "active user to user: " + mUser.getIdentifier()); + sSetActiveUserForRoleMethod.setActiveUserForRole(roleName, + mUser.getIdentifier(), 0); + } + } + } + // For each of the current holders, check if it is still qualified, redo grant if so, or // remove it otherwise. List<String> currentPackageNames = mUserRoleManager.getRoleHolders(roleName); @@ -232,6 +257,11 @@ public class RoleControllerServiceImpl extends RoleControllerService { boolean added = false; if (role.isExclusive()) { + if (role.getExclusivity() == Role.EXCLUSIVITY_PROFILE_GROUP) { + sSetActiveUserForRoleMethod.setActiveUserForRole(roleName, mUser.getIdentifier(), + flags); + } + List<String> currentPackageNames = mUserRoleManager.getRoleHolders(roleName); int currentPackageNamesSize = currentPackageNames.size(); for (int i = 0; i < currentPackageNamesSize; i++) { diff --git a/PermissionController/role-controller/java/com/android/role/controller/util/RoleFlags.java b/PermissionController/role-controller/java/com/android/role/controller/util/RoleFlags.java new file mode 100644 index 000000000..2c5a247b6 --- /dev/null +++ b/PermissionController/role-controller/java/com/android/role/controller/util/RoleFlags.java @@ -0,0 +1,45 @@ +/* + * 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.role.controller.util; + +import android.os.Build; + +import androidx.annotation.ChecksSdkIntAtLeast; + +import java.util.Objects; + +/** Util class for getting shared feature flag check logic. */ +public final class RoleFlags { + private RoleFlags() { /* cannot be instantiated */ } + + /** + * Returns whether profile group exclusive roles are available. Profile exclusive roles are + * available on B+ + */ + @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.BAKLAVA) + public static boolean isProfileGroupExclusivityAvailable() { + // TODO(b/372743073): change to isAtLeastB once available + return isAtLeastB() && com.android.permission.flags.Flags.crossUserRoleEnabled(); + } + + // TODO(b/372743073): remove once SdkLevel.isAtLeastB available + @ChecksSdkIntAtLeast(api = 36 /* BUILD_VERSION_CODES.Baklava */) + public static boolean isAtLeastB() { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA + || Objects.equals(Build.VERSION.CODENAME, "Baklava"); + } +} diff --git a/PermissionController/role-controller/java/com/android/role/controller/util/UserUtils.java b/PermissionController/role-controller/java/com/android/role/controller/util/UserUtils.java index 1b6926ef8..f3cb7926a 100644 --- a/PermissionController/role-controller/java/com/android/role/controller/util/UserUtils.java +++ b/PermissionController/role-controller/java/com/android/role/controller/util/UserUtils.java @@ -24,6 +24,7 @@ import android.os.UserHandle; import android.os.UserManager; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import com.android.modules.utils.build.SdkLevel; @@ -111,4 +112,24 @@ public final class UserUtils { return context.createContextAsUser(user, 0); } } + + /** + * Returns the parent of a given user, or user if it has no parent (e.g. it is the primary + * user) + */ + @NonNull + public static UserHandle getProfileParentOrSelf(@NonNull UserHandle user, + @NonNull Context context) { + UserHandle profileParent = getProfileParent(user, context); + // If profile parent user is null, then original user is the parent + return profileParent != null ? profileParent : user; + } + + /** Returns the parent of a given user. */ + @Nullable + private static UserHandle getProfileParent(UserHandle user, @NonNull Context context) { + Context userContext = getUserContext(context, user); + UserManager userManager = userContext.getSystemService(UserManager.class); + return userManager.getProfileParent(user); + } } |