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