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