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