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