1 /* 2 * Copyright (C) 2022 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 17 package com.android.server.healthconnect.permission; 18 19 import static android.Manifest.permission.INTERACT_ACROSS_USERS_FULL; 20 import static android.content.pm.PackageManager.PERMISSION_GRANTED; 21 22 import android.annotation.NonNull; 23 import android.annotation.Nullable; 24 import android.app.ActivityManager; 25 import android.content.Context; 26 import android.content.Intent; 27 import android.content.pm.PackageInfo; 28 import android.content.pm.PackageManager; 29 import android.health.connect.HealthConnectManager; 30 import android.health.connect.HealthPermissions; 31 import android.os.Binder; 32 import android.os.UserHandle; 33 import android.util.ArrayMap; 34 import android.util.ArraySet; 35 36 import com.android.server.healthconnect.storage.datatypehelpers.HealthDataCategoryPriorityHelper; 37 38 import java.time.Instant; 39 import java.time.Period; 40 import java.util.ArrayList; 41 import java.util.List; 42 import java.util.Map; 43 import java.util.Objects; 44 import java.util.Optional; 45 import java.util.Set; 46 47 /** 48 * A handler for HealthConnect permission-related logic. 49 * 50 * @hide 51 */ 52 public final class HealthConnectPermissionHelper { 53 private static final Period GRANT_TIME_TO_START_ACCESS_DATE_PERIOD = Period.ofDays(30); 54 55 private static final int MASK_PERMISSION_FLAGS = 56 PackageManager.FLAG_PERMISSION_USER_SET 57 | PackageManager.FLAG_PERMISSION_USER_FIXED 58 | PackageManager.FLAG_PERMISSION_AUTO_REVOKED; 59 60 private final Context mContext; 61 private final PackageManager mPackageManager; 62 private final Set<String> mHealthPermissions; 63 private final HealthPermissionIntentAppsTracker mPermissionIntentAppsTracker; 64 private final FirstGrantTimeManager mFirstGrantTimeManager; 65 66 /** 67 * Constructs a {@link HealthConnectPermissionHelper}. 68 * 69 * @param context the service context. 70 * @param packageManager a {@link PackageManager} instance. 71 * @param healthPermissions a {@link Set} of permissions that are recognized as 72 * HealthConnect-defined permissions. 73 * @param permissionIntentTracker a {@link 74 * com.android.server.healthconnect.permission.HealthPermissionIntentAppsTracker} instance 75 * that tracks apps allowed to request health permissions. 76 */ HealthConnectPermissionHelper( Context context, PackageManager packageManager, Set<String> healthPermissions, HealthPermissionIntentAppsTracker permissionIntentTracker, FirstGrantTimeManager firstGrantTimeManager)77 public HealthConnectPermissionHelper( 78 Context context, 79 PackageManager packageManager, 80 Set<String> healthPermissions, 81 HealthPermissionIntentAppsTracker permissionIntentTracker, 82 FirstGrantTimeManager firstGrantTimeManager) { 83 mContext = context; 84 mPackageManager = packageManager; 85 mHealthPermissions = healthPermissions; 86 mPermissionIntentAppsTracker = permissionIntentTracker; 87 mFirstGrantTimeManager = firstGrantTimeManager; 88 } 89 90 /** 91 * See {@link HealthConnectManager#grantHealthPermission}. 92 * 93 * <p>NOTE: Once permission grant is successful, the package name will also be appended to the 94 * end of the priority list corresponding to {@code permissionName}'s health permission 95 * category. 96 */ grantHealthPermission( @onNull String packageName, @NonNull String permissionName, @NonNull UserHandle user)97 public void grantHealthPermission( 98 @NonNull String packageName, @NonNull String permissionName, @NonNull UserHandle user) { 99 Objects.requireNonNull(packageName); 100 Objects.requireNonNull(permissionName); 101 enforceManageHealthPermissions(/* message= */ "grantHealthPermission"); 102 enforceValidHealthPermission(permissionName); 103 UserHandle checkedUser = UserHandle.of(handleIncomingUser(user.getIdentifier())); 104 enforceValidPackage(packageName, checkedUser); 105 enforceSupportPermissionsUsageIntent(packageName, checkedUser); 106 final long token = Binder.clearCallingIdentity(); 107 try { 108 mPackageManager.grantRuntimePermission(packageName, permissionName, checkedUser); 109 mPackageManager.updatePermissionFlags( 110 permissionName, 111 packageName, 112 MASK_PERMISSION_FLAGS, 113 PackageManager.FLAG_PERMISSION_USER_SET, 114 checkedUser); 115 addToPriorityListIfRequired(packageName, permissionName); 116 117 } finally { 118 Binder.restoreCallingIdentity(token); 119 } 120 } 121 122 /** See {@link HealthConnectManager#revokeHealthPermission}. */ revokeHealthPermission( @onNull String packageName, @NonNull String permissionName, @Nullable String reason, @NonNull UserHandle user)123 public void revokeHealthPermission( 124 @NonNull String packageName, 125 @NonNull String permissionName, 126 @Nullable String reason, 127 @NonNull UserHandle user) { 128 Objects.requireNonNull(packageName); 129 Objects.requireNonNull(permissionName); 130 enforceManageHealthPermissions(/* message= */ "revokeHealthPermission"); 131 enforceValidHealthPermission(permissionName); 132 UserHandle checkedUser = UserHandle.of(handleIncomingUser(user.getIdentifier())); 133 enforceValidPackage(packageName, checkedUser); 134 final long token = Binder.clearCallingIdentity(); 135 try { 136 boolean isAlreadyDenied = 137 mPackageManager.checkPermission(permissionName, packageName) 138 == PackageManager.PERMISSION_DENIED; 139 int permissionFlags = 140 mPackageManager.getPermissionFlags(permissionName, packageName, checkedUser); 141 if (!isAlreadyDenied) { 142 mPackageManager.revokeRuntimePermission( 143 packageName, permissionName, checkedUser, reason); 144 } 145 if (isAlreadyDenied 146 && (permissionFlags & PackageManager.FLAG_PERMISSION_USER_SET) != 0) { 147 permissionFlags = permissionFlags | PackageManager.FLAG_PERMISSION_USER_FIXED; 148 } else { 149 permissionFlags = permissionFlags | PackageManager.FLAG_PERMISSION_USER_SET; 150 } 151 permissionFlags = permissionFlags & ~PackageManager.FLAG_PERMISSION_AUTO_REVOKED; 152 mPackageManager.updatePermissionFlags( 153 permissionName, 154 packageName, 155 MASK_PERMISSION_FLAGS, 156 permissionFlags, 157 checkedUser); 158 159 removeFromPriorityListIfRequired(packageName, permissionName); 160 161 } finally { 162 Binder.restoreCallingIdentity(token); 163 } 164 } 165 166 /** See {@link HealthConnectManager#revokeAllHealthPermissions}. */ 167 @SuppressWarnings("NullAway") // TODO(b/317029272): fix this suppression revokeAllHealthPermissions( @onNull String packageName, @Nullable String reason, @NonNull UserHandle user)168 public void revokeAllHealthPermissions( 169 @NonNull String packageName, @Nullable String reason, @NonNull UserHandle user) { 170 Objects.requireNonNull(packageName); 171 enforceManageHealthPermissions(/* message= */ "revokeAllHealthPermissions"); 172 UserHandle checkedUser = UserHandle.of(handleIncomingUser(user.getIdentifier())); 173 enforceValidPackage(packageName, checkedUser); 174 final long token = Binder.clearCallingIdentity(); 175 try { 176 revokeAllHealthPermissionsUnchecked(packageName, checkedUser, reason); 177 } finally { 178 Binder.restoreCallingIdentity(token); 179 } 180 } 181 182 /** See {@link HealthConnectManager#getGrantedHealthPermissions}. */ 183 @NonNull getGrantedHealthPermissions( @onNull String packageName, @NonNull UserHandle user)184 public List<String> getGrantedHealthPermissions( 185 @NonNull String packageName, @NonNull UserHandle user) { 186 Objects.requireNonNull(packageName); 187 enforceManageHealthPermissions(/* message= */ "getGrantedHealthPermissions"); 188 UserHandle checkedUser = UserHandle.of(handleIncomingUser(user.getIdentifier())); 189 enforceValidPackage(packageName, checkedUser); 190 final long token = Binder.clearCallingIdentity(); 191 try { 192 return getGrantedHealthPermissionsUnchecked(packageName, checkedUser); 193 } finally { 194 Binder.restoreCallingIdentity(token); 195 } 196 } 197 198 /** See {@link HealthConnectManager#getHealthPermissionsFlags(String, List)}. */ 199 @NonNull getHealthPermissionsFlags( @onNull String packageName, @NonNull UserHandle user, @NonNull List<String> permissions)200 public Map<String, Integer> getHealthPermissionsFlags( 201 @NonNull String packageName, 202 @NonNull UserHandle user, 203 @NonNull List<String> permissions) { 204 Objects.requireNonNull(packageName); 205 Objects.requireNonNull(user); 206 Objects.requireNonNull(permissions); 207 208 enforceManageHealthPermissions(/* message= */ "getHealthPermissionsFlags"); 209 UserHandle checkedUser = UserHandle.of(handleIncomingUser(user.getIdentifier())); 210 enforceValidPackage(packageName, checkedUser); 211 final long token = Binder.clearCallingIdentity(); 212 try { 213 return getHealthPermissionsFlagsUnchecked(packageName, checkedUser, permissions); 214 } finally { 215 Binder.restoreCallingIdentity(token); 216 } 217 } 218 219 /** See {@link HealthConnectManager#setHealthPermissionsUserFixedFlagValue(String, List)}. */ setHealthPermissionsUserFixedFlagValue( @onNull String packageName, @NonNull UserHandle user, @NonNull List<String> permissions, boolean value)220 public void setHealthPermissionsUserFixedFlagValue( 221 @NonNull String packageName, 222 @NonNull UserHandle user, 223 @NonNull List<String> permissions, 224 boolean value) { 225 Objects.requireNonNull(packageName); 226 Objects.requireNonNull(user); 227 Objects.requireNonNull(permissions); 228 229 enforceManageHealthPermissions(/* message= */ "setHealthPermissionsUserFixedFlagValue"); 230 UserHandle checkedUser = UserHandle.of(handleIncomingUser(user.getIdentifier())); 231 enforceValidPackage(packageName, checkedUser); 232 final long token = Binder.clearCallingIdentity(); 233 try { 234 setHealthPermissionsUserFixedFlagValueUnchecked( 235 packageName, checkedUser, permissions, value); 236 } finally { 237 Binder.restoreCallingIdentity(token); 238 } 239 } 240 241 /** 242 * Returns {@code true} if there is at least one granted permission for the provided {@code 243 * packageName}, {@code false} otherwise. 244 */ hasGrantedHealthPermissions( @onNull String packageName, @NonNull UserHandle user)245 public boolean hasGrantedHealthPermissions( 246 @NonNull String packageName, @NonNull UserHandle user) { 247 return !getGrantedHealthPermissions(packageName, user).isEmpty(); 248 } 249 250 /** 251 * Returns the date from which an app can read / write health data. See {@link 252 * HealthConnectManager#getHealthDataHistoricalAccessStartDate} 253 */ getHealthDataStartDateAccess(String packageName, UserHandle user)254 public Optional<Instant> getHealthDataStartDateAccess(String packageName, UserHandle user) 255 throws IllegalArgumentException { 256 Objects.requireNonNull(packageName); 257 enforceManageHealthPermissions(/* message= */ "getHealthDataStartDateAccess"); 258 UserHandle checkedUser = UserHandle.of(handleIncomingUser(user.getIdentifier())); 259 enforceValidPackage(packageName, checkedUser); 260 261 return mFirstGrantTimeManager 262 .getFirstGrantTime(packageName, checkedUser) 263 .map(grantTime -> grantTime.minus(GRANT_TIME_TO_START_ACCESS_DATE_PERIOD)) 264 .or(Optional::empty); 265 } 266 267 /** 268 * Same as {@link #getHealthDataStartDateAccess(String, UserHandle)} except this method also 269 * throws {@link IllegalAccessException} if health permission is in an incorrect state where 270 * first grant time can't be fetched. 271 */ 272 @NonNull getHealthDataStartDateAccessOrThrow(String packageName, UserHandle user)273 public Instant getHealthDataStartDateAccessOrThrow(String packageName, UserHandle user) { 274 Optional<Instant> startDateAccess = getHealthDataStartDateAccess(packageName, user); 275 if (startDateAccess.isEmpty()) { 276 throwExceptionIncorrectPermissionState(); 277 } 278 return startDateAccess.get(); 279 } 280 throwExceptionIncorrectPermissionState()281 private void throwExceptionIncorrectPermissionState() { 282 throw new IllegalStateException( 283 "Incorrect health permission state, likely" 284 + " because the calling application's manifest does not specify handling " 285 + Intent.ACTION_VIEW_PERMISSION_USAGE 286 + " with " 287 + HealthConnectManager.CATEGORY_HEALTH_PERMISSIONS); 288 } 289 addToPriorityListIfRequired(String packageName, String permissionName)290 private void addToPriorityListIfRequired(String packageName, String permissionName) { 291 if (HealthPermissions.isWritePermission(permissionName)) { 292 HealthDataCategoryPriorityHelper.getInstance() 293 .appendToPriorityList( 294 packageName, 295 HealthPermissions.getHealthDataCategoryForWritePermission( 296 permissionName), 297 mContext, 298 /* isInactiveApp= */ false); 299 } 300 } 301 removeFromPriorityListIfRequired(String packageName, String permissionName)302 private void removeFromPriorityListIfRequired(String packageName, String permissionName) { 303 if (HealthPermissions.isWritePermission(permissionName)) { 304 HealthDataCategoryPriorityHelper.getInstance() 305 .maybeRemoveAppFromPriorityList( 306 packageName, 307 HealthPermissions.getHealthDataCategoryForWritePermission( 308 permissionName), 309 this, 310 mContext.getUser()); 311 } 312 } 313 314 @NonNull getGrantedHealthPermissionsUnchecked( @onNull String packageName, @NonNull UserHandle user)315 private List<String> getGrantedHealthPermissionsUnchecked( 316 @NonNull String packageName, @NonNull UserHandle user) { 317 PackageInfo packageInfo = 318 getPackageInfoUnchecked( 319 packageName, 320 user, 321 PackageManager.PackageInfoFlags.of(PackageManager.GET_PERMISSIONS)); 322 323 if (packageInfo.requestedPermissions == null) { 324 return List.of(); 325 } 326 327 List<String> grantedHealthPerms = new ArrayList<>(packageInfo.requestedPermissions.length); 328 for (int i = 0; i < packageInfo.requestedPermissions.length; i++) { 329 String currPerm = packageInfo.requestedPermissions[i]; 330 if (mHealthPermissions.contains(currPerm) 331 && ((packageInfo.requestedPermissionsFlags[i] 332 & PackageInfo.REQUESTED_PERMISSION_GRANTED) 333 != 0)) { 334 grantedHealthPerms.add(currPerm); 335 } 336 } 337 return grantedHealthPerms; 338 } 339 340 @NonNull getHealthPermissionsFlagsUnchecked( @onNull String packageName, @NonNull UserHandle user, @NonNull List<String> permissions)341 private Map<String, Integer> getHealthPermissionsFlagsUnchecked( 342 @NonNull String packageName, 343 @NonNull UserHandle user, 344 @NonNull List<String> permissions) { 345 enforceValidHealthPermissions(packageName, user, permissions); 346 347 Map<String, Integer> result = new ArrayMap<>(); 348 349 for (String permission : permissions) { 350 result.put( 351 permission, mPackageManager.getPermissionFlags(permission, packageName, user)); 352 } 353 354 return result; 355 } 356 setHealthPermissionsUserFixedFlagValueUnchecked( @onNull String packageName, @NonNull UserHandle user, @NonNull List<String> permissions, boolean value)357 private void setHealthPermissionsUserFixedFlagValueUnchecked( 358 @NonNull String packageName, 359 @NonNull UserHandle user, 360 @NonNull List<String> permissions, 361 boolean value) { 362 enforceValidHealthPermissions(packageName, user, permissions); 363 364 int flagMask = PackageManager.FLAG_PERMISSION_USER_FIXED; 365 int flagValues = value ? PackageManager.FLAG_PERMISSION_USER_FIXED : 0; 366 367 for (String permission : permissions) { 368 mPackageManager.updatePermissionFlags( 369 permission, packageName, flagMask, flagValues, user); 370 } 371 } 372 revokeAllHealthPermissionsUnchecked( String packageName, UserHandle user, String reason)373 private void revokeAllHealthPermissionsUnchecked( 374 String packageName, UserHandle user, String reason) { 375 List<String> grantedHealthPermissions = 376 getGrantedHealthPermissionsUnchecked(packageName, user); 377 for (String perm : grantedHealthPermissions) { 378 mPackageManager.revokeRuntimePermission(packageName, perm, user, reason); 379 mPackageManager.updatePermissionFlags( 380 perm, 381 packageName, 382 MASK_PERMISSION_FLAGS, 383 PackageManager.FLAG_PERMISSION_USER_SET, 384 user); 385 removeFromPriorityListIfRequired(packageName, perm); 386 } 387 } 388 enforceValidHealthPermission(String permissionName)389 private void enforceValidHealthPermission(String permissionName) { 390 if (!mHealthPermissions.contains(permissionName)) { 391 throw new IllegalArgumentException("invalid health permission"); 392 } 393 } 394 getPackageInfoUnchecked( String packageName, UserHandle user, PackageManager.PackageInfoFlags flags)395 private PackageInfo getPackageInfoUnchecked( 396 String packageName, UserHandle user, PackageManager.PackageInfoFlags flags) { 397 try { 398 PackageManager packageManager = 399 mContext.createContextAsUser(user, /* flags= */ 0).getPackageManager(); 400 401 return packageManager.getPackageInfo(packageName, flags); 402 } catch (PackageManager.NameNotFoundException e) { 403 throw new IllegalArgumentException("invalid package", e); 404 } 405 } 406 enforceValidPackage(String packageName, UserHandle user)407 private void enforceValidPackage(String packageName, UserHandle user) { 408 getPackageInfoUnchecked(packageName, user, PackageManager.PackageInfoFlags.of(0)); 409 } 410 enforceManageHealthPermissions(String message)411 private void enforceManageHealthPermissions(String message) { 412 mContext.enforceCallingOrSelfPermission( 413 HealthPermissions.MANAGE_HEALTH_PERMISSIONS, message); 414 } 415 enforceSupportPermissionsUsageIntent(String packageName, UserHandle userHandle)416 private void enforceSupportPermissionsUsageIntent(String packageName, UserHandle userHandle) { 417 if (!mPermissionIntentAppsTracker.supportsPermissionUsageIntent(packageName, userHandle)) { 418 throw new SecurityException( 419 "Package " 420 + packageName 421 + " for " 422 + userHandle.toString() 423 + " doesn't support health permissions usage intent."); 424 } 425 } 426 427 /** 428 * Checks input user id and converts it to positive id if needed, returns converted user id. 429 * 430 * @throws java.lang.SecurityException if the caller is affecting different users without 431 * holding the {@link INTERACT_ACROSS_USERS_FULL} permission. 432 */ handleIncomingUser(int userId)433 private int handleIncomingUser(int userId) { 434 int callingUserId = UserHandle.getUserHandleForUid(Binder.getCallingUid()).getIdentifier(); 435 if (userId == callingUserId) { 436 return userId; 437 } 438 439 boolean canInteractAcrossUsersFull = 440 mContext.checkCallingOrSelfPermission(INTERACT_ACROSS_USERS_FULL) 441 == PERMISSION_GRANTED; 442 if (canInteractAcrossUsersFull) { 443 // If the UserHandle.CURRENT has been passed (negative value), 444 // convert it to positive userId. 445 if (userId == UserHandle.CURRENT.getIdentifier()) { 446 return ActivityManager.getCurrentUser(); 447 } 448 return userId; 449 } 450 451 throw new SecurityException( 452 "Permission denied. Need to run as either the calling user id (" 453 + callingUserId 454 + "), or with " 455 + INTERACT_ACROSS_USERS_FULL 456 + " permission"); 457 } 458 enforceValidHealthPermissions( String packageName, UserHandle user, List<String> permissions)459 private void enforceValidHealthPermissions( 460 String packageName, UserHandle user, List<String> permissions) { 461 PackageInfo packageInfo = 462 getPackageInfoUnchecked( 463 packageName, 464 user, 465 PackageManager.PackageInfoFlags.of(PackageManager.GET_PERMISSIONS)); 466 467 Set<String> requestedPermissions = new ArraySet<>(packageInfo.requestedPermissions); 468 469 for (String permission : permissions) { 470 if (!requestedPermissions.contains(permission)) { 471 throw new IllegalArgumentException( 472 "undeclared permission " + permission + " for package " + packageName); 473 } 474 475 enforceValidHealthPermission(permission); 476 } 477 } 478 } 479