• 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_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