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

Commit 094c98e2 authored by Eran Messeri's avatar Eran Messeri
Browse files

Do not apply password quality to the parent

Prevent password quality set by the admin from applying on the primary
user, if targeting Android S and above:
* If the DPC calls setPasswordQuality on the parent profile DPM
  instance, throw.
* If the DPC calls setPasswordQuality on the regular DPM instance,
  simply ignore it on the primary profile.

This is guarded by the ChangeCompat framework.

Note about testing: As it proved difficult to mock/fake the
compatibility testing framework, the check is done via the injector,
which can be easily overridden.

Bug: 165573442
Test: atest com.android.cts.devicepolicy.MixedManagedProfileOwnerTest#testResetPasswordWithToken com.android.cts.devicepolicy.MixedManagedProfileOwnerTest#testGetPasswordExpiration FrameworksServicesTests:DevicePolicyManagerTest ManagedProfileTest
Test: atest FrameworksServicesTests:DevicePolicyManagerTest
Change-Id: I59354066dafb02fe1d464fd1ddabf3969a4d0f1b
parent d8e23012
Loading
Loading
Loading
Loading
+12 −0
Original line number Diff line number Diff line
@@ -98,6 +98,8 @@ class ActiveAdmin {
    private static final String TAG_PASSWORD_HISTORY_LENGTH = "password-history-length";
    private static final String TAG_MIN_PASSWORD_LENGTH = "min-password-length";
    private static final String TAG_PASSWORD_QUALITY = "password-quality";
    private static final String TAG_PASSWORD_QUALITY_APPLIES_TO_PARENT =
            "password-quality-applies-parent";
    private static final String TAG_POLICIES = "policies";
    private static final String TAG_CROSS_PROFILE_WIDGET_PROVIDERS =
            "cross-profile-widget-providers";
@@ -143,6 +145,7 @@ class ActiveAdmin {

    @NonNull
    PasswordPolicy mPasswordPolicy = new PasswordPolicy();
    boolean mPasswordPolicyAppliesToParent = true;

    @DevicePolicyManager.PasswordComplexity
    int mPasswordComplexity = PASSWORD_COMPLEXITY_NONE;
@@ -333,6 +336,9 @@ class ActiveAdmin {
                writeAttributeValueToXml(
                        out, TAG_MIN_PASSWORD_NONLETTER, mPasswordPolicy.nonLetter);
            }

            writeAttributeValueToXml(out, TAG_PASSWORD_QUALITY_APPLIES_TO_PARENT,
                    mPasswordPolicyAppliesToParent);
        }
        if (passwordHistoryLength != DEF_PASSWORD_HISTORY_LENGTH) {
            writeAttributeValueToXml(
@@ -626,6 +632,9 @@ class ActiveAdmin {
            } else if (TAG_MIN_PASSWORD_NONLETTER.equals(tag)) {
                mPasswordPolicy.nonLetter = Integer.parseInt(
                        parser.getAttributeValue(null, ATTR_VALUE));
            } else if (TAG_PASSWORD_QUALITY_APPLIES_TO_PARENT.equals(tag)) {
                mPasswordPolicyAppliesToParent = Boolean.parseBoolean(
                        parser.getAttributeValue(null, ATTR_VALUE));
            } else if (TAG_MAX_TIME_TO_UNLOCK.equals(tag)) {
                maximumTimeToUnlock = Long.parseLong(
                        parser.getAttributeValue(null, ATTR_VALUE));
@@ -995,6 +1004,9 @@ class ActiveAdmin {
        pw.print("minimumPasswordNonLetter=");
        pw.println(mPasswordPolicy.nonLetter);

        pw.print("passwordPolicyAppliesToParent=");
        pw.println(mPasswordPolicyAppliesToParent);

        pw.print("maximumTimeToUnlock=");
        pw.println(maximumTimeToUnlock);

+55 −5
Original line number Diff line number Diff line
@@ -155,10 +155,12 @@ import android.app.admin.SystemUpdateInfo;
import android.app.admin.SystemUpdatePolicy;
import android.app.admin.UnsafeStateException;
import android.app.backup.IBackupManager;
import android.app.compat.CompatChanges;
import android.app.trust.TrustManager;
import android.app.usage.UsageStatsManagerInternal;
import android.compat.annotation.ChangeId;
import android.compat.annotation.EnabledAfter;
import android.compat.annotation.EnabledSince;
import android.content.ActivityNotFoundException;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
@@ -527,6 +529,19 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
    @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.Q)
    private static final long USE_SET_LOCATION_ENABLED = 117835097L;
    /**
     * Admin apps targeting Android S+ may not use
     * {@link android.app.admin.DevicePolicyManager#setPasswordQuality} to set password quality
     * on the {@code DevicePolicyManager} instance obtained by calling
     * {@link android.app.admin.DevicePolicyManager#getParentProfileInstance}.
     * Instead, they should use
     * {@link android.app.admin.DevicePolicyManager#setRequiredPasswordComplexity} to set
     * coarse-grained password requirements device-wide.
     */
    @ChangeId
    @EnabledSince(targetSdkVersion = Build.VERSION_CODES.S)
    private static final long PREVENT_SETTING_PASSWORD_QUALITY_ON_PARENT = 165573442L;
    final Context mContext;
    final Injector mInjector;
    final IPackageManager mIPackageManager;
@@ -1396,6 +1411,10 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
        public long systemCurrentTimeMillis() {
            return System.currentTimeMillis();
        }
        public boolean isChangeEnabled(long changeId, String packageName, int userId) {
            return CompatChanges.isChangeEnabled(changeId, packageName, UserHandle.of(userId));
        }
    }
    /**
@@ -3417,6 +3436,11 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
                getTargetSdk(profileOwner.getPackageName(), userHandle) > Build.VERSION_CODES.M;
    }
    private boolean canSetPasswordQualityOnParent(String packageName, int userId) {
        return !mInjector.isChangeEnabled(
                PREVENT_SETTING_PASSWORD_QUALITY_ON_PARENT, packageName, userId);
    }
    @Override
    public void setPasswordQuality(ComponentName who, int quality, boolean parent) {
        if (!mHasFeature) {
@@ -3425,7 +3449,18 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
        Objects.requireNonNull(who, "ComponentName is null");
        validateQualityConstant(quality);
        final int userId = mInjector.userHandleGetCallingUserId();
        final CallerIdentity caller = getCallerIdentity(who);
        Preconditions.checkCallAuthorization(
                isProfileOwner(caller) || isDeviceOwner(caller) || isSystemUid(caller));
        final boolean qualityMayApplyToParent =
                canSetPasswordQualityOnParent(who.getPackageName(), caller.getUserId());
        if (!qualityMayApplyToParent) {
            Preconditions.checkArgument(!parent,
                    "Profile Owner may not apply password quality requirements device-wide");
        }
        final int userId = caller.getUserId();
        synchronized (getLockObject()) {
            ActiveAdmin ap = getActiveAdminForCallerLocked(
                    who, DeviceAdminInfo.USES_POLICY_LIMIT_PASSWORD, parent);
@@ -3434,6 +3469,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
                if (passwordPolicy.quality != quality) {
                    passwordPolicy.quality = quality;
                    ap.mPasswordComplexity = PASSWORD_COMPLEXITY_NONE;
                    ap.mPasswordPolicyAppliesToParent = qualityMayApplyToParent;
                    resetInactivePasswordRequirementsIfRPlus(userId, ap);
                    updatePasswordValidityCheckpointLocked(userId, parent);
                    updatePasswordQualityCacheForUserGroup(userId);
@@ -4165,9 +4201,18 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
        synchronized (getLockObject()) {
            List<ActiveAdmin> admins = getActiveAdminsForLockscreenPoliciesLocked(userId);
            for (ActiveAdmin admin : admins) {
                final boolean isAdminOfUser = userId == admin.getUserHandle().getIdentifier();
                // Use the password metrics from the admin in one of three cases:
                // (1) The admin is of the user we're getting the minimum metrics for. The admin
                //     always affects the user it's managing. This applies also to the parent
                //     ActiveAdmin instance: It'd have the same user handle.
                // (2) The mPasswordPolicyAppliesToParent field is true: That indicates the
                //     call to setPasswordQuality was made by an admin that may affect the parent.
                if (isAdminOfUser || admin.mPasswordPolicyAppliesToParent) {
                    adminMetrics.add(admin.mPasswordPolicy.getMinMetrics());
                }
            }
        }
        return PasswordMetrics.merge(adminMetrics);
    }
@@ -4257,13 +4302,17 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
                    /* shouldIncludeProfileAdmins */ (user) -> user.id == profileUser
                    || !mLockPatternUtils.isSeparateProfileChallengeEnabled(user.id));
            ArrayList<PasswordMetrics> adminMetrics = new ArrayList<>(admins.size());
            int maxRequiredComplexity = PASSWORD_COMPLEXITY_NONE;
            for (ActiveAdmin admin : admins) {
                adminMetrics.add(admin.mPasswordPolicy.getMinMetrics());
                if (isDeviceOwner(admin) || isProfileOwnerUncheckedLocked(admin.info.getComponent(),
                        admin.getUserHandle().getIdentifier())) {
                    maxRequiredComplexity = Math.max(maxRequiredComplexity,
                            admin.mPasswordComplexity);
                }
            }
            //TODO: Take complexity into account, would need to take complexity from all admins
            //in the admins list.
            return PasswordMetrics.validatePasswordMetrics(PasswordMetrics.merge(adminMetrics),
                    PASSWORD_COMPLEXITY_NONE, false, metrics).isEmpty();
                    maxRequiredComplexity, false, metrics).isEmpty();
        }
    }
