Add password expiration plumbing

* Set aggregated expiration values with DPM
* Fix min/max logic when aggregating, and fix unit test
* Add expiration tests when checking if policies are active
* Add expire-password to uses-policies set
* Handle password refresh (clear notifications and sec. holds)
* Handle password expiration (warning and/or wipe synced data)
* Unit tests for provider-level methods
* Refactor common security notification logic
* Placeholder notification strings (need final)

Bug: 3197935
Change-Id: Idf1975edd81dd7f55729156dc6b1002b7d09841f
This commit is contained in:
Andy Stadler 2010-12-01 12:58:36 -08:00
parent d1ee5b8fa5
commit 1ca111c19c
8 changed files with 473 additions and 103 deletions

View File

@ -751,6 +751,27 @@ save attachment.</string>
<!-- "Setup could not finish" dialog action button -->
<string name="account_setup_failed_dlg_edit_details_action">Edit details</string>
<!-- Notification ticker when device password is getting ready to expire [CHAR_LIMIT=80] -->
<string name="password_expire_warning_ticker_fmt">
Account \"<xliff:g id="account">%s</xliff:g>\" requires you to update your screen
unlock code.</string>
<!-- Notification content title when device password is getting ready to expire
[CHAR_LIMIT=28] -->
<string name="password_expire_warning_content_title">New screen unlock required</string>
<!-- Notification content text when device password is getting ready to expire
[CHAR_LIMIT=2 lines] -->
<string name="password_expire_warning_content_text_fmt">
Account \"<xliff:g id="account">%s</xliff:g>\" requires you to update your screen
unlock code. Touch here to update it.</string>
<!-- Notification ticker when device password has expired [CHAR_LIMIT=80] -->
<string name="password_expired_ticker">Your screen unlock code has expired.</string>
<!-- Notification content title when device password has expired [CHAR_LIMIT=28] -->
<string name="password_expired_content_title">New screen unlock required</string>
<!-- Notification content text when device password has expired [CHAR_LIMIT=2 lines] -->
<string name="password_expired_content_text">
Your screen unlock code has expired. Touch here to update it.</string>
<!-- On AccountSettingsXL, dialog text if you try to exit in/out/eas fragment (server settings)
without checking/saving [CHAR LIMIT=none]-->
<string name="account_settings_exit_server_settings">Discard unsaved changes?</string>

View File

@ -21,5 +21,6 @@
<watch-login />
<force-lock />
<wipe-data />
<expire-password />
</uses-policies>
</device-admin>

View File

