• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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