Additional SecurityPolicy functionality

* Begin wiring into system DevicePolicyManager requirements
* Semi-real implementations of isSupported() & isActive()
* Added new API (placeholder) updatePolicies()
* Updated existing unit tests as needed

Bug: 2387961
This commit is contained in:
Andrew Stadler 2010-02-01 16:48:16 -08:00
parent ecb1af8041
commit d62860821c
5 changed files with 191 additions and 17 deletions

View File

@ -193,6 +193,20 @@
</intent-filter>
</receiver>
<!-- Support for DeviceAdmin / DevicePolicyManager. See SecurityPolicy class for impl. -->
<receiver
android:name=".SecurityPolicy$PolicyAdmin"
android:label="@string/device_admin_label"
android:description="@string/device_admin_description"
android:permission="android.permission.BIND_DEVICE_ADMIN" >
<meta-data
android:name="android.app.device_admin"
android:resource="@xml/device_admin" />
<intent-filter>
<action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
</intent-filter>
</receiver>
<service
android:name=".service.MailService"
android:enabled="false"

View File

@ -577,4 +577,11 @@
<!-- Message that appears if the AccountManager cannot create the system Account -->
<string name="system_account_create_failed">The AccountManager could not create the Account; please try again.</string>
<!-- Strings that support the DeviceAdmin / DevicePolicyManager API -->
<!-- Name of the DeviceAdmin (seen in settings - anywhere else?) -->
<string name="device_admin_label">Email Device Administrator</string>
<!-- Long-form description of the DeviceAdmin (seen in settings - anywhere else?) -->
<string name="device_admin_description">Email Device Administrator - Long Description</string>
</resources>

24
res/xml/device_admin.xml Normal file
View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2010 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.
-->
<device-admin xmlns:android="http://schemas.android.com/apk/res/android">
<uses-policies>
<limit-password />
<watch-login />
<limit-unlock />
<wipe-data />
</uses-policies>
</device-admin>

View File

