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 --> <!-- "Setup could not finish" dialog action button -->
<string name="account_setup_failed_dlg_edit_details_action">Edit details</string> <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) <!-- On AccountSettingsXL, dialog text if you try to exit in/out/eas fragment (server settings)
without checking/saving [CHAR LIMIT=none]--> without checking/saving [CHAR LIMIT=none]-->
<string name="account_settings_exit_server_settings">Discard unsaved changes?</string> <string name="account_settings_exit_server_settings">Discard unsaved changes?</string>

View File

@ -21,5 +21,6 @@
<watch-login /> <watch-login />
<force-lock /> <force-lock />
<wipe-data /> <wipe-data />
<expire-password />
</uses-policies> </uses-policies>
</device-admin> </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_SECURITY_NEEDED = 1;
public static final int NOTIFICATION_ID_EXCHANGE_CALENDAR_ADDED = 2; 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_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_NEW_MESSAGES = 0x10000000;
private static final int NOTIFICATION_ID_BASE_LOGIN_WARNING = 0x20000000; private static final int NOTIFICATION_ID_BASE_LOGIN_WARNING = 0x20000000;
@ -69,6 +71,60 @@ public class NotificationController {
return sInstance; 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 * @return the "new message" notification ID for an account. It just assumes
* accountID won't be too huge. Any other smarter/cleaner way? * 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.Account;
import com.android.email.provider.EmailContent.AccountColumns; 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.DeviceAdminReceiver;
import android.app.admin.DevicePolicyManager; import android.app.admin.DevicePolicyManager;
import android.content.ComponentName; import android.content.ComponentName;
@ -32,8 +29,6 @@ import android.content.ContentValues;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.database.Cursor; import android.database.Cursor;
import android.media.AudioManager;
import android.net.Uri;
import android.os.Parcel; import android.os.Parcel;
import android.os.Parcelable; import android.os.Parcelable;
import android.util.Log; import android.util.Log;
@ -59,6 +54,7 @@ public class SecurityPolicy {
private static final String[] ACCOUNT_SECURITY_PROJECTION = new String[] { private static final String[] ACCOUNT_SECURITY_PROJECTION = new String[] {
AccountColumns.ID, AccountColumns.SECURITY_FLAGS AccountColumns.ID, AccountColumns.SECURITY_FLAGS
}; };
private static final int ACCOUNT_SECURITY_COLUMN_ID = 0;
private static final int ACCOUNT_SECURITY_COLUMN_FLAGS = 1; private static final int ACCOUNT_SECURITY_COLUMN_FLAGS = 1;
/** /**
@ -98,7 +94,7 @@ public class SecurityPolicy {
* max screen lock time take the min * max screen lock time take the min
* require remote wipe take the max (logical or) * require remote wipe take the max (logical or)
* password history take the max (strongest mode) * 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) * password complex chars take the max (strongest mode)
* *
* @return a policy representing the strongest aggregate. If no policy sets are defined, * @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; int maxScreenLockTime = Integer.MAX_VALUE;
boolean requireRemoteWipe = false; boolean requireRemoteWipe = false;
int passwordHistory = Integer.MIN_VALUE; int passwordHistory = Integer.MIN_VALUE;
int passwordExpiration = Integer.MIN_VALUE; int passwordExpirationDays = Integer.MAX_VALUE;
int passwordComplexChars = Integer.MIN_VALUE; int passwordComplexChars = Integer.MIN_VALUE;
Cursor c = mContext.getContentResolver().query(Account.CONTENT_URI, Cursor c = mContext.getContentResolver().query(Account.CONTENT_URI,
@ -134,8 +130,9 @@ public class SecurityPolicy {
if (p.mPasswordHistory > 0) { if (p.mPasswordHistory > 0) {
passwordHistory = Math.max(p.mPasswordHistory, passwordHistory); passwordHistory = Math.max(p.mPasswordHistory, passwordHistory);
} }
if (p.mPasswordExpiration > 0) { if (p.mPasswordExpirationDays > 0) {
passwordExpiration = Math.max(p.mPasswordExpiration, passwordExpiration); passwordExpirationDays =
Math.min(p.mPasswordExpirationDays, passwordExpirationDays);
} }
if (p.mPasswordComplexChars > 0) { if (p.mPasswordComplexChars > 0) {
passwordComplexChars = Math.max(p.mPasswordComplexChars, passwordComplexChars = Math.max(p.mPasswordComplexChars,
@ -155,11 +152,11 @@ public class SecurityPolicy {
if (maxPasswordFails == Integer.MAX_VALUE) maxPasswordFails = 0; if (maxPasswordFails == Integer.MAX_VALUE) maxPasswordFails = 0;
if (maxScreenLockTime == Integer.MAX_VALUE) maxScreenLockTime = 0; if (maxScreenLockTime == Integer.MAX_VALUE) maxScreenLockTime = 0;
if (passwordHistory == Integer.MIN_VALUE) passwordHistory = 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; if (passwordComplexChars == Integer.MIN_VALUE) passwordComplexChars = 0;
return new PolicySet(minPasswordLength, passwordMode, maxPasswordFails, return new PolicySet(minPasswordLength, passwordMode, maxPasswordFails,
maxScreenLockTime, requireRemoteWipe, passwordExpiration, passwordHistory, maxScreenLockTime, requireRemoteWipe, passwordExpirationDays, passwordHistory,
passwordComplexChars); passwordComplexChars);
} else { } else {
return NO_POLICY_SET; 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. * 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 * @param policies the policies requested, or null to check aggregate stored policies
* @return true if the policies are active, false if not active * @return true if the policies are active, false if not active
*/ */
@ -249,8 +252,20 @@ public class SecurityPolicy {
return false; return false;
} }
} }
if (policies.mPasswordExpiration > 0) { if (policies.mPasswordExpirationDays > 0) {
// TODO Complete when DPM supports this // 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 (policies.mPasswordHistory > 0) {
if (dpm.getPasswordHistoryLength(mAdminName) < policies.mPasswordHistory) { if (dpm.getPasswordHistoryLength(mAdminName) < policies.mPasswordHistory) {
@ -293,8 +308,9 @@ public class SecurityPolicy {
dpm.setMaximumTimeToLock(mAdminName, policies.mMaxScreenLockTime * 1000); dpm.setMaximumTimeToLock(mAdminName, policies.mMaxScreenLockTime * 1000);
// local wipe (failed passwords limit) // local wipe (failed passwords limit)
dpm.setMaximumFailedPasswordsForWipe(mAdminName, policies.mMaxPasswordFails); dpm.setMaximumFailedPasswordsForWipe(mAdminName, policies.mMaxPasswordFails);
// password expiration (days until a password expires) // password expiration (days until a password expires). API takes mSec.
// TODO set this when DPM allows it dpm.setPasswordExpirationTimeout(mAdminName,
policies.getDPManagerPasswordExpirationTimeout());
// password history length (number of previous passwords that may not be reused) // password history length (number of previous passwords that may not be reused)
dpm.setPasswordHistoryLength(mAdminName, policies.mPasswordHistory); dpm.setPasswordHistoryLength(mAdminName, policies.mPasswordHistory);
// password minimum complex characters // 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: * 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 * Setting it gives us an indication that it was blocked, and clearing it gives EAS a
* signal to try syncing again. * 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) { if (newState) {
account.mFlags |= Account.FLAGS_SECURITY_HOLD; account.mFlags |= Account.FLAGS_SECURITY_HOLD;
} else { } else {
@ -315,7 +334,7 @@ public class SecurityPolicy {
} }
ContentValues cv = new ContentValues(); ContentValues cv = new ContentValues();
cv.put(AccountColumns.FLAGS, account.mFlags); 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) { public void policiesRequired(long accountId) {
Account account = EmailContent.Account.restoreAccountWithId(mContext, accountId); Account account = EmailContent.Account.restoreAccountWithId(mContext, accountId);
// Mark the account as "on hold". // Mark the account as "on hold".
setAccountHoldFlag(account, true); setAccountHoldFlag(mContext, account, true);
// Otherwise, put up a notification
// Put up a notification
String tickerText = mContext.getString(R.string.security_notification_ticker_fmt, String tickerText = mContext.getString(R.string.security_notification_ticker_fmt,
account.getDisplayName()); account.getDisplayName());
String contentTitle = mContext.getString(R.string.security_notification_content_title); String contentTitle = mContext.getString(R.string.security_notification_content_title);
String contentText = account.getDisplayName(); 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); Intent intent = AccountSecurity.actionUpdateSecurityIntent(mContext, accountId);
PendingIntent pending = NotificationController.getInstance(mContext).postAccountNotification(
PendingIntent.getActivity(mContext, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); account, tickerText, contentTitle, contentText, intent,
NotificationController.NOTIFICATION_ID_SECURITY_NEEDED);
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);
} }
/** /**
@ -372,9 +367,8 @@ public class SecurityPolicy {
* cleared now. * cleared now.
*/ */
public void clearNotification(long accountId) { public void clearNotification(long accountId) {
NotificationManager notificationManager = NotificationController.getInstance(mContext).cancelNotification(
(NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); NotificationController.NOTIFICATION_ID_SECURITY_NEEDED);
notificationManager.cancel(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; private static final long PASSWORD_COMPLEX_CHARS_MASK = 31L << PASSWORD_COMPLEX_CHARS_SHIFT;
public static final int PASSWORD_COMPLEX_CHARS_MAX = 31; 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 mMinPasswordLength;
/*package*/ final int mPasswordMode; /*package*/ final int mPasswordMode;
/*package*/ final int mMaxPasswordFails; /*package*/ final int mMaxPasswordFails;
/*package*/ final int mMaxScreenLockTime; /*package*/ final int mMaxScreenLockTime;
/*package*/ final boolean mRequireRemoteWipe; /*package*/ final boolean mRequireRemoteWipe;
/*package*/ final int mPasswordExpiration; /*package*/ final int mPasswordExpirationDays;
/*package*/ final int mPasswordHistory; /*package*/ final int mPasswordHistory;
/*package*/ final int mPasswordComplexChars; /*package*/ final int mPasswordComplexChars;
@ -465,10 +464,13 @@ public class SecurityPolicy {
* @param maxPasswordFails (0=not enforced) * @param maxPasswordFails (0=not enforced)
* @param maxScreenLockTime in seconds (0=not enforced) * @param maxScreenLockTime in seconds (0=not enforced)
* @param requireRemoteWipe * @param requireRemoteWipe
* @param passwordExpirationDays in days (0=not enforced)
* @param passwordHistory (0=not enforced)
* @param passwordComplexChars (0=not enforced)
* @throws IllegalArgumentException for illegal arguments. * @throws IllegalArgumentException for illegal arguments.
*/ */
public PolicySet(int minPasswordLength, int passwordMode, int maxPasswordFails, 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 { int passwordHistory, int passwordComplexChars) throws IllegalArgumentException {
// If we're not enforcing passwords, make sure we clean up related values, since EAS // 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 // can send non-zero values for any or all of these
@ -478,7 +480,7 @@ public class SecurityPolicy {
minPasswordLength = 0; minPasswordLength = 0;
passwordComplexChars = 0; passwordComplexChars = 0;
passwordHistory = 0; passwordHistory = 0;
passwordExpiration = 0; passwordExpirationDays = 0;
} else { } else {
if ((passwordMode != PASSWORD_MODE_SIMPLE) && if ((passwordMode != PASSWORD_MODE_SIMPLE) &&
(passwordMode != PASSWORD_MODE_STRONG)) { (passwordMode != PASSWORD_MODE_STRONG)) {
@ -493,7 +495,7 @@ public class SecurityPolicy {
if (minPasswordLength > PASSWORD_LENGTH_MAX) { if (minPasswordLength > PASSWORD_LENGTH_MAX) {
throw new IllegalArgumentException("password length"); throw new IllegalArgumentException("password length");
} }
if (passwordExpiration > PASSWORD_EXPIRATION_MAX) { if (passwordExpirationDays > PASSWORD_EXPIRATION_MAX) {
throw new IllegalArgumentException("password expiration"); throw new IllegalArgumentException("password expiration");
} }
if (passwordHistory > PASSWORD_HISTORY_MAX) { if (passwordHistory > PASSWORD_HISTORY_MAX) {
@ -516,7 +518,7 @@ public class SecurityPolicy {
mMaxPasswordFails = maxPasswordFails; mMaxPasswordFails = maxPasswordFails;
mMaxScreenLockTime = maxScreenLockTime; mMaxScreenLockTime = maxScreenLockTime;
mRequireRemoteWipe = requireRemoteWipe; mRequireRemoteWipe = requireRemoteWipe;
mPasswordExpiration = passwordExpiration; mPasswordExpirationDays = passwordExpirationDays;
mPasswordHistory = passwordHistory; mPasswordHistory = passwordHistory;
mPasswordComplexChars = passwordComplexChars; mPasswordComplexChars = passwordComplexChars;
} }
@ -542,7 +544,7 @@ public class SecurityPolicy {
mMaxScreenLockTime = mMaxScreenLockTime =
(int) ((flags & SCREEN_LOCK_TIME_MASK) >> SCREEN_LOCK_TIME_SHIFT); (int) ((flags & SCREEN_LOCK_TIME_MASK) >> SCREEN_LOCK_TIME_SHIFT);
mRequireRemoteWipe = 0 != (flags & REQUIRE_REMOTE_WIPE); mRequireRemoteWipe = 0 != (flags & REQUIRE_REMOTE_WIPE);
mPasswordExpiration = mPasswordExpirationDays =
(int) ((flags & PASSWORD_EXPIRATION_MASK) >> PASSWORD_EXPIRATION_SHIFT); (int) ((flags & PASSWORD_EXPIRATION_MASK) >> PASSWORD_EXPIRATION_SHIFT);
mPasswordHistory = mPasswordHistory =
(int) ((flags & PASSWORD_HISTORY_MASK) >> PASSWORD_HISTORY_SHIFT); (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 * Record flags (and a sync key for the flags) into an Account
* Note: the hash code is defined as the encoding used in 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(mMaxPasswordFails);
dest.writeInt(mMaxScreenLockTime); dest.writeInt(mMaxScreenLockTime);
dest.writeInt(mRequireRemoteWipe ? 1 : 0); dest.writeInt(mRequireRemoteWipe ? 1 : 0);
dest.writeInt(mPasswordExpiration); dest.writeInt(mPasswordExpirationDays);
dest.writeInt(mPasswordHistory); dest.writeInt(mPasswordHistory);
dest.writeInt(mPasswordComplexChars); dest.writeInt(mPasswordComplexChars);
} }
@ -648,7 +664,7 @@ public class SecurityPolicy {
mMaxPasswordFails = in.readInt(); mMaxPasswordFails = in.readInt();
mMaxScreenLockTime = in.readInt(); mMaxScreenLockTime = in.readInt();
mRequireRemoteWipe = in.readInt() == 1; mRequireRemoteWipe = in.readInt() == 1;
mPasswordExpiration = in.readInt(); mPasswordExpirationDays = in.readInt();
mPasswordHistory = in.readInt(); mPasswordHistory = in.readInt();
mPasswordComplexChars = in.readInt(); mPasswordComplexChars = in.readInt();
} }
@ -669,7 +685,7 @@ public class SecurityPolicy {
flags |= REQUIRE_REMOTE_WIPE; flags |= REQUIRE_REMOTE_WIPE;
} }
flags |= (long)mPasswordHistory << PASSWORD_HISTORY_SHIFT; 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; flags |= (long)mPasswordComplexChars << PASSWORD_COMPLEX_CHARS_SHIFT;
return flags; return flags;
} }
@ -679,7 +695,7 @@ public class SecurityPolicy {
return "{ " + "pw-len-min=" + mMinPasswordLength + " pw-mode=" + mPasswordMode return "{ " + "pw-len-min=" + mMinPasswordLength + " pw-mode=" + mPasswordMode
+ " pw-fails-max=" + mMaxPasswordFails + " screenlock-max=" + " pw-fails-max=" + mMaxPasswordFails + " screenlock-max="
+ mMaxScreenLockTime + " remote-wipe-req=" + mRequireRemoteWipe + mMaxScreenLockTime + " remote-wipe-req=" + mRequireRemoteWipe
+ " pw-expiration=" + mPasswordExpiration + " pw-expiration=" + mPasswordExpirationDays
+ " pw-history=" + mPasswordHistory + " pw-history=" + mPasswordHistory
+ " pw-complex-chars=" + mPasswordComplexChars + "}"; + " 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. * Device Policy administrator. This is primarily a listener for device state changes.
* Note: This is instantiated by incoming messages. * Note: This is instantiated by incoming messages.
@ -778,7 +927,20 @@ public class SecurityPolicy {
*/ */
@Override @Override
public void onPasswordChanged(Context context, Intent intent) { public void onPasswordChanged(Context context, Intent intent) {
// Clear security holds (if any)
Account.clearSecurityHoldOnAllAccounts(context); 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 context
* @param accountId the account for the mailbox * @param accountId the account to scrub
*/ */
public static void deleteAllAccountAttachmentFiles(Context context, long accountId) { public static void deleteAllAccountAttachmentFiles(Context context, long accountId) {
File[] files = getAttachmentDirectory(context, accountId).listFiles(); File[] files = getAttachmentDirectory(context, accountId).listFiles();
if (files == null) return;
for (File file : files) { for (File file : files) {
boolean result = file.delete(); boolean result = file.delete();
if (!result) { if (!result) {

View File

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

View File

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

View File

@ -17,10 +17,14 @@
package com.android.email; package com.android.email;
import com.android.email.SecurityPolicy.PolicySet; 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.EmailProvider;
import com.android.email.provider.ProviderTestUtils; import com.android.email.provider.ProviderTestUtils;
import com.android.email.provider.EmailContent.Account; import com.android.email.provider.EmailContent.Account;
import com.android.email.provider.EmailContent.AccountColumns; 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.ContentUris;
import android.content.ContentValues; import android.content.ContentValues;
@ -53,8 +57,9 @@ public class SecurityPolicyTests extends ProviderTestCase2<EmailProvider> {
@Override @Override
protected void setUp() throws Exception { protected void setUp() throws Exception {
super.setUp(); super.setUp();
mMockContext = new MockContext2(getMockContext(), this.mContext); 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.mMinPasswordLength);
assertEquals(0, ps.mMaxScreenLockTime); assertEquals(0, ps.mMaxScreenLockTime);
assertEquals(0, ps.mMaxPasswordFails); assertEquals(0, ps.mMaxPasswordFails);
assertEquals(0, ps.mPasswordExpiration); assertEquals(0, ps.mPasswordExpirationDays);
assertEquals(0, ps.mPasswordHistory); assertEquals(0, ps.mPasswordHistory);
assertEquals(0, ps.mPasswordComplexChars); assertEquals(0, ps.mPasswordComplexChars);
@ -167,7 +172,7 @@ public class SecurityPolicyTests extends ProviderTestCase2<EmailProvider> {
assertEquals(p3ain, p3aout); assertEquals(p3ain, p3aout);
// Repeat that test with fully-populated policies // 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); p3bin.writeAccount(a3, null, true, mMockContext);
PolicySet p3bout = sp.computeAggregatePolicy(); PolicySet p3bout = sp.computeAggregatePolicy();
assertNotNull(p3bout); 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 // 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 // fail count and lock timer - min logic - will *not* change because larger #s here
// wipe required - OR logic - will *not* change here because false // 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); PolicySet p4in = new PolicySet(20, PolicySet.PASSWORD_MODE_STRONG, 25, 26, false, 0, 5, 7);
Account a4 = ProviderTestUtils.setupAccount("sec-4", false, mMockContext); Account a4 = ProviderTestUtils.setupAccount("sec-4", false, mMockContext);
p4in.writeAccount(a4, null, true, 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(PolicySet.PASSWORD_MODE_STRONG, p4out.mPasswordMode);
assertEquals(15, p4out.mMaxPasswordFails); assertEquals(15, p4out.mMaxPasswordFails);
assertEquals(16, p4out.mMaxScreenLockTime); assertEquals(16, p4out.mMaxScreenLockTime);
assertEquals(1, p4out.mPasswordExpiration); assertEquals(6, p4out.mPasswordExpirationDays);
assertEquals(5, p4out.mPasswordHistory); assertEquals(5, p4out.mPasswordHistory);
assertEquals(7, p4out.mPasswordComplexChars); assertEquals(7, p4out.mPasswordComplexChars);
assertFalse(p4out.mRequireRemoteWipe); 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) // 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 // 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 // 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 // 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); Account a5 = ProviderTestUtils.setupAccount("sec-5", false, mMockContext);
p5in.writeAccount(a5, null, true, mMockContext); p5in.writeAccount(a5, null, true, mMockContext);
PolicySet p5out = sp.computeAggregatePolicy(); PolicySet p5out = sp.computeAggregatePolicy();
@ -205,7 +213,7 @@ public class SecurityPolicyTests extends ProviderTestCase2<EmailProvider> {
assertEquals(PolicySet.PASSWORD_MODE_STRONG, p5out.mPasswordMode); assertEquals(PolicySet.PASSWORD_MODE_STRONG, p5out.mPasswordMode);
assertEquals(5, p5out.mMaxPasswordFails); assertEquals(5, p5out.mMaxPasswordFails);
assertEquals(6, p5out.mMaxScreenLockTime); assertEquals(6, p5out.mMaxScreenLockTime);
assertEquals(6, p5out.mPasswordExpiration); assertEquals(1, p5out.mPasswordExpirationDays);
assertEquals(5, p4out.mPasswordHistory); assertEquals(5, p4out.mPasswordHistory);
assertEquals(7, p4out.mPasswordComplexChars); assertEquals(7, p4out.mPasswordComplexChars);
assertTrue(p5out.mRequireRemoteWipe); assertTrue(p5out.mRequireRemoteWipe);
@ -242,7 +250,7 @@ public class SecurityPolicyTests extends ProviderTestCase2<EmailProvider> {
assertEquals(PolicySet.PASSWORD_LENGTH_MAX, p.mMinPasswordLength); assertEquals(PolicySet.PASSWORD_LENGTH_MAX, p.mMinPasswordLength);
assertEquals(0, p.mMaxPasswordFails); assertEquals(0, p.mMaxPasswordFails);
assertEquals(0, p.mMaxScreenLockTime); assertEquals(0, p.mMaxScreenLockTime);
assertEquals(0, p.mPasswordExpiration); assertEquals(0, p.mPasswordExpirationDays);
assertEquals(0, p.mPasswordHistory); assertEquals(0, p.mPasswordHistory);
assertEquals(0, p.mPasswordComplexChars); assertEquals(0, p.mPasswordComplexChars);
assertFalse(p.mRequireRemoteWipe); assertFalse(p.mRequireRemoteWipe);
@ -252,7 +260,7 @@ public class SecurityPolicyTests extends ProviderTestCase2<EmailProvider> {
assertEquals(0, p.mMinPasswordLength); assertEquals(0, p.mMinPasswordLength);
assertEquals(0, p.mMaxPasswordFails); assertEquals(0, p.mMaxPasswordFails);
assertEquals(0, p.mMaxScreenLockTime); assertEquals(0, p.mMaxScreenLockTime);
assertEquals(0, p.mPasswordExpiration); assertEquals(0, p.mPasswordExpirationDays);
assertEquals(0, p.mPasswordHistory); assertEquals(0, p.mPasswordHistory);
assertEquals(0, p.mPasswordComplexChars); assertEquals(0, p.mPasswordComplexChars);
assertFalse(p.mRequireRemoteWipe); assertFalse(p.mRequireRemoteWipe);
@ -263,7 +271,7 @@ public class SecurityPolicyTests extends ProviderTestCase2<EmailProvider> {
assertEquals(0, p.mMinPasswordLength); assertEquals(0, p.mMinPasswordLength);
assertEquals(PolicySet.PASSWORD_MAX_FAILS_MAX, p.mMaxPasswordFails); assertEquals(PolicySet.PASSWORD_MAX_FAILS_MAX, p.mMaxPasswordFails);
assertEquals(0, p.mMaxScreenLockTime); assertEquals(0, p.mMaxScreenLockTime);
assertEquals(0, p.mPasswordExpiration); assertEquals(0, p.mPasswordExpirationDays);
assertEquals(0, p.mPasswordHistory); assertEquals(0, p.mPasswordHistory);
assertEquals(0, p.mPasswordComplexChars); assertEquals(0, p.mPasswordComplexChars);
assertFalse(p.mRequireRemoteWipe); assertFalse(p.mRequireRemoteWipe);
@ -274,7 +282,7 @@ public class SecurityPolicyTests extends ProviderTestCase2<EmailProvider> {
assertEquals(0, p.mMinPasswordLength); assertEquals(0, p.mMinPasswordLength);
assertEquals(0, p.mMaxPasswordFails); assertEquals(0, p.mMaxPasswordFails);
assertEquals(PolicySet.SCREEN_LOCK_TIME_MAX, p.mMaxScreenLockTime); assertEquals(PolicySet.SCREEN_LOCK_TIME_MAX, p.mMaxScreenLockTime);
assertEquals(0, p.mPasswordExpiration); assertEquals(0, p.mPasswordExpirationDays);
assertEquals(0, p.mPasswordHistory); assertEquals(0, p.mPasswordHistory);
assertEquals(0, p.mPasswordComplexChars); assertEquals(0, p.mPasswordComplexChars);
assertFalse(p.mRequireRemoteWipe); assertFalse(p.mRequireRemoteWipe);
@ -284,7 +292,7 @@ public class SecurityPolicyTests extends ProviderTestCase2<EmailProvider> {
assertEquals(0, p.mMinPasswordLength); assertEquals(0, p.mMinPasswordLength);
assertEquals(0, p.mMaxPasswordFails); assertEquals(0, p.mMaxPasswordFails);
assertEquals(0, p.mMaxScreenLockTime); assertEquals(0, p.mMaxScreenLockTime);
assertEquals(0, p.mPasswordExpiration); assertEquals(0, p.mPasswordExpirationDays);
assertEquals(0, p.mPasswordHistory); assertEquals(0, p.mPasswordHistory);
assertEquals(0, p.mPasswordComplexChars); assertEquals(0, p.mPasswordComplexChars);
assertTrue(p.mRequireRemoteWipe); assertTrue(p.mRequireRemoteWipe);
@ -295,7 +303,7 @@ public class SecurityPolicyTests extends ProviderTestCase2<EmailProvider> {
assertEquals(0, p.mMinPasswordLength); assertEquals(0, p.mMinPasswordLength);
assertEquals(0, p.mMaxPasswordFails); assertEquals(0, p.mMaxPasswordFails);
assertEquals(0, p.mMaxScreenLockTime); 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.mPasswordHistory);
assertEquals(0, p.mPasswordComplexChars); assertEquals(0, p.mPasswordComplexChars);
assertFalse(p.mRequireRemoteWipe); assertFalse(p.mRequireRemoteWipe);
@ -306,7 +314,7 @@ public class SecurityPolicyTests extends ProviderTestCase2<EmailProvider> {
assertEquals(0, p.mMinPasswordLength); assertEquals(0, p.mMinPasswordLength);
assertEquals(0, p.mMaxPasswordFails); assertEquals(0, p.mMaxPasswordFails);
assertEquals(0, p.mMaxScreenLockTime); assertEquals(0, p.mMaxScreenLockTime);
assertEquals(0, p.mPasswordExpiration); assertEquals(0, p.mPasswordExpirationDays);
assertEquals(PolicySet.PASSWORD_HISTORY_MAX, p.mPasswordHistory); assertEquals(PolicySet.PASSWORD_HISTORY_MAX, p.mPasswordHistory);
assertEquals(0, p.mPasswordComplexChars); assertEquals(0, p.mPasswordComplexChars);
assertFalse(p.mRequireRemoteWipe); assertFalse(p.mRequireRemoteWipe);
@ -317,7 +325,7 @@ public class SecurityPolicyTests extends ProviderTestCase2<EmailProvider> {
assertEquals(0, p.mMinPasswordLength); assertEquals(0, p.mMinPasswordLength);
assertEquals(0, p.mMaxPasswordFails); assertEquals(0, p.mMaxPasswordFails);
assertEquals(0, p.mMaxScreenLockTime); assertEquals(0, p.mMaxScreenLockTime);
assertEquals(0, p.mPasswordExpiration); assertEquals(0, p.mPasswordExpirationDays);
assertEquals(0, p.mPasswordHistory); assertEquals(0, p.mPasswordHistory);
assertEquals(PolicySet.PASSWORD_COMPLEX_CHARS_MAX, p.mPasswordComplexChars); assertEquals(PolicySet.PASSWORD_COMPLEX_CHARS_MAX, p.mPasswordComplexChars);
assertFalse(p.mRequireRemoteWipe); assertFalse(p.mRequireRemoteWipe);
@ -365,7 +373,7 @@ public class SecurityPolicyTests extends ProviderTestCase2<EmailProvider> {
// confirm clear until set // confirm clear until set
Account a1a = Account.restoreAccountWithId(mMockContext, a1.mId); Account a1a = Account.restoreAccountWithId(mMockContext, a1.mId);
assertEquals(Account.FLAGS_NOTIFY_NEW_MAIL, a1a.mFlags); 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); assertEquals(Account.FLAGS_NOTIFY_NEW_MAIL | Account.FLAGS_SECURITY_HOLD, a1.mFlags);
Account a1b = Account.restoreAccountWithId(mMockContext, a1.mId); Account a1b = Account.restoreAccountWithId(mMockContext, a1.mId);
assertEquals(Account.FLAGS_NOTIFY_NEW_MAIL | Account.FLAGS_SECURITY_HOLD, a1b.mFlags); 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 // confirm set until cleared
Account a2a = Account.restoreAccountWithId(mMockContext, a2.mId); Account a2a = Account.restoreAccountWithId(mMockContext, a2.mId);
assertEquals(Account.FLAGS_VIBRATE_ALWAYS | Account.FLAGS_SECURITY_HOLD, a2a.mFlags); 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); assertEquals(Account.FLAGS_VIBRATE_ALWAYS, a2.mFlags);
Account a2b = Account.restoreAccountWithId(mMockContext, a2.mId); Account a2b = Account.restoreAccountWithId(mMockContext, a2.mId);
assertEquals(Account.FLAGS_VIBRATE_ALWAYS, a2b.mFlags); assertEquals(Account.FLAGS_VIBRATE_ALWAYS, a2b.mFlags);
} }
private static class MockController extends Controller { // private static class MockController extends Controller {
protected MockController(Context context) { // protected MockController(Context context) {
super(context); // super(context);
} // }
} // }
/** /**
* Test the response to disabling DeviceAdmin status * 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() { public void testDisableAdmin() {
Account a1 = ProviderTestUtils.setupAccount("disable-1", false, mMockContext); 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 // Simulate revoke of device admin; directly call deleteSecuredAccounts, which is normally
// called from a background thread // called from a background thread
MockController mockController = new MockController(mMockContext); // MockController mockController = new MockController(mMockContext);
Controller.injectMockControllerForTest(mockController); // Controller.injectMockControllerForTest(mockController);
try { // try {
sp.deleteSecuredAccounts(mMockContext); // sp.deleteSecuredAccounts(mMockContext);
PolicySet after2 = sp.getAggregatePolicy(); // PolicySet after2 = sp.getAggregatePolicy();
assertEquals(SecurityPolicy.NO_POLICY_SET, after2); // assertEquals(SecurityPolicy.NO_POLICY_SET, after2);
Account a1b = Account.restoreAccountWithId(mMockContext, a1.mId); // Account a1b = Account.restoreAccountWithId(mMockContext, a1.mId);
assertNull(a1b); // assertNull(a1b);
Account a2b = Account.restoreAccountWithId(mMockContext, a2.mId); // Account a2b = Account.restoreAccountWithId(mMockContext, a2.mId);
assertNull(a2b); // assertNull(a2b);
Account a3b = Account.restoreAccountWithId(mMockContext, a3.mId); // Account a3b = Account.restoreAccountWithId(mMockContext, a3.mId);
assertNull(a3b.mSecuritySyncKey); // assertNull(a3b.mSecuritySyncKey);
} finally { // } finally {
Controller.injectMockControllerForTest(null); // 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);
}
} }