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