diff options
author | 2025-03-03 14:26:34 +0000 | |
---|---|---|
committer | 2025-03-05 11:58:49 +0000 | |
commit | fc0ef475b7c4feca01587a3754cea86658b56546 (patch) | |
tree | 6c27e8d57cf11463ade67aa049d92a9becf8cbd7 /src | |
parent | 87e10955c937acc8d8f925c77e38ad454c4c2fd9 (diff) |
Fix isBlockedByAdmin implementation to resolve cross profile access.
* Ensure that CrossProfileIntentForwarderActivity resolution is accurate.
This relies on the @hide property `targetUserId` by accessing it via
reflection as there is no API that can surface this information for
`module_current` targets.
* Add test cases for devices that may be configured to have more than
one managed profile, and may not be compatible with AOSP source code
assumption to ensure CrossProfile detection fails closed.
Bug: b/394231676
Test: atest MediaProviderTests:UserManagerStateTest
Flag: EXEMPT bugfix
Change-Id: I0770a5723b67fe963bdaa9c5b8e6f7ca12eff6b4
Diffstat (limited to 'src')
-rw-r--r-- | src/com/android/providers/media/photopicker/data/UserManagerState.java | 393 |
1 files changed, 226 insertions, 167 deletions
diff --git a/src/com/android/providers/media/photopicker/data/UserManagerState.java b/src/com/android/providers/media/photopicker/data/UserManagerState.java index 58b70e87e..de6af1b0c 100644 --- a/src/com/android/providers/media/photopicker/data/UserManagerState.java +++ b/src/com/android/providers/media/photopicker/data/UserManagerState.java @@ -21,10 +21,12 @@ import static androidx.core.util.Preconditions.checkNotNull; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresApi; +import android.annotation.SuppressLint; import android.app.ActivityManager; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; import android.content.pm.UserProperties; import android.content.res.Resources; import android.graphics.drawable.Drawable; @@ -44,6 +46,7 @@ import com.android.providers.media.photopicker.data.model.UserId; import com.android.providers.media.photopicker.ui.TabFragment; import com.android.providers.media.photopicker.util.CrossProfileUtils; +import java.lang.reflect.Field; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -54,9 +57,7 @@ import java.util.Map; * Interface to query user ids {@link UserId} */ public interface UserManagerState { - /** - * Whether there are more than 1 user profiles associated with the current user. - */ + /** Whether there are more than 1 user profiles associated with the current user. */ boolean isMultiUserProfiles(); /** @@ -73,9 +74,9 @@ public interface UserManagerState { /** * A Map of all the profiles with their cross profile allowed status from current user. - * key : userId of a profile - * Value : cross profile allowed status of a user profile corresponding to user id with - * current user . + * + * <p>key : userId of a profile Value : cross profile allowed status of a user profile + * corresponding to user id with current user . */ @NonNull Map<UserId, Boolean> getCrossProfileAllowedStatusForAll(); @@ -86,16 +87,13 @@ public interface UserManagerState { */ int getProfileCount(); - /** - * - * A {@link MutableLiveData} to check if cross profile interaction allowed or not. - */ + /** A {@link MutableLiveData} to check if cross profile interaction allowed or not. */ @NonNull MutableLiveData<Map<UserId, Boolean>> getCrossProfileAllowed(); /** - * A list of all user profile ids including current user that need to be shown - * separately in PhotoPicker + * A list of all user profile ids including current user that need to be shown separately in + * PhotoPicker */ @NonNull List<UserId> getAllUserProfileIds(); @@ -105,38 +103,32 @@ public interface UserManagerState { */ void updateProfileOffValuesAndPostCrossProfileStatus(); - /** - * Updates on/off values of all the user profiles - */ + /** Updates on/off values of all the user profiles */ void updateProfileOffValues(); - /** - * Waits for Media Provider of the user profile corresponding to userId to be available. - */ + /** Waits for Media Provider of the user profile corresponding to userId to be available. */ void waitForMediaProviderToBeAvailable(UserId userId); /** - * Get if it is allowed to access the otherUser profile from current user ( current user : - * the user profile that started the photo picker activity) - **/ + * Get if it is allowed to access the otherUser profile from current user ( current user : the + * user profile that started the photo picker activity) + */ @NonNull boolean isCrossProfileAllowedToUser(UserId otherUser); - /** - * A {@link MutableLiveData} to check if there are multiple user profiles or not - */ + /** A {@link MutableLiveData} to check if there are multiple user profiles or not */ @NonNull MutableLiveData<Boolean> getIsMultiUserProfiles(); /** - * Resets the user ids. This is usually called as a result of receiving broadcast that - * any profile has been added or removed. + * Resets the user ids. This is usually called as a result of receiving broadcast that any + * profile has been added or removed. */ void resetUserIds(); /** - * Resets the user ids and set their cross profile values. This is usually called as a result - * of receiving broadcast that any profile has been added or removed. + * Resets the user ids and set their cross profile values. This is usually called as a result of + * receiving broadcast that any profile has been added or removed. */ void resetUserIdsAndSetCrossProfileValues(Intent intent); @@ -152,39 +144,31 @@ public interface UserManagerState { void setIntentAndCheckRestrictions(Intent intent); /** - * Whether cross profile access corresponding to the userID is blocked - * by admin for the current user. + * Whether cross profile access corresponding to the userID is blocked by admin for the current + * user. */ boolean isBlockedByAdmin(UserId userId); - /** - * Whether profile corresponding to the userID is on or off. - */ + /** Whether profile corresponding to the userID is on or off. */ boolean isProfileOff(UserId userId); - /** - * A map of all user profile labels corresponding to all profile userIds - */ + /** A map of all user profile labels corresponding to all profile userIds */ Map<UserId, String> getProfileLabelsForAll(); /** * Returns whether a user should be shown in the PhotoPicker depending on its quite mode status. * - * @return One of {@link UserProperties.SHOW_IN_QUIET_MODE_PAUSED}, - * {@link UserProperties.SHOW_IN_QUIET_MODE_HIDDEN}, or - * {@link UserProperties.SHOW_IN_QUIET_MODE_DEFAULT} depending on whether the profile - * should be shown in quiet mode or not. + * @return One of {@link UserProperties.SHOW_IN_QUIET_MODE_PAUSED}, {@link + * UserProperties.SHOW_IN_QUIET_MODE_HIDDEN}, or {@link + * UserProperties.SHOW_IN_QUIET_MODE_DEFAULT} depending on whether the profile should be + * shown in quiet mode or not. */ int getShowInQuietMode(UserId userId); - /** - * A map of all user profile Icon ids corresponding to all profile userIds - */ + /** A map of all user profile Icon ids corresponding to all profile userIds */ Map<UserId, Drawable> getProfileBadgeForAll(); - /** - * Set a user as a current user profile - **/ + /** Set a user as a current user profile */ void setUserAsCurrentUserProfile(UserId userId); /** @@ -193,8 +177,7 @@ public interface UserManagerState { boolean isUserSelectedAsCurrentUserProfile(UserId userId); /** - * Creates an implementation of {@link UserManagerState}. - * Todo(b/319067964): make this singleton + * Creates an implementation of {@link UserManagerState}. Todo(b/319067964): make this singleton */ static UserManagerState create(Context context) { return new RuntimeUserManagerState(context); @@ -212,37 +195,37 @@ public interface UserManagerState { private static final int SHOW_IN_QUIET_MODE_DEFAULT = -1; private final Context mContext; - // This is the user profile that started the photo picker activity. That's why it cannot - // change in a UserIdManager instance. + // This is the user profile that started the photo picker activity. That's why + // it cannot change in a UserIdManager instance. private final UserId mCurrentUser; private final Handler mHandler; private Map<UserId, Runnable> mIsProviderAvailableRunnableMap = new HashMap<>(); - // This is the user profile selected in the photo picker. Photo picker will display media - // for this user. It could be different from mCurrentUser. + // This is the user profile selected in the photo picker. Photo picker will + // display media for this user. It could be different from mCurrentUser. private UserId mCurrentUserProfile = null; - // A map of user profile ids (Except current user) with a Boolean value that represents - // whether corresponding user profile is blocked by admin or not. - private Map<UserId , Boolean> mIsProfileBlockedByAdminMap = new HashMap<>(); + // A map of user profile ids (Except current user) with a Boolean value that + // represents whether corresponding user profile is blocked by admin or not. + private Map<UserId, Boolean> mIsProfileBlockedByAdminMap = new HashMap<>(); - // A map of user profile ids (Except current user) with a Boolean value that represents - // whether corresponding user profile is on or off. - private Map<UserId , Boolean> mProfileOffStatus = new HashMap<>(); + // A map of user profile ids (Except current user) with a Boolean value that + // represents whether corresponding user profile is on or off. + private Map<UserId, Boolean> mProfileOffStatus = new HashMap<>(); private final MutableLiveData<Boolean> mIsMultiUserProfiles = new MutableLiveData<>(); - // A list of all user profile Ids present on the device that require a separate tab to show - // in PhotoPicker. It also includes currentUser/contextUser. + // A list of all user profile Ids present on the device that require a separate + // tab to show in PhotoPicker. It also includes currentUser/contextUser. private List<UserId> mUserProfileIds = new ArrayList<>(); private UserManager mUserManager; /** * This live data will be posted every time when a user profile change occurs in the - * background such as turning on/off/adding/removing a user profile. The complete map - * will be reinitiated again in {@link #getCrossProfileAllowedStatusForAll()} and will - * be posted into the below mutable live data. This live data will be observed later in - * {@link TabFragment}. - **/ + * background such as turning on/off/adding/removing a user profile. The complete map will + * be reinitiated again in {@link #getCrossProfileAllowedStatusForAll()} and will be posted + * into the below mutable live data. This live data will be observed later in {@link + * TabFragment}. + */ private final MutableLiveData<Map<UserId, Boolean>> mCrossProfileAllowedStatus = new MutableLiveData<>(); @@ -277,8 +260,8 @@ public interface UserManagerState { return; } - // Here there could be other profiles too , that we do not want to show anywhere in - // photo picker at all. + // Here there could be other profiles too , that we do not want to show anywhere + // in photo picker at all. final List<UserHandle> userProfiles = mUserManager.getUserProfiles(); if (SdkLevel.isAtLeastV()) { for (UserHandle userHandle : userProfiles) { @@ -289,13 +272,13 @@ public interface UserManagerState { // an owner profile itself. if (getSystemUser().getIdentifier() != userHandle.getIdentifier() && userProperties.getShowInSharingSurfaces() - == userProperties.SHOW_IN_SHARING_SURFACES_SEPARATE) { + == userProperties.SHOW_IN_SHARING_SURFACES_SEPARATE) { mUserProfileIds.add(userId); } } } else { - // if sdk version is less than V, then maximum two profiles with separate tab could - // only be available + // if sdk version is less than V, then maximum two profiles with separate tab + // could only be available for (UserHandle userHandle : userProfiles) { if (mUserManager.isManagedProfile(userHandle.getIdentifier())) { mUserProfileIds.add(UserId.of(userHandle)); @@ -311,7 +294,7 @@ public interface UserManagerState { } @Override - public int getProfileCount() { + public int getProfileCount() { return mUserProfileIds.size(); } @@ -364,11 +347,11 @@ public interface UserManagerState { @Override public void setIntentAndCheckRestrictions(Intent intent) { assertMainThread(); - // The below method should be called even if only one profile is present on the device - // because we want to have current profile off value and blocked by admin values in the - // corresponding maps + // The below method should be called even if only one profile is present on the + // device because we want to have current profile off value and blocked by admin + // values + // in the corresponding maps updateCrossProfileValues(intent); - } @Override @@ -431,13 +414,14 @@ public interface UserManagerState { } } } + @Override public void waitForMediaProviderToBeAvailable(UserId userId) { assertMainThread(); - // Remove callbacks if any pre-available callbacks are present in the message queue for - // given user + // Remove callbacks if any pre-available callbacks are present in the message + // queue for given user stopWaitingForProviderToBeAvailableForUser(userId); - if (CrossProfileUtils.isMediaProviderAvailable(userId , mContext)) { + if (CrossProfileUtils.isMediaProviderAvailable(userId, mContext)) { mProfileOffStatus.put(userId, false); updateAndPostCrossProfileStatus(); return; @@ -446,37 +430,49 @@ public interface UserManagerState { } private void waitForProviderToBeAvailable(UserId userId, int numOfTries) { - // The runnable should make sure to post update on the live data if it is changed. - Runnable runnable = () -> { - try { - // We stop the recursive check when - // 1. the provider is available - // 2. the profile is in quiet mode, i.e. provider will not be available - // 3. after maximum retries - if (CrossProfileUtils.isMediaProviderAvailable(userId, mContext)) { - mProfileOffStatus.put(userId, false); - updateAndPostCrossProfileStatus(); - return; - } - - if (CrossProfileUtils.isQuietModeEnabled(userId, mContext)) { - return; - } - - if (numOfTries <= PROVIDER_AVAILABILITY_MAX_RETRIES) { - Log.d(TAG, "MediaProvider is not available. Retry after " - + PROVIDER_AVAILABILITY_CHECK_DELAY); - waitForProviderToBeAvailable(userId, numOfTries + 1); - return; - } - - Log.w(TAG, "Failed waiting for MediaProvider for user:" + userId - + " to be available"); - } catch (Exception e) { - Log.e(TAG, "An error occurred in runnable while waiting for " - + "MediaProvider for user:" + userId + " to be available", e); - } - }; + // The runnable should make sure to post update on the live data if it is + // changed. + Runnable runnable = + () -> { + try { + // We stop the recursive check when + // 1. the provider is available + // 2. the profile is in quiet mode, i.e. provider will not be available + // 3. after maximum retries + if (CrossProfileUtils.isMediaProviderAvailable(userId, mContext)) { + mProfileOffStatus.put(userId, false); + updateAndPostCrossProfileStatus(); + return; + } + + if (CrossProfileUtils.isQuietModeEnabled(userId, mContext)) { + return; + } + + if (numOfTries <= PROVIDER_AVAILABILITY_MAX_RETRIES) { + Log.d( + TAG, + "MediaProvider is not available. Retry after " + + PROVIDER_AVAILABILITY_CHECK_DELAY); + waitForProviderToBeAvailable(userId, numOfTries + 1); + return; + } + + Log.w( + TAG, + "Failed waiting for MediaProvider for user:" + + userId + + " to be available"); + } catch (Exception e) { + Log.e( + TAG, + "An error occurred in runnable while waiting for " + + "MediaProvider for user:" + + userId + + " to be available", + e); + } + }; mIsProviderAvailableRunnableMap.put(userId, runnable); mHandler.postDelayed(runnable, PROVIDER_AVAILABILITY_CHECK_DELAY); } @@ -536,80 +532,134 @@ public interface UserManagerState { return null; } return isCrossProfileStrategyDelegatedToParent(userHandle) - ? mUserManager.getProfileParent(userHandle) : userHandle; + ? mUserManager.getProfileParent(userHandle) + : userHandle; } - /** - * {@link #setBlockedByAdminValue(Intent)} Based on assumption that the only profiles with - * {@link UserProperties.CROSS_PROFILE_CONTENT_SHARING_NO_DELEGATION} could be systemUser - * and managedUser(if available). + * Updates Cross Profile access for all UserProfiles in {@code mUserProfileIds} + * + * <p>This method looks at a variety of situations for each Profile and decides if the + * profile's content is accessible by the current process owner user id. + * + * <p>- UserProperties attributes for CrossProfileDelegation are checked first - + * CrossProfileIntentForwardingActivitys are resolved via the process owner's + * PackageManager, and are considered when evaluating cross profile to the target profile. * - * Todo(b/319567023):Refactor the below {@link #setBlockedByAdminValue(Intent)} to - * avoid assumptions mentioned above. + * <p>- In the event none of the above checks succeeds, the profile is considered to be + * inaccessible to the current process user, and is thus marked as "BlockedByAdmin". + * + * @param intent The intent Photopicker is currently running under, for + * CrossProfileForwardActivity checking. */ + @SuppressLint("DiscouragedPrivateApi") private void setBlockedByAdminValue(Intent intent) { if (intent == null) { - Log.e(TAG, "No intent specified to check if cross profile forwarding is" - + " allowed."); + Log.e( + TAG, + "No intent specified to check if cross profile forwarding is" + + " allowed."); return; } - // List of all user profile ids that context user cannot access - List<UserId> canNotForwardToUserProfiles = new ArrayList<>(); + Map<UserId, Boolean> profileIsAccessibleToProcessOwner = new HashMap<>(); + List<UserId> delegatedFromParent = new ArrayList<>(); - /* - * List of all user profile ids that have cross profile access among themselves. - * It contains parent user and child profiles with user property - * {@link UserProperties.CROSS_PROFILE_CONTENT_SHARING_DELEGATE_FROM_PARENT} - */ - List<UserId> parentOrDelegatedFromParent = new ArrayList<>(); + final PackageManager pm = mContext.getPackageManager(); - // Userprofile to check cross profile intentForwarderActivity for - UserHandle needToCheck = null; + // Resolve CrossProfile activities for all user profiles that Photopicker is + // aware of. + for (UserId userId : mUserProfileIds) { - if (mUserManager == null) { - Log.e(TAG, "Cannot obtain user manager"); - return; - } + // If the UserId is the system user, exit early. + if (userId.getIdentifier() == mCurrentUser.getIdentifier()) { + profileIsAccessibleToProcessOwner.put(userId, true); + continue; + } - for (UserId userId : mUserProfileIds) { - /* - * All user profiles with user property - * {@link UserProperties.CROSS_PROFILE_CONTENT_SHARING_DELEGATE_FROM_PARENT} - * can access each other including its parent. - */ - if (userId.equals(getSystemUser()) - || isCrossProfileStrategyDelegatedToParent(userId.getUserHandle())) { - parentOrDelegatedFromParent.add(userId); - } else { - needToCheck = userId.getUserHandle(); + // This UserId delegates its strategy to the parent profile + if (isCrossProfileStrategyDelegatedToParent(userId.getUserHandle())) { + delegatedFromParent.add(userId); + continue; } - } - // When context user is a managed user , then will replace needToCheck with its parent - // to check cross profile intentForwarderActivity for. - if (needToCheck != null && needToCheck.equals(mCurrentUser.getUserHandle())) { - needToCheck = mUserManager.getProfileParent(mCurrentUser.getUserHandle()); + // Clear out the component & package before attempting to match + Intent intentToCheck = (Intent) intent.clone(); + intentToCheck.setComponent(null); + intentToCheck.setPackage(null); + + for (ResolveInfo resolveInfo : + pm.queryIntentActivities( + intentToCheck, PackageManager.MATCH_DEFAULT_ONLY)) { + + // If the activity is a CrossProfileIntentForwardingActivity, inspect its + // targetUserId to + // see if it targets the user we are currently checking for. + if (resolveInfo.isCrossProfileIntentForwarderActivity()) { + + /* + * IMPORTANT: This is a reflection based hack to ensure the profile is + * actually the installer of the CrossProfileIntentForwardingActivity. + * + * ResolveInfo.targetUserId exists, but is a hidden API not available to + * mainline modules, and no such API exists, so it is accessed via + * reflection below. All exceptions are caught to protect against + * reflection related issues such as: + * NoSuchFieldException / IllegalAccessException / SecurityException. + * + * In the event of an exception, the code fails "closed" for the current + * profile to avoid showing content that should not be visible. + */ + try { + Field targetUserIdField = + resolveInfo.getClass().getDeclaredField("targetUserId"); + targetUserIdField.setAccessible(true); + int targetUserId = (int) targetUserIdField.get(resolveInfo); + + if (targetUserId == userId.getIdentifier()) { + profileIsAccessibleToProcessOwner.put(userId, true); + + // Don't need to look further, exit the loop. + break; + } + + } catch (NoSuchFieldException + | IllegalAccessException + | SecurityException ex) { + // Couldn't check the targetUserId via reflection, so fail without + // further + // iterations. + Log.e(TAG, "Could not access targetUserId via reflection.", ex); + break; + } catch (Exception ex) { + Log.e(TAG, "Exception occurred during cross profile checks", ex); + } + } + } + // Fail case, was unable to find a suitable Activity for this user. + profileIsAccessibleToProcessOwner.putIfAbsent(userId, false); } - final PackageManager packageManager = mContext.getPackageManager(); - if (needToCheck != null && !CrossProfileUtils.isIntentAllowedCrossProfileAccessFromUser( - intent, packageManager, - getProfileToCheckCrossProfileAccess(mCurrentUser.getUserHandle()))) { - if (parentOrDelegatedFromParent.contains(UserId.of(needToCheck))) { - // if user profile cannot access its parent then all direct child profiles with - // delegated from parent will also be inaccessible. - canNotForwardToUserProfiles.addAll(parentOrDelegatedFromParent); - } else { - canNotForwardToUserProfiles.add(UserId.of(needToCheck)); - } + // For profiles that delegate their access to the parent, set the access for + // those profiles equal to the same as their parent. + for (UserId userId : delegatedFromParent) { + UserHandle parent = + mUserManager.getProfileParent(UserHandle.of(userId.getIdentifier())); + profileIsAccessibleToProcessOwner.put( + userId, + profileIsAccessibleToProcessOwner.getOrDefault( + UserId.of(parent), /* default= */ false)); } mIsProfileBlockedByAdminMap.clear(); for (UserId userId : mUserProfileIds) { - mIsProfileBlockedByAdminMap.put(userId, - canNotForwardToUserProfiles.contains(userId)); + mIsProfileBlockedByAdminMap.put( + // Boolean inversion seems strange, but this map is the opposite of what was + // calculated, (which are blocked, rather than which are accessible) so the + // boolean needs to be inverted. + userId, + !profileIsAccessibleToProcessOwner.getOrDefault( + userId, /* default= */ false)); } } @@ -630,6 +680,7 @@ public interface UserManagerState { return profileLabels; } + private String getProfileLabel(UserHandle userHandle) { if (SdkLevel.isAtLeastV()) { Context userContext = mContext.createContextAsUser(userHandle, 0 /* flags */); @@ -641,7 +692,7 @@ public interface UserManagerState { } return userManager.getProfileLabel(); } catch (Resources.NotFoundException e) { - //Todo(b/318530691): Handle the label for the profile that is not defined + // Todo(b/318530691): Handle the label for the profile that is not defined // already } } @@ -675,7 +726,9 @@ public interface UserManagerState { } return userManager.getUserBadge(); } catch (Resources.NotFoundException e) { - //Todo(b/318530691): Handle the icon for the profile that is not defined already + // Todo(b/318530691): Handle the icon for the profile that is not defined + // already + Log.i(TAG, "failed to find resource"); } } return null; @@ -707,6 +760,7 @@ public interface UserManagerState { assertMainThread(); return mIsProfileBlockedByAdminMap.get(userId); } + @Override public boolean isProfileOff(UserId userId) { assertMainThread(); @@ -716,10 +770,15 @@ public interface UserManagerState { private void assertMainThread() { if (Looper.getMainLooper().isCurrentThread()) return; - throw new IllegalStateException("UserManagerState methods are expected to be called" - + "from main thread. " + (Looper.myLooper() == null ? "" : "Current thread " - + Looper.myLooper().getThread() + ", Main thread " - + Looper.getMainLooper().getThread())); + throw new IllegalStateException( + "UserManagerState methods are expected to be called" + + "from main thread. " + + (Looper.myLooper() == null + ? "" + : "Current thread " + + Looper.myLooper().getThread() + + ", Main thread " + + Looper.getMainLooper().getThread())); } } } |