@@ -4354,6 +4403,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
                    admin.mPasswordComplexity = passwordComplexity;
                    // Reset the password policy.
                    admin.mPasswordPolicy = new PasswordPolicy();
                    admin.mPasswordPolicyAppliesToParent = true;
                    updatePasswordValidityCheckpointLocked(caller.getUserId(), calledOnParent);
                    updatePasswordQualityCacheForUserGroup(caller.getUserId());
                    saveSettingsLocked(caller.getUserId());
+30 −0
Original line number Diff line number Diff line
@@ -127,6 +127,8 @@ public class DevicePolicyManagerServiceTestable extends DevicePolicyManagerServi
        // Used as an override when set to nonzero.
        private long mCurrentTimeMillis = 0;

        private final Map<Long, Pair<String, Integer>> mEnabledChanges = new ArrayMap<>();

        public MockInjector(MockSystemServices services, DpmMockContext context) {
            super(context);
            this.services = services;
@@ -487,5 +489,33 @@ public class DevicePolicyManagerServiceTestable extends DevicePolicyManagerServi
        public long systemCurrentTimeMillis() {
            return mCurrentTimeMillis != 0 ? mCurrentTimeMillis : System.currentTimeMillis();
        }

        public void setChangeEnabledForPackage(
                long changeId, boolean enabled, String packageName, int userId) {
            if (enabled) {
                mEnabledChanges.put(changeId, Pair.create(packageName, userId));
            } else {
                mEnabledChanges.remove(changeId);
            }
        }

        public void clearEnabledChanges() {
            mEnabledChanges.clear();
        }

        @Override
        public boolean isChangeEnabled(long changeId, String packageName, int userId) {
            Pair<String, Integer> packageAndUser = mEnabledChanges.get(changeId);
            if (packageAndUser == null) {
                return false;
            }

            if (!packageAndUser.first.equals(packageName)
                    || !packageAndUser.second.equals(userId)) {
                return false;
            }

            return true;
        }
    }
}
+79 −5
Original line number Diff line number Diff line
@@ -5090,11 +5090,11 @@ public class DevicePolicyManagerTest extends DpmTestBase {
        doReturn(true).when(getServices().lockPatternUtils)
                .isSeparateProfileChallengeEnabled(managedProfileUserId);

        dpm.setPasswordQuality(admin1, DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC);
        parentDpm.setPasswordQuality(admin1, DevicePolicyManager.PASSWORD_QUALITY_NUMERIC);
        dpm.setRequiredPasswordComplexity(PASSWORD_COMPLEXITY_HIGH);
        parentDpm.setRequiredPasswordComplexity(PASSWORD_COMPLEXITY_MEDIUM);

        when(getServices().lockSettingsInternal.getUserPasswordMetrics(UserHandle.USER_SYSTEM))
                .thenReturn(computeForPassword("1234".getBytes()));
                .thenReturn(computeForPassword("184342".getBytes()));

        // Numeric password is compliant with current requirement (QUALITY_NUMERIC set explicitly
        // on the parent admin)
@@ -5107,6 +5107,68 @@ public class DevicePolicyManagerTest extends DpmTestBase {
        managedProfileUserId)).isFalse();
    }

    @Test
    public void testCanSetPasswordRequirementOnParentPreS() throws Exception {
        final int managedProfileUserId = CALLER_USER_HANDLE;
        final int managedProfileAdminUid =
                UserHandle.getUid(managedProfileUserId, DpmMockContext.SYSTEM_UID);
        mContext.binder.callingUid = managedProfileAdminUid;
        addManagedProfile(admin1, managedProfileAdminUid, admin1, VERSION_CODES.R);
        dpms.mMockInjector.setChangeEnabledForPackage(165573442L, false,
                admin1.getPackageName(), managedProfileUserId);

        parentDpm.setPasswordQuality(admin1, DevicePolicyManager.PASSWORD_QUALITY_COMPLEX);
        assertThat(parentDpm.getPasswordQuality(admin1))
                .isEqualTo(DevicePolicyManager.PASSWORD_QUALITY_COMPLEX);
    }

    @Test
    public void testCannotSetPasswordRequirementOnParent() throws Exception {
        final int managedProfileUserId = CALLER_USER_HANDLE;
        final int managedProfileAdminUid =
                UserHandle.getUid(managedProfileUserId, DpmMockContext.SYSTEM_UID);
        mContext.binder.callingUid = managedProfileAdminUid;
        addManagedProfile(admin1, managedProfileAdminUid, admin1);
        dpms.mMockInjector.setChangeEnabledForPackage(165573442L, true,
                admin1.getPackageName(), managedProfileUserId);

        try {
            assertExpectException(IllegalArgumentException.class, null, () ->
                    parentDpm.setPasswordQuality(
                            admin1, DevicePolicyManager.PASSWORD_QUALITY_COMPLEX));
        } finally {
            dpms.mMockInjector.clearEnabledChanges();
        }
    }

    @Test
    public void testPasswordQualityAppliesToParentPreS() throws Exception {
        final int managedProfileUserId = CALLER_USER_HANDLE;
        final int managedProfileAdminUid =
                UserHandle.getUid(managedProfileUserId, DpmMockContext.SYSTEM_UID);
        mContext.binder.callingUid = managedProfileAdminUid;
        addManagedProfile(admin1, managedProfileAdminUid, admin1, VERSION_CODES.R);
        when(getServices().userManager.getProfileParent(CALLER_USER_HANDLE))
                .thenReturn(new UserInfo(UserHandle.USER_SYSTEM, "user system", 0));

        dpm.setPasswordQuality(admin1, DevicePolicyManager.PASSWORD_QUALITY_COMPLEX);
        assertThat(parentDpm.getPasswordQuality(null))
                .isEqualTo(DevicePolicyManager.PASSWORD_QUALITY_COMPLEX);
    }

    @Test
    public void testPasswordQualityDoesNotApplyToParentPostS() throws Exception {
        final int managedProfileUserId = CALLER_USER_HANDLE;
        final int managedProfileAdminUid =
                UserHandle.getUid(managedProfileUserId, DpmMockContext.SYSTEM_UID);
        mContext.binder.callingUid = managedProfileAdminUid;
        addManagedProfile(admin1, managedProfileAdminUid, admin1, VERSION_CODES.R);

        dpm.setPasswordQuality(admin1, DevicePolicyManager.PASSWORD_QUALITY_COMPLEX);
        assertThat(parentDpm.getPasswordQuality(admin1))
                .isEqualTo(DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED);
    }

    private void setActivePasswordState(PasswordMetrics passwordMetrics)
            throws Exception {
        final int userHandle = UserHandle.getUserId(mContext.binder.callingUid);
@@ -7014,19 +7076,31 @@ public class DevicePolicyManagerTest extends DpmTestBase {
     * @param adminUid uid of the admin package.
     * @param copyFromAdmin package information for {@code admin} will be built based on this
     *     component's information.
     * @param appTargetSdk admin's target SDK level
     */
    private void addManagedProfile(
            ComponentName admin, int adminUid, ComponentName copyFromAdmin) throws Exception {
            ComponentName admin, int adminUid, ComponentName copyFromAdmin, int appTargetSdk)
            throws Exception {
        final int userId = UserHandle.getUserId(adminUid);
        getServices().addUser(userId, 0, UserManager.USER_TYPE_PROFILE_MANAGED,
                UserHandle.USER_SYSTEM);
        mContext.callerPermissions.addAll(OWNER_SETUP_PERMISSIONS);
        setUpPackageManagerForFakeAdmin(admin, adminUid, copyFromAdmin);
        setUpPackageManagerForFakeAdmin(admin, adminUid, /* enabledSetting= */ null,
                appTargetSdk, copyFromAdmin);
        dpm.setActiveAdmin(admin, false, userId);
        assertThat(dpm.setProfileOwner(admin, null, userId)).isTrue();
        mContext.callerPermissions.removeAll(OWNER_SETUP_PERMISSIONS);
    }

    /**
     * Same as {@code addManagedProfile} above, except using development API level as the API
     * level of the admin.
     */
    private void addManagedProfile(
            ComponentName admin, int adminUid, ComponentName copyFromAdmin) throws Exception {
        addManagedProfile(admin, adminUid, copyFromAdmin, VERSION_CODES.CUR_DEVELOPMENT);
    }

    /**
     * Convert String[] to StringParceledListSlice.
     */