1 /*
2  * Copyright (C) 2018 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 android.permission.cts;
18 
19 import static android.Manifest.permission.ACCESS_BACKGROUND_LOCATION;
20 import static android.Manifest.permission.ACCESS_COARSE_LOCATION;
21 import static android.Manifest.permission.ACCESS_FINE_LOCATION;
22 import static android.Manifest.permission.BODY_SENSORS;
23 import static android.Manifest.permission.READ_CALENDAR;
24 import static android.Manifest.permission.READ_CONTACTS;
25 import static android.Manifest.permission.WRITE_CALENDAR;
26 import static android.app.AppOpsManager.MODE_ALLOWED;
27 import static android.app.AppOpsManager.MODE_FOREGROUND;
28 import static android.app.AppOpsManager.permissionToOp;
29 import static android.content.pm.PackageManager.PERMISSION_DENIED;
30 import static android.content.pm.PackageManager.PERMISSION_GRANTED;
31 import static android.permission.PermissionControllerManager.COUNT_ONLY_WHEN_GRANTED;
32 import static android.permission.PermissionControllerManager.REASON_INSTALLER_POLICY_VIOLATION;
33 import static android.permission.PermissionControllerManager.REASON_MALWARE;
34 import static android.permission.cts.PermissionUtils.grantPermission;
35 import static android.permission.cts.PermissionUtils.isGranted;
36 import static android.permission.cts.PermissionUtils.isPermissionGranted;
37 
38 import static com.android.compatibility.common.util.SystemUtil.callWithShellPermissionIdentity;
39 import static com.android.compatibility.common.util.SystemUtil.eventually;
40 import static com.android.compatibility.common.util.SystemUtil.runShellCommand;
41 import static com.android.compatibility.common.util.SystemUtil.runShellCommandOrThrow;
42 import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
43 
44 import static com.google.common.truth.Truth.assertThat;
45 
46 import static java.util.Collections.singletonList;
47 
48 import android.app.AppOpsManager;
49 import android.app.UiAutomation;
50 import android.content.Context;
51 import android.content.pm.PermissionGroupInfo;
52 import android.permission.PermissionControllerManager;
53 import android.permission.RuntimePermissionPresentationInfo;
54 import android.platform.test.annotations.AppModeFull;
55 
56 import androidx.annotation.NonNull;
57 import androidx.test.InstrumentationRegistry;
58 import androidx.test.runner.AndroidJUnit4;
59 
60 import org.junit.After;
61 import org.junit.AfterClass;
62 import org.junit.Before;
63 import org.junit.BeforeClass;
64 import org.junit.Test;
65 import org.junit.runner.RunWith;
66 
67 import java.util.ArrayList;
68 import java.util.Collections;
69 import java.util.List;
70 import java.util.Map;
71 import java.util.concurrent.CompletableFuture;
72 import java.util.concurrent.Executor;
73 import java.util.concurrent.atomic.AtomicBoolean;
74 import java.util.concurrent.atomic.AtomicReference;
75 
76 /**
77  * Test {@link PermissionControllerManager}
78  */
79 @RunWith(AndroidJUnit4.class)
80 @AppModeFull(reason = "Instant apps cannot talk to permission controller")
81 public class PermissionControllerTest {
82     private static final String APK =
83             "/data/local/tmp/cts-permission/CtsAppThatAccessesLocationOnCommand.apk";
84     private static final String APP = "android.permission.cts.appthataccesseslocation";
85     private static final String APK2 =
86             "/data/local/tmp/cts-permission/"
87                     + "CtsAppThatRequestsCalendarContactsBodySensorCustomPermission.apk";
88     private static final String APP2 = "android.permission.cts.appthatrequestcustompermission";
89     private static final String CUSTOM_PERMISSION =
90             "android.permission.cts.appthatrequestcustompermission.TEST_PERMISSION";
91 
92     private static final UiAutomation sUiAutomation =
93             InstrumentationRegistry.getInstrumentation().getUiAutomation();
94     private static final Context sContext = InstrumentationRegistry.getTargetContext();
95     private static final PermissionControllerManager sController =
96             sContext.getSystemService(PermissionControllerManager.class);
97 
98     @Before
99     @After
resetAppState()100     public void resetAppState() {
101         runWithShellPermissionIdentity(() -> {
102             sUiAutomation.grantRuntimePermission(APP, ACCESS_FINE_LOCATION);
103             sUiAutomation.grantRuntimePermission(APP, ACCESS_BACKGROUND_LOCATION);
104             setAppOp(APP, ACCESS_FINE_LOCATION, MODE_ALLOWED);
105         });
106     }
107 
108     @BeforeClass
installApp()109     public static void installApp() {
110         runShellCommandOrThrow("pm install -r -g " + APK);
111         runShellCommandOrThrow("pm install -r " + APK2);
112     }
113 
114     @AfterClass
uninstallApp()115     public static void uninstallApp() {
116         runShellCommand("pm uninstall " + APP);
117         runShellCommand("pm uninstall " + APP2);
118     }
119 
revokePermissions( @onNull Map<String, List<String>> request, boolean doDryRun, int reason, @NonNull Executor executor)120     private @NonNull Map<String, List<String>> revokePermissions(
121             @NonNull Map<String, List<String>> request, boolean doDryRun, int reason,
122             @NonNull Executor executor) throws Exception {
123         AtomicReference<Map<String, List<String>>> result = new AtomicReference<>();
124 
125         sController.revokeRuntimePermissions(request, doDryRun, reason, executor,
126                 new PermissionControllerManager.OnRevokeRuntimePermissionsCallback() {
127                     @Override
128                     public void onRevokeRuntimePermissions(@NonNull Map<String, List<String>> r) {
129                         synchronized (result) {
130                             result.set(r);
131                             result.notifyAll();
132                         }
133                     }
134                 });
135 
136         synchronized (result) {
137             while (result.get() == null) {
138                 result.wait();
139             }
140         }
141 
142         return result.get();
143     }
144 
revokePermissions( @onNull Map<String, List<String>> request, boolean doDryRun, boolean adoptShell)145     private @NonNull Map<String, List<String>> revokePermissions(
146             @NonNull Map<String, List<String>> request, boolean doDryRun, boolean adoptShell)
147             throws Exception {
148         if (adoptShell) {
149             Map<String, List<String>> revokeRet =
150                     callWithShellPermissionIdentity(() -> revokePermissions(
151                             request, doDryRun, REASON_MALWARE, sContext.getMainExecutor()));
152             return revokeRet;
153         }
154         return revokePermissions(request, doDryRun, REASON_MALWARE, sContext.getMainExecutor());
155     }
156 
revokePermissions( @onNull Map<String, List<String>> request, boolean doDryRun)157     private @NonNull Map<String, List<String>> revokePermissions(
158             @NonNull Map<String, List<String>> request, boolean doDryRun) throws Exception {
159         return revokePermissions(request, doDryRun, true);
160     }
161 
setAppOp(@onNull String pkg, @NonNull String perm, int mode)162     private void setAppOp(@NonNull String pkg, @NonNull String perm, int mode) throws Exception {
163         sContext.getSystemService(AppOpsManager.class).setUidMode(permissionToOp(perm),
164                 sContext.getPackageManager().getPackageUid(pkg, 0), mode);
165     }
166 
buildRevokeRequest(@onNull String app, @NonNull String permission)167     private Map<String, List<String>> buildRevokeRequest(@NonNull String app,
168             @NonNull String permission) {
169         return Collections.singletonMap(app, singletonList(permission));
170     }
171 
assertRuntimePermissionLabelsAreValid(List<String> runtimePermissions, List<RuntimePermissionPresentationInfo> permissionInfos, int expectedRuntimeGranted, String app)172     private void assertRuntimePermissionLabelsAreValid(List<String> runtimePermissions,
173             List<RuntimePermissionPresentationInfo> permissionInfos, int expectedRuntimeGranted,
174             String app) throws Exception {
175         int numRuntimeGranted = 0;
176         for (String permission : runtimePermissions) {
177             if (isPermissionGranted(app, permission)) {
178                 numRuntimeGranted++;
179             }
180         }
181         assertThat(numRuntimeGranted).isEqualTo(expectedRuntimeGranted);
182 
183         ArrayList<CharSequence> maybeStandardPermissionLabels = new ArrayList<>();
184         ArrayList<CharSequence> nonStandardPermissionLabels = new ArrayList<>();
185         for (PermissionGroupInfo permGroup : sContext.getPackageManager().getAllPermissionGroups(
186                 0)) {
187             CharSequence permissionGroupLabel = permGroup.loadLabel(sContext.getPackageManager());
188             if (permGroup.packageName.equals("android")) {
189                 maybeStandardPermissionLabels.add(permissionGroupLabel);
190             } else {
191                 nonStandardPermissionLabels.add(permissionGroupLabel);
192             }
193         }
194 
195         int numInfosGranted = 0;
196 
197         for (RuntimePermissionPresentationInfo permissionInfo : permissionInfos) {
198             CharSequence permissionGroupLabel = permissionInfo.getLabel();
199 
200             // PermissionInfo should be included in exactly one of existing (possibly) standard
201             // or nonstandard permission groups
202             if (permissionInfo.isStandard()) {
203                 assertThat(maybeStandardPermissionLabels).contains(permissionGroupLabel);
204             } else {
205                 assertThat(nonStandardPermissionLabels).contains(permissionGroupLabel);
206             }
207             if (permissionInfo.isGranted()) {
208                 numInfosGranted++;
209             }
210         }
211 
212         // Each permissionInfo represents one or more runtime permissions, but we don't have a
213         // mapping, so we check that we have at least as many runtimePermissions as permissionInfos
214         assertThat(numRuntimeGranted).isAtLeast(numInfosGranted);
215     }
216 
217     @Test
revokePermissionsDryRunSinglePermission()218     public void revokePermissionsDryRunSinglePermission() throws Exception {
219         Map<String, List<String>> request = buildRevokeRequest(APP, ACCESS_BACKGROUND_LOCATION);
220 
221         Map<String, List<String>> result = revokePermissions(request, true);
222 
223         assertThat(result.size()).isEqualTo(1);
224         assertThat(result.get(APP)).isNotNull();
225         assertThat(result.get(APP)).containsExactly(ACCESS_BACKGROUND_LOCATION);
226     }
227 
228     @Test
revokePermissionsSinglePermission()229     public void revokePermissionsSinglePermission() throws Exception {
230         Map<String, List<String>> request = buildRevokeRequest(APP, ACCESS_BACKGROUND_LOCATION);
231 
232         revokePermissions(request, false);
233 
234         assertThat(sContext.getPackageManager().checkPermission(ACCESS_BACKGROUND_LOCATION,
235                 APP)).isEqualTo(PERMISSION_DENIED);
236     }
237 
238     @Test
revokePermissionsDoNotAlreadyRevokedPermission()239     public void revokePermissionsDoNotAlreadyRevokedPermission() throws Exception {
240         // Properly revoke the permission
241         runWithShellPermissionIdentity(() -> {
242             sUiAutomation.revokeRuntimePermission(APP, ACCESS_BACKGROUND_LOCATION);
243             setAppOp(APP, ACCESS_FINE_LOCATION, MODE_FOREGROUND);
244         });
245 
246         Map<String, List<String>> request = buildRevokeRequest(APP, ACCESS_BACKGROUND_LOCATION);
247         Map<String, List<String>> result = revokePermissions(request, false);
248 
249         assertThat(result).isEmpty();
250     }
251 
252     @Test
revokePermissionsDryRunForegroundPermission()253     public void revokePermissionsDryRunForegroundPermission() throws Exception {
254         assertThat(sContext.getPackageManager().checkPermission(ACCESS_FINE_LOCATION,
255                 APP)).isEqualTo(PERMISSION_GRANTED);
256 
257         Map<String, List<String>> request = buildRevokeRequest(APP, ACCESS_FINE_LOCATION);
258         Map<String, List<String>> result = revokePermissions(request, true);
259 
260         assertThat(result.size()).isEqualTo(1);
261         assertThat(result.get(APP)).isNotNull();
262         assertThat(result.get(APP)).containsExactly(ACCESS_FINE_LOCATION,
263                 ACCESS_BACKGROUND_LOCATION, ACCESS_COARSE_LOCATION);
264     }
265 
266     @Test
revokePermissionsUnrequestedPermission()267     public void revokePermissionsUnrequestedPermission() throws Exception {
268         Map<String, List<String>> request = buildRevokeRequest(APP, READ_CONTACTS);
269 
270         Map<String, List<String>> result = revokePermissions(request, false);
271 
272         assertThat(result).isEmpty();
273     }
274 
275     @Test
revokeFromUnknownPackage()276     public void revokeFromUnknownPackage() throws Exception {
277         Map<String, List<String>> request = buildRevokeRequest("invalid.app", READ_CONTACTS);
278 
279         Map<String, List<String>> result = revokePermissions(request, false);
280 
281         assertThat(result).isEmpty();
282     }
283 
284     @Test
revokePermissionsFromUnknownPermission()285     public void revokePermissionsFromUnknownPermission() throws Exception {
286         Map<String, List<String>> request = buildRevokeRequest(APP, "unknown.permission");
287 
288         Map<String, List<String>> result = revokePermissions(request, false);
289 
290         assertThat(result).isEmpty();
291     }
292 
293     @Test
revokePermissionsPolicyViolationFromWrongPackage()294     public void revokePermissionsPolicyViolationFromWrongPackage() throws Exception {
295         Map<String, List<String>> request = buildRevokeRequest(APP, ACCESS_FINE_LOCATION);
296         Map<String, List<String>> result = callWithShellPermissionIdentity(
297                 () -> revokePermissions(request,
298                         false, REASON_INSTALLER_POLICY_VIOLATION, sContext.getMainExecutor()));
299         assertThat(result).isEmpty();
300     }
301 
302     @Test
revokePermissionsWithExecutorForCallback()303     public void revokePermissionsWithExecutorForCallback() throws Exception {
304         Map<String, List<String>> request = buildRevokeRequest(APP, ACCESS_BACKGROUND_LOCATION);
305 
306         AtomicBoolean wasRunOnExecutor = new AtomicBoolean();
307         runWithShellPermissionIdentity(() ->
308                 revokePermissions(request, true, REASON_MALWARE, command -> {
309                     wasRunOnExecutor.set(true);
310                     command.run();
311                 }));
312 
313         assertThat(wasRunOnExecutor.get()).isTrue();
314     }
315 
316     @Test(expected = NullPointerException.class)
revokePermissionsWithNullPkg()317     public void revokePermissionsWithNullPkg() throws Exception {
318         Map<String, List<String>> request = Collections.singletonMap(null,
319                 singletonList(ACCESS_FINE_LOCATION));
320 
321         revokePermissions(request, true);
322     }
323 
324     @Test(expected = NullPointerException.class)
revokePermissionsWithNullPermissions()325     public void revokePermissionsWithNullPermissions() throws Exception {
326         Map<String, List<String>> request = Collections.singletonMap(APP, null);
327 
328         revokePermissions(request, true);
329     }
330 
331     @Test(expected = NullPointerException.class)
revokePermissionsWithNullPermission()332     public void revokePermissionsWithNullPermission() throws Exception {
333         Map<String, List<String>> request = Collections.singletonMap(APP,
334                 singletonList(null));
335 
336         revokePermissions(request, true);
337     }
338 
339     @Test(expected = NullPointerException.class)
revokePermissionsWithNullRequests()340     public void revokePermissionsWithNullRequests() {
341         sController.revokeRuntimePermissions(null, false, REASON_MALWARE,
342                 sContext.getMainExecutor(),
343                 new PermissionControllerManager.OnRevokeRuntimePermissionsCallback() {
344                     @Override
345                     public void onRevokeRuntimePermissions(
346                             @NonNull Map<String, List<String>> revoked) {
347                     }
348                 });
349     }
350 
351     @Test(expected = NullPointerException.class)
revokePermissionsWithNullCallback()352     public void revokePermissionsWithNullCallback() {
353         Map<String, List<String>> request = buildRevokeRequest(APP, ACCESS_BACKGROUND_LOCATION);
354 
355         sController.revokeRuntimePermissions(request, false, REASON_MALWARE,
356                 sContext.getMainExecutor(), null);
357     }
358 
359     @Test(expected = NullPointerException.class)
revokePermissionsWithNullExecutor()360     public void revokePermissionsWithNullExecutor() {
361         Map<String, List<String>> request = buildRevokeRequest(APP, ACCESS_BACKGROUND_LOCATION);
362 
363         sController.revokeRuntimePermissions(request, false, REASON_MALWARE, null,
364                 new PermissionControllerManager.OnRevokeRuntimePermissionsCallback() {
365                     @Override
366                     public void onRevokeRuntimePermissions(
367                             @NonNull Map<String, List<String>> revoked) {
368 
369                     }
370                 });
371     }
372 
373     @Test(expected = SecurityException.class)
revokePermissionsWithoutPermission()374     public void revokePermissionsWithoutPermission() throws Exception {
375         Map<String, List<String>> request = buildRevokeRequest(APP, ACCESS_BACKGROUND_LOCATION);
376 
377         // This will fail as the test-app does not have the required permission
378         revokePermissions(request, true, false);
379     }
380 
381     @Test
getAppPermissionsForApp()382     public void getAppPermissionsForApp() throws Exception {
383         CompletableFuture<List<RuntimePermissionPresentationInfo>> futurePermissionInfos =
384                 new CompletableFuture<>();
385 
386         List<String> runtimePermissions;
387         List<RuntimePermissionPresentationInfo> permissionInfos;
388 
389         sUiAutomation.adoptShellPermissionIdentity();
390         try {
391             sController.getAppPermissions(APP, futurePermissionInfos::complete, null);
392             runtimePermissions = PermissionUtils.getRuntimePermissions(APP);
393             assertThat(runtimePermissions).isNotEmpty();
394             permissionInfos = futurePermissionInfos.get();
395         } finally {
396             sUiAutomation.dropShellPermissionIdentity();
397         }
398 
399         assertRuntimePermissionLabelsAreValid(runtimePermissions, permissionInfos, 3, APP);
400     }
401 
402     @Test
getAppPermissionsForCustomApp()403     public void getAppPermissionsForCustomApp() throws Exception {
404         CompletableFuture<List<RuntimePermissionPresentationInfo>> futurePermissionInfos =
405                 new CompletableFuture<>();
406 
407         // Grant all requested permissions except READ_CALENDAR
408         sUiAutomation.grantRuntimePermission(APP2, CUSTOM_PERMISSION);
409         PermissionUtils.grantPermission(APP2, BODY_SENSORS);
410         PermissionUtils.grantPermission(APP2, READ_CONTACTS);
411         PermissionUtils.grantPermission(APP2, WRITE_CALENDAR);
412 
413         List<String> runtimePermissions;
414         List<RuntimePermissionPresentationInfo> permissionInfos;
415         sUiAutomation.adoptShellPermissionIdentity();
416         try {
417             sController.getAppPermissions(APP2, futurePermissionInfos::complete, null);
418             runtimePermissions = PermissionUtils.getRuntimePermissions(APP2);
419 
420             permissionInfos = futurePermissionInfos.get();
421         } finally {
422             sUiAutomation.dropShellPermissionIdentity();
423         }
424 
425         assertThat(permissionInfos).isNotEmpty();
426         assertThat(runtimePermissions.size()).isEqualTo(6);
427         assertRuntimePermissionLabelsAreValid(runtimePermissions, permissionInfos, 4, APP2);
428     }
429 
430     @Test
revokePermissionAutomaticallyExtendsToWholeGroup()431     public void revokePermissionAutomaticallyExtendsToWholeGroup() throws Exception {
432         grantPermission(APP2, READ_CALENDAR);
433         grantPermission(APP2, WRITE_CALENDAR);
434 
435         runWithShellPermissionIdentity(
436                 () -> {
437                     sController.revokeRuntimePermission(APP2, READ_CALENDAR);
438 
439                     eventually(() -> {
440                         assertThat(isGranted(APP2, READ_CALENDAR)).isEqualTo(false);
441                         // revokePermission automatically extends the revocation to whole group
442                         assertThat(isGranted(APP2, WRITE_CALENDAR)).isEqualTo(false);
443                     });
444                 });
445     }
446 
447     @Test
revokePermissionCustom()448     public void revokePermissionCustom() throws Exception {
449         sUiAutomation.grantRuntimePermission(APP2, CUSTOM_PERMISSION);
450 
451         runWithShellPermissionIdentity(
452                 () -> {
453                     sController.revokeRuntimePermission(APP2, CUSTOM_PERMISSION);
454 
455                     eventually(() -> {
456                         assertThat(isPermissionGranted(APP2, CUSTOM_PERMISSION)).isEqualTo(false);
457                     });
458                 });
459     }
460 
461     @Test
revokePermissionWithInvalidPkg()462     public void revokePermissionWithInvalidPkg() throws Exception {
463         // No return value, call is ignored
464         runWithShellPermissionIdentity(
465                 () -> sController.revokeRuntimePermission("invalid.package", READ_CALENDAR));
466     }
467 
468     @Test
revokePermissionWithInvalidPermission()469     public void revokePermissionWithInvalidPermission() throws Exception {
470         // No return value, call is ignored
471         runWithShellPermissionIdentity(
472                 () -> sController.revokeRuntimePermission(APP2, "invalid.permission"));
473     }
474 
475     @Test(expected = NullPointerException.class)
revokePermissionWithNullPkg()476     public void revokePermissionWithNullPkg() throws Exception {
477         sController.revokeRuntimePermission(null, READ_CALENDAR);
478     }
479 
480     @Test(expected = NullPointerException.class)
revokePermissionWithNullPermission()481     public void revokePermissionWithNullPermission() throws Exception {
482         sController.revokeRuntimePermission(APP2, null);
483     }
484 
485     // TODO: Add more tests for countPermissionAppsGranted when the method can be safely called
486     //       multiple times in a row
487 
488     @Test
countPermissionAppsGranted()489     public void countPermissionAppsGranted() {
490         runWithShellPermissionIdentity(
491                 () -> {
492                     CompletableFuture<Integer> numApps = new CompletableFuture<>();
493 
494                     sController.countPermissionApps(singletonList(ACCESS_FINE_LOCATION),
495                             COUNT_ONLY_WHEN_GRANTED, numApps::complete, null);
496 
497                     // TODO: Better would be to count before, grant a permission, count again and
498                     //       then compare before and after
499                     assertThat(numApps.get()).isAtLeast(1);
500                 });
501     }
502 
503     @Test(expected = NullPointerException.class)
countPermissionAppsNullPermission()504     public void countPermissionAppsNullPermission() {
505         sController.countPermissionApps(null, 0, (n) -> { }, null);
506     }
507 
508     @Test(expected = IllegalArgumentException.class)
countPermissionAppsInvalidFlags()509     public void countPermissionAppsInvalidFlags() {
510         sController.countPermissionApps(singletonList(ACCESS_FINE_LOCATION), -1, (n) -> { }, null);
511     }
512 
513     @Test(expected = NullPointerException.class)
countPermissionAppsNullCallback()514     public void countPermissionAppsNullCallback() {
515         sController.countPermissionApps(singletonList(ACCESS_FINE_LOCATION), 0, null, null);
516     }
517 }
518