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.Intent;
27 import android.content.pm.PackageInfo;
28 import android.content.pm.PackageManager;
29 import android.health.connect.HealthConnectManager;
30 import android.health.connect.HealthPermissions;
31 import android.os.Binder;
32 import android.os.UserHandle;
33 import android.util.ArrayMap;
34 import android.util.ArraySet;
35 
36 import com.android.server.healthconnect.storage.datatypehelpers.HealthDataCategoryPriorityHelper;
37 
38 import java.time.Instant;
39 import java.time.Period;
40 import java.util.ArrayList;
41 import java.util.List;
42 import java.util.Map;
43 import java.util.Objects;
44 import java.util.Optional;
45 import java.util.Set;
46 
47 /**
48  * A handler for HealthConnect permission-related logic.
49  *
50  * @hide
51  */
52 public final class HealthConnectPermissionHelper {
53     private static final Period GRANT_TIME_TO_START_ACCESS_DATE_PERIOD = Period.ofDays(30);
54 
55     private static final int MASK_PERMISSION_FLAGS =
56             PackageManager.FLAG_PERMISSION_USER_SET
57                     | PackageManager.FLAG_PERMISSION_USER_FIXED
58                     | PackageManager.FLAG_PERMISSION_AUTO_REVOKED;
59 
60     private final Context mContext;
61     private final PackageManager mPackageManager;
62     private final Set<String> mHealthPermissions;
63     private final HealthPermissionIntentAppsTracker mPermissionIntentAppsTracker;
64     private final FirstGrantTimeManager mFirstGrantTimeManager;
65 
66     /**
67      * Constructs a {@link HealthConnectPermissionHelper}.
68      *
69      * @param context the service context.
70      * @param packageManager a {@link PackageManager} instance.
71      * @param healthPermissions a {@link Set} of permissions that are recognized as
72      *     HealthConnect-defined permissions.
73      * @param permissionIntentTracker a {@link
74      *     com.android.server.healthconnect.permission.HealthPermissionIntentAppsTracker} instance
75      *     that tracks apps allowed to request health permissions.
76      */
HealthConnectPermissionHelper( Context context, PackageManager packageManager, Set<String> healthPermissions, HealthPermissionIntentAppsTracker permissionIntentTracker, FirstGrantTimeManager firstGrantTimeManager)77     public HealthConnectPermissionHelper(
78             Context context,
79             PackageManager packageManager,
80             Set<String> healthPermissions,
81             HealthPermissionIntentAppsTracker permissionIntentTracker,
82             FirstGrantTimeManager firstGrantTimeManager) {
83         mContext = context;
84         mPackageManager = packageManager;
85         mHealthPermissions = healthPermissions;
86         mPermissionIntentAppsTracker = permissionIntentTracker;
87         mFirstGrantTimeManager = firstGrantTimeManager;
88     }
89 
90     /**
91      * See {@link HealthConnectManager#grantHealthPermission}.
92      *
93      * <p>NOTE: Once permission grant is successful, the package name will also be appended to the
94      * end of the priority list corresponding to {@code permissionName}'s health permission
95      * category.
96      */
grantHealthPermission( @onNull String packageName, @NonNull String permissionName, @NonNull UserHandle user)97     public void grantHealthPermission(
98             @NonNull String packageName, @NonNull String permissionName, @NonNull UserHandle user) {
99         Objects.requireNonNull(packageName);
100         Objects.requireNonNull(permissionName);
101         enforceManageHealthPermissions(/* message= */ "grantHealthPermission");
102         enforceValidHealthPermission(permissionName);
103         UserHandle checkedUser = UserHandle.of(handleIncomingUser(user.getIdentifier()));
104         enforceValidPackage(packageName, checkedUser);
105         enforceSupportPermissionsUsageIntent(packageName, checkedUser);
106         final long token = Binder.clearCallingIdentity();
107         try {
108             mPackageManager.grantRuntimePermission(packageName, permissionName, checkedUser);
109             mPackageManager.updatePermissionFlags(
110                     permissionName,
111                     packageName,
112                     MASK_PERMISSION_FLAGS,
113                     PackageManager.FLAG_PERMISSION_USER_SET,
114                     checkedUser);
115             addToPriorityListIfRequired(packageName, permissionName);
116 
117         } finally {
118             Binder.restoreCallingIdentity(token);
119         }
120     }
121 
122     /** See {@link HealthConnectManager#revokeHealthPermission}. */
revokeHealthPermission( @onNull String packageName, @NonNull String permissionName, @Nullable String reason, @NonNull UserHandle user)123     public void revokeHealthPermission(
124             @NonNull String packageName,
125             @NonNull String permissionName,
126             @Nullable String reason,
127             @NonNull UserHandle user) {
128         Objects.requireNonNull(packageName);
129         Objects.requireNonNull(permissionName);
130         enforceManageHealthPermissions(/* message= */ "revokeHealthPermission");
131         enforceValidHealthPermission(permissionName);
132         UserHandle checkedUser = UserHandle.of(handleIncomingUser(user.getIdentifier()));
133         enforceValidPackage(packageName, checkedUser);
134         final long token = Binder.clearCallingIdentity();
135         try {
136             boolean isAlreadyDenied =
137                     mPackageManager.checkPermission(permissionName, packageName)
138                             == PackageManager.PERMISSION_DENIED;
139             int permissionFlags =
140                     mPackageManager.getPermissionFlags(permissionName, packageName, checkedUser);
141             if (!isAlreadyDenied) {
142                 mPackageManager.revokeRuntimePermission(
143                         packageName, permissionName, checkedUser, reason);
144             }
145             if (isAlreadyDenied
146                     && (permissionFlags & PackageManager.FLAG_PERMISSION_USER_SET) != 0) {
147                 permissionFlags = permissionFlags | PackageManager.FLAG_PERMISSION_USER_FIXED;
148             } else {
149                 permissionFlags = permissionFlags | PackageManager.FLAG_PERMISSION_USER_SET;
150             }
151             permissionFlags = permissionFlags & ~PackageManager.FLAG_PERMISSION_AUTO_REVOKED;
152             mPackageManager.updatePermissionFlags(
153                     permissionName,
154                     packageName,
155                     MASK_PERMISSION_FLAGS,
156                     permissionFlags,
157                     checkedUser);
158 
159             removeFromPriorityListIfRequired(packageName, permissionName);
160 
161         } finally {
162             Binder.restoreCallingIdentity(token);
163         }
164     }
165 
166     /** See {@link HealthConnectManager#revokeAllHealthPermissions}. */
167     @SuppressWarnings("NullAway") // TODO(b/317029272): fix this suppression
revokeAllHealthPermissions( @onNull String packageName, @Nullable String reason, @NonNull UserHandle user)168     public void revokeAllHealthPermissions(
169             @NonNull String packageName, @Nullable String reason, @NonNull UserHandle user) {
170         Objects.requireNonNull(packageName);
171         enforceManageHealthPermissions(/* message= */ "revokeAllHealthPermissions");
172         UserHandle checkedUser = UserHandle.of(handleIncomingUser(user.getIdentifier()));
173         enforceValidPackage(packageName, checkedUser);
174         final long token = Binder.clearCallingIdentity();
175         try {
176             revokeAllHealthPermissionsUnchecked(packageName, checkedUser, reason);
177         } finally {
178             Binder.restoreCallingIdentity(token);
179         }
180     }
181 
182     /** See {@link HealthConnectManager#getGrantedHealthPermissions}. */
183     @NonNull
getGrantedHealthPermissions( @onNull String packageName, @NonNull UserHandle user)184     public List<String> getGrantedHealthPermissions(
185             @NonNull String packageName, @NonNull UserHandle user) {
186         Objects.requireNonNull(packageName);
187         enforceManageHealthPermissions(/* message= */ "getGrantedHealthPermissions");
188         UserHandle checkedUser = UserHandle.of(handleIncomingUser(user.getIdentifier()));
189         enforceValidPackage(packageName, checkedUser);
190         final long token = Binder.clearCallingIdentity();
191         try {
192             return getGrantedHealthPermissionsUnchecked(packageName, checkedUser);
193         } finally {
194             Binder.restoreCallingIdentity(token);
195         }
196     }
197 
198     /** See {@link HealthConnectManager#getHealthPermissionsFlags(String, List)}. */
199     @NonNull
getHealthPermissionsFlags( @onNull String packageName, @NonNull UserHandle user, @NonNull List<String> permissions)200     public Map<String, Integer> getHealthPermissionsFlags(
201             @NonNull String packageName,
202             @NonNull UserHandle user,
203             @NonNull List<String> permissions) {
204         Objects.requireNonNull(packageName);
205         Objects.requireNonNull(user);
206         Objects.requireNonNull(permissions);
207 
208         enforceManageHealthPermissions(/* message= */ "getHealthPermissionsFlags");
209         UserHandle checkedUser = UserHandle.of(handleIncomingUser(user.getIdentifier()));
210         enforceValidPackage(packageName, checkedUser);
211         final long token = Binder.clearCallingIdentity();
212         try {
213             return getHealthPermissionsFlagsUnchecked(packageName, checkedUser, permissions);
214         } finally {
215             Binder.restoreCallingIdentity(token);
216         }
217     }
218 
219     /** See {@link HealthConnectManager#setHealthPermissionsUserFixedFlagValue(String, List)}. */
setHealthPermissionsUserFixedFlagValue( @onNull String packageName, @NonNull UserHandle user, @NonNull List<String> permissions, boolean value)220     public void setHealthPermissionsUserFixedFlagValue(
221             @NonNull String packageName,
222             @NonNull UserHandle user,
223             @NonNull List<String> permissions,
224             boolean value) {
225         Objects.requireNonNull(packageName);
226         Objects.requireNonNull(user);
227         Objects.requireNonNull(permissions);
228 
229         enforceManageHealthPermissions(/* message= */ "setHealthPermissionsUserFixedFlagValue");
230         UserHandle checkedUser = UserHandle.of(handleIncomingUser(user.getIdentifier()));
231         enforceValidPackage(packageName, checkedUser);
232         final long token = Binder.clearCallingIdentity();
233         try {
234             setHealthPermissionsUserFixedFlagValueUnchecked(
235                     packageName, checkedUser, permissions, value);
236         } finally {
237             Binder.restoreCallingIdentity(token);
238         }
239     }
240 
241     /**
242      * Returns {@code true} if there is at least one granted permission for the provided {@code
243      * packageName}, {@code false} otherwise.
244      */
hasGrantedHealthPermissions( @onNull String packageName, @NonNull UserHandle user)245     public boolean hasGrantedHealthPermissions(
246             @NonNull String packageName, @NonNull UserHandle user) {
247         return !getGrantedHealthPermissions(packageName, user).isEmpty();
248     }
249 
250     /**
251      * Returns the date from which an app can read / write health data. See {@link
252      * HealthConnectManager#getHealthDataHistoricalAccessStartDate}
253      */
getHealthDataStartDateAccess(String packageName, UserHandle user)254     public Optional<Instant> getHealthDataStartDateAccess(String packageName, UserHandle user)
255             throws IllegalArgumentException {
256         Objects.requireNonNull(packageName);
257         enforceManageHealthPermissions(/* message= */ "getHealthDataStartDateAccess");
258         UserHandle checkedUser = UserHandle.of(handleIncomingUser(user.getIdentifier()));
259         enforceValidPackage(packageName, checkedUser);
260 
261         return mFirstGrantTimeManager
262                 .getFirstGrantTime(packageName, checkedUser)
263                 .map(grantTime -> grantTime.minus(GRANT_TIME_TO_START_ACCESS_DATE_PERIOD))
264                 .or(Optional::empty);
265     }
266 
267     /**
268      * Same as {@link #getHealthDataStartDateAccess(String, UserHandle)} except this method also
269      * throws {@link IllegalAccessException} if health permission is in an incorrect state where
270      * first grant time can't be fetched.
271      */
272     @NonNull
getHealthDataStartDateAccessOrThrow(String packageName, UserHandle user)273     public Instant getHealthDataStartDateAccessOrThrow(String packageName, UserHandle user) {
274         Optional<Instant> startDateAccess = getHealthDataStartDateAccess(packageName, user);
275         if (startDateAccess.isEmpty()) {
276             throwExceptionIncorrectPermissionState();
277         }
278         return startDateAccess.get();
279     }
280 
throwExceptionIncorrectPermissionState()281     private void throwExceptionIncorrectPermissionState() {
282         throw new IllegalStateException(
283                 "Incorrect health permission state, likely"
284                         + " because the calling application's manifest does not specify handling "
285                         + Intent.ACTION_VIEW_PERMISSION_USAGE
286                         + " with "
287                         + HealthConnectManager.CATEGORY_HEALTH_PERMISSIONS);
288     }
289 
addToPriorityListIfRequired(String packageName, String permissionName)290     private void addToPriorityListIfRequired(String packageName, String permissionName) {
291         if (HealthPermissions.isWritePermission(permissionName)) {
292             HealthDataCategoryPriorityHelper.getInstance()
293                     .appendToPriorityList(
294                             packageName,
295                             HealthPermissions.getHealthDataCategoryForWritePermission(
296                                     permissionName),
297                             mContext,
298                             /* isInactiveApp= */ false);
299         }
300     }
301 
removeFromPriorityListIfRequired(String packageName, String permissionName)302     private void removeFromPriorityListIfRequired(String packageName, String permissionName) {
303         if (HealthPermissions.isWritePermission(permissionName)) {
304             HealthDataCategoryPriorityHelper.getInstance()
305                     .maybeRemoveAppFromPriorityList(
306                             packageName,
307                             HealthPermissions.getHealthDataCategoryForWritePermission(
308                                     permissionName),
309                             this,
310                             mContext.getUser());
311         }
312     }
313 
314     @NonNull
getGrantedHealthPermissionsUnchecked( @onNull String packageName, @NonNull UserHandle user)315     private List<String> getGrantedHealthPermissionsUnchecked(
316             @NonNull String packageName, @NonNull UserHandle user) {
317         PackageInfo packageInfo =
318                 getPackageInfoUnchecked(
319                         packageName,
320                         user,
321                         PackageManager.PackageInfoFlags.of(PackageManager.GET_PERMISSIONS));
322 
323         if (packageInfo.requestedPermissions == null) {
324             return List.of();
325         }
326 
327         List<String> grantedHealthPerms = new ArrayList<>(packageInfo.requestedPermissions.length);
328         for (int i = 0; i < packageInfo.requestedPermissions.length; i++) {
329             String currPerm = packageInfo.requestedPermissions[i];
330             if (mHealthPermissions.contains(currPerm)
331                     && ((packageInfo.requestedPermissionsFlags[i]
332                                     & PackageInfo.REQUESTED_PERMISSION_GRANTED)
333                             != 0)) {
334                 grantedHealthPerms.add(currPerm);
335             }
336         }
337         return grantedHealthPerms;
338     }
339 
340     @NonNull
getHealthPermissionsFlagsUnchecked( @onNull String packageName, @NonNull UserHandle user, @NonNull List<String> permissions)341     private Map<String, Integer> getHealthPermissionsFlagsUnchecked(
342             @NonNull String packageName,
343             @NonNull UserHandle user,
344             @NonNull List<String> permissions) {
345         enforceValidHealthPermissions(packageName, user, permissions);
346 
347         Map<String, Integer> result = new ArrayMap<>();
348 
349         for (String permission : permissions) {
350             result.put(
351                     permission, mPackageManager.getPermissionFlags(permission, packageName, user));
352         }
353 
354         return result;
355     }
356 
setHealthPermissionsUserFixedFlagValueUnchecked( @onNull String packageName, @NonNull UserHandle user, @NonNull List<String> permissions, boolean value)357     private void setHealthPermissionsUserFixedFlagValueUnchecked(
358             @NonNull String packageName,
359             @NonNull UserHandle user,
360             @NonNull List<String> permissions,
361             boolean value) {
362         enforceValidHealthPermissions(packageName, user, permissions);
363 
364         int flagMask = PackageManager.FLAG_PERMISSION_USER_FIXED;
365         int flagValues = value ? PackageManager.FLAG_PERMISSION_USER_FIXED : 0;
366 
367         for (String permission : permissions) {
368             mPackageManager.updatePermissionFlags(
369                     permission, packageName, flagMask, flagValues, user);
370         }
371     }
372 
revokeAllHealthPermissionsUnchecked( String packageName, UserHandle user, String reason)373     private void revokeAllHealthPermissionsUnchecked(
374             String packageName, UserHandle user, String reason) {
375         List<String> grantedHealthPermissions =
376                 getGrantedHealthPermissionsUnchecked(packageName, user);
377         for (String perm : grantedHealthPermissions) {
378             mPackageManager.revokeRuntimePermission(packageName, perm, user, reason);
379             mPackageManager.updatePermissionFlags(
380                     perm,
381                     packageName,
382                     MASK_PERMISSION_FLAGS,
383                     PackageManager.FLAG_PERMISSION_USER_SET,
384                     user);
385             removeFromPriorityListIfRequired(packageName, perm);
386         }
387     }
388 
enforceValidHealthPermission(String permissionName)389     private void enforceValidHealthPermission(String permissionName) {
390         if (!mHealthPermissions.contains(permissionName)) {
391             throw new IllegalArgumentException("invalid health permission");
392         }
393     }
394 
getPackageInfoUnchecked( String packageName, UserHandle user, PackageManager.PackageInfoFlags flags)395     private PackageInfo getPackageInfoUnchecked(
396             String packageName, UserHandle user, PackageManager.PackageInfoFlags flags) {
397         try {
398             PackageManager packageManager =
399                     mContext.createContextAsUser(user, /* flags= */ 0).getPackageManager();
400 
401             return packageManager.getPackageInfo(packageName, flags);
402         } catch (PackageManager.NameNotFoundException e) {
403             throw new IllegalArgumentException("invalid package", e);
404         }
405     }
406 
enforceValidPackage(String packageName, UserHandle user)407     private void enforceValidPackage(String packageName, UserHandle user) {
408         getPackageInfoUnchecked(packageName, user, PackageManager.PackageInfoFlags.of(0));
409     }
410 
enforceManageHealthPermissions(String message)411     private void enforceManageHealthPermissions(String message) {
412         mContext.enforceCallingOrSelfPermission(
413                 HealthPermissions.MANAGE_HEALTH_PERMISSIONS, message);
414     }
415 
enforceSupportPermissionsUsageIntent(String packageName, UserHandle userHandle)416     private void enforceSupportPermissionsUsageIntent(String packageName, UserHandle userHandle) {
417         if (!mPermissionIntentAppsTracker.supportsPermissionUsageIntent(packageName, userHandle)) {
418             throw new SecurityException(
419                     "Package "
420                             + packageName
421                             + " for "
422                             + userHandle.toString()
423                             + " doesn't support health permissions usage intent.");
424         }
425     }
426 
427     /**
428      * Checks input user id and converts it to positive id if needed, returns converted user id.
429      *
430      * @throws java.lang.SecurityException if the caller is affecting different users without
431      *     holding the {@link INTERACT_ACROSS_USERS_FULL} permission.
432      */
handleIncomingUser(int userId)433     private int handleIncomingUser(int userId) {
434         int callingUserId = UserHandle.getUserHandleForUid(Binder.getCallingUid()).getIdentifier();
435         if (userId == callingUserId) {
436             return userId;
437         }
438 
439         boolean canInteractAcrossUsersFull =
440                 mContext.checkCallingOrSelfPermission(INTERACT_ACROSS_USERS_FULL)
441                         == PERMISSION_GRANTED;
442         if (canInteractAcrossUsersFull) {
443             // If the UserHandle.CURRENT has been passed (negative value),
444             // convert it to positive userId.
445             if (userId == UserHandle.CURRENT.getIdentifier()) {
446                 return ActivityManager.getCurrentUser();
447             }
448             return userId;
449         }
450 
451         throw new SecurityException(
452                 "Permission denied. Need to run as either the calling user id ("
453                         + callingUserId
454                         + "), or with "
455                         + INTERACT_ACROSS_USERS_FULL
456                         + " permission");
457     }
458 
enforceValidHealthPermissions( String packageName, UserHandle user, List<String> permissions)459     private void enforceValidHealthPermissions(
460             String packageName, UserHandle user, List<String> permissions) {
461         PackageInfo packageInfo =
462                 getPackageInfoUnchecked(
463                         packageName,
464                         user,
465                         PackageManager.PackageInfoFlags.of(PackageManager.GET_PERMISSIONS));
466 
467         Set<String> requestedPermissions = new ArraySet<>(packageInfo.requestedPermissions);
468 
469         for (String permission : permissions) {
470             if (!requestedPermissions.contains(permission)) {
471                 throw new IllegalArgumentException(
472                         "undeclared permission " + permission + " for package " + packageName);
473             }
474 
475             enforceValidHealthPermission(permission);
476         }
477     }
478 }
479