@ -19,6 +19,8 @@ package com.android.email;
import com.android.email.provider.EmailContent.Account;
import android.app.DeviceAdmin;
import android.app.DevicePolicyManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
@ -28,8 +30,18 @@ import android.database.Cursor;
*/
public class SecurityPolicy {
/** STOPSHIP - ok to check in true for now, but must be false for shipping */
/** DO NOT CHECK IN WHILE 'true' */
private static final boolean DEBUG_ALWAYS_ACTIVE = true;
private static SecurityPolicy sInstance = null;
private Context mContext;
private DevicePolicyManager mDPM;
private ComponentName mAdminName;
private PolicySet mAggregatePolicy;
private static final PolicySet NO_POLICY_SET =
new PolicySet(0, PolicySet.PASSWORD_MODE_NONE, 0, 0, false);
/**
* This projection on Account is for scanning/reading
@ -42,6 +54,15 @@ public class SecurityPolicy {
private static final String WHERE_ACCOUNT_SECURITY_NONZERO =
Account.SECURITY_FLAGS + " IS NOT NULL AND " + Account.SECURITY_FLAGS + "!=0";
/**
* These are hardcoded limits based on knowledge of the current DevicePolicyManager
* and screen lock mechanisms. Wherever possible, these should be replaced with queries of
* dynamic capabilities of the device (e.g. what password modes are supported?)
*/
private static final int LIMIT_MIN_PASSWORD_LENGTH = 16;
private static final int LIMIT_PASSWORD_MODE = PolicySet.PASSWORD_MODE_STRONG;
private static final int LIMIT_SCREENLOCK_TIME = PolicySet.SCREEN_LOCK_TIME_MAX;
/**
* Get the security policy instance
*/
@ -57,6 +78,9 @@ public class SecurityPolicy {
*/
private SecurityPolicy(Context context) {
mContext = context;
mDPM = null;
mAdminName = new ComponentName(context, PolicyAdmin.class);
mAggregatePolicy = null;
}
/**
@ -76,7 +100,8 @@ public class SecurityPolicy {
* max screen lock time take the min
* require remote wipe take the max (logical or)
*
* @return a policy representing the strongest aggregate, or null if none are defined
* @return a policy representing the strongest aggregate. If no policy sets are defined,
* a lightweight "nothing required" policy will be returned. Never null.
*/
/* package */ PolicySet computeAggregatePolicy() {
boolean policiesFound = false;
@ -117,39 +142,107 @@ public class SecurityPolicy {
return new PolicySet(minPasswordLength, passwordMode, maxPasswordFails,
maxScreenLockTime, requireRemoteWipe);
} else {
return null;
return NO_POLICY_SET;
}
}
/**
* Query used to determine if a given policy is "possible" (irrespective of current
* Get the dpm. This mainly allows us to make some utility calls without it, for testing.
*/
private synchronized DevicePolicyManager getDPM() {
if (mDPM == null) {
mDPM = (DevicePolicyManager) mContext.getSystemService(Context.DEVICE_POLICY_SERVICE);
}
return mDPM;
}
/**
* API: Query used to determine if a given policy is "possible" (irrespective of current
* device state. This is used when creating new accounts.
*
* TO BE IMPLEMENTED
* TODO: This is hardcoded based on knowledge of the current DevicePolicyManager
* and screen lock mechanisms. It would be nice to replace these tests with something
* more dynamic.
*
* @param policies the policies requested
* @return true if the policies are supported, false if not supported
*/
public boolean isSupported(PolicySet policies) {
if (policies.mMinPasswordLength > LIMIT_MIN_PASSWORD_LENGTH) {
return false;
}
if (policies.mPasswordMode > LIMIT_PASSWORD_MODE ) {
return false;
}
// No limit on password fail count
if (policies.mMaxScreenLockTime > LIMIT_SCREENLOCK_TIME ) {
return false;
}
// No limit on remote wipe capable
return true;
}
/**
* Query used to determine if a given policy is "active" (the device is operating at
* the required security level). This is used when creating new accounts.
*
* TO BE IMPLEMENTED
* API: Report that policies may have been updated due to rewriting values in an Account.
*/
public synchronized void updatePolicies() {
mAggregatePolicy = null;
}
/**
* API: Query used to determine if a given policy is "active" (the device is operating at
* the required security level). This is used when creating new accounts. This method
* is for queries only, and does not trigger any change in device state.
*
* @param policies the policies requested
* @return true if the policies are active, false if not active
*/
public boolean isActive(PolicySet policies) {
return true;
DevicePolicyManager dpm = getDPM();
if (dpm.isAdminActive(mAdminName)) {
// check each policy
PolicySet aggregate;
synchronized (this) {
if (mAggregatePolicy == null) {
mAggregatePolicy = computeAggregatePolicy();
}
aggregate = mAggregatePolicy;
}
// quick check for the "empty set" of no policies
if (aggregate == NO_POLICY_SET) {
return true;
}
// check each policy explicitly
if (aggregate.mMinPasswordLength > 0) {
if (dpm.getPasswordMinimumLength(mAdminName) < aggregate.mMinPasswordLength) {
return false;
}
}
if (aggregate.mPasswordMode > 0) {
if (dpm.getPasswordQuality(mAdminName) < aggregate.getDPManagerPasswordMode()) {
return false;
}
if (!dpm.isActivePasswordSufficient()) {
return false;
}
}
if (aggregate.mMaxScreenLockTime > 0) {
// Note, we use seconds, dpm uses milliseconds
if (dpm.getMaximumTimeToLock(mAdminName) > aggregate.mMaxScreenLockTime * 1000) {
return false;
}
}
// password failures are counted locally - no test required here
// no check required for remote wipe (it's supported, if we're the admin)
}
// return false, not active - unless debugging enabled
return DEBUG_ALWAYS_ACTIVE;
}
/**
* Sync service should call this any time a sync fails due to isActive() returning false.
* This will kick off the acquire-admin-state process and/or increase the security level.
* This will kick off the notify-acquire-admin-state process and/or increase the security level.
* The caller needs to write the required policies into this account before making this call.
*
* @param accountId the account for which sync cannot proceed
@ -196,7 +289,7 @@ public class SecurityPolicy {
* @param minPasswordLength (0=not enforced)
* @param passwordMode
* @param maxPasswordFails (0=not enforced)
* @param maxScreenLockTime (0=not enforced)
* @param maxScreenLockTime in seconds (0=not enforced)
* @param requireRemoteWipe
*/
public PolicySet(int minPasswordLength, int passwordMode, int maxPasswordFails,
@ -245,6 +338,20 @@ public class SecurityPolicy {
mRequireRemoteWipe = 0 != (flags & REQUIRE_REMOTE_WIPE);
}
/**
* Helper to map DevicePolicyManager password modes to our internal encoding.
*/
public int getDPManagerPasswordMode() {
switch (mPasswordMode) {
case PASSWORD_MODE_SIMPLE:
return DevicePolicyManager.PASSWORD_QUALITY_NUMERIC;
case PASSWORD_MODE_STRONG:
return DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC;
default:
return DevicePolicyManager .PASSWORD_QUALITY_UNSPECIFIED;
}
}
/**
* Record flags (and a sync key for the flags) into an Account
* Note: the hash code is defined as the encoding used in Account

View File

@ -25,6 +25,7 @@ import com.android.email.provider.EmailContent.AccountColumns;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.ContextWrapper;
import android.net.Uri;
import android.test.ProviderTestCase2;
import android.test.suitebuilder.annotation.MediumTest;
@ -38,6 +39,9 @@ public class SecurityPolicyTests extends ProviderTestCase2<EmailProvider> {
private Context mMockContext;
private static final PolicySet EMPTY_POLICY_SET =
new PolicySet(0, PolicySet.PASSWORD_MODE_NONE, 0, 0, false);
public SecurityPolicyTests() {
super(EmailProvider.class, EmailProvider.EMAIL_AUTHORITY);
}
@ -46,7 +50,7 @@ public class SecurityPolicyTests extends ProviderTestCase2<EmailProvider> {
protected void setUp() throws Exception {
super.setUp();
mMockContext = getMockContext();
mMockContext = new MockContext2(getMockContext(), this.mContext);
}
/**
@ -57,6 +61,24 @@ public class SecurityPolicyTests extends ProviderTestCase2<EmailProvider> {
super.tearDown();
}
/**
* Private context wrapper used to add back getPackageName() for these tests
*/
private static class MockContext2 extends ContextWrapper {
private final Context mRealContext;
public MockContext2(Context mockContext, Context realContext) {
super(mockContext);
mRealContext = realContext;
}
@Override
public String getPackageName() {
return mRealContext.getPackageName();
}
}
/**
* Retrieve the security policy object, and inject the mock context so it works as expected
*/
@ -72,17 +94,17 @@ public class SecurityPolicyTests extends ProviderTestCase2<EmailProvider> {
public void testAggregator() {
SecurityPolicy sp = getSecurityPolicy();
// with no accounts, should return null
assertNull(sp.computeAggregatePolicy());
// with no accounts, should return empty set
assertTrue(EMPTY_POLICY_SET.equals(sp.computeAggregatePolicy()));
// with accounts having no security, return null
// with accounts having no security, empty set
Account a1 = ProviderTestUtils.setupAccount("no-sec-1", false, mMockContext);
a1.mSecurityFlags = 0;
a1.save(mMockContext);
Account a2 = ProviderTestUtils.setupAccount("no-sec-2", false, mMockContext);
a2.mSecurityFlags = 0;
a2.save(mMockContext);
assertNull(sp.computeAggregatePolicy());
assertTrue(EMPTY_POLICY_SET.equals(sp.computeAggregatePolicy()));
// with a single account in security mode, should return same security as in account
PolicySet p3in = new PolicySet(10, PolicySet.PASSWORD_MODE_SIMPLE, 15, 16, false);
@ -142,7 +164,7 @@ public class SecurityPolicyTests extends ProviderTestCase2<EmailProvider> {
Account a2 = ProviderTestUtils.setupAccount("no-sec-2", false, mMockContext);
a2.mSecurityFlags = 0;
a2.save(mMockContext);
assertNull(sp.computeAggregatePolicy());
assertTrue(EMPTY_POLICY_SET.equals(sp.computeAggregatePolicy()));
}
/**