1 /*
2  * Copyright (C) 2019 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.permissionpolicy.cts;
18 
19 import static android.Manifest.permission.READ_EXTERNAL_STORAGE;
20 import static android.app.AppOpsManager.MODE_ALLOWED;
21 import static android.app.AppOpsManager.OPSTR_LEGACY_STORAGE;
22 import static android.permission.cts.PermissionUtils.isGranted;
23 import static android.permissionpolicy.cts.RestrictedStoragePermissionSharedUidTest.StorageState.DENIED;
24 import static android.permissionpolicy.cts.RestrictedStoragePermissionSharedUidTest.StorageState.ISOLATED;
25 import static android.permissionpolicy.cts.RestrictedStoragePermissionSharedUidTest.StorageState.NON_ISOLATED;
26 
27 import static com.android.compatibility.common.util.SystemUtil.eventually;
28 import static com.android.compatibility.common.util.SystemUtil.runShellCommand;
29 import static com.android.compatibility.common.util.SystemUtil.runShellCommandOrThrow;
30 import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
31 
32 import static com.google.common.truth.Truth.assertWithMessage;
33 
34 import static java.lang.Integer.min;
35 
36 import android.app.AppOpsManager;
37 import android.content.Context;
38 import android.content.pm.PackageManager;
39 import android.os.Build;
40 import android.platform.test.annotations.AppModeFull;
41 import android.util.Log;
42 
43 import androidx.annotation.NonNull;
44 import androidx.test.platform.app.InstrumentationRegistry;
45 
46 import org.junit.After;
47 import org.junit.Test;
48 import org.junit.runner.RunWith;
49 import org.junit.runners.Parameterized;
50 import org.junit.runners.Parameterized.Parameter;
51 import org.junit.runners.Parameterized.Parameters;
52 
53 import java.util.ArrayList;
54 
55 @AppModeFull(reason = "Instant apps cannot access other app's properties")
56 @RunWith(Parameterized.class)
57 public class RestrictedStoragePermissionSharedUidTest {
58     private static final String LOG_TAG =
59             RestrictedStoragePermissionSharedUidTest.class.getSimpleName();
60 
61     public enum StorageState {
62         /** The app has non-isolated storage */
63         NON_ISOLATED,
64 
65         /** The app has isolated storage */
66         ISOLATED,
67 
68         /** The read-external-storage permission cannot be granted */
69         DENIED
70     }
71 
72     /**
73      * An app that is tested
74      */
75     private static class TestApp {
76         private static @NonNull Context sContext =
77                 InstrumentationRegistry.getInstrumentation().getContext();
78         private static @NonNull AppOpsManager sAppOpsManager =
79                 sContext.getSystemService(AppOpsManager.class);
80         private static @NonNull PackageManager sPackageManager = sContext.getPackageManager();
81 
82         private final String mApk;
83         private final String mPkg;
84 
85         public final boolean isRestricted;
86         public final boolean hasRequestedLegacyExternalStorage;
87 
TestApp(@onNull String apk, @NonNull String pkg, boolean isRestricted, @NonNull boolean hasRequestedLegacyExternalStorage)88         TestApp(@NonNull String apk, @NonNull String pkg, boolean isRestricted,
89                 @NonNull boolean hasRequestedLegacyExternalStorage) {
90             mApk = apk;
91             mPkg = pkg;
92 
93             this.isRestricted = isRestricted;
94             this.hasRequestedLegacyExternalStorage = hasRequestedLegacyExternalStorage;
95         }
96 
97         /**
98          * Assert that the read-external-storage permission was granted or not granted.
99          *
100          * @param expectGranted {@code true} if the permission is expected to be granted
101          */
assertStoragePermGranted(boolean expectGranted)102         void assertStoragePermGranted(boolean expectGranted) {
103             eventually(() -> assertWithMessage(this + " read storage granted").that(
104                     isGranted(mPkg, READ_EXTERNAL_STORAGE)).isEqualTo(expectGranted));
105         }
106 
107         /**
108          * Assert that the app has non-isolated storage
109          *
110          * @param expectGranted {@code true} if the app is expected to have non-isolated storage
111          */
assertHasNotIsolatedStorage(boolean expectHasNotIsolatedStorage)112         void assertHasNotIsolatedStorage(boolean expectHasNotIsolatedStorage) {
113             eventually(() -> runWithShellPermissionIdentity(() -> {
114                 int uid = sContext.getPackageManager().getPackageUid(mPkg, 0);
115                 if (expectHasNotIsolatedStorage) {
116                     assertWithMessage(this + " legacy storage mode").that(
117                             sAppOpsManager.unsafeCheckOpRawNoThrow(OPSTR_LEGACY_STORAGE, uid,
118                             mPkg)).isEqualTo(MODE_ALLOWED);
119                 } else {
120                     assertWithMessage(this + " legacy storage mode").that(
121                             sAppOpsManager.unsafeCheckOpRawNoThrow(OPSTR_LEGACY_STORAGE, uid,
122                             mPkg)).isNotEqualTo(MODE_ALLOWED);
123                 }
124             }));
125         }
126 
getTargetSDK()127         int getTargetSDK() throws Exception {
128             return sPackageManager.getApplicationInfo(mPkg, 0).targetSdkVersion;
129         }
130 
install()131         void install() {
132             if (isRestricted) {
133                 runShellCommandOrThrow(
134                         "pm install -g --force-queryable --restrict-permissions " + mApk);
135             } else {
136                 runShellCommandOrThrow("pm install -g --force-queryable " + mApk);
137             }
138         }
139 
uninstall()140         void uninstall() {
141             runShellCommand("pm uninstall " + mPkg);
142         }
143 
144         @Override
toString()145         public String toString() {
146             return mPkg.substring(PKG_PREFIX.length());
147         }
148     }
149 
150     /**
151      * Placeholder for "no app". The properties are chosen that when combined with another app, the
152      * other app always decides the resulting property,
153      */
154     private static class NoApp extends TestApp {
NoApp()155         NoApp() {
156             super("", PKG_PREFIX + "(none)", true, false);
157         }
158 
assertStoragePermGranted(boolean ignored)159         void assertStoragePermGranted(boolean ignored) {
160             // empty
161         }
162 
assertHasNotIsolatedStorage(boolean ignored)163         void assertHasNotIsolatedStorage(boolean ignored) {
164             // empty
165         }
166 
167         @Override
getTargetSDK()168         int getTargetSDK() {
169             return 10000;
170         }
171 
172         @Override
install()173         public void install() {
174             // empty
175         }
176 
177         @Override
uninstall()178         public void uninstall() {
179             // empty
180         }
181     }
182 
183     private static final String APK_PATH = "/data/local/tmp/cts-permissionpolicy/";
184     private static final String PKG_PREFIX = "android.permissionpolicy.cts.legacystoragewithshareduid.";
185 
186     private static final TestApp[] TEST_APPS = new TestApp[]{
187             new TestApp(APK_PATH + "CtsLegacyStorageNotIsolatedWithSharedUid.apk",
188                     PKG_PREFIX + "notisolated", false, true),
189             new TestApp(APK_PATH + "CtsLegacyStorageIsolatedWithSharedUid.apk",
190                     PKG_PREFIX + "isolated", false, false),
191             new TestApp(APK_PATH + "CtsLegacyStorageRestrictedWithSharedUid.apk",
192                     PKG_PREFIX + "restricted", true, false),
193             new TestApp(APK_PATH + "CtsLegacyStorageRestrictedSdk28WithSharedUid.apk",
194                     PKG_PREFIX + "restrictedsdk28", true, true),
195             new NoApp()};
196 
197     /**
198      * First app to be tested. This is the first in an entry created by {@link
199      * #getTestAppCombinations}
200      */
201     @Parameter(0)
202     public @NonNull TestApp app1;
203 
204     /**
205      * Second app to be tested. This is the second in an entry created by {@link
206      * #getTestAppCombinations}
207      */
208     @Parameter(1)
209     public @NonNull TestApp app2;
210 
211     /**
212      * Run this test for all combination of two tests-apps out of {@link #TEST_APPS}. This includes
213      * the {@link NoApp}, i.e. we also test a single test-app by itself.
214      *
215      * @return All combinations of two test-apps
216      */
217     @Parameters(name = "{0} and {1}")
getTestAppCombinations()218     public static Iterable<Object[]> getTestAppCombinations() {
219         ArrayList<Object[]> parameters = new ArrayList<>();
220 
221         for (int firstApp = 0; firstApp < TEST_APPS.length; firstApp++) {
222             for (int secondApp = firstApp + 1; secondApp < TEST_APPS.length; secondApp++) {
223                 parameters.add(new Object[]{TEST_APPS[firstApp], TEST_APPS[secondApp]});
224             }
225         }
226 
227         return parameters;
228     }
229 
230     @Test
checkExceptedStorageStateForAppsSharingUid()231     public void checkExceptedStorageStateForAppsSharingUid() throws Exception {
232         app1.install();
233         app2.install();
234 
235         int targetSDK = min(app1.getTargetSDK(), app2.getTargetSDK());
236         boolean isRestricted = app1.isRestricted && app2.isRestricted;
237         boolean hasRequestedLegacyExternalStorage =
238                 app1.hasRequestedLegacyExternalStorage || app2.hasRequestedLegacyExternalStorage;
239 
240         StorageState expectedState;
241         if (isRestricted) {
242             if (targetSDK < Build.VERSION_CODES.Q) {
243                 expectedState = DENIED;
244             } else {
245                 expectedState = ISOLATED;
246             }
247         } else if (hasRequestedLegacyExternalStorage && targetSDK <= Build.VERSION_CODES.Q) {
248             expectedState = NON_ISOLATED;
249         } else {
250             expectedState = ISOLATED;
251         }
252 
253         Log.i(LOG_TAG, "Expected state=" + expectedState);
254 
255         app1.assertStoragePermGranted(expectedState != DENIED);
256         app2.assertStoragePermGranted(expectedState != DENIED);
257 
258         if (expectedState != DENIED) {
259             app1.assertHasNotIsolatedStorage(expectedState == NON_ISOLATED);
260             app2.assertHasNotIsolatedStorage(expectedState == NON_ISOLATED);
261         }
262     }
263 
264     @After
uninstallAllTestPackages()265     public void uninstallAllTestPackages() {
266         app1.uninstall();
267         app2.uninstall();
268     }
269 }
270