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.pm.PackageInfo; 27 import android.content.pm.PackageManager; 28 import android.health.connect.HealthConnectManager; 29 import android.health.connect.HealthPermissions; 30 import android.os.Binder; 31 import android.os.UserHandle; 32 33 import com.android.server.healthconnect.storage.datatypehelpers.HealthDataCategoryPriorityHelper; 34 35 import java.time.Instant; 36 import java.time.Period; 37 import java.util.ArrayList; 38 import java.util.List; 39 import java.util.Objects; 40 import java.util.Set; 41 42 /** 43 * A handler for HealthConnect permission-related logic. 44 * 45 * @hide 46 */ 47 public final class HealthConnectPermissionHelper { 48 private static final Period GRANT_TIME_TO_START_ACCESS_DATE_PERIOD = Period.ofDays(30); 49 50 private static final int MASK_PERMISSION_FLAGS = 51 PackageManager.FLAG_PERMISSION_USER_SET 52 | PackageManager.FLAG_PERMISSION_USER_FIXED 53 | PackageManager.FLAG_PERMISSION_AUTO_REVOKED; 54 55 private final Context mContext; 56 private final PackageManager mPackageManager; 57 private final Set<String> mHealthPermissions; 58 private final HealthPermissionIntentAppsTracker mPermissionIntentAppsTracker; 59 private final FirstGrantTimeManager mFirstGrantTimeManager; 60 61 /** 62 * Constructs a {@link HealthConnectPermissionHelper}. 63 * 64 * @param context the service context. 65 * @param packageManager a {@link PackageManager} instance. 66 * @param healthPermissions a {@link Set} of permissions that are recognized as 67 * HealthConnect-defined permissions. 68 * @param permissionIntentTracker a {@link 69 * com.android.server.healthconnect.permission.HealthPermissionIntentAppsTracker} instance 70 * that tracks apps allowed to request health permissions. 71 */ HealthConnectPermissionHelper( Context context, PackageManager packageManager, Set<String> healthPermissions, HealthPermissionIntentAppsTracker permissionIntentTracker, FirstGrantTimeManager firstGrantTimeManager)72 public HealthConnectPermissionHelper( 73 Context context, 74 PackageManager packageManager, 75 Set<String> healthPermissions, 76 HealthPermissionIntentAppsTracker permissionIntentTracker, 77 FirstGrantTimeManager firstGrantTimeManager) { 78 mContext = context; 79 mPackageManager = packageManager; 80 mHealthPermissions = healthPermissions; 81 mPermissionIntentAppsTracker = permissionIntentTracker; 82 mFirstGrantTimeManager = firstGrantTimeManager; 83 } 84 85 /** 86 * See {@link HealthConnectManager#grantHealthPermission}. 87 * 88 * <p>NOTE: Once permission grant is successful, the package name will also be appended to the 89 * end of the priority list corresponding to {@code permissionName}'s health permission 90 * category. 91 */ grantHealthPermission( @onNull String packageName, @NonNull String permissionName, @NonNull UserHandle user)92 public void grantHealthPermission( 93 @NonNull String packageName, @NonNull String permissionName, @NonNull UserHandle user) { 94 Objects.requireNonNull(packageName); 95 Objects.requireNonNull(permissionName); 96 enforceManageHealthPermissions(/* message= */ "grantHealthPermission"); 97 enforceValidPermission(permissionName); 98 UserHandle checkedUser = UserHandle.of(handleIncomingUser(user.getIdentifier())); 99 enforceValidPackage(packageName, checkedUser); 100 enforceSupportPermissionsUsageIntent(packageName, checkedUser); 101 final long token = Binder.clearCallingIdentity(); 102 try { 103 mPackageManager.grantRuntimePermission(packageName, permissionName, checkedUser); 104 mPackageManager.updatePermissionFlags( 105 permissionName, 106 packageName, 107 MASK_PERMISSION_FLAGS, 108 PackageManager.FLAG_PERMISSION_USER_SET, 109 checkedUser); 110 addToPriorityListIfRequired(packageName, permissionName); 111 112 } finally { 113 Binder.restoreCallingIdentity(token); 114 } 115 } 116 117 /** See {@link HealthConnectManager#revokeHealthPermission}. */ revokeHealthPermission( @onNull String packageName, @NonNull String permissionName, @Nullable String reason, @NonNull UserHandle user)118 public void revokeHealthPermission( 119 @NonNull String packageName, 120 @NonNull String permissionName, 121 @Nullable String reason, 122 @NonNull UserHandle user) { 123 Objects.requireNonNull(packageName); 124 Objects.requireNonNull(permissionName); 125 enforceManageHealthPermissions(/* message= */ "revokeHealthPermission"); 126 enforceValidPermission(permissionName); 127 UserHandle checkedUser = UserHandle.of(handleIncomingUser(user.getIdentifier())); 128 enforceValidPackage(packageName, checkedUser); 129 final long token = Binder.clearCallingIdentity(); 130 try { 131 boolean isAlreadyDenied = 132 mPackageManager.checkPermission(permissionName, packageName) 133 == PackageManager.PERMISSION_DENIED; 134 int permissionFlags = 135 mPackageManager.getPermissionFlags(permissionName, packageName, checkedUser); 136 if (!isAlreadyDenied) { 137 mPackageManager.revokeRuntimePermission( 138 packageName, permissionName, checkedUser, reason); 139 } 140 if (isAlreadyDenied 141 && (permissionFlags & PackageManager.FLAG_PERMISSION_USER_SET) != 0) { 142 permissionFlags = permissionFlags | PackageManager.FLAG_PERMISSION_USER_FIXED; 143 } else { 144 permissionFlags = permissionFlags | PackageManager.FLAG_PERMISSION_USER_SET; 145 } 146 permissionFlags = permissionFlags & ~PackageManager.FLAG_PERMISSION_AUTO_REVOKED; 147 mPackageManager.updatePermissionFlags( 148 permissionName, 149 packageName, 150 MASK_PERMISSION_FLAGS, 151 permissionFlags, 152 checkedUser); 153 removeFromPriorityListIfRequired(packageName, permissionName); 154 } finally { 155 Binder.restoreCallingIdentity(token); 156 } 157 } 158 159 /** See {@link HealthConnectManager#revokeAllHealthPermissions}. */ revokeAllHealthPermissions( @onNull String packageName, @Nullable String reason, @NonNull UserHandle user)160 public void revokeAllHealthPermissions( 161 @NonNull String packageName, @Nullable String reason, @NonNull UserHandle user) { 162 Objects.requireNonNull(packageName); 163 enforceManageHealthPermissions(/* message= */ "revokeAllHealthPermissions"); 164 UserHandle checkedUser = UserHandle.of(handleIncomingUser(user.getIdentifier())); 165 enforceValidPackage(packageName, checkedUser); 166 final long token = Binder.clearCallingIdentity(); 167 try { 168 revokeAllHealthPermissionsUnchecked(packageName, checkedUser, reason); 169 } finally { 170 Binder.restoreCallingIdentity(token); 171 } 172 } 173 174 /** See {@link HealthConnectManager#getGrantedHealthPermissions}. */ 175 @NonNull getGrantedHealthPermissions( @onNull String packageName, @NonNull UserHandle user)176 public List<String> getGrantedHealthPermissions( 177 @NonNull String packageName, @NonNull UserHandle user) { 178 Objects.requireNonNull(packageName); 179 enforceManageHealthPermissions(/* message= */ "getGrantedHealthPermissions"); 180 UserHandle checkedUser = UserHandle.of(handleIncomingUser(user.getIdentifier())); 181 enforceValidPackage(packageName, checkedUser); 182 final long token = Binder.clearCallingIdentity(); 183 try { 184 return getGrantedHealthPermissionsUnchecked(packageName, checkedUser); 185 } finally { 186 Binder.restoreCallingIdentity(token); 187 } 188 } 189 190 /** 191 * Returns {@code true} if there is at least one granted permission for the provided {@code 192 * packageName}, {@code false} otherwise. 193 */ hasGrantedHealthPermissions( @onNull String packageName, @NonNull UserHandle user)194 public boolean hasGrantedHealthPermissions( 195 @NonNull String packageName, @NonNull UserHandle user) { 196 return !getGrantedHealthPermissions(packageName, user).isEmpty(); 197 } 198 199 /** 200 * Returns the date from which an app can read / write health data. See {@link 201 * HealthConnectManager#getHealthDataHistoricalAccessStartDate} 202 */ 203 @Nullable getHealthDataStartDateAccess(String packageName, UserHandle user)204 public Instant getHealthDataStartDateAccess(String packageName, UserHandle user) 205 throws IllegalArgumentException { 206 Objects.requireNonNull(packageName); 207 enforceManageHealthPermissions(/* message= */ "getHealthDataStartDateAccess"); 208 UserHandle checkedUser = UserHandle.of(handleIncomingUser(user.getIdentifier())); 209 enforceValidPackage(packageName, checkedUser); 210 211 Instant grantTimeDate = mFirstGrantTimeManager.getFirstGrantTime(packageName, checkedUser); 212 if (grantTimeDate == null) { 213 return null; 214 } 215 216 return grantTimeDate.minus(GRANT_TIME_TO_START_ACCESS_DATE_PERIOD); 217 } 218 addToPriorityListIfRequired(String packageName, String permissionName)219 private void addToPriorityListIfRequired(String packageName, String permissionName) { 220 if (HealthPermissions.isWritePermission(permissionName)) { 221 HealthDataCategoryPriorityHelper.getInstance() 222 .appendToPriorityList( 223 packageName, 224 HealthPermissions.getHealthDataCategory(permissionName), 225 mContext); 226 } 227 } 228 removeFromPriorityListIfRequired(String packageName, String permissionName)229 private void removeFromPriorityListIfRequired(String packageName, String permissionName) { 230 if (HealthPermissions.isWritePermission(permissionName)) { 231 HealthDataCategoryPriorityHelper.getInstance() 232 .removeFromPriorityList( 233 packageName, 234 HealthPermissions.getHealthDataCategory(permissionName), 235 this, 236 mContext.getUser()); 237 } 238 } 239 240 @NonNull getGrantedHealthPermissionsUnchecked( @onNull String packageName, @NonNull UserHandle user)241 private List<String> getGrantedHealthPermissionsUnchecked( 242 @NonNull String packageName, @NonNull UserHandle user) { 243 PackageInfo packageInfo; 244 try { 245 PackageManager packageManager = 246 mContext.createContextAsUser(user, /* flags= */ 0).getPackageManager(); 247 packageInfo = 248 packageManager.getPackageInfo( 249 packageName, 250 PackageManager.PackageInfoFlags.of(PackageManager.GET_PERMISSIONS)); 251 } catch (PackageManager.NameNotFoundException e) { 252 throw new IllegalArgumentException("Invalid package", e); 253 } 254 255 if (packageInfo.requestedPermissions == null) { 256 return List.of(); 257 } 258 259 List<String> grantedHealthPerms = new ArrayList<>(packageInfo.requestedPermissions.length); 260 for (int i = 0; i < packageInfo.requestedPermissions.length; i++) { 261 String currPerm = packageInfo.requestedPermissions[i]; 262 if (mHealthPermissions.contains(currPerm) 263 && ((packageInfo.requestedPermissionsFlags[i] 264 & PackageInfo.REQUESTED_PERMISSION_GRANTED) 265 != 0)) { 266 grantedHealthPerms.add(currPerm); 267 } 268 } 269 return grantedHealthPerms; 270 } 271 revokeAllHealthPermissionsUnchecked( String packageName, UserHandle user, String reason)272 private void revokeAllHealthPermissionsUnchecked( 273 String packageName, UserHandle user, String reason) { 274 List<String> grantedHealthPermissions = 275 getGrantedHealthPermissionsUnchecked(packageName, user); 276 for (String perm : grantedHealthPermissions) { 277 mPackageManager.revokeRuntimePermission(packageName, perm, user, reason); 278 mPackageManager.updatePermissionFlags( 279 perm, 280 packageName, 281 MASK_PERMISSION_FLAGS, 282 PackageManager.FLAG_PERMISSION_USER_SET, 283 user); 284 removeFromPriorityListIfRequired(packageName, perm); 285 } 286 } 287 enforceValidPermission(String permissionName)288 private void enforceValidPermission(String permissionName) { 289 if (!mHealthPermissions.contains(permissionName)) { 290 throw new IllegalArgumentException("invalid health permission"); 291 } 292 } 293 enforceValidPackage(String packageName, UserHandle user)294 private void enforceValidPackage(String packageName, UserHandle user) { 295 try { 296 PackageManager packageManager = 297 mContext.createContextAsUser(user, /* flags= */ 0).getPackageManager(); 298 299 packageManager.getPackageInfo(packageName, PackageManager.PackageInfoFlags.of(0)); 300 } catch (PackageManager.NameNotFoundException e) { 301 throw new IllegalArgumentException("invalid package", e); 302 } 303 } 304 enforceManageHealthPermissions(String message)305 private void enforceManageHealthPermissions(String message) { 306 mContext.enforceCallingOrSelfPermission( 307 HealthPermissions.MANAGE_HEALTH_PERMISSIONS, message); 308 } 309 enforceSupportPermissionsUsageIntent(String packageName, UserHandle userHandle)310 private void enforceSupportPermissionsUsageIntent(String packageName, UserHandle userHandle) { 311 if (!mPermissionIntentAppsTracker.supportsPermissionUsageIntent(packageName, userHandle)) { 312 throw new SecurityException( 313 "Package " 314 + packageName 315 + " for " 316 + userHandle.toString() 317 + " doesn't support health permissions usage intent."); 318 } 319 } 320 321 /** 322 * Checks input user id and converts it to positive id if needed, returns converted user id. 323 * 324 * @throws java.lang.SecurityException if the caller is affecting different users without 325 * holding the {@link INTERACT_ACROSS_USERS_FULL} permission. 326 */ handleIncomingUser(int userId)327 private int handleIncomingUser(int userId) { 328 int callingUserId = UserHandle.getUserHandleForUid(Binder.getCallingUid()).getIdentifier(); 329 if (userId == callingUserId) { 330 return userId; 331 } 332 333 boolean canInteractAcrossUsersFull = 334 mContext.checkCallingOrSelfPermission(INTERACT_ACROSS_USERS_FULL) 335 == PERMISSION_GRANTED; 336 if (canInteractAcrossUsersFull) { 337 // If the UserHandle.CURRENT has been passed (negative value), 338 // convert it to positive userId. 339 if (userId == UserHandle.CURRENT.getIdentifier()) { 340 return ActivityManager.getCurrentUser(); 341 } 342 return userId; 343 } 344 345 throw new SecurityException( 346 "Permission denied. Need to run as either the calling user id (" 347 + callingUserId 348 + "), or with " 349 + INTERACT_ACROSS_USERS_FULL 350 + " permission"); 351 } 352 } 353