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 21 import android.annotation.NonNull; 22 import android.content.AttributionSource; 23 import android.content.Context; 24 import android.health.connect.HealthPermissions; 25 import android.health.connect.internal.datatypes.RecordInternal; 26 import android.health.connect.internal.datatypes.utils.RecordMapper; 27 import android.health.connect.internal.datatypes.utils.RecordTypePermissionCategoryMapper; 28 import android.permission.PermissionManager; 29 import android.util.ArrayMap; 30 import android.util.ArraySet; 31 32 import com.android.server.healthconnect.storage.datatypehelpers.RecordHelper; 33 import com.android.server.healthconnect.storage.utils.RecordHelperProvider; 34 35 import java.util.Collections; 36 import java.util.List; 37 import java.util.Map; 38 import java.util.Set; 39 40 /** 41 * Helper class to force caller of data apis to hold api required permissions. 42 * 43 * @hide 44 */ 45 public class DataPermissionEnforcer { 46 private final PermissionManager mPermissionManager; 47 private final Context mContext; 48 DataPermissionEnforcer(PermissionManager permissionManager, Context context)49 public DataPermissionEnforcer(PermissionManager permissionManager, Context context) { 50 mPermissionManager = permissionManager; 51 mContext = context; 52 } 53 54 /** Enforces default write permissions for given recordTypeIds */ enforceRecordIdsWritePermissions( List<Integer> recordTypeIds, AttributionSource attributionSource)55 public void enforceRecordIdsWritePermissions( 56 List<Integer> recordTypeIds, AttributionSource attributionSource) { 57 enforceRecordIdWritePermissionInternal(recordTypeIds, attributionSource); 58 } 59 60 /** Enforces default read permissions for given recordTypeIds */ enforceRecordIdsReadPermissions( List<Integer> recordTypeIds, AttributionSource attributionSource)61 public void enforceRecordIdsReadPermissions( 62 List<Integer> recordTypeIds, AttributionSource attributionSource) { 63 for (Integer recordTypeId : recordTypeIds) { 64 String permissionName = 65 HealthPermissions.getHealthReadPermission( 66 RecordTypePermissionCategoryMapper 67 .getHealthPermissionCategoryForRecordType(recordTypeId)); 68 enforceRecordPermission( 69 permissionName, attributionSource, recordTypeId, /* isReadPermission= */ true); 70 } 71 } 72 73 /** 74 * Enforces that caller has either read or write permissions for given recordTypeId. Returns 75 * flag which indicates that caller is allowed to read only records written by itself. 76 */ enforceReadAccessAndGetEnforceSelfRead( int recordTypeId, AttributionSource attributionSource)77 public boolean enforceReadAccessAndGetEnforceSelfRead( 78 int recordTypeId, AttributionSource attributionSource) { 79 boolean enforceSelfRead = false; 80 try { 81 enforceRecordIdsReadPermissions( 82 Collections.singletonList(recordTypeId), attributionSource); 83 } catch (SecurityException readSecurityException) { 84 try { 85 enforceRecordIdsWritePermissions( 86 Collections.singletonList(recordTypeId), attributionSource); 87 // Apps are always allowed to read self data if they have insert 88 // permission. 89 enforceSelfRead = true; 90 } catch (SecurityException writeSecurityException) { 91 throw readSecurityException; 92 } 93 } 94 return enforceSelfRead; 95 } 96 97 /** 98 * Enforces that caller has all write permissions to write given records. Includes permissions 99 * for writing optional extra data if it's present in given records. 100 */ enforceRecordsWritePermissions( List<RecordInternal<?>> recordInternals, AttributionSource attributionSource)101 public void enforceRecordsWritePermissions( 102 List<RecordInternal<?>> recordInternals, AttributionSource attributionSource) { 103 Map<Integer, Set<String>> recordTypeIdToExtraPerms = new ArrayMap<>(); 104 105 for (RecordInternal<?> recordInternal : recordInternals) { 106 int recordTypeId = recordInternal.getRecordType(); 107 RecordHelper<?> recordHelper = 108 RecordHelperProvider.getInstance().getRecordHelper(recordTypeId); 109 110 if (!recordTypeIdToExtraPerms.containsKey(recordTypeId)) { 111 recordTypeIdToExtraPerms.put(recordTypeId, new ArraySet<>()); 112 } 113 114 recordHelper.checkRecordOperationsAreEnabled(recordInternal); 115 recordTypeIdToExtraPerms 116 .get(recordTypeId) 117 .addAll(recordHelper.getRequiredExtraWritePermissions(recordInternal)); 118 } 119 120 // Check main write permissions for given recordIds 121 enforceRecordIdWritePermissionInternal( 122 recordTypeIdToExtraPerms.keySet().stream().toList(), attributionSource); 123 124 // Check extra write permissions for given records 125 for (Integer recordTypeId : recordTypeIdToExtraPerms.keySet()) { 126 for (String permissionName : recordTypeIdToExtraPerms.get(recordTypeId)) { 127 enforceRecordPermission( 128 permissionName, 129 attributionSource, 130 recordTypeId, 131 /* isReadPermission= */ false); 132 } 133 } 134 } 135 136 /** Enforces that caller has any of given permissions. */ enforceAnyOfPermissions(@onNull String... permissions)137 public void enforceAnyOfPermissions(@NonNull String... permissions) { 138 for (var permission : permissions) { 139 if (mContext.checkCallingPermission(permission) == PERMISSION_GRANTED) { 140 return; 141 } 142 } 143 throw new SecurityException( 144 "Caller requires one of the following permissions: " 145 + String.join(", ", permissions)); 146 } 147 148 /** 149 * Collects extra read permissions to its grant state. Used to not expose extra data if caller 150 * doesn't have corresponding permission. 151 */ collectExtraReadPermissionToStateMapping( int recordTypeId, AttributionSource attributionSource)152 public Map<String, Boolean> collectExtraReadPermissionToStateMapping( 153 int recordTypeId, AttributionSource attributionSource) { 154 RecordHelper<?> recordHelper = 155 RecordHelperProvider.getInstance().getRecordHelper(recordTypeId); 156 if (recordHelper.getExtraReadPermissions().isEmpty()) { 157 return Collections.emptyMap(); 158 } 159 160 Map<String, Boolean> mapping = new ArrayMap<>(); 161 for (String permissionName : recordHelper.getExtraReadPermissions()) { 162 mapping.put(permissionName, isPermissionGranted(permissionName, attributionSource)); 163 } 164 return mapping; 165 } 166 collectExtraWritePermissionStateMapping( List<RecordInternal<?>> recordInternals, AttributionSource attributionSource)167 public Map<String, Boolean> collectExtraWritePermissionStateMapping( 168 List<RecordInternal<?>> recordInternals, AttributionSource attributionSource) { 169 Map<String, Boolean> mapping = new ArrayMap<>(); 170 for (RecordInternal<?> recordInternal : recordInternals) { 171 int recordTypeId = recordInternal.getRecordType(); 172 RecordHelper<?> recordHelper = 173 RecordHelperProvider.getInstance().getRecordHelper(recordTypeId); 174 175 for (String permName : recordHelper.getExtraWritePermissions()) { 176 mapping.put(permName, isPermissionGranted(permName, attributionSource)); 177 } 178 } 179 return mapping; 180 } 181 enforceRecordIdWritePermissionInternal( List<Integer> recordTypeIds, AttributionSource attributionSource)182 private void enforceRecordIdWritePermissionInternal( 183 List<Integer> recordTypeIds, AttributionSource attributionSource) { 184 for (Integer recordTypeId : recordTypeIds) { 185 String permissionName = 186 HealthPermissions.getHealthWritePermission( 187 RecordTypePermissionCategoryMapper 188 .getHealthPermissionCategoryForRecordType(recordTypeId)); 189 enforceRecordPermission( 190 permissionName, attributionSource, recordTypeId, /* isReadPermission= */ false); 191 } 192 } 193 enforceRecordPermission( String permissionName, AttributionSource attributionSource, int recordTypeId, boolean isReadPermission)194 private void enforceRecordPermission( 195 String permissionName, 196 AttributionSource attributionSource, 197 int recordTypeId, 198 boolean isReadPermission) { 199 if (!isPermissionGranted(permissionName, attributionSource)) { 200 String prohibitedAction = 201 isReadPermission ? "to read to record type" : " to write to record type "; 202 throw new SecurityException( 203 "Caller doesn't have " 204 + permissionName 205 + prohibitedAction 206 + RecordMapper.getInstance() 207 .getRecordIdToExternalRecordClassMap() 208 .get(recordTypeId)); 209 } 210 } 211 isPermissionGranted( String permissionName, AttributionSource attributionSource)212 private boolean isPermissionGranted( 213 String permissionName, AttributionSource attributionSource) { 214 return mPermissionManager.checkPermissionForStartDataDelivery( 215 permissionName, attributionSource, null) 216 == PERMISSION_GRANTED; 217 } 218 } 219