@ -44,6 +44,8 @@ public class NotificationController {
public static final int NOTIFICATION_ID_SECURITY_NEEDED = 1;
public static final int NOTIFICATION_ID_EXCHANGE_CALENDAR_ADDED = 2;
public static final int NOTIFICATION_ID_ATTACHMENT_WARNING = 3;
public static final int NOTIFICATION_ID_PASSWORD_EXPIRING = 4;
public static final int NOTIFICATION_ID_PASSWORD_EXPIRED = 5;
private static final int NOTIFICATION_ID_BASE_NEW_MESSAGES = 0x10000000;
private static final int NOTIFICATION_ID_BASE_LOGIN_WARNING = 0x20000000;
@ -69,6 +71,60 @@ public class NotificationController {
return sInstance;
}
/**
* Generic notifier for any account. Uses notification rules from account.
*
* @param account The account for which the notification is posted
* @param ticker String for ticker
* @param contentTitle String for notification content title
* @param contentText String for notification content text
* @param intent The intent to launch from the notification
* @param notificationId The notification id
*/
public void postAccountNotification(Account account, String ticker, String contentTitle,
String contentText, Intent intent, int notificationId) {
// Pending Intent
PendingIntent pending =
PendingIntent.getActivity(mContext, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
// Ringtone & Vibration
String ringtoneString = account.getRingtone();
Uri ringTone = (ringtoneString == null) ? null : Uri.parse(ringtoneString);
boolean vibrate = 0 != (account.mFlags & Account.FLAGS_VIBRATE_ALWAYS);
boolean vibrateWhenSilent = 0 != (account.mFlags & Account.FLAGS_VIBRATE_WHEN_SILENT);
// Use the account's notification rules for sound & vibrate (but always notify)
boolean nowSilent =
mAudioManager.getRingerMode() == AudioManager.RINGER_MODE_VIBRATE;
int defaults = Notification.DEFAULT_LIGHTS;
if (vibrate || (vibrateWhenSilent && nowSilent)) {
defaults |= Notification.DEFAULT_VIBRATE;
}
// Notification
Notification.Builder nb = new Notification.Builder(mContext);
nb.setSmallIcon(R.drawable.stat_notify_email_generic);
nb.setTicker(ticker);
nb.setContentTitle(contentTitle);
nb.setContentText(contentText);
nb.setContentIntent(pending);
nb.setSound(ringTone);
nb.setDefaults(defaults);
Notification notification = nb.getNotification();
mNotificationManager.notify(notificationId, notification);
}
/**
* Generic notification canceler.
* @param notificationId The notification id
*/
public void cancelNotification(int notificationId) {
mNotificationManager.cancel(notificationId);
}
/**
* @return the "new message" notification ID for an account. It just assumes
* accountID won't be too huge. Any other smarter/cleaner way?

View File

@ -21,9 +21,6 @@ import com.android.email.provider.EmailContent;
import com.android.email.provider.EmailContent.Account;
import com.android.email.provider.EmailContent.AccountColumns;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.admin.DeviceAdminReceiver;
import android.app.admin.DevicePolicyManager;
import android.content.ComponentName;
@ -32,8 +29,6 @@ import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.media.AudioManager;
import android.net.Uri;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.Log;
@ -59,6 +54,7 @@ public class SecurityPolicy {
private static final String[] ACCOUNT_SECURITY_PROJECTION = new String[] {
AccountColumns.ID, AccountColumns.SECURITY_FLAGS
};
private static final int ACCOUNT_SECURITY_COLUMN_ID = 0;
private static final int ACCOUNT_SECURITY_COLUMN_FLAGS = 1;
/**
@ -98,7 +94,7 @@ public class SecurityPolicy {
* max screen lock time take the min
* require remote wipe take the max (logical or)
* password history take the max (strongest mode)
* password expiration take the max (strongest mode)
* password expiration take the min (strongest mode)
* password complex chars take the max (strongest mode)
*
* @return a policy representing the strongest aggregate. If no policy sets are defined,
@ -113,7 +109,7 @@ public class SecurityPolicy {
int maxScreenLockTime = Integer.MAX_VALUE;
boolean requireRemoteWipe = false;
int passwordHistory = Integer.MIN_VALUE;
int passwordExpiration = Integer.MIN_VALUE;
int passwordExpirationDays = Integer.MAX_VALUE;
int passwordComplexChars = Integer.MIN_VALUE;
Cursor c = mContext.getContentResolver().query(Account.CONTENT_URI,
@ -134,8 +130,9 @@ public class SecurityPolicy {
if (p.mPasswordHistory > 0) {
passwordHistory = Math.max(p.mPasswordHistory, passwordHistory);
}
if (p.mPasswordExpiration > 0) {
passwordExpiration = Math.max(p.mPasswordExpiration, passwordExpiration);
if (p.mPasswordExpirationDays > 0) {
passwordExpirationDays =
Math.min(p.mPasswordExpirationDays, passwordExpirationDays);
}
if (p.mPasswordComplexChars > 0) {
passwordComplexChars = Math.max(p.mPasswordComplexChars,
@ -155,11 +152,11 @@ public class SecurityPolicy {
if (maxPasswordFails == Integer.MAX_VALUE) maxPasswordFails = 0;
if (maxScreenLockTime == Integer.MAX_VALUE) maxScreenLockTime = 0;
if (passwordHistory == Integer.MIN_VALUE) passwordHistory = 0;
if (passwordExpiration == Integer.MIN_VALUE) passwordExpiration = 0;
if (passwordExpirationDays == Integer.MAX_VALUE) passwordExpirationDays = 0;
if (passwordComplexChars == Integer.MIN_VALUE) passwordComplexChars = 0;
return new PolicySet(minPasswordLength, passwordMode, maxPasswordFails,
maxScreenLockTime, requireRemoteWipe, passwordExpiration, passwordHistory,
maxScreenLockTime, requireRemoteWipe, passwordExpirationDays, passwordHistory,
passwordComplexChars);
} else {
return NO_POLICY_SET;
@ -215,6 +212,12 @@ public class SecurityPolicy {
*
* This method is for queries only, and does not trigger any change in device state.
*
* NOTE: If there are multiple accounts with password expiration policies, the device
* password will be set to expire in the shortest required interval (most secure). This method
* will return 'false' as soon as the password expires - irrespective of which account caused
* the expiration. In other words, all accounts (that require expiration) will run/stop
* based on the requirements of the account with the shortest interval.
*
* @param policies the policies requested, or null to check aggregate stored policies
* @return true if the policies are active, false if not active
*/
@ -249,8 +252,20 @@ public class SecurityPolicy {
return false;
}
}
if (policies.mPasswordExpiration > 0) {
// TODO Complete when DPM supports this
if (policies.mPasswordExpirationDays > 0) {
// confirm that expirations are currently set
long currentTimeout = dpm.getPasswordExpirationTimeout(mAdminName);
if (currentTimeout == 0
|| currentTimeout > policies.getDPManagerPasswordExpirationTimeout()) {
return false;
}
// confirm that the current password hasn't expired
long expirationDate = dpm.getPasswordExpiration(mAdminName);
long timeUntilExpiration = expirationDate - System.currentTimeMillis();
boolean expired = timeUntilExpiration < 0;
if (expired) {
return false;
}
}
if (policies.mPasswordHistory > 0) {
if (dpm.getPasswordHistoryLength(mAdminName) < policies.mPasswordHistory) {
@ -293,8 +308,9 @@ public class SecurityPolicy {
dpm.setMaximumTimeToLock(mAdminName, policies.mMaxScreenLockTime * 1000);
// local wipe (failed passwords limit)
dpm.setMaximumFailedPasswordsForWipe(mAdminName, policies.mMaxPasswordFails);
// password expiration (days until a password expires)
// TODO set this when DPM allows it
// password expiration (days until a password expires). API takes mSec.
dpm.setPasswordExpirationTimeout(mAdminName,
policies.getDPManagerPasswordExpirationTimeout());
// password history length (number of previous passwords that may not be reused)
dpm.setPasswordHistoryLength(mAdminName, policies.mPasswordHistory);
// password minimum complex characters
@ -306,8 +322,11 @@ public class SecurityPolicy {
* API: Set/Clear the "hold" flag in any account. This flag serves a dual purpose:
* Setting it gives us an indication that it was blocked, and clearing it gives EAS a
* signal to try syncing again.
* @param context
* @param account The account to update
* @param newState true = security hold, false = free to sync
*/
public void setAccountHoldFlag(Account account, boolean newState) {
public static void setAccountHoldFlag(Context context, Account account, boolean newState) {
if (newState) {
account.mFlags |= Account.FLAGS_SECURITY_HOLD;
} else {
@ -315,7 +334,7 @@ public class SecurityPolicy {
}
ContentValues cv = new ContentValues();
cv.put(AccountColumns.FLAGS, account.mFlags);
account.update(mContext, cv);
account.update(context, cv);
}
/**
@ -328,43 +347,19 @@ public class SecurityPolicy {
*/
public void policiesRequired(long accountId) {
Account account = EmailContent.Account.restoreAccountWithId(mContext, accountId);
// Mark the account as "on hold".
setAccountHoldFlag(account, true);
// Otherwise, put up a notification
setAccountHoldFlag(mContext, account, true);
// Put up a notification
String tickerText = mContext.getString(R.string.security_notification_ticker_fmt,
account.getDisplayName());
String contentTitle = mContext.getString(R.string.security_notification_content_title);
String contentText = account.getDisplayName();
String ringtoneString = account.getRingtone();
Uri ringTone = (ringtoneString == null) ? null : Uri.parse(ringtoneString);
boolean vibrate = 0 != (account.mFlags & Account.FLAGS_VIBRATE_ALWAYS);
boolean vibrateWhenSilent = 0 != (account.mFlags & Account.FLAGS_VIBRATE_WHEN_SILENT);
Intent intent = AccountSecurity.actionUpdateSecurityIntent(mContext, accountId);
PendingIntent pending =
PendingIntent.getActivity(mContext, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
Notification notification = new Notification(R.drawable.stat_notify_email_generic,
tickerText, System.currentTimeMillis());
notification.setLatestEventInfo(mContext, contentTitle, contentText, pending);
// Use the account's notification rules for sound & vibrate (but always notify)
AudioManager audioManager =
(AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
boolean nowSilent =
audioManager.getRingerMode() == AudioManager.RINGER_MODE_VIBRATE;
notification.sound = ringTone;
if (vibrate || (vibrateWhenSilent && nowSilent)) {
notification.defaults |= Notification.DEFAULT_VIBRATE;
}
notification.flags |= Notification.FLAG_SHOW_LIGHTS;
notification.defaults |= Notification.DEFAULT_LIGHTS;
NotificationManager notificationManager =
(NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.notify(NotificationController.NOTIFICATION_ID_SECURITY_NEEDED,
notification);
NotificationController.getInstance(mContext).postAccountNotification(
account, tickerText, contentTitle, contentText, intent,
NotificationController.NOTIFICATION_ID_SECURITY_NEEDED);
}
/**
@ -372,9 +367,8 @@ public class SecurityPolicy {
* cleared now.
*/
public void clearNotification(long accountId) {
NotificationManager notificationManager =
(NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.cancel(NotificationController.NOTIFICATION_ID_SECURITY_NEEDED);
NotificationController.getInstance(mContext).cancelNotification(
NotificationController.NOTIFICATION_ID_SECURITY_NEEDED);
}
/**
@ -429,12 +423,17 @@ public class SecurityPolicy {
private static final long PASSWORD_COMPLEX_CHARS_MASK = 31L << PASSWORD_COMPLEX_CHARS_SHIFT;
public static final int PASSWORD_COMPLEX_CHARS_MAX = 31;
/* Convert days to mSec (used for password expiration) */
private static final long DAYS_TO_MSEC = 24 * 60 * 60 * 1000;
/* Small offset (2 minutes) added to policy expiration to make user testing easier. */
private static final long EXPIRATION_OFFSET_MSEC = 2 * 60 * 1000;
/*package*/ final int mMinPasswordLength;
/*package*/ final int mPasswordMode;
/*package*/ final int mMaxPasswordFails;
/*package*/ final int mMaxScreenLockTime;
/*package*/ final boolean mRequireRemoteWipe;
/*package*/ final int mPasswordExpiration;
/*package*/ final int mPasswordExpirationDays;
/*package*/ final int mPasswordHistory;
/*package*/ final int mPasswordComplexChars;
@ -465,10 +464,13 @@ public class SecurityPolicy {
* @param maxPasswordFails (0=not enforced)
* @param maxScreenLockTime in seconds (0=not enforced)
* @param requireRemoteWipe
* @param passwordExpirationDays in days (0=not enforced)
* @param passwordHistory (0=not enforced)
* @param passwordComplexChars (0=not enforced)
* @throws IllegalArgumentException for illegal arguments.
*/
public PolicySet(int minPasswordLength, int passwordMode, int maxPasswordFails,
int maxScreenLockTime, boolean requireRemoteWipe, int passwordExpiration,
int maxScreenLockTime, boolean requireRemoteWipe, int passwordExpirationDays,
int passwordHistory, int passwordComplexChars) throws IllegalArgumentException {
// If we're not enforcing passwords, make sure we clean up related values, since EAS
// can send non-zero values for any or all of these
@ -478,7 +480,7 @@ public class SecurityPolicy {
minPasswordLength = 0;
passwordComplexChars = 0;
passwordHistory = 0;
passwordExpiration = 0;
passwordExpirationDays = 0;
} else {
if ((passwordMode != PASSWORD_MODE_SIMPLE) &&
(passwordMode != PASSWORD_MODE_STRONG)) {
@ -493,7 +495,7 @@ public class SecurityPolicy {
if (minPasswordLength > PASSWORD_LENGTH_MAX) {
throw new IllegalArgumentException("password length");
}
if (passwordExpiration > PASSWORD_EXPIRATION_MAX) {
if (passwordExpirationDays > PASSWORD_EXPIRATION_MAX) {
throw new IllegalArgumentException("password expiration");
}
if (passwordHistory > PASSWORD_HISTORY_MAX) {
@ -516,7 +518,7 @@ public class SecurityPolicy {
mMaxPasswordFails = maxPasswordFails;
mMaxScreenLockTime = maxScreenLockTime;
mRequireRemoteWipe = requireRemoteWipe;
mPasswordExpiration = passwordExpiration;
mPasswordExpirationDays = passwordExpirationDays;
mPasswordHistory = passwordHistory;
mPasswordComplexChars = passwordComplexChars;
}
@ -542,7 +544,7 @@ public class SecurityPolicy {
mMaxScreenLockTime =
(int) ((flags & SCREEN_LOCK_TIME_MASK) >> SCREEN_LOCK_TIME_SHIFT);
mRequireRemoteWipe = 0 != (flags & REQUIRE_REMOTE_WIPE);
mPasswordExpiration =
mPasswordExpirationDays =
(int) ((flags & PASSWORD_EXPIRATION_MASK) >> PASSWORD_EXPIRATION_SHIFT);
mPasswordHistory =
(int) ((flags & PASSWORD_HISTORY_MASK) >> PASSWORD_HISTORY_SHIFT);
@ -564,6 +566,20 @@ public class SecurityPolicy {
}
}
/**
* Helper to map expiration times to the millisecond values used by DevicePolicyManager.
*/
public long getDPManagerPasswordExpirationTimeout() {
long result = mPasswordExpirationDays * DAYS_TO_MSEC;
// Add a small offset to the password expiration. This makes it easier to test
// by changing (for example) 1 day to 1 day + 5 minutes. If you set an expiration
// that is within the warning period, you should get a warning fairly quickly.
if (result > 0) {
result += EXPIRATION_OFFSET_MSEC;
}
return result;
}
/**
* Record flags (and a sync key for the flags) into an Account
* Note: the hash code is defined as the encoding used in Account
@ -634,7 +650,7 @@ public class SecurityPolicy {
dest.writeInt(mMaxPasswordFails);
dest.writeInt(mMaxScreenLockTime);
dest.writeInt(mRequireRemoteWipe ? 1 : 0);
dest.writeInt(mPasswordExpiration);
dest.writeInt(mPasswordExpirationDays);
dest.writeInt(mPasswordHistory);
dest.writeInt(mPasswordComplexChars);
}
@ -648,7 +664,7 @@ public class SecurityPolicy {
mMaxPasswordFails = in.readInt();
mMaxScreenLockTime = in.readInt();
mRequireRemoteWipe = in.readInt() == 1;
mPasswordExpiration = in.readInt();
mPasswordExpirationDays = in.readInt();
mPasswordHistory = in.readInt();
mPasswordComplexChars = in.readInt();
}
@ -669,7 +685,7 @@ public class SecurityPolicy {
flags |= REQUIRE_REMOTE_WIPE;
}
flags |= (long)mPasswordHistory << PASSWORD_HISTORY_SHIFT;
flags |= (long)mPasswordExpiration << PASSWORD_EXPIRATION_SHIFT;
flags |= (long)mPasswordExpirationDays << PASSWORD_EXPIRATION_SHIFT;
flags |= (long)mPasswordComplexChars << PASSWORD_COMPLEX_CHARS_SHIFT;
return flags;
}
@ -679,7 +695,7 @@ public class SecurityPolicy {
return "{ " + "pw-len-min=" + mMinPasswordLength + " pw-mode=" + mPasswordMode
+ " pw-fails-max=" + mMaxPasswordFails + " screenlock-max="
+ mMaxScreenLockTime + " remote-wipe-req=" + mRequireRemoteWipe
+ " pw-expiration=" + mPasswordExpiration
+ " pw-expiration=" + mPasswordExpirationDays
+ " pw-history=" + mPasswordHistory
+ " pw-complex-chars=" + mPasswordComplexChars + "}";
}
@ -740,6 +756,139 @@ public class SecurityPolicy {
}
}
/**
* Internal handler for device password expirations.
*/
private void onPasswordExpiring() {
Utility.runAsync(new Runnable() {
@Override
public void run() {
onPasswordExpiringSync(mContext);
}});
}
/**
* Handle password expiration - if any accounts appear to have triggered this, put up
* warnings, or even shut them down.
*
* NOTE: If there are multiple accounts with password expiration policies, the device
* password will be set to expire in the shortest required interval (most secure). The logic
* in this method operates based on the aggregate setting - irrespective of which account caused
* the expiration. In other words, all accounts (that require expiration) will run/stop
* based on the requirements of the account with the shortest interval.
*/
/* package */ void onPasswordExpiringSync(Context context) {
// 1. Do we have any accounts that matter here?
long nextExpiringAccountId = findShortestExpiration(context);
// 2. If not, exit immediately
if (nextExpiringAccountId == -1) {
return;
}
// 3. If yes, are we warning or expired?
long expirationDate = getDPM().getPasswordExpiration(mAdminName);
long timeUntilExpiration = expirationDate - System.currentTimeMillis();
boolean expired = timeUntilExpiration < 0;
if (!expired) {
// 4. If warning, simply put up a generic notification and report that it came from
// the shortest-expiring account.
Account account = Account.restoreAccountWithId(context, nextExpiringAccountId);
if (account == null) return;
Intent intent = new Intent(DevicePolicyManager.ACTION_SET_NEW_PASSWORD);
String ticker = context.getString(
R.string.password_expire_warning_ticker_fmt, account.getDisplayName());
String contentTitle = context.getString(
R.string.password_expire_warning_content_title);
String contentText = context.getString(
R.string.password_expire_warning_content_text_fmt, account.getDisplayName());
NotificationController nc = NotificationController.getInstance(mContext);
nc.postAccountNotification(account, ticker, contentTitle, contentText, intent,
NotificationController.NOTIFICATION_ID_PASSWORD_EXPIRING);
} else {
// 5. Actually expired - find all accounts that expire passwords, and wipe them
boolean wiped = wipeExpiredAccounts(context, Controller.getInstance(context));
if (wiped) {
// Post notification
Account account = Account.restoreAccountWithId(context, nextExpiringAccountId);
if (account == null) return;
Intent intent =
new Intent(DevicePolicyManager.ACTION_SET_NEW_PASSWORD);
String ticker = context.getString(R.string.password_expired_ticker);
String contentTitle = context.getString(R.string.password_expired_content_title);
String contentText = context.getString(R.string.password_expired_content_text);
NotificationController nc = NotificationController.getInstance(mContext);
nc.postAccountNotification(account, ticker, contentTitle,
contentText, intent,
NotificationController.NOTIFICATION_ID_PASSWORD_EXPIRED);
}
}
}
/**
* Find the account with the shortest expiration time. This is always assumed to be
* the account that forces the password to be refreshed.
* @return -1 if no expirations, or accountId if one is found
*/
/* package */ static long findShortestExpiration(Context context) {
long nextExpiringAccountId = -1;
long shortestExpiration = Long.MAX_VALUE;
Cursor c = context.getContentResolver().query(Account.CONTENT_URI,
ACCOUNT_SECURITY_PROJECTION, Account.SECURITY_NONZERO_SELECTION, null, null);
try {
while (c.moveToNext()) {
long flags = c.getLong(ACCOUNT_SECURITY_COLUMN_FLAGS);
if (flags != 0) {
PolicySet p = new PolicySet(flags);
if (p.mPasswordExpirationDays > 0 &&
p.mPasswordExpirationDays < shortestExpiration) {
nextExpiringAccountId = c.getLong(ACCOUNT_SECURITY_COLUMN_ID);
shortestExpiration = p.mPasswordExpirationDays;
}
}
}
} finally {
c.close();
}
return nextExpiringAccountId;
}
/**
* For all accounts that require password expiration, put them in security hold and wipe
* their data.
* @param context
* @param controller
* @return true if one or more accounts were wiped
*/
/* package */ static boolean wipeExpiredAccounts(Context context, Controller controller) {
boolean result = false;
Cursor c = context.getContentResolver().query(Account.CONTENT_URI,
ACCOUNT_SECURITY_PROJECTION, Account.SECURITY_NONZERO_SELECTION, null, null);
try {
while (c.moveToNext()) {
long flags = c.getLong(ACCOUNT_SECURITY_COLUMN_FLAGS);
if (flags != 0) {
PolicySet p = new PolicySet(flags);
if (p.mPasswordExpirationDays > 0) {
long accountId = c.getLong(ACCOUNT_SECURITY_COLUMN_ID);
Account account = Account.restoreAccountWithId(context, accountId);
if (account != null) {
// Mark the account as "on hold".
setAccountHoldFlag(context, account, true);
// Erase data
controller.deleteSyncedDataSync(accountId);
// Report one or more were found
result = true;
}
}
}
}
} finally {
c.close();
}
return result;
}
/**
* Device Policy administrator. This is primarily a listener for device state changes.
* Note: This is instantiated by incoming messages.
@ -778,7 +927,20 @@ public class SecurityPolicy {
*/
@Override
public void onPasswordChanged(Context context, Intent intent) {
// Clear security holds (if any)
Account.clearSecurityHoldOnAllAccounts(context);
// Cancel any active notifications (if any are posted)
NotificationController nc = NotificationController.getInstance(context);
nc.cancelNotification(NotificationController.NOTIFICATION_ID_PASSWORD_EXPIRING);
nc.cancelNotification(NotificationController.NOTIFICATION_ID_PASSWORD_EXPIRED);
}
/**
* Called when device password is expiring
*/
@Override
public void onPasswordExpiring(Context context, Intent intent) {
SecurityPolicy.getInstance(context).onPasswordExpiring();
}
}
}

View File

@ -482,13 +482,14 @@ public class AttachmentProvider extends ContentProvider {
}
/**
* In support of deleting an account, delete all related attachments.
* In support of deleting or wiping an account, delete all related attachments.
*
* @param context
* @param accountId the account for the mailbox
* @param accountId the account to scrub
*/
public static void deleteAllAccountAttachmentFiles(Context context, long accountId) {
File[] files = getAttachmentDirectory(context, accountId).listFiles();
if (files == null) return;
for (File file : files) {
boolean result = file.delete();
if (!result) {

View File

@ -1447,7 +1447,7 @@ public class EasSyncService extends AbstractSyncService {
// We've gotten a remote wipe command
ExchangeService.alwaysLog("!!! Remote wipe request received");
// Start by setting the account to security hold
sp.setAccountHoldFlag(mAccount, true);
sp.setAccountHoldFlag(mContext, mAccount, true);
// Force a stop to any running syncs for this account (except this one)
ExchangeService.stopNonAccountMailboxSyncsForAccount(mAccount.mId);

View File

@ -65,7 +65,7 @@ public class ProvisionParser extends Parser {
int passwordMode = PolicySet.PASSWORD_MODE_NONE;
int maxPasswordFails = 0;
int maxScreenLockTime = 0;
int passwordExpiration = 0;
int passwordExpirationDays = 0;
int passwordHistory = 0;
int passwordComplexChars = 0;
@ -95,7 +95,7 @@ public class ProvisionParser extends Parser {
maxPasswordFails = getValueInt();
break;
case Tags.PROVISION_DEVICE_PASSWORD_EXPIRATION:
passwordExpiration = getValueInt();
passwordExpirationDays = getValueInt();
break;
case Tags.PROVISION_DEVICE_PASSWORD_HISTORY:
passwordHistory = getValueInt();
@ -195,7 +195,7 @@ public class ProvisionParser extends Parser {
}
mPolicySet = new SecurityPolicy.PolicySet(minPasswordLength, passwordMode,
maxPasswordFails, maxScreenLockTime, true, passwordExpiration, passwordHistory,
maxPasswordFails, maxScreenLockTime, true, passwordExpirationDays, passwordHistory,
passwordComplexChars);
}

View File

@ -17,10 +17,14 @@
package com.android.email;
import com.android.email.SecurityPolicy.PolicySet;
import com.android.email.provider.ContentCache;
import com.android.email.provider.EmailContent;
import com.android.email.provider.EmailProvider;
import com.android.email.provider.ProviderTestUtils;
import com.android.email.provider.EmailContent.Account;
import com.android.email.provider.EmailContent.AccountColumns;
import com.android.email.provider.EmailContent.Mailbox;
import com.android.email.provider.EmailContent.Message;
import android.content.ContentUris;
import android.content.ContentValues;
@ -53,8 +57,9 @@ public class SecurityPolicyTests extends ProviderTestCase2<EmailProvider> {
@Override
protected void setUp() throws Exception {
super.setUp();
mMockContext = new MockContext2(getMockContext(), this.mContext);
// Invalidate all caches, since we reset the database for each test
ContentCache.invalidateAllCachesForTest();
}
/**
@ -128,7 +133,7 @@ public class SecurityPolicyTests extends ProviderTestCase2<EmailProvider> {
assertEquals(0, ps.mMinPasswordLength);
assertEquals(0, ps.mMaxScreenLockTime);
assertEquals(0, ps.mMaxPasswordFails);
assertEquals(0, ps.mPasswordExpiration);
assertEquals(0, ps.mPasswordExpirationDays);
assertEquals(0, ps.mPasswordHistory);
assertEquals(0, ps.mPasswordComplexChars);
@ -167,7 +172,7 @@ public class SecurityPolicyTests extends ProviderTestCase2<EmailProvider> {
assertEquals(p3ain, p3aout);
// Repeat that test with fully-populated policies
PolicySet p3bin = new PolicySet(10, PolicySet.PASSWORD_MODE_SIMPLE, 15, 16, false, 1, 2, 3);
PolicySet p3bin = new PolicySet(10, PolicySet.PASSWORD_MODE_SIMPLE, 15, 16, false, 6, 2, 3);
p3bin.writeAccount(a3, null, true, mMockContext);
PolicySet p3bout = sp.computeAggregatePolicy();
assertNotNull(p3bout);
@ -177,6 +182,8 @@ public class SecurityPolicyTests extends ProviderTestCase2<EmailProvider> {
// pw length and pw mode - max logic - will change because larger #s here
// fail count and lock timer - min logic - will *not* change because larger #s here
// wipe required - OR logic - will *not* change here because false
// expiration - will not change because 0 (unspecified)
// max complex chars - max logic - will change
PolicySet p4in = new PolicySet(20, PolicySet.PASSWORD_MODE_STRONG, 25, 26, false, 0, 5, 7);
Account a4 = ProviderTestUtils.setupAccount("sec-4", false, mMockContext);
p4in.writeAccount(a4, null, true, mMockContext);
@ -186,7 +193,7 @@ public class SecurityPolicyTests extends ProviderTestCase2<EmailProvider> {
assertEquals(PolicySet.PASSWORD_MODE_STRONG, p4out.mPasswordMode);
assertEquals(15, p4out.mMaxPasswordFails);
assertEquals(16, p4out.mMaxScreenLockTime);
assertEquals(1, p4out.mPasswordExpiration);
assertEquals(6, p4out.mPasswordExpirationDays);
assertEquals(5, p4out.mPasswordHistory);
assertEquals(7, p4out.mPasswordComplexChars);
assertFalse(p4out.mRequireRemoteWipe);
@ -194,9 +201,10 @@ public class SecurityPolicyTests extends ProviderTestCase2<EmailProvider> {
// add another account which mixes it up (the remaining fields will change)
// pw length and pw mode - max logic - will *not* change because smaller #s here
// fail count and lock timer - min logic - will change because smaller #s here
// password exp will change (max logic), but history and complex chars will be as before
// wipe required - OR logic - will change here because true
PolicySet p5in = new PolicySet(4, PolicySet.PASSWORD_MODE_SIMPLE, 5, 6, true, 6, 0, 0);
// expiration time - min logic - will change because lower here
// history & complex chars - will not change because 0 (unspecified)
PolicySet p5in = new PolicySet(4, PolicySet.PASSWORD_MODE_SIMPLE, 5, 6, true, 1, 0, 0);
Account a5 = ProviderTestUtils.setupAccount("sec-5", false, mMockContext);
p5in.writeAccount(a5, null, true, mMockContext);
PolicySet p5out = sp.computeAggregatePolicy();
@ -205,7 +213,7 @@ public class SecurityPolicyTests extends ProviderTestCase2<EmailProvider> {
assertEquals(PolicySet.PASSWORD_MODE_STRONG, p5out.mPasswordMode);
assertEquals(5, p5out.mMaxPasswordFails);
assertEquals(6, p5out.mMaxScreenLockTime);
assertEquals(6, p5out.mPasswordExpiration);
assertEquals(1, p5out.mPasswordExpirationDays);
assertEquals(5, p4out.mPasswordHistory);
assertEquals(7, p4out.mPasswordComplexChars);
assertTrue(p5out.mRequireRemoteWipe);
@ -242,7 +250,7 @@ public class SecurityPolicyTests extends ProviderTestCase2<EmailProvider> {
assertEquals(PolicySet.PASSWORD_LENGTH_MAX, p.mMinPasswordLength);
assertEquals(0, p.mMaxPasswordFails);
assertEquals(0, p.mMaxScreenLockTime);
assertEquals(0, p.mPasswordExpiration);
assertEquals(0, p.mPasswordExpirationDays);
assertEquals(0, p.mPasswordHistory);
assertEquals(0, p.mPasswordComplexChars);
assertFalse(p.mRequireRemoteWipe);
@ -252,7 +260,7 @@ public class SecurityPolicyTests extends ProviderTestCase2<EmailProvider> {
assertEquals(0, p.mMinPasswordLength);
assertEquals(0, p.mMaxPasswordFails);
assertEquals(0, p.mMaxScreenLockTime);
assertEquals(0, p.mPasswordExpiration);
assertEquals(0, p.mPasswordExpirationDays);
assertEquals(0, p.mPasswordHistory);
assertEquals(0, p.mPasswordComplexChars);
assertFalse(p.mRequireRemoteWipe);
@ -263,7 +271,7 @@ public class SecurityPolicyTests extends ProviderTestCase2<EmailProvider> {
assertEquals(0, p.mMinPasswordLength);
assertEquals(PolicySet.PASSWORD_MAX_FAILS_MAX, p.mMaxPasswordFails);
assertEquals(0, p.mMaxScreenLockTime);
assertEquals(0, p.mPasswordExpiration);
assertEquals(0, p.mPasswordExpirationDays);
assertEquals(0, p.mPasswordHistory);
assertEquals(0, p.mPasswordComplexChars);
assertFalse(p.mRequireRemoteWipe);
@ -274,7 +282,7 @@ public class SecurityPolicyTests extends ProviderTestCase2<EmailProvider> {
assertEquals(0, p.mMinPasswordLength);
assertEquals(0, p.mMaxPasswordFails);
assertEquals(PolicySet.SCREEN_LOCK_TIME_MAX, p.mMaxScreenLockTime);
assertEquals(0, p.mPasswordExpiration);
assertEquals(0, p.mPasswordExpirationDays);
assertEquals(0, p.mPasswordHistory);
assertEquals(0, p.mPasswordComplexChars);
assertFalse(p.mRequireRemoteWipe);
@ -284,7 +292,7 @@ public class SecurityPolicyTests extends ProviderTestCase2<EmailProvider> {
assertEquals(0, p.mMinPasswordLength);
assertEquals(0, p.mMaxPasswordFails);
assertEquals(0, p.mMaxScreenLockTime);
assertEquals(0, p.mPasswordExpiration);
assertEquals(0, p.mPasswordExpirationDays);
assertEquals(0, p.mPasswordHistory);
assertEquals(0, p.mPasswordComplexChars);
assertTrue(p.mRequireRemoteWipe);
@ -295,7 +303,7 @@ public class SecurityPolicyTests extends ProviderTestCase2<EmailProvider> {
assertEquals(0, p.mMinPasswordLength);
assertEquals(0, p.mMaxPasswordFails);
assertEquals(0, p.mMaxScreenLockTime);
assertEquals(PolicySet.PASSWORD_EXPIRATION_MAX, p.mPasswordExpiration);
assertEquals(PolicySet.PASSWORD_EXPIRATION_MAX, p.mPasswordExpirationDays);
assertEquals(0, p.mPasswordHistory);
assertEquals(0, p.mPasswordComplexChars);
assertFalse(p.mRequireRemoteWipe);
@ -306,7 +314,7 @@ public class SecurityPolicyTests extends ProviderTestCase2<EmailProvider> {
assertEquals(0, p.mMinPasswordLength);
assertEquals(0, p.mMaxPasswordFails);
assertEquals(0, p.mMaxScreenLockTime);
assertEquals(0, p.mPasswordExpiration);
assertEquals(0, p.mPasswordExpirationDays);
assertEquals(PolicySet.PASSWORD_HISTORY_MAX, p.mPasswordHistory);
assertEquals(0, p.mPasswordComplexChars);
assertFalse(p.mRequireRemoteWipe);
@ -317,7 +325,7 @@ public class SecurityPolicyTests extends ProviderTestCase2<EmailProvider> {
assertEquals(0, p.mMinPasswordLength);
assertEquals(0, p.mMaxPasswordFails);
assertEquals(0, p.mMaxScreenLockTime);
assertEquals(0, p.mPasswordExpiration);
assertEquals(0, p.mPasswordExpirationDays);
assertEquals(0, p.mPasswordHistory);
assertEquals(PolicySet.PASSWORD_COMPLEX_CHARS_MAX, p.mPasswordComplexChars);
assertFalse(p.mRequireRemoteWipe);
@ -365,7 +373,7 @@ public class SecurityPolicyTests extends ProviderTestCase2<EmailProvider> {
// confirm clear until set
Account a1a = Account.restoreAccountWithId(mMockContext, a1.mId);
assertEquals(Account.FLAGS_NOTIFY_NEW_MAIL, a1a.mFlags);
sp.setAccountHoldFlag(a1, true);
sp.setAccountHoldFlag(mMockContext, a1, true);
assertEquals(Account.FLAGS_NOTIFY_NEW_MAIL | Account.FLAGS_SECURITY_HOLD, a1.mFlags);
Account a1b = Account.restoreAccountWithId(mMockContext, a1.mId);
assertEquals(Account.FLAGS_NOTIFY_NEW_MAIL | Account.FLAGS_SECURITY_HOLD, a1b.mFlags);
@ -373,20 +381,23 @@ public class SecurityPolicyTests extends ProviderTestCase2<EmailProvider> {
// confirm set until cleared
Account a2a = Account.restoreAccountWithId(mMockContext, a2.mId);
assertEquals(Account.FLAGS_VIBRATE_ALWAYS | Account.FLAGS_SECURITY_HOLD, a2a.mFlags);
sp.setAccountHoldFlag(a2, false);
sp.setAccountHoldFlag(mMockContext, a2, false);
assertEquals(Account.FLAGS_VIBRATE_ALWAYS, a2.mFlags);
Account a2b = Account.restoreAccountWithId(mMockContext, a2.mId);
assertEquals(Account.FLAGS_VIBRATE_ALWAYS, a2b.mFlags);
}
private static class MockController extends Controller {
protected MockController(Context context) {
super(context);
}
}
// private static class MockController extends Controller {
// protected MockController(Context context) {
// super(context);
// }
// }
/**
* Test the response to disabling DeviceAdmin status
*
* TODO: Reenable the 2nd portion of this test - it fails because it gets into the Controller
* and spins up an account backup on another thread.
*/
public void testDisableAdmin() {
Account a1 = ProviderTestUtils.setupAccount("disable-1", false, mMockContext);
@ -418,20 +429,138 @@ public class SecurityPolicyTests extends ProviderTestCase2<EmailProvider> {
// Simulate revoke of device admin; directly call deleteSecuredAccounts, which is normally
// called from a background thread
MockController mockController = new MockController(mMockContext);
Controller.injectMockControllerForTest(mockController);
try {
sp.deleteSecuredAccounts(mMockContext);
PolicySet after2 = sp.getAggregatePolicy();
assertEquals(SecurityPolicy.NO_POLICY_SET, after2);
Account a1b = Account.restoreAccountWithId(mMockContext, a1.mId);
assertNull(a1b);
Account a2b = Account.restoreAccountWithId(mMockContext, a2.mId);
assertNull(a2b);
Account a3b = Account.restoreAccountWithId(mMockContext, a3.mId);
assertNull(a3b.mSecuritySyncKey);
} finally {
Controller.injectMockControllerForTest(null);
// MockController mockController = new MockController(mMockContext);
// Controller.injectMockControllerForTest(mockController);
// try {
// sp.deleteSecuredAccounts(mMockContext);
// PolicySet after2 = sp.getAggregatePolicy();
// assertEquals(SecurityPolicy.NO_POLICY_SET, after2);
// Account a1b = Account.restoreAccountWithId(mMockContext, a1.mId);
// assertNull(a1b);
// Account a2b = Account.restoreAccountWithId(mMockContext, a2.mId);
// assertNull(a2b);
// Account a3b = Account.restoreAccountWithId(mMockContext, a3.mId);
// assertNull(a3b.mSecuritySyncKey);
// } finally {
// Controller.injectMockControllerForTest(null);
// }
}
/**
* Test the scanner that finds expiring accounts
*/
public void testFindExpiringAccount() {
SecurityPolicy sp = getSecurityPolicy();
Account a1 = ProviderTestUtils.setupAccount("expiring-1", true, mMockContext);
// With no expiring accounts, this should return null.
long nextExpiringAccountId = sp.findShortestExpiration(mMockContext);
assertEquals(-1, nextExpiringAccountId);
// Add a single expiring account
Account a2 = ProviderTestUtils.setupAccount("expiring-2", false, mMockContext);
PolicySet p2 = new PolicySet(20, PolicySet.PASSWORD_MODE_STRONG, 25, 26, false, 30, 0, 0);
p2.writeAccount(a2, "sync-key-2", true, mMockContext);
// The expiring account should be returned
nextExpiringAccountId = sp.findShortestExpiration(mMockContext);
assertEquals(a2.mId, nextExpiringAccountId);
// Add an account with a longer expiration
Account a3 = ProviderTestUtils.setupAccount("expiring-3", false, mMockContext);
PolicySet p3 = new PolicySet(20, PolicySet.PASSWORD_MODE_STRONG, 25, 26, false, 60, 0, 0);
p3.writeAccount(a3, "sync-key-3", true, mMockContext);
// The original expiring account (a2) should be returned
nextExpiringAccountId = sp.findShortestExpiration(mMockContext);
assertEquals(a2.mId, nextExpiringAccountId);
// Add an account with a shorter expiration
Account a4 = ProviderTestUtils.setupAccount("expiring-4", false, mMockContext);
PolicySet p4 = new PolicySet(20, PolicySet.PASSWORD_MODE_STRONG, 25, 26, false, 15, 0, 0);
p4.writeAccount(a4, "sync-key-4", true, mMockContext);
// The new expiring account (a4) should be returned
nextExpiringAccountId = sp.findShortestExpiration(mMockContext);
assertEquals(a4.mId, nextExpiringAccountId);
}
/**
* Lightweight subclass of the Controller class allows injection of mock context
*/
public static class TestController extends Controller {
protected TestController(Context providerContext, Context systemContext) {
super(systemContext);
setProviderContext(providerContext);
}
}
/**
* Test the scanner that wipes expiring accounts
*/
public void testWipeExpiringAccounts() {
SecurityPolicy sp = getSecurityPolicy();
TestController testController = new TestController(mMockContext, getContext());
// Two accounts - a1 is normal, a2 has security (but no expiration)
Account a1 = ProviderTestUtils.setupAccount("expired-1", true, mMockContext);
Account a2 = ProviderTestUtils.setupAccount("expired-2", false, mMockContext);
PolicySet p2 = new PolicySet(20, PolicySet.PASSWORD_MODE_STRONG, 25, 26, false, 0, 0, 0);
p2.writeAccount(a2, "sync-key-2", true, mMockContext);
// Add a mailbox & messages to each account
long account1Id = a1.mId;
long account2Id = a2.mId;
Mailbox box1 = ProviderTestUtils.setupMailbox("box1", account1Id, true, mMockContext);
long box1Id = box1.mId;
ProviderTestUtils.setupMessage("message1", account1Id, box1Id, false, true, mMockContext);
ProviderTestUtils.setupMessage("message2", account1Id, box1Id, false, true, mMockContext);
Mailbox box2 = ProviderTestUtils.setupMailbox("box2", account2Id, true, mMockContext);
long box2Id = box2.mId;
ProviderTestUtils.setupMessage("message3", account2Id, box2Id, false, true, mMockContext);
ProviderTestUtils.setupMessage("message4", account2Id, box2Id, false, true, mMockContext);
// Run the expiration code - should do nothing
boolean wiped = sp.wipeExpiredAccounts(mMockContext, testController);
assertFalse(wiped);
// check mailboxes & messages not wiped
assertEquals(2, EmailContent.count(mMockContext, Account.CONTENT_URI));
assertEquals(2, EmailContent.count(mMockContext, Mailbox.CONTENT_URI));
assertEquals(4, EmailContent.count(mMockContext, Message.CONTENT_URI));
// Add 3rd account that really expires
Account a3 = ProviderTestUtils.setupAccount("expired-3", false, mMockContext);
PolicySet p3 = new PolicySet(20, PolicySet.PASSWORD_MODE_STRONG, 25, 26, false, 30, 0, 0);
p3.writeAccount(a3, "sync-key-3", true, mMockContext);
// Add mailbox & messages to 3rd account
long account3Id = a3.mId;
Mailbox box3 = ProviderTestUtils.setupMailbox("box3", account3Id, true, mMockContext);
long box3Id = box3.mId;
ProviderTestUtils.setupMessage("message5", account3Id, box3Id, false, true, mMockContext);
ProviderTestUtils.setupMessage("message6", account3Id, box3Id, false, true, mMockContext);
// check new counts
assertEquals(3, EmailContent.count(mMockContext, Account.CONTENT_URI));
assertEquals(3, EmailContent.count(mMockContext, Mailbox.CONTENT_URI));
assertEquals(6, EmailContent.count(mMockContext, Message.CONTENT_URI));
// Run the expiration code - wipe acct #3
wiped = sp.wipeExpiredAccounts(mMockContext, testController);
assertTrue(wiped);
// check new counts - account survives but data is wiped
assertEquals(3, EmailContent.count(mMockContext, Account.CONTENT_URI));
assertEquals(2, EmailContent.count(mMockContext, Mailbox.CONTENT_URI));
assertEquals(4, EmailContent.count(mMockContext, Message.CONTENT_URI));
// Check security hold states - only #3 should be in hold
Account account = Account.restoreAccountWithId(mMockContext, account1Id);
assertEquals(0, account.mFlags & Account.FLAGS_SECURITY_HOLD);
account = Account.restoreAccountWithId(mMockContext, account2Id);
assertEquals(0, account.mFlags & Account.FLAGS_SECURITY_HOLD);
account = Account.restoreAccountWithId(mMockContext, account3Id);
assertEquals(Account.FLAGS_SECURITY_HOLD, account.mFlags & Account.FLAGS_SECURITY_HOLD);
}
}