1 /* 2 * Copyright (C) 2023 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.content.pm.PackageManager.PERMISSION_GRANTED; 20 import static android.health.connect.HealthPermissions.READ_HEART_RATE; 21 22 import static java.util.stream.Collectors.toSet; 23 24 import android.content.AttributionSource; 25 import android.content.Context; 26 import android.content.pm.PackageManager; 27 import android.health.connect.internal.datatypes.RecordInternal; 28 import android.health.connect.internal.datatypes.utils.HealthConnectMappings; 29 import android.os.UserHandle; 30 import android.permission.PermissionManager; 31 import android.util.ArrayMap; 32 import android.util.ArraySet; 33 34 import com.android.healthfitness.flags.Flags; 35 import com.android.server.healthconnect.storage.datatypehelpers.RecordHelper; 36 import com.android.server.healthconnect.storage.utils.InternalHealthConnectMappings; 37 38 import java.util.Collections; 39 import java.util.List; 40 import java.util.Map; 41 import java.util.Set; 42 43 /** 44 * Helper class to force caller of data apis to hold api required permissions. 45 * 46 * @hide 47 */ 48 public class DataPermissionEnforcer { 49 private final PermissionManager mPermissionManager; 50 private final Context mContext; 51 private final HealthConnectMappings mHealthConnectMappings; 52 private final InternalHealthConnectMappings mInternalHealthConnectMappings; 53 DataPermissionEnforcer( PermissionManager permissionManager, Context context, InternalHealthConnectMappings internalHealthConnectMappings)54 public DataPermissionEnforcer( 55 PermissionManager permissionManager, 56 Context context, 57 InternalHealthConnectMappings internalHealthConnectMappings) { 58 mPermissionManager = permissionManager; 59 mContext = context; 60 mHealthConnectMappings = internalHealthConnectMappings.getExternalMappings(); 61 mInternalHealthConnectMappings = internalHealthConnectMappings; 62 } 63 64 /** Enforces default write permissions for given recordTypeIds */ enforceRecordIdsWritePermissions( List<Integer> recordTypeIds, AttributionSource attributionSource)65 public void enforceRecordIdsWritePermissions( 66 List<Integer> recordTypeIds, AttributionSource attributionSource) { 67 enforceRecordIdWritePermissionInternal(recordTypeIds, attributionSource); 68 } 69 70 /** Enforces default read permissions for given recordTypeIds */ enforceRecordIdsReadPermissions( List<Integer> recordTypeIds, AttributionSource attributionSource)71 public void enforceRecordIdsReadPermissions( 72 List<Integer> recordTypeIds, AttributionSource attributionSource) { 73 for (Integer recordTypeId : recordTypeIds) { 74 String permissionName = 75 mHealthConnectMappings.getHealthReadPermission( 76 mHealthConnectMappings.getHealthPermissionCategoryForRecordType( 77 recordTypeId)); 78 enforceRecordPermission( 79 permissionName, attributionSource, recordTypeId, /* isReadPermission= */ true); 80 } 81 } 82 83 /** 84 * Enforces that caller has either read or write permissions for given recordTypeId. Returns 85 * flag which indicates that caller is allowed to read only records written by itself. 86 */ enforceReadAccessAndGetEnforceSelfRead( int recordTypeId, AttributionSource attributionSource)87 public boolean enforceReadAccessAndGetEnforceSelfRead( 88 int recordTypeId, AttributionSource attributionSource) { 89 boolean enforceSelfRead = false; 90 try { 91 enforceRecordIdsReadPermissions( 92 Collections.singletonList(recordTypeId), attributionSource); 93 } catch (SecurityException readSecurityException) { 94 try { 95 enforceRecordIdsWritePermissions( 96 Collections.singletonList(recordTypeId), attributionSource); 97 // Apps are always allowed to read self data if they have insert 98 // permission. 99 enforceSelfRead = true; 100 } catch (SecurityException writeSecurityException) { 101 throw readSecurityException; 102 } 103 } 104 return enforceSelfRead; 105 } 106 107 // TODO(b/312952346): Consider refactoring how permission enforcement is done within 108 // HealthConnectServiceImpl. This goes beyond just this method. 109 /** 110 * Enforces that the caller has either read or write permissions for all the given recordTypes, 111 * and returns {@code true} if the caller is allowed to read only records written by itself, 112 * false otherwise. 113 * 114 * @throws SecurityException if the app has neither read nor write permissions for any of the 115 * specified record types. 116 */ enforceReadAccessAndGetEnforceSelfRead( List<Integer> recordTypes, AttributionSource attributionSource)117 public boolean enforceReadAccessAndGetEnforceSelfRead( 118 List<Integer> recordTypes, AttributionSource attributionSource) { 119 boolean enforceSelfRead = false; 120 for (int recordTypeId : recordTypes) { 121 enforceSelfRead |= 122 enforceReadAccessAndGetEnforceSelfRead(recordTypeId, attributionSource); 123 } 124 return enforceSelfRead; 125 } 126 127 /** 128 * Enforces that caller has all write permissions to write given records. Includes permissions 129 * for writing optional extra data if it's present in given records. 130 */ enforceRecordsWritePermissions( List<RecordInternal<?>> recordInternals, AttributionSource attributionSource)131 public void enforceRecordsWritePermissions( 132 List<RecordInternal<?>> recordInternals, AttributionSource attributionSource) { 133 Map<Integer, Set<String>> recordTypeIdToExtraPerms = new ArrayMap<>(); 134 135 for (RecordInternal<?> recordInternal : recordInternals) { 136 int recordTypeId = recordInternal.getRecordType(); 137 RecordHelper<?> recordHelper = 138 mInternalHealthConnectMappings.getRecordHelper(recordTypeId); 139 140 if (!recordTypeIdToExtraPerms.containsKey(recordTypeId)) { 141 recordTypeIdToExtraPerms.put(recordTypeId, new ArraySet<>()); 142 } 143 144 recordTypeIdToExtraPerms 145 .get(recordTypeId) 146 .addAll(recordHelper.getRequiredExtraWritePermissions(recordInternal)); 147 } 148 149 // Check main write permissions for given recordIds 150 enforceRecordIdWritePermissionInternal( 151 recordTypeIdToExtraPerms.keySet().stream().toList(), attributionSource); 152 153 // Check extra write permissions for given records 154 for (Integer recordTypeId : recordTypeIdToExtraPerms.keySet()) { 155 for (String permissionName : recordTypeIdToExtraPerms.get(recordTypeId)) { 156 enforceRecordPermission( 157 permissionName, 158 attributionSource, 159 recordTypeId, 160 /* isReadPermission= */ false); 161 } 162 } 163 } 164 165 /** Enforces that caller has any of given permissions. */ enforceAnyOfPermissions(String... permissions)166 public void enforceAnyOfPermissions(String... permissions) { 167 for (var permission : permissions) { 168 if (mContext.checkCallingPermission(permission) == PERMISSION_GRANTED) { 169 return; 170 } 171 } 172 throw new SecurityException( 173 "Caller requires one of the following permissions: " 174 + String.join(", ", permissions)); 175 } 176 177 /** 178 * Returns granted extra read permissions. 179 * 180 * <p>Used to not expose extra data if caller doesn't have corresponding permission. 181 */ collectGrantedExtraReadPermissions( Set<Integer> recordTypeIds, AttributionSource attributionSource)182 public Set<String> collectGrantedExtraReadPermissions( 183 Set<Integer> recordTypeIds, AttributionSource attributionSource) { 184 return recordTypeIds.stream() 185 .map(mInternalHealthConnectMappings::getRecordHelper) 186 .flatMap(recordHelper -> recordHelper.getExtraReadPermissions().stream()) 187 .distinct() 188 .filter(permission -> isPermissionGranted(permission, attributionSource)) 189 .collect(toSet()); 190 } 191 collectExtraWritePermissionStateMapping( List<RecordInternal<?>> recordInternals, AttributionSource attributionSource)192 public ArrayMap<String, Boolean> collectExtraWritePermissionStateMapping( 193 List<RecordInternal<?>> recordInternals, AttributionSource attributionSource) { 194 ArrayMap<String, Boolean> mapping = new ArrayMap<>(); 195 for (RecordInternal<?> recordInternal : recordInternals) { 196 int recordTypeId = recordInternal.getRecordType(); 197 RecordHelper<?> recordHelper = 198 mInternalHealthConnectMappings.getRecordHelper(recordTypeId); 199 200 for (String permName : recordHelper.getExtraWritePermissions()) { 201 mapping.put(permName, isPermissionGranted(permName, attributionSource)); 202 } 203 } 204 return mapping; 205 } 206 enforceRecordIdWritePermissionInternal( List<Integer> recordTypeIds, AttributionSource attributionSource)207 private void enforceRecordIdWritePermissionInternal( 208 List<Integer> recordTypeIds, AttributionSource attributionSource) { 209 for (Integer recordTypeId : recordTypeIds) { 210 String permissionName = 211 mHealthConnectMappings.getHealthWritePermission( 212 mHealthConnectMappings.getHealthPermissionCategoryForRecordType( 213 recordTypeId)); 214 enforceRecordPermission( 215 permissionName, attributionSource, recordTypeId, /* isReadPermission= */ false); 216 } 217 } 218 enforceRecordPermission( String permissionName, AttributionSource attributionSource, int recordTypeId, boolean isReadPermission)219 private void enforceRecordPermission( 220 String permissionName, 221 AttributionSource attributionSource, 222 int recordTypeId, 223 boolean isReadPermission) { 224 if (!isPermissionGranted(permissionName, attributionSource)) { 225 String prohibitedAction = 226 isReadPermission ? " to read to record type" : " to write to record type "; 227 throw new SecurityException( 228 "Caller doesn't have " 229 + permissionName 230 + prohibitedAction 231 + mHealthConnectMappings 232 .getRecordIdToExternalRecordClassMap() 233 .get(recordTypeId)); 234 } 235 236 // Deny access to HealthConnect API if READ_HEART_RATE is a split permission. 237 if (Flags.replaceBodySensorPermissionEnabled() && permissionName.equals(READ_HEART_RATE)) { 238 String packageName = attributionSource.getPackageName(); 239 if (packageName == null) { 240 throw new SecurityException("Caller packageName is null"); 241 } 242 243 UserHandle user = UserHandle.getUserHandleForUid(attributionSource.getUid()); 244 int permissionFlags = 245 mContext.getPackageManager() 246 .getPermissionFlags(permissionName, packageName, user); 247 int targetSdk; 248 try { 249 targetSdk = 250 PackageInfoUtils.getPackageInfoUnchecked( 251 packageName, 252 user, 253 PackageManager.PackageInfoFlags.of(0), 254 mContext) 255 .applicationInfo 256 .targetSdkVersion; 257 } catch (Exception e) { 258 throw new SecurityException( 259 "Caller package is not found. Unable to determine permission state."); 260 } 261 262 if (HealthConnectPermissionHelper.isFromSplitPermission(permissionFlags, targetSdk)) { 263 throw new SecurityException( 264 "Caller is requesting HealthConnect API access from " 265 + "an implicitly granted split permission: " 266 + permissionName 267 + mHealthConnectMappings 268 .getRecordIdToExternalRecordClassMap() 269 .get(recordTypeId)); 270 } 271 } 272 } 273 isPermissionGranted( String permissionName, AttributionSource attributionSource)274 private boolean isPermissionGranted( 275 String permissionName, AttributionSource attributionSource) { 276 return mPermissionManager.checkPermissionForDataDelivery( 277 permissionName, attributionSource, null) 278 == PERMISSION_GRANTED; 279 } 280 } 281