• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2022 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.Manifest.permission.INTERACT_ACROSS_USERS_FULL;
20 import static android.content.pm.PackageManager.PERMISSION_GRANTED;
21 
22 import android.annotation.NonNull;
23 import android.annotation.Nullable;
24 import android.app.ActivityManager;
25 import android.content.Context;
26 import android.content.pm.PackageInfo;
27 import android.content.pm.PackageManager;
28 import android.health.connect.HealthConnectManager;
29 import android.health.connect.HealthPermissions;
30 import android.os.Binder;
31 import android.os.UserHandle;
32 
33 import com.android.server.healthconnect.storage.datatypehelpers.HealthDataCategoryPriorityHelper;
34 
35 import java.time.Instant;
36 import java.time.Period;
37 import java.util.ArrayList;
38 import java.util.List;
39 import java.util.Objects;
40 import java.util.Set;
41 
42 /**
43  * A handler for HealthConnect permission-related logic.
44  *
45  * @hide
46  */
47 public final class HealthConnectPermissionHelper {
48     private static final Period GRANT_TIME_TO_START_ACCESS_DATE_PERIOD = Period.ofDays(30);
49 
50     private static final int MASK_PERMISSION_FLAGS =
51             PackageManager.FLAG_PERMISSION_USER_SET
52                     | PackageManager.FLAG_PERMISSION_USER_FIXED
53                     | PackageManager.FLAG_PERMISSION_AUTO_REVOKED;
54 
55     private final Context mContext;
56     private final PackageManager mPackageManager;
57     private final Set<String> mHealthPermissions;
58     private final HealthPermissionIntentAppsTracker mPermissionIntentAppsTracker;
59     private final FirstGrantTimeManager mFirstGrantTimeManager;
60 
61     /**
62      * Constructs a {@link HealthConnectPermissionHelper}.
63      *
64      * @param context the service context.
65      * @param packageManager a {@link PackageManager} instance.
66      * @param healthPermissions a {@link Set} of permissions that are recognized as
67      *     HealthConnect-defined permissions.
68      * @param permissionIntentTracker a {@link
69      *     com.android.server.healthconnect.permission.HealthPermissionIntentAppsTracker} instance
70      *     that tracks apps allowed to request health permissions.
71      */
HealthConnectPermissionHelper( Context context, PackageManager packageManager, Set<String> healthPermissions, HealthPermissionIntentAppsTracker permissionIntentTracker, FirstGrantTimeManager firstGrantTimeManager)72     public HealthConnectPermissionHelper(
73             Context context,
74             PackageManager packageManager,
75             Set<String> healthPermissions,
76             HealthPermissionIntentAppsTracker permissionIntentTracker,
77             FirstGrantTimeManager firstGrantTimeManager) {
78         mContext = context;
79         mPackageManager = packageManager;
80         mHealthPermissions = healthPermissions;
81         mPermissionIntentAppsTracker = permissionIntentTracker;
82         mFirstGrantTimeManager = firstGrantTimeManager;
83     }
84 
85     /**
86      * See {@link HealthConnectManager#grantHealthPermission}.
87      *
88      * <p>NOTE: Once permission grant is successful, the package name will also be appended to the
89      * end of the priority list corresponding to {@code permissionName}'s health permission
90      * category.
91      */
grantHealthPermission( @onNull String packageName, @NonNull String permissionName, @NonNull UserHandle user)92     public void grantHealthPermission(
93             @NonNull String packageName, @NonNull String permissionName, @NonNull UserHandle user) {
94         Objects.requireNonNull(packageName);
95         Objects.requireNonNull(permissionName);
96         enforceManageHealthPermissions(/* message= */ "grantHealthPermission");
97         enforceValidPermission(permissionName);
98         UserHandle checkedUser = UserHandle.of(handleIncomingUser(user.getIdentifier()));
99         enforceValidPackage(packageName, checkedUser);
100         enforceSupportPermissionsUsageIntent(packageName, checkedUser);
101         final long token = Binder.clearCallingIdentity();
102         try {
103             mPackageManager.grantRuntimePermission(packageName, permissionName, checkedUser);
104             mPackageManager.updatePermissionFlags(
105                     permissionName,
106                     packageName,
107                     MASK_PERMISSION_FLAGS,
108                     PackageManager.FLAG_PERMISSION_USER_SET,
109                     checkedUser);
110             addToPriorityListIfRequired(packageName, permissionName);
111 
112         } finally {
113             Binder.restoreCallingIdentity(token);
114         }
115     }
116 
117     /** See {@link HealthConnectManager#revokeHealthPermission}. */
revokeHealthPermission( @onNull String packageName, @NonNull String permissionName, @Nullable String reason, @NonNull UserHandle user)118     public void revokeHealthPermission(
119             @NonNull String packageName,
120             @NonNull String permissionName,
121             @Nullable String reason,
122             @NonNull UserHandle user) {
123         Objects.requireNonNull(packageName);
124         Objects.requireNonNull(permissionName);
125         enforceManageHealthPermissions(/* message= */ "revokeHealthPermission");
126         enforceValidPermission(permissionName);
127         UserHandle checkedUser = UserHandle.of(handleIncomingUser(user.getIdentifier()));
128         enforceValidPackage(packageName, checkedUser);
129         final long token = Binder.clearCallingIdentity();
130         try {
131             boolean isAlreadyDenied =
132                     mPackageManager.checkPermission(permissionName, packageName)
133                             == PackageManager.PERMISSION_DENIED;
134             int permissionFlags =
135                     mPackageManager.getPermissionFlags(permissionName, packageName, checkedUser);
136             if (!isAlreadyDenied) {
137                 mPackageManager.revokeRuntimePermission(
138                         packageName, permissionName, checkedUser, reason);
139             }
140             if (isAlreadyDenied
141                     && (permissionFlags & PackageManager.FLAG_PERMISSION_USER_SET) != 0) {
142                 permissionFlags = permissionFlags | PackageManager.FLAG_PERMISSION_USER_FIXED;
143             } else {
144                 permissionFlags = permissionFlags | PackageManager.FLAG_PERMISSION_USER_SET;
145             }
146             permissionFlags = permissionFlags & ~PackageManager.FLAG_PERMISSION_AUTO_REVOKED;
147             mPackageManager.updatePermissionFlags(
148                     permissionName,
149                     packageName,
150                     MASK_PERMISSION_FLAGS,
151                     permissionFlags,
152                     checkedUser);
153             removeFromPriorityListIfRequired(packageName, permissionName);
154         } finally {
155             Binder.restoreCallingIdentity(token);
156         }
157     }
158 
159     /** See {@link HealthConnectManager#revokeAllHealthPermissions}. */
revokeAllHealthPermissions( @onNull String packageName, @Nullable String reason, @NonNull UserHandle user)160     public void revokeAllHealthPermissions(
161             @NonNull String packageName, @Nullable String reason, @NonNull UserHandle user) {
162         Objects.requireNonNull(packageName);
163         enforceManageHealthPermissions(/* message= */ "revokeAllHealthPermissions");
164         UserHandle checkedUser = UserHandle.of(handleIncomingUser(user.getIdentifier()));
165         enforceValidPackage(packageName, checkedUser);
166         final long token = Binder.clearCallingIdentity();
167         try {
168             revokeAllHealthPermissionsUnchecked(packageName, checkedUser, reason);
169         } finally {
170             Binder.restoreCallingIdentity(token);
171         }
172     }
173 
174     /** See {@link HealthConnectManager#getGrantedHealthPermissions}. */
175     @NonNull
getGrantedHealthPermissions( @onNull String packageName, @NonNull UserHandle user)176     public List<String> getGrantedHealthPermissions(
177             @NonNull String packageName, @NonNull UserHandle user) {
178         Objects.requireNonNull(packageName);
179         enforceManageHealthPermissions(/* message= */ "getGrantedHealthPermissions");
180         UserHandle checkedUser = UserHandle.of(handleIncomingUser(user.getIdentifier()));
181         enforceValidPackage(packageName, checkedUser);
182         final long token = Binder.clearCallingIdentity();
183         try {
184             return getGrantedHealthPermissionsUnchecked(packageName, checkedUser);
185         } finally {
186             Binder.restoreCallingIdentity(token);
187         }
188     }
189 
190     /**
191      * Returns {@code true} if there is at least one granted permission for the provided {@code
192      * packageName}, {@code false} otherwise.
193      */
hasGrantedHealthPermissions( @onNull String packageName, @NonNull UserHandle user)194     public boolean hasGrantedHealthPermissions(
195             @NonNull String packageName, @NonNull UserHandle user) {
196         return !getGrantedHealthPermissions(packageName, user).isEmpty();
197     }
198 
199     /**
200      * Returns the date from which an app can read / write health data. See {@link
201      * HealthConnectManager#getHealthDataHistoricalAccessStartDate}
202      */
203     @Nullable
getHealthDataStartDateAccess(String packageName, UserHandle user)204     public Instant getHealthDataStartDateAccess(String packageName, UserHandle user)
205             throws IllegalArgumentException {
206         Objects.requireNonNull(packageName);
207         enforceManageHealthPermissions(/* message= */ "getHealthDataStartDateAccess");
208         UserHandle checkedUser = UserHandle.of(handleIncomingUser(user.getIdentifier()));
209         enforceValidPackage(packageName, checkedUser);
210 
211         Instant grantTimeDate = mFirstGrantTimeManager.getFirstGrantTime(packageName, checkedUser);
212         if (grantTimeDate == null) {
213             return null;
214         }
215 
216         return grantTimeDate.minus(GRANT_TIME_TO_START_ACCESS_DATE_PERIOD);
217     }
218 
addToPriorityListIfRequired(String packageName, String permissionName)219     private void addToPriorityListIfRequired(String packageName, String permissionName) {
220         if (HealthPermissions.isWritePermission(permissionName)) {
221             HealthDataCategoryPriorityHelper.getInstance()
222                     .appendToPriorityList(
223                             packageName,
224                             HealthPermissions.getHealthDataCategory(permissionName),
225                             mContext);
226         }
227     }
228 
removeFromPriorityListIfRequired(String packageName, String permissionName)229     private void removeFromPriorityListIfRequired(String packageName, String permissionName) {
230         if (HealthPermissions.isWritePermission(permissionName)) {
231             HealthDataCategoryPriorityHelper.getInstance()
232                     .removeFromPriorityList(
233                             packageName,
234                             HealthPermissions.getHealthDataCategory(permissionName),
235                             this,
236                             mContext.getUser());
237         }
238     }
239 
240     @NonNull
getGrantedHealthPermissionsUnchecked( @onNull String packageName, @NonNull UserHandle user)241     private List<String> getGrantedHealthPermissionsUnchecked(
242             @NonNull String packageName, @NonNull UserHandle user) {
243         PackageInfo packageInfo;
244         try {
245             PackageManager packageManager =
246                     mContext.createContextAsUser(user, /* flags= */ 0).getPackageManager();
247             packageInfo =
248                     packageManager.getPackageInfo(
249                             packageName,
250                             PackageManager.PackageInfoFlags.of(PackageManager.GET_PERMISSIONS));
251         } catch (PackageManager.NameNotFoundException e) {
252             throw new IllegalArgumentException("Invalid package", e);
253         }
254 
255         if (packageInfo.requestedPermissions == null) {
256             return List.of();
257         }
258 
259         List<String> grantedHealthPerms = new ArrayList<>(packageInfo.requestedPermissions.length);
260         for (int i = 0; i < packageInfo.requestedPermissions.length; i++) {
261             String currPerm = packageInfo.requestedPermissions[i];
262             if (mHealthPermissions.contains(currPerm)
263                     && ((packageInfo.requestedPermissionsFlags[i]
264                                     & PackageInfo.REQUESTED_PERMISSION_GRANTED)
265                             != 0)) {
266                 grantedHealthPerms.add(currPerm);
267             }
268         }
269         return grantedHealthPerms;
270     }
271 
revokeAllHealthPermissionsUnchecked( String packageName, UserHandle user, String reason)272     private void revokeAllHealthPermissionsUnchecked(
273             String packageName, UserHandle user, String reason) {
274         List<String> grantedHealthPermissions =
275                 getGrantedHealthPermissionsUnchecked(packageName, user);
276         for (String perm : grantedHealthPermissions) {
277             mPackageManager.revokeRuntimePermission(packageName, perm, user, reason);
278             mPackageManager.updatePermissionFlags(
279                     perm,
280                     packageName,
281                     MASK_PERMISSION_FLAGS,
282                     PackageManager.FLAG_PERMISSION_USER_SET,
283                     user);
284             removeFromPriorityListIfRequired(packageName, perm);
285         }
286     }
287 
enforceValidPermission(String permissionName)288     private void enforceValidPermission(String permissionName) {
289         if (!mHealthPermissions.contains(permissionName)) {
290             throw new IllegalArgumentException("invalid health permission");
291         }
292     }
293 
enforceValidPackage(String packageName, UserHandle user)294     private void enforceValidPackage(String packageName, UserHandle user) {
295         try {
296             PackageManager packageManager =
297                     mContext.createContextAsUser(user, /* flags= */ 0).getPackageManager();
298 
299             packageManager.getPackageInfo(packageName, PackageManager.PackageInfoFlags.of(0));
300         } catch (PackageManager.NameNotFoundException e) {
301             throw new IllegalArgumentException("invalid package", e);
302         }
303     }
304 
enforceManageHealthPermissions(String message)305     private void enforceManageHealthPermissions(String message) {
306         mContext.enforceCallingOrSelfPermission(
307                 HealthPermissions.MANAGE_HEALTH_PERMISSIONS, message);
308     }
309 
enforceSupportPermissionsUsageIntent(String packageName, UserHandle userHandle)310     private void enforceSupportPermissionsUsageIntent(String packageName, UserHandle userHandle) {
311         if (!mPermissionIntentAppsTracker.supportsPermissionUsageIntent(packageName, userHandle)) {
312             throw new SecurityException(
313                     "Package "
314                             + packageName
315                             + " for "
316                             + userHandle.toString()
317                             + " doesn't support health permissions usage intent.");
318         }
319     }
320 
321     /**
322      * Checks input user id and converts it to positive id if needed, returns converted user id.
323      *
324      * @throws java.lang.SecurityException if the caller is affecting different users without
325      *     holding the {@link INTERACT_ACROSS_USERS_FULL} permission.
326      */
handleIncomingUser(int userId)327     private int handleIncomingUser(int userId) {
328         int callingUserId = UserHandle.getUserHandleForUid(Binder.getCallingUid()).getIdentifier();
329         if (userId == callingUserId) {
330             return userId;
331         }
332 
333         boolean canInteractAcrossUsersFull =
334                 mContext.checkCallingOrSelfPermission(INTERACT_ACROSS_USERS_FULL)
335                         == PERMISSION_GRANTED;
336         if (canInteractAcrossUsersFull) {
337             // If the UserHandle.CURRENT has been passed (negative value),
338             // convert it to positive userId.
339             if (userId == UserHandle.CURRENT.getIdentifier()) {
340                 return ActivityManager.getCurrentUser();
341             }
342             return userId;
343         }
344 
345         throw new SecurityException(
346                 "Permission denied. Need to run as either the calling user id ("
347                         + callingUserId
348                         + "), or with "
349                         + INTERACT_ACROSS_USERS_FULL
350                         + " permission");
351     }
352 }
353