SecurityPolicy.java revision d09cff08882e553afce919865a2cc60b657d4659
1/* 2 * Copyright (C) 2010 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.email; 18 19import com.android.email.service.EmailBroadcastProcessorService; 20import com.android.emailcommon.Logging; 21import com.android.emailcommon.provider.Account; 22import com.android.emailcommon.provider.EmailContent; 23import com.android.emailcommon.provider.EmailContent.AccountColumns; 24import com.android.emailcommon.provider.EmailContent.PolicyColumns; 25import com.android.emailcommon.provider.Policy; 26import com.android.emailcommon.utility.Utility; 27import com.google.common.annotations.VisibleForTesting; 28 29import android.app.admin.DeviceAdminInfo; 30import android.app.admin.DeviceAdminReceiver; 31import android.app.admin.DevicePolicyManager; 32import android.content.ComponentName; 33import android.content.ContentResolver; 34import android.content.ContentValues; 35import android.content.Context; 36import android.content.Intent; 37import android.database.Cursor; 38import android.os.Environment; 39import android.util.Log; 40 41/** 42 * Utility functions to support reading and writing security policies, and handshaking the device 43 * into and out of various security states. 44 */ 45public class SecurityPolicy { 46 private static final String TAG = "Email/SecurityPolicy"; 47 private static SecurityPolicy sInstance = null; 48 private Context mContext; 49 private DevicePolicyManager mDPM; 50 private final ComponentName mAdminName; 51 private Policy mAggregatePolicy; 52 53 // Messages used for DevicePolicyManager callbacks 54 private static final int DEVICE_ADMIN_MESSAGE_ENABLED = 1; 55 private static final int DEVICE_ADMIN_MESSAGE_DISABLED = 2; 56 private static final int DEVICE_ADMIN_MESSAGE_PASSWORD_CHANGED = 3; 57 private static final int DEVICE_ADMIN_MESSAGE_PASSWORD_EXPIRING = 4; 58 59 private static final String HAS_PASSWORD_EXPIRATION = 60 PolicyColumns.PASSWORD_EXPIRATION_DAYS + ">0"; 61 62 /** 63 * Get the security policy instance 64 */ 65 public synchronized static SecurityPolicy getInstance(Context context) { 66 if (sInstance == null) { 67 sInstance = new SecurityPolicy(context.getApplicationContext()); 68 } 69 return sInstance; 70 } 71 72 /** 73 * Private constructor (one time only) 74 */ 75 private SecurityPolicy(Context context) { 76 mContext = context.getApplicationContext(); 77 mDPM = null; 78 mAdminName = new ComponentName(context, PolicyAdmin.class); 79 mAggregatePolicy = null; 80 } 81 82 /** 83 * For testing only: Inject context into already-created instance 84 */ 85 /* package */ void setContext(Context context) { 86 mContext = context; 87 } 88 89 /** 90 * Compute the aggregate policy for all accounts that require it, and record it. 91 * 92 * The business logic is as follows: 93 * min password length take the max 94 * password mode take the max (strongest mode) 95 * max password fails take the min 96 * max screen lock time take the min 97 * require remote wipe take the max (logical or) 98 * password history take the max (strongest mode) 99 * password expiration take the min (strongest mode) 100 * password complex chars take the max (strongest mode) 101 * encryption take the max (logical or) 102 * encryption (external) take the max (logical or) 103 * 104 * @return a policy representing the strongest aggregate. If no policy sets are defined, 105 * a lightweight "nothing required" policy will be returned. Never null. 106 */ 107 @VisibleForTesting 108 Policy computeAggregatePolicy() { 109 boolean policiesFound = false; 110 Policy aggregate = new Policy(); 111 aggregate.mPasswordMinLength = Integer.MIN_VALUE; 112 aggregate.mPasswordMode = Integer.MIN_VALUE; 113 aggregate.mPasswordMaxFails = Integer.MAX_VALUE; 114 aggregate.mPasswordHistory = Integer.MIN_VALUE; 115 aggregate.mPasswordExpirationDays = Integer.MAX_VALUE; 116 aggregate.mPasswordComplexChars = Integer.MIN_VALUE; 117 aggregate.mMaxScreenLockTime = Integer.MAX_VALUE; 118 aggregate.mRequireRemoteWipe = false; 119 aggregate.mRequireEncryption = false; 120 aggregate.mRequireEncryptionExternal = false; 121 122 Cursor c = mContext.getContentResolver().query(Policy.CONTENT_URI, 123 Policy.CONTENT_PROJECTION, null, null, null); 124 Policy policy = new Policy(); 125 try { 126 while (c.moveToNext()) { 127 policy.restore(c); 128 if (Email.DEBUG) { 129 Log.d(TAG, "Aggregate from: " + policy); 130 } 131 aggregate.mPasswordMinLength = 132 Math.max(policy.mPasswordMinLength, aggregate.mPasswordMinLength); 133 aggregate.mPasswordMode = Math.max(policy.mPasswordMode, aggregate.mPasswordMode); 134 if (policy.mPasswordMaxFails > 0) { 135 aggregate.mPasswordMaxFails = 136 Math.min(policy.mPasswordMaxFails, aggregate.mPasswordMaxFails); 137 } 138 if (policy.mMaxScreenLockTime > 0) { 139 aggregate.mMaxScreenLockTime = Math.min(policy.mMaxScreenLockTime, 140 aggregate.mMaxScreenLockTime); 141 } 142 if (policy.mPasswordHistory > 0) { 143 aggregate.mPasswordHistory = 144 Math.max(policy.mPasswordHistory, aggregate.mPasswordHistory); 145 } 146 if (policy.mPasswordExpirationDays > 0) { 147 aggregate.mPasswordExpirationDays = 148 Math.min(policy.mPasswordExpirationDays, aggregate.mPasswordExpirationDays); 149 } 150 if (policy.mPasswordComplexChars > 0) { 151 aggregate.mPasswordComplexChars = Math.max(policy.mPasswordComplexChars, 152 aggregate.mPasswordComplexChars); 153 } 154 aggregate.mRequireRemoteWipe |= policy.mRequireRemoteWipe; 155 aggregate.mRequireEncryption |= policy.mRequireEncryption; 156 aggregate.mRequireEncryptionExternal |= policy.mRequireEncryptionExternal; 157 aggregate.mDontAllowCamera |= policy.mDontAllowCamera; 158 policiesFound = true; 159 } 160 } finally { 161 c.close(); 162 } 163 if (policiesFound) { 164 // final cleanup pass converts any untouched min/max values to zero (not specified) 165 if (aggregate.mPasswordMinLength == Integer.MIN_VALUE) aggregate.mPasswordMinLength = 0; 166 if (aggregate.mPasswordMode == Integer.MIN_VALUE) aggregate.mPasswordMode = 0; 167 if (aggregate.mPasswordMaxFails == Integer.MAX_VALUE) aggregate.mPasswordMaxFails = 0; 168 if (aggregate.mMaxScreenLockTime == Integer.MAX_VALUE) aggregate.mMaxScreenLockTime = 0; 169 if (aggregate.mPasswordHistory == Integer.MIN_VALUE) aggregate.mPasswordHistory = 0; 170 if (aggregate.mPasswordExpirationDays == Integer.MAX_VALUE) 171 aggregate.mPasswordExpirationDays = 0; 172 if (aggregate.mPasswordComplexChars == Integer.MIN_VALUE) 173 aggregate.mPasswordComplexChars = 0; 174 if (Email.DEBUG) { 175 Log.d(TAG, "Calculated Aggregate: " + aggregate); 176 } 177 return aggregate; 178 } 179 if (Email.DEBUG) { 180 Log.d(TAG, "Calculated Aggregate: no policy"); 181 } 182 return Policy.NO_POLICY; 183 } 184 185 /** 186 * Return updated aggregate policy, from cached value if possible 187 */ 188 public synchronized Policy getAggregatePolicy() { 189 if (mAggregatePolicy == null) { 190 mAggregatePolicy = computeAggregatePolicy(); 191 } 192 return mAggregatePolicy; 193 } 194 195 /** 196 * Get the dpm. This mainly allows us to make some utility calls without it, for testing. 197 */ 198 /* package */ synchronized DevicePolicyManager getDPM() { 199 if (mDPM == null) { 200 mDPM = (DevicePolicyManager) mContext.getSystemService(Context.DEVICE_POLICY_SERVICE); 201 } 202 return mDPM; 203 } 204 205 /** 206 * API: Report that policies may have been updated due to rewriting values in an Account. 207 * @param accountId the account that has been updated, -1 if unknown/deleted 208 */ 209 public synchronized void policiesUpdated(long accountId) { 210 mAggregatePolicy = null; 211 } 212 213 /** 214 * API: Report that policies may have been updated *and* the caller vouches that the 215 * change is a reduction in policies. This forces an immediate change to device state. 216 * Typically used when deleting accounts, although we may use it for server-side policy 217 * rollbacks. 218 */ 219 public void reducePolicies() { 220 if (Email.DEBUG) { 221 Log.d(TAG, "reducePolicies"); 222 } 223 policiesUpdated(-1); 224 setActivePolicies(); 225 } 226 227 /** 228 * API: Query if the proposed set of policies are supported on the device. 229 * 230 * @param policy the polices that were requested 231 * @return boolean if supported 232 */ 233 public boolean isSupported(Policy policy) { 234 // IMPLEMENTATION: At this time, the only policy which might not be supported is 235 // encryption (which requires low-level systems support). Other policies are fully 236 // supported by the framework and do not need to be checked. 237 if (policy.mRequireEncryption) { 238 int encryptionStatus = getDPM().getStorageEncryptionStatus(); 239 if (encryptionStatus == DevicePolicyManager.ENCRYPTION_STATUS_UNSUPPORTED) { 240 return false; 241 } 242 } 243 if (policy.mRequireEncryptionExternal) { 244 // At this time, we only support "external encryption" when it is provided by virtue 245 // of emulating the external storage inside an encrypted device. 246 if (!policy.mRequireEncryption) return false; 247 if (Environment.isExternalStorageRemovable()) return false; 248 if (!Environment.isExternalStorageEmulated()) return false; 249 } 250 251 // If we ever support devices that can't disable cameras for any reason, we should 252 // indicate as such in the mDontAllowCamera policy 253 254 return true; 255 } 256 257 /** 258 * API: Remove any unsupported policies 259 * 260 * This is used when we have a set of polices that have been requested, but the server 261 * is willing to allow unsupported policies to be considered optional. 262 * 263 * @param policy the polices that were requested 264 * @return the same PolicySet if all are supported; A replacement PolicySet if any 265 * unsupported policies were removed 266 */ 267 public Policy clearUnsupportedPolicies(Policy policy) { 268 // IMPLEMENTATION: At this time, the only policy which might not be supported is 269 // encryption (which requires low-level systems support). Other policies are fully 270 // supported by the framework and do not need to be checked. 271 if (policy.mRequireEncryption) { 272 int encryptionStatus = getDPM().getStorageEncryptionStatus(); 273 if (encryptionStatus == DevicePolicyManager.ENCRYPTION_STATUS_UNSUPPORTED) { 274 policy.mRequireEncryption = false; 275 } 276 } 277 // At this time, we only support "external encryption" when it is provided by virtue 278 // of emulating the external storage inside an encrypted device. 279 if (policy.mRequireEncryptionExternal) { 280 if (Environment.isExternalStorageRemovable() 281 || !Environment.isExternalStorageEmulated()) { 282 policy.mRequireEncryptionExternal = false; 283 } 284 } 285 286 // If we ever support devices that can't disable cameras for any reason, we should 287 // clear the mDontAllowCamera policy 288 289 return policy; 290 } 291 292 /** 293 * API: Query used to determine if a given policy is "active" (the device is operating at 294 * the required security level). 295 * 296 * @param policy the policies requested, or null to check aggregate stored policies 297 * @return true if the requested policies are active, false if not. 298 */ 299 public boolean isActive(Policy policy) { 300 int reasons = getInactiveReasons(policy); 301 if (Email.DEBUG && (reasons != 0)) { 302 StringBuilder sb = new StringBuilder("isActive for " + policy + ": "); 303 if (reasons == 0) { 304 sb.append("true"); 305 } else { 306 sb.append("FALSE -> "); 307 } 308 if ((reasons & INACTIVE_NEED_ACTIVATION) != 0) { 309 sb.append("no_admin "); 310 } 311 if ((reasons & INACTIVE_NEED_CONFIGURATION) != 0) { 312 sb.append("config "); 313 } 314 if ((reasons & INACTIVE_NEED_PASSWORD) != 0) { 315 sb.append("password "); 316 } 317 if ((reasons & INACTIVE_NEED_ENCRYPTION) != 0) { 318 sb.append("encryption "); 319 } 320 Log.d(TAG, sb.toString()); 321 } 322 return reasons == 0; 323 } 324 325 /** 326 * Return bits from isActive: Device Policy Manager has not been activated 327 */ 328 public final static int INACTIVE_NEED_ACTIVATION = 1; 329 330 /** 331 * Return bits from isActive: Some required configuration is not correct (no user action). 332 */ 333 public final static int INACTIVE_NEED_CONFIGURATION = 2; 334 335 /** 336 * Return bits from isActive: Password needs to be set or updated 337 */ 338 public final static int INACTIVE_NEED_PASSWORD = 4; 339 340 /** 341 * Return bits from isActive: Encryption has not be enabled 342 */ 343 public final static int INACTIVE_NEED_ENCRYPTION = 8; 344 345 /** 346 * API: Query used to determine if a given policy is "active" (the device is operating at 347 * the required security level). 348 * 349 * This can be used when syncing a specific account, by passing a specific set of policies 350 * for that account. Or, it can be used at any time to compare the device 351 * state against the aggregate set of device policies stored in all accounts. 352 * 353 * This method is for queries only, and does not trigger any change in device state. 354 * 355 * NOTE: If there are multiple accounts with password expiration policies, the device 356 * password will be set to expire in the shortest required interval (most secure). This method 357 * will return 'false' as soon as the password expires - irrespective of which account caused 358 * the expiration. In other words, all accounts (that require expiration) will run/stop 359 * based on the requirements of the account with the shortest interval. 360 * 361 * @param policy the policies requested, or null to check aggregate stored policies 362 * @return zero if the requested policies are active, non-zero bits indicates that more work 363 * is needed (typically, by the user) before the required security polices are fully active. 364 */ 365 public int getInactiveReasons(Policy policy) { 366 // select aggregate set if needed 367 if (policy == null) { 368 policy = getAggregatePolicy(); 369 } 370 // quick check for the "empty set" of no policies 371 if (policy == Policy.NO_POLICY) { 372 return 0; 373 } 374 int reasons = 0; 375 DevicePolicyManager dpm = getDPM(); 376 if (isActiveAdmin()) { 377 // check each policy explicitly 378 if (policy.mPasswordMinLength > 0) { 379 if (dpm.getPasswordMinimumLength(mAdminName) < policy.mPasswordMinLength) { 380 reasons |= INACTIVE_NEED_PASSWORD; 381 } 382 } 383 if (policy.mPasswordMode > 0) { 384 if (dpm.getPasswordQuality(mAdminName) < policy.getDPManagerPasswordQuality()) { 385 reasons |= INACTIVE_NEED_PASSWORD; 386 } 387 if (!dpm.isActivePasswordSufficient()) { 388 reasons |= INACTIVE_NEED_PASSWORD; 389 } 390 } 391 if (policy.mMaxScreenLockTime > 0) { 392 // Note, we use seconds, dpm uses milliseconds 393 if (dpm.getMaximumTimeToLock(mAdminName) > policy.mMaxScreenLockTime * 1000) { 394 reasons |= INACTIVE_NEED_CONFIGURATION; 395 } 396 } 397 if (policy.mPasswordExpirationDays > 0) { 398 // confirm that expirations are currently set 399 long currentTimeout = dpm.getPasswordExpirationTimeout(mAdminName); 400 if (currentTimeout == 0 401 || currentTimeout > policy.getDPManagerPasswordExpirationTimeout()) { 402 reasons |= INACTIVE_NEED_PASSWORD; 403 } 404 // confirm that the current password hasn't expired 405 long expirationDate = dpm.getPasswordExpiration(mAdminName); 406 long timeUntilExpiration = expirationDate - System.currentTimeMillis(); 407 boolean expired = timeUntilExpiration < 0; 408 if (expired) { 409 reasons |= INACTIVE_NEED_PASSWORD; 410 } 411 } 412 if (policy.mPasswordHistory > 0) { 413 if (dpm.getPasswordHistoryLength(mAdminName) < policy.mPasswordHistory) { 414 reasons |= INACTIVE_NEED_PASSWORD; 415 } 416 } 417 if (policy.mPasswordComplexChars > 0) { 418 if (dpm.getPasswordMinimumNonLetter(mAdminName) < policy.mPasswordComplexChars) { 419 reasons |= INACTIVE_NEED_PASSWORD; 420 } 421 } 422 if (policy.mRequireEncryption) { 423 int encryptionStatus = getDPM().getStorageEncryptionStatus(); 424 if (encryptionStatus != DevicePolicyManager.ENCRYPTION_STATUS_ACTIVE) { 425 reasons |= INACTIVE_NEED_ENCRYPTION; 426 } 427 } 428 // TODO: If we ever support external storage encryption as a first-class feature, 429 // it will need to be checked here. For now, if there is a policy request for 430 // external storage encryption, it's sufficient that we've activated internal 431 // storage encryption. 432 433 // password failures are counted locally - no test required here 434 // no check required for remote wipe (it's supported, if we're the admin) 435 436 // If we made it all the way, reasons == 0 here. Otherwise it's a list of grievances. 437 return reasons; 438 } 439 // return false, not active 440 return INACTIVE_NEED_ACTIVATION; 441 } 442 443 /** 444 * Set the requested security level based on the aggregate set of requests. 445 * If the set is empty, we release our device administration. If the set is non-empty, 446 * we only proceed if we are already active as an admin. 447 */ 448 public void setActivePolicies() { 449 DevicePolicyManager dpm = getDPM(); 450 // compute aggregate set of policies 451 Policy aggregatePolicy = getAggregatePolicy(); 452 // if empty set, detach from policy manager 453 if (aggregatePolicy == Policy.NO_POLICY) { 454 if (Email.DEBUG) { 455 Log.d(TAG, "setActivePolicies: none, remove admin"); 456 } 457 dpm.removeActiveAdmin(mAdminName); 458 } else if (isActiveAdmin()) { 459 if (Email.DEBUG) { 460 Log.d(TAG, "setActivePolicies: " + aggregatePolicy); 461 } 462 // set each policy in the policy manager 463 // password mode & length 464 dpm.setPasswordQuality(mAdminName, aggregatePolicy.getDPManagerPasswordQuality()); 465 dpm.setPasswordMinimumLength(mAdminName, aggregatePolicy.mPasswordMinLength); 466 // screen lock time 467 dpm.setMaximumTimeToLock(mAdminName, aggregatePolicy.mMaxScreenLockTime * 1000); 468 // local wipe (failed passwords limit) 469 dpm.setMaximumFailedPasswordsForWipe(mAdminName, aggregatePolicy.mPasswordMaxFails); 470 // password expiration (days until a password expires). API takes mSec. 471 dpm.setPasswordExpirationTimeout(mAdminName, 472 aggregatePolicy.getDPManagerPasswordExpirationTimeout()); 473 // password history length (number of previous passwords that may not be reused) 474 dpm.setPasswordHistoryLength(mAdminName, aggregatePolicy.mPasswordHistory); 475 // password minimum complex characters. 476 // Note, in Exchange, "complex chars" simply means "non alpha", but in the DPM, 477 // setting the quality to complex also defaults min symbols=1 and min numeric=1. 478 // We always / safely clear minSymbols & minNumeric to zero (there is no policy 479 // configuration in which we explicitly require a minimum number of digits or symbols.) 480 dpm.setPasswordMinimumSymbols(mAdminName, 0); 481 dpm.setPasswordMinimumNumeric(mAdminName, 0); 482 dpm.setPasswordMinimumNonLetter(mAdminName, aggregatePolicy.mPasswordComplexChars); 483 // Device capabilities 484 dpm.setCameraDisabled(mAdminName, aggregatePolicy.mDontAllowCamera); 485 486 // encryption required 487 dpm.setStorageEncryption(mAdminName, aggregatePolicy.mRequireEncryption); 488 // TODO: If we ever support external storage encryption as a first-class feature, 489 // it will need to be set here. For now, if there is a policy request for 490 // external storage encryption, it's sufficient that we've activated internal 491 // storage encryption. 492 } 493 } 494 495 /** 496 * Convenience method; see javadoc below 497 */ 498 public static void setAccountHoldFlag(Context context, long accountId, boolean newState) { 499 Account account = Account.restoreAccountWithId(context, accountId); 500 if (account != null) { 501 setAccountHoldFlag(context, account, newState); 502 } 503 } 504 505 /** 506 * API: Set/Clear the "hold" flag in any account. This flag serves a dual purpose: 507 * Setting it gives us an indication that it was blocked, and clearing it gives EAS a 508 * signal to try syncing again. 509 * @param context 510 * @param account the account whose hold flag is to be set/cleared 511 * @param newState true = security hold, false = free to sync 512 */ 513 public static void setAccountHoldFlag(Context context, Account account, boolean newState) { 514 if (newState) { 515 account.mFlags |= Account.FLAGS_SECURITY_HOLD; 516 } else { 517 account.mFlags &= ~Account.FLAGS_SECURITY_HOLD; 518 } 519 ContentValues cv = new ContentValues(); 520 cv.put(AccountColumns.FLAGS, account.mFlags); 521 account.update(context, cv); 522 } 523 524 /** 525 * API: Sync service should call this any time a sync fails due to isActive() returning false. 526 * This will kick off the notify-acquire-admin-state process and/or increase the security level. 527 * The caller needs to write the required policies into this account before making this call. 528 * Should not be called from UI thread - uses DB lookups to prepare new notifications 529 * 530 * @param accountId the account for which sync cannot proceed 531 */ 532 public void policiesRequired(long accountId) { 533 Account account = Account.restoreAccountWithId(mContext, accountId); 534 // In case the account has been deleted, just return 535 if (account == null) return; 536 if (Email.DEBUG) { 537 if (account.mPolicyKey == 0) { 538 Log.d(TAG, "policiesRequired for " + account.mDisplayName + ": none"); 539 } else { 540 Policy policy = Policy.restorePolicyWithId(mContext, account.mPolicyKey); 541 if (policy == null) { 542 Log.w(TAG, "No policy??"); 543 } else { 544 Log.d(TAG, "policiesRequired for " + account.mDisplayName + ": " + policy); 545 } 546 } 547 } 548 549 // Mark the account as "on hold". 550 setAccountHoldFlag(mContext, account, true); 551 552 // Put up a notification 553 NotificationController.getInstance(mContext).showSecurityNeededNotification(account); 554 } 555 556 /** 557 * Called from the notification's intent receiver to register that the notification can be 558 * cleared now. 559 */ 560 public void clearNotification(long accountId) { 561 NotificationController.getInstance(mContext).cancelSecurityNeededNotification(); 562 } 563 564 /** 565 * API: Remote wipe (from server). This is final, there is no confirmation. It will only 566 * return to the caller if there is an unexpected failure. 567 */ 568 public void remoteWipe() { 569 DevicePolicyManager dpm = getDPM(); 570 if (dpm.isAdminActive(mAdminName)) { 571 dpm.wipeData(0); 572 } else { 573 Log.d(Logging.LOG_TAG, "Could not remote wipe because not device admin."); 574 } 575 } 576 /** 577 * If we are not the active device admin, try to become so. 578 * 579 * Also checks for any policies that we have added during the lifetime of this app. 580 * This catches the case where the user granted an earlier (smaller) set of policies 581 * but an app upgrade requires that new policies be granted. 582 * 583 * @return true if we are already active, false if we are not 584 */ 585 public boolean isActiveAdmin() { 586 DevicePolicyManager dpm = getDPM(); 587 return dpm.isAdminActive(mAdminName) 588 && dpm.hasGrantedPolicy(mAdminName, DeviceAdminInfo.USES_POLICY_EXPIRE_PASSWORD) 589 && dpm.hasGrantedPolicy(mAdminName, DeviceAdminInfo.USES_ENCRYPTED_STORAGE); 590 } 591 592 /** 593 * Report admin component name - for making calls into device policy manager 594 */ 595 public ComponentName getAdminComponent() { 596 return mAdminName; 597 } 598 599 /** 600 * Delete all accounts whose security flags aren't zero (i.e. they have security enabled). 601 * This method is synchronous, so it should normally be called within a worker thread (the 602 * exception being for unit tests) 603 * 604 * @param context the caller's context 605 */ 606 /*package*/ void deleteSecuredAccounts(Context context) { 607 ContentResolver cr = context.getContentResolver(); 608 // Find all accounts with security and delete them 609 Cursor c = cr.query(Account.CONTENT_URI, EmailContent.ID_PROJECTION, 610 Account.SECURITY_NONZERO_SELECTION, null, null); 611 try { 612 Log.w(TAG, "Email administration disabled; deleting " + c.getCount() + 613 " secured account(s)"); 614 while (c.moveToNext()) { 615 Controller.getInstance(context).deleteAccountSync( 616 c.getLong(EmailContent.ID_PROJECTION_COLUMN), context); 617 } 618 } finally { 619 c.close(); 620 } 621 policiesUpdated(-1); 622 } 623 624 /** 625 * Internal handler for enabled->disabled transitions. Deletes all secured accounts. 626 * Must call from worker thread, not on UI thread. 627 */ 628 /*package*/ void onAdminEnabled(boolean isEnabled) { 629 if (!isEnabled) { 630 deleteSecuredAccounts(mContext); 631 } 632 } 633 634 /** 635 * Handle password expiration - if any accounts appear to have triggered this, put up 636 * warnings, or even shut them down. 637 * 638 * NOTE: If there are multiple accounts with password expiration policies, the device 639 * password will be set to expire in the shortest required interval (most secure). The logic 640 * in this method operates based on the aggregate setting - irrespective of which account caused 641 * the expiration. In other words, all accounts (that require expiration) will run/stop 642 * based on the requirements of the account with the shortest interval. 643 */ 644 private void onPasswordExpiring(Context context) { 645 // 1. Do we have any accounts that matter here? 646 long nextExpiringAccountId = findShortestExpiration(context); 647 648 // 2. If not, exit immediately 649 if (nextExpiringAccountId == -1) { 650 return; 651 } 652 653 // 3. If yes, are we warning or expired? 654 long expirationDate = getDPM().getPasswordExpiration(mAdminName); 655 long timeUntilExpiration = expirationDate - System.currentTimeMillis(); 656 boolean expired = timeUntilExpiration < 0; 657 if (!expired) { 658 // 4. If warning, simply put up a generic notification and report that it came from 659 // the shortest-expiring account. 660 NotificationController.getInstance(mContext).showPasswordExpiringNotification( 661 nextExpiringAccountId); 662 } else { 663 // 5. Actually expired - find all accounts that expire passwords, and wipe them 664 boolean wiped = wipeExpiredAccounts(context, Controller.getInstance(context)); 665 if (wiped) { 666 NotificationController.getInstance(mContext).showPasswordExpiredNotification( 667 nextExpiringAccountId); 668 } 669 } 670 } 671 672 /** 673 * Find the account with the shortest expiration time. This is always assumed to be 674 * the account that forces the password to be refreshed. 675 * @return -1 if no expirations, or accountId if one is found 676 */ 677 @VisibleForTesting 678 /*package*/ static long findShortestExpiration(Context context) { 679 long policyId = Utility.getFirstRowLong(context, Policy.CONTENT_URI, Policy.ID_PROJECTION, 680 HAS_PASSWORD_EXPIRATION, null, PolicyColumns.PASSWORD_EXPIRATION_DAYS + " ASC", 681 EmailContent.ID_PROJECTION_COLUMN, -1L); 682 if (policyId < 0) return -1L; 683 return Policy.getAccountIdWithPolicyKey(context, policyId); 684 } 685 686 /** 687 * For all accounts that require password expiration, put them in security hold and wipe 688 * their data. 689 * @param context 690 * @param controller 691 * @return true if one or more accounts were wiped 692 */ 693 @VisibleForTesting 694 /*package*/ static boolean wipeExpiredAccounts(Context context, Controller controller) { 695 boolean result = false; 696 Cursor c = context.getContentResolver().query(Policy.CONTENT_URI, 697 Policy.ID_PROJECTION, HAS_PASSWORD_EXPIRATION, null, null); 698 try { 699 while (c.moveToNext()) { 700 long policyId = c.getLong(Policy.ID_PROJECTION_COLUMN); 701 long accountId = Policy.getAccountIdWithPolicyKey(context, policyId); 702 if (accountId < 0) continue; 703 Account account = Account.restoreAccountWithId(context, accountId); 704 if (account != null) { 705 // Mark the account as "on hold". 706 setAccountHoldFlag(context, account, true); 707 // Erase data 708 controller.deleteSyncedDataSync(accountId); 709 // Report one or more were found 710 result = true; 711 } 712 } 713 } finally { 714 c.close(); 715 } 716 return result; 717 } 718 719 /** 720 * Callback from EmailBroadcastProcessorService. This provides the workers for the 721 * DeviceAdminReceiver calls. These should perform the work directly and not use async 722 * threads for completion. 723 */ 724 public static void onDeviceAdminReceiverMessage(Context context, int message) { 725 SecurityPolicy instance = SecurityPolicy.getInstance(context); 726 switch (message) { 727 case DEVICE_ADMIN_MESSAGE_ENABLED: 728 instance.onAdminEnabled(true); 729 break; 730 case DEVICE_ADMIN_MESSAGE_DISABLED: 731 instance.onAdminEnabled(false); 732 break; 733 case DEVICE_ADMIN_MESSAGE_PASSWORD_CHANGED: 734 // TODO make a small helper for this 735 // Clear security holds (if any) 736 Account.clearSecurityHoldOnAllAccounts(context); 737 // Cancel any active notifications (if any are posted) 738 NotificationController.getInstance(context).cancelPasswordExpirationNotifications(); 739 break; 740 case DEVICE_ADMIN_MESSAGE_PASSWORD_EXPIRING: 741 instance.onPasswordExpiring(instance.mContext); 742 break; 743 } 744 } 745 746 /** 747 * Device Policy administrator. This is primarily a listener for device state changes. 748 * Note: This is instantiated by incoming messages. 749 * Note: This is actually a BroadcastReceiver and must remain within the guidelines required 750 * for proper behavior, including avoidance of ANRs. 751 * Note: We do not implement onPasswordFailed() because the default behavior of the 752 * DevicePolicyManager - complete local wipe after 'n' failures - is sufficient. 753 */ 754 public static class PolicyAdmin extends DeviceAdminReceiver { 755 756 /** 757 * Called after the administrator is first enabled. 758 */ 759 @Override 760 public void onEnabled(Context context, Intent intent) { 761 EmailBroadcastProcessorService.processDevicePolicyMessage(context, 762 DEVICE_ADMIN_MESSAGE_ENABLED); 763 } 764 765 /** 766 * Called prior to the administrator being disabled. 767 */ 768 @Override 769 public void onDisabled(Context context, Intent intent) { 770 EmailBroadcastProcessorService.processDevicePolicyMessage(context, 771 DEVICE_ADMIN_MESSAGE_DISABLED); 772 } 773 774 /** 775 * Called when the user asks to disable administration; we return a warning string that 776 * will be presented to the user 777 */ 778 @Override 779 public CharSequence onDisableRequested(Context context, Intent intent) { 780 return context.getString(R.string.disable_admin_warning); 781 } 782 783 /** 784 * Called after the user has changed their password. 785 */ 786 @Override 787 public void onPasswordChanged(Context context, Intent intent) { 788 EmailBroadcastProcessorService.processDevicePolicyMessage(context, 789 DEVICE_ADMIN_MESSAGE_PASSWORD_CHANGED); 790 } 791 792 /** 793 * Called when device password is expiring 794 */ 795 @Override 796 public void onPasswordExpiring(Context context, Intent intent) { 797 EmailBroadcastProcessorService.processDevicePolicyMessage(context, 798 DEVICE_ADMIN_MESSAGE_PASSWORD_EXPIRING); 799 } 800 } 801} 802