diff --git a/services/core/java/com/android/server/pm/CrossProfileIntentFilter.java b/services/core/java/com/android/server/pm/CrossProfileIntentFilter.java index f5910fa3143c7..718756f858601 100644 --- a/services/core/java/com/android/server/pm/CrossProfileIntentFilter.java +++ b/services/core/java/com/android/server/pm/CrossProfileIntentFilter.java @@ -16,6 +16,7 @@ package com.android.server.pm; +import android.annotation.IntDef; import android.content.Intent; import android.content.IntentFilter; import android.os.UserHandle; @@ -30,6 +31,8 @@ import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import java.io.IOException; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; /** * The {@link PackageManagerService} maintains some {@link CrossProfileIntentFilter}s for each user. @@ -41,13 +44,52 @@ class CrossProfileIntentFilter extends WatchedIntentFilter { private static final String ATTR_FLAGS = "flags"; private static final String ATTR_OWNER_PACKAGE = "ownerPackage"; private static final String ATTR_FILTER = "filter"; + private static final String ATTR_ACCESS_CONTROL = "accessControl"; private static final String TAG = "CrossProfileIntentFilter"; + /** + * AccessControlLevel provides level of access for user to create/modify + * {@link CrossProfileIntentFilter} in {@link com.android.server.pm.Settings}. + * Each AccessControlLevel have value assigned, the higher the value + * implies higher restriction for creation/modification. + * AccessControlLevel allows us to protect against malicious changes in user's + * {@link CrossProfileIntentFilter}s, which might add/remove {@link CrossProfileIntentFilter} + * leading to unprecedented results. + */ + @IntDef(prefix = {"ACCESS_LEVEL_"}, value = { + ACCESS_LEVEL_ALL, + ACCESS_LEVEL_SYSTEM, + ACCESS_LEVEL_SYSTEM_ADD_ONLY, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface AccessControlLevel { + } + + /** + * ACCESS_LEVEL_ALL signifies that irrespective of user we would allow + * access(addition/modification/removal) for CrossProfileIntentFilter. + * This is the default access control level. + */ + public static final int ACCESS_LEVEL_ALL = 0; + + /** + * ACCESS_LEVEL_SYSTEM signifies that only system/root user would be able to + * access(addition/modification/removal) CrossProfileIntentFilter. + */ + public static final int ACCESS_LEVEL_SYSTEM = 10; + + /** + * ACCESS_LEVEL_SYSTEM_ADD_ONLY signifies that only system/root user would be able to add + * CrossProfileIntentFilter but not modify/remove. Once added, it cannot be modified or removed. + */ + public static final int ACCESS_LEVEL_SYSTEM_ADD_ONLY = 20; + // If the intent matches the IntentFilter, then it can be forwarded to this userId. final int mTargetUserId; final String mOwnerPackage; // packageName of the app. final int mFlags; + final int mAccessControlLevel; // The cache for snapshots, so they are not rebuilt if the base object has not // changed. @@ -65,10 +107,16 @@ class CrossProfileIntentFilter extends WatchedIntentFilter { CrossProfileIntentFilter(IntentFilter filter, String ownerPackage, int targetUserId, int flags) { + this(filter, ownerPackage, targetUserId, flags, ACCESS_LEVEL_ALL); + } + + CrossProfileIntentFilter(IntentFilter filter, String ownerPackage, int targetUserId, + int flags, @AccessControlLevel int accessControlLevel) { super(filter); mTargetUserId = targetUserId; mOwnerPackage = ownerPackage; mFlags = flags; + mAccessControlLevel = accessControlLevel; mSnapshot = makeCache(); } @@ -77,12 +125,18 @@ class CrossProfileIntentFilter extends WatchedIntentFilter { this(filter.mFilter, ownerPackage, targetUserId, flags); } + CrossProfileIntentFilter(WatchedIntentFilter filter, String ownerPackage, int targetUserId, + int flags, @AccessControlLevel int accessControlLevel) { + this(filter.mFilter, ownerPackage, targetUserId, flags, accessControlLevel); + } + // Copy constructor used only to create a snapshot. private CrossProfileIntentFilter(CrossProfileIntentFilter f) { super(f); mTargetUserId = f.mTargetUserId; mOwnerPackage = f.mOwnerPackage; mFlags = f.mFlags; + mAccessControlLevel = f.mAccessControlLevel; mSnapshot = new SnapshotCache.Sealed(); } @@ -98,9 +152,14 @@ class CrossProfileIntentFilter extends WatchedIntentFilter { return mOwnerPackage; } + public @AccessControlLevel int getAccessControlLevel() { + return mAccessControlLevel; + } + CrossProfileIntentFilter(TypedXmlPullParser parser) throws XmlPullParserException, IOException { mTargetUserId = parser.getAttributeInt(null, ATTR_TARGET_USER_ID, UserHandle.USER_NULL); mOwnerPackage = getStringFromXml(parser, ATTR_OWNER_PACKAGE, ""); + mAccessControlLevel = parser.getAttributeInt(null, ATTR_ACCESS_CONTROL, ACCESS_LEVEL_ALL); mFlags = parser.getAttributeInt(null, ATTR_FLAGS, 0); mSnapshot = makeCache(); @@ -151,6 +210,7 @@ class CrossProfileIntentFilter extends WatchedIntentFilter { serializer.attributeInt(null, ATTR_TARGET_USER_ID, mTargetUserId); serializer.attributeInt(null, ATTR_FLAGS, mFlags); serializer.attribute(null, ATTR_OWNER_PACKAGE, mOwnerPackage); + serializer.attributeInt(null, ATTR_ACCESS_CONTROL, mAccessControlLevel); serializer.startTag(null, ATTR_FILTER); mFilter.writeToXml(serializer); serializer.endTag(null, ATTR_FILTER); @@ -165,7 +225,8 @@ class CrossProfileIntentFilter extends WatchedIntentFilter { boolean equalsIgnoreFilter(CrossProfileIntentFilter other) { return mTargetUserId == other.mTargetUserId && mOwnerPackage.equals(other.mOwnerPackage) - && mFlags == other.mFlags; + && mFlags == other.mFlags + && mAccessControlLevel == other.mAccessControlLevel; } public CrossProfileIntentFilter snapshot() { diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java index 8b40f6510e7ef..e798adf1c22a8 100644 --- a/services/core/java/com/android/server/pm/PackageManagerService.java +++ b/services/core/java/com/android/server/pm/PackageManagerService.java @@ -3350,6 +3350,12 @@ public class PackageManagerService implements PackageSender, TestUtilityService android.Manifest.permission.INTERACT_ACROSS_USERS_FULL, null); int callingUid = Binder.getCallingUid(); enforceOwnerRights(snapshot, ownerPackage, callingUid); + + // Verifying that current calling uid should be able to add {@link CrossProfileIntentFilter} + // for source and target user + mUserManager.enforceCrossProfileIntentFilterAccess(sourceUserId, targetUserId, callingUid, + /* addCrossProfileIntentFilter */ true); + PackageManagerServiceUtils.enforceShellRestriction(mInjector.getUserManagerInternal(), UserManager.DISALLOW_DEBUGGING_FEATURES, callingUid, sourceUserId); if (intentFilter.countActions() == 0) { @@ -3358,7 +3364,8 @@ public class PackageManagerService implements PackageSender, TestUtilityService } synchronized (mLock) { CrossProfileIntentFilter newFilter = new CrossProfileIntentFilter(intentFilter, - ownerPackage, targetUserId, flags); + ownerPackage, targetUserId, flags, mUserManager + .getCrossProfileIntentFilterAccessControl(sourceUserId, targetUserId)); CrossProfileIntentResolver resolver = mSettings.editCrossProfileIntentResolverLPw(sourceUserId); ArrayList existing = resolver.findFilters(intentFilter); @@ -4572,7 +4579,11 @@ public class PackageManagerService implements PackageSender, TestUtilityService ArraySet set = new ArraySet<>(resolver.filterSet()); for (CrossProfileIntentFilter filter : set) { - if (filter.getOwnerPackage().equals(ownerPackage)) { + //Only remove if calling user is allowed based on access control of + // {@link CrossProfileIntentFilter} + if (filter.getOwnerPackage().equals(ownerPackage) + && mUserManager.isCrossProfileIntentFilterAccessible(sourceUserId, + filter.mTargetUserId, /* addCrossProfileIntentFilter */ false)) { resolver.removeFilter(filter); } } diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java index 0c1c265428c68..92ace7af56318 100644 --- a/services/core/java/com/android/server/pm/UserManagerService.java +++ b/services/core/java/com/android/server/pm/UserManagerService.java @@ -1866,6 +1866,103 @@ public class UserManagerService extends IUserManager.Stub { return mLocalService.exists(userId); } + /** + * Returns user's {@link CrossProfileIntentFilter.AccessControlLevel}, which is derived from + * {@link UserTypeDetails}. If user does not have defined their access control level, + * returns default {@link CrossProfileIntentFilter#ACCESS_LEVEL_ALL} + */ + private @CrossProfileIntentFilter.AccessControlLevel int + getCrossProfileIntentFilterAccessControl(@UserIdInt int userId) { + final UserTypeDetails userTypeDetails = getUserTypeDetailsNoChecks(userId); + return userTypeDetails != null ? userTypeDetails.getCrossProfileIntentFilterAccessControl() + : CrossProfileIntentFilter.ACCESS_LEVEL_ALL; + } + + /** + * Verifies if calling user is allowed to access {@link CrossProfileIntentFilter} between given + * source and target user. + * @param sourceUserId userId for which CrossProfileIntentFilter would be configured + * @param targetUserId target user where we can resolve given intent filter + * @param callingUid user accessing api + * @param addCrossProfileIntentFilter if the operation is addition or not. + * @throws SecurityException is calling user is not allowed to access. + */ + public void enforceCrossProfileIntentFilterAccess( + int sourceUserId, int targetUserId, + int callingUid, boolean addCrossProfileIntentFilter) { + if (!isCrossProfileIntentFilterAccessible(sourceUserId, targetUserId, + addCrossProfileIntentFilter)) { + throw new SecurityException("CrossProfileIntentFilter cannot be accessed by user " + + callingUid); + } + } + + /** + * Checks if {@link CrossProfileIntentFilter} can be accessed by calling user for given source + * and target user. There are following rules of access + * 1. For {@link CrossProfileIntentFilter#ACCESS_LEVEL_ALL}, + * irrespective of user we would allow access(addition/modification/removal) + * 2. For {@link CrossProfileIntentFilter#ACCESS_LEVEL_SYSTEM}, + * only system/root user would be able to access(addition/modification/removal) + * 3. For {@link CrossProfileIntentFilter#ACCESS_LEVEL_SYSTEM_ADD_ONLY}, + * only system/root user would be able to add but not modify/remove. Once added, it cannot be + * modified or removed + * @param sourceUserId userId for which CrossProfileIntentFilter would be configured + * @param targetUserId target user where we can resolve given intent filter + * @param addCrossProfileIntentFilter if the operation is addition or not. + * @return true if {@link CrossProfileIntentFilter} can be accessed by calling user + */ + public boolean isCrossProfileIntentFilterAccessible(int sourceUserId, int targetUserId, + boolean addCrossProfileIntentFilter) { + int effectiveAccessControl = + getCrossProfileIntentFilterAccessControl(sourceUserId, targetUserId); + + /* + For {@link CrossProfileIntentFilter#ACCESS_LEVEL_SYSTEM}, if accessing user is not + system or root disallowing access to {@link CrossProfileIntentFilter} + */ + if (CrossProfileIntentFilter.ACCESS_LEVEL_SYSTEM == effectiveAccessControl + && !PackageManagerServiceUtils.isSystemOrRoot()) { + return false; + } + + /* + For {@link CrossProfileIntentFilter#ACCESS_LEVEL_SYSTEM_ADD_ONLY}, allowing only + system user to add {@link CrossProfileIntentFilter}. All users(including system) are + disallowed to modify/remove. + */ + if (CrossProfileIntentFilter.ACCESS_LEVEL_SYSTEM_ADD_ONLY == effectiveAccessControl + && (!addCrossProfileIntentFilter || !PackageManagerServiceUtils.isSystemOrRoot())) { + return false; + } + return true; + } + + /** + * Returns {@link CrossProfileIntentFilter.AccessControlLevel} + * that should be assigned to {@link CrossProfileIntentFilter} + * computed from source user's and target user's + * {@link CrossProfileIntentFilter.AccessControlLevel}. + * The Access Level is configured per {@link CrossProfileIntentFilter} and its property of edge + * between source and target user e.g. for all {@link CrossProfileIntentFilter}s configured + * between Primary user and Clone profile should have access level of + * {@link CrossProfileIntentFilter#ACCESS_LEVEL_SYSTEM} which is driven by highest + * access value from source or target. The higher value means higher restrictions. + * @param sourceUserId userId of source user for whom CrossProfileIntentFilter will be stored + * @param targetUserId userId of target user for whom Cross Profile access would be allowed + * @return least privileged {@link CrossProfileIntentFilter.AccessControlLevel} from source or + * target user. + */ + public @CrossProfileIntentFilter.AccessControlLevel int + getCrossProfileIntentFilterAccessControl(int sourceUserId, int targetUserId) { + int sourceAccessControlLevel, + targetAccessControlLevel, effectiveAccessControl; + sourceAccessControlLevel = getCrossProfileIntentFilterAccessControl(sourceUserId); + targetAccessControlLevel = getCrossProfileIntentFilterAccessControl(targetUserId); + effectiveAccessControl = Math.max(sourceAccessControlLevel, targetAccessControlLevel); + return effectiveAccessControl; + } + @Override public void setUserName(@UserIdInt int userId, String name) { checkManageUsersPermission("rename users"); diff --git a/services/core/java/com/android/server/pm/UserTypeDetails.java b/services/core/java/com/android/server/pm/UserTypeDetails.java index 4aad1a7f38812..2f3ca664c1266 100644 --- a/services/core/java/com/android/server/pm/UserTypeDetails.java +++ b/services/core/java/com/android/server/pm/UserTypeDetails.java @@ -163,6 +163,14 @@ public final class UserTypeDetails { */ private final boolean mIsCredentialSharableWithParent; + /** + * Denotes the default access control for {@link CrossProfileIntentFilter} of user profile. + * + *

Default value is {@link CrossProfileIntentFilter#ACCESS_LEVEL_ALL} + */ + private final @CrossProfileIntentFilter.AccessControlLevel int + mCrossProfileIntentFilterAccessControl; + private UserTypeDetails(@NonNull String name, boolean enabled, int maxAllowed, @UserInfoFlag int baseType, @UserInfoFlag int defaultUserInfoPropertyFlags, int label, int maxAllowedPerParent, @@ -174,7 +182,8 @@ public final class UserTypeDetails { @Nullable Bundle defaultSecureSettings, @Nullable List defaultCrossProfileIntentFilters, boolean isMediaSharedWithParent, - boolean isCredentialSharableWithParent) { + boolean isCredentialSharableWithParent, + @CrossProfileIntentFilter.AccessControlLevel int accessControlLevel) { this.mName = name; this.mEnabled = enabled; this.mMaxAllowed = maxAllowed; @@ -195,6 +204,7 @@ public final class UserTypeDetails { this.mDarkThemeBadgeColors = darkThemeBadgeColors; this.mIsMediaSharedWithParent = isMediaSharedWithParent; this.mIsCredentialSharableWithParent = isCredentialSharableWithParent; + this.mCrossProfileIntentFilterAccessControl = accessControlLevel; } /** @@ -327,6 +337,16 @@ public final class UserTypeDetails { return mIsCredentialSharableWithParent; } + /** + * Returning user's {@link CrossProfileIntentFilter.AccessControlLevel}. If not explicitly + * configured, default value is {@link CrossProfileIntentFilter#ACCESS_LEVEL_ALL} + * @return user's {@link CrossProfileIntentFilter.AccessControlLevel} + */ + public @CrossProfileIntentFilter.AccessControlLevel int + getCrossProfileIntentFilterAccessControl() { + return mCrossProfileIntentFilterAccessControl; + } + /** Returns a {@link Bundle} representing the default user restrictions. */ @NonNull Bundle getDefaultRestrictions() { return BundleUtils.clone(mDefaultRestrictions); @@ -420,6 +440,8 @@ public final class UserTypeDetails { private @DrawableRes int mBadgeNoBackground = Resources.ID_NULL; private boolean mIsMediaSharedWithParent = false; private boolean mIsCredentialSharableWithParent = false; + private @CrossProfileIntentFilter.AccessControlLevel int + mCrossProfileIntentFilterAccessControl = CrossProfileIntentFilter.ACCESS_LEVEL_ALL; public Builder setName(String name) { mName = name; @@ -519,6 +541,16 @@ public final class UserTypeDetails { return this; } + /** + * Sets {@link CrossProfileIntentFilter.AccessControlLevel} for the user. + * @param accessControlLevel default access control for user + */ + public Builder setCrossProfileIntentFilterAccessControl( + @CrossProfileIntentFilter.AccessControlLevel int accessControlLevel) { + mCrossProfileIntentFilterAccessControl = accessControlLevel; + return this; + } + /** * Sets shared media property for the user. * @param isCredentialSharableWithParent the value to be set, true or false @@ -571,7 +603,8 @@ public final class UserTypeDetails { mDefaultSecureSettings, mDefaultCrossProfileIntentFilters, mIsMediaSharedWithParent, - mIsCredentialSharableWithParent); + mIsCredentialSharableWithParent, + mCrossProfileIntentFilterAccessControl); } private boolean hasBadge() { diff --git a/services/core/java/com/android/server/pm/UserTypeFactory.java b/services/core/java/com/android/server/pm/UserTypeFactory.java index 0878da49df5b1..857a9757b03a1 100644 --- a/services/core/java/com/android/server/pm/UserTypeFactory.java +++ b/services/core/java/com/android/server/pm/UserTypeFactory.java @@ -122,6 +122,8 @@ public final class UserTypeFactory { .setLabel(0) .setDefaultRestrictions(null) .setIsMediaSharedWithParent(true) + .setCrossProfileIntentFilterAccessControl( + CrossProfileIntentFilter.ACCESS_LEVEL_SYSTEM) .setIsCredentialSharableWithParent(true); }