summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorWinson <chiuwinson@google.com>2019-10-02 12:41:29 -0700
committerWinson <chiuwinson@google.com>2019-11-21 12:01:41 -0800
commitd9d17367670eb930d74d2e2ffeeb3c0e9bea0a23 (patch)
tree38a6bcc86d8166a2bdb1122e0dbd5f4bad20e5ce
parent64ed3ec44c68664ade3c7fa04f692a7144479ca6 (diff)
Overlayable actor enforcement
Validates that the caller of an OverlayManager API that mutates state is actually allowed to act on the target as defined in the target's overlayable tag. <overlayable name="MyResources" actor="namespace/name"> An actor is valid if any of the following is true: - is root/system - is the target overlay package - has the CHANGE_OVERLAY_PACKAGES permission and an actor is not defined - is the same package name as the sole resolved Activity for the actor specified in the overlayable definition, with only pre-installed, namespaced actors currently supported Bug: 119442583 Bug: 135052950 Test: atest SystemConfigNamedActorTest Test: atest com.android.server.om Change-Id: If56b9e8366852eaef84f6bb25c3e6871eaa3f219
-rw-r--r--core/java/android/content/om/OverlayableInfo.java120
-rw-r--r--core/java/android/content/res/ApkAssets.java15
-rw-r--r--core/java/com/android/server/SystemConfig.java52
-rw-r--r--core/jni/android_content_res_ApkAssets.cpp56
-rw-r--r--libs/androidfw/include/androidfw/LoadedArsc.h2
-rw-r--r--services/core/java/com/android/server/om/OverlayActorEnforcer.java261
-rw-r--r--services/core/java/com/android/server/om/OverlayManagerService.java151
-rw-r--r--services/tests/servicestests/Android.bp1
-rw-r--r--services/tests/servicestests/src/com/android/server/om/OverlayActorEnforcerTests.kt197
-rw-r--r--services/tests/servicestests/src/com/android/server/systemconfig/SystemConfigNamedActorTest.kt232
-rw-r--r--services/tests/servicestests/src/com/android/server/systemconfig/SystemConfigTest.java (renamed from services/tests/servicestests/src/com/android/server/SystemConfigTest.java)4
11 files changed, 1065 insertions, 26 deletions
diff --git a/core/java/android/content/om/OverlayableInfo.java b/core/java/android/content/om/OverlayableInfo.java
new file mode 100644
index 000000000000..5923907b11e7
--- /dev/null
+++ b/core/java/android/content/om/OverlayableInfo.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content.om;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+import com.android.internal.util.DataClass;
+
+import java.util.Objects;
+
+/**
+ * Immutable info on an overlayable defined inside a target package.
+ *
+ * @hide
+ */
+@DataClass(genSetters = false, genEqualsHashCode = true, genHiddenConstructor = true)
+public final class OverlayableInfo {
+
+ /**
+ * The "name" attribute of the overlayable tag. Used to identify the set of resources overlaid.
+ */
+ @NonNull
+ public final String name;
+
+ /**
+ * The "actor" attribute of the overlayable tag. Used to signal which apps are allowed to
+ * modify overlay state for this overlayable.
+ */
+ @Nullable
+ public final String actor;
+
+ // CHECKSTYLE:OFF Generated code
+ //
+
+
+
+ // Code below generated by codegen v1.0.3.
+ //
+ // DO NOT MODIFY!
+ // CHECKSTYLE:OFF Generated code
+ //
+ // To regenerate run:
+ // $ codegen $ANDROID_BUILD_TOP/frameworks/base/core/java/android/content/om/OverlayableInfo.java
+
+
+ /**
+ * Creates a new OverlayableInfo.
+ *
+ * @param name
+ * The "name" attribute of the overlayable tag. Used to identify the set of resources overlaid.
+ * @param actor
+ * The "actor" attribute of the overlayable tag. Used to signal which apps are allowed to
+ * modify overlay state for this overlayable.
+ * @hide
+ */
+ @DataClass.Generated.Member
+ public OverlayableInfo(
+ @NonNull String name,
+ @Nullable String actor) {
+ this.name = name;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, name);
+ this.actor = actor;
+
+ // onConstructed(); // You can define this method to get a callback
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public boolean equals(Object o) {
+ // You can override field equality logic by defining either of the methods like:
+ // boolean fieldNameEquals(OverlayableInfo other) { ... }
+ // boolean fieldNameEquals(FieldType otherValue) { ... }
+
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ @SuppressWarnings("unchecked")
+ OverlayableInfo that = (OverlayableInfo) o;
+ //noinspection PointlessBooleanExpression
+ return true
+ && Objects.equals(name, that.name)
+ && Objects.equals(actor, that.actor);
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public int hashCode() {
+ // You can override field hashCode logic by defining methods like:
+ // int fieldNameHashCode() { ... }
+
+ int _hash = 1;
+ _hash = 31 * _hash + Objects.hashCode(name);
+ _hash = 31 * _hash + Objects.hashCode(actor);
+ return _hash;
+ }
+
+ @DataClass.Generated(
+ time = 1570059850579L,
+ codegenVersion = "1.0.3",
+ sourceFile = "frameworks/base/core/java/android/content/om/OverlayableInfo.java",
+ inputSignatures = "public final @android.annotation.NonNull java.lang.String name\npublic final @android.annotation.Nullable java.lang.String actor\nclass OverlayableInfo extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genSetters=false, genEqualsHashCode=true, genHiddenConstructor=true)")
+ @Deprecated
+ private void __metadata() {}
+
+}
diff --git a/core/java/android/content/res/ApkAssets.java b/core/java/android/content/res/ApkAssets.java
index de1d514d0a5b..ad375552837d 100644
--- a/core/java/android/content/res/ApkAssets.java
+++ b/core/java/android/content/res/ApkAssets.java
@@ -18,6 +18,7 @@ package android.content.res;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UnsupportedAppUsage;
+import android.content.om.OverlayableInfo;
import android.content.res.loader.ResourcesProvider;
import android.text.TextUtils;
@@ -254,6 +255,17 @@ public final class ApkAssets {
}
}
+ /** @hide */
+ @Nullable
+ public OverlayableInfo getOverlayableInfo(String overlayableName) throws IOException {
+ return nativeGetOverlayableInfo(mNativePtr, overlayableName);
+ }
+
+ /** @hide */
+ public boolean definesOverlayable() throws IOException {
+ return nativeDefinesOverlayable(mNativePtr);
+ }
+
/**
* Returns false if the underlying APK was changed since this ApkAssets was loaded.
*/
@@ -305,4 +317,7 @@ public final class ApkAssets {
private static native long nativeGetStringBlock(long ptr);
private static native boolean nativeIsUpToDate(long ptr);
private static native long nativeOpenXml(long ptr, @NonNull String fileName) throws IOException;
+ private static native @Nullable OverlayableInfo nativeGetOverlayableInfo(long ptr,
+ String overlayableName) throws IOException;
+ private static native boolean nativeDefinesOverlayable(long ptr) throws IOException;
}
diff --git a/core/java/com/android/server/SystemConfig.java b/core/java/com/android/server/SystemConfig.java
index ed7f5de83fd1..49a73ee7790f 100644
--- a/core/java/com/android/server/SystemConfig.java
+++ b/core/java/com/android/server/SystemConfig.java
@@ -18,6 +18,7 @@ package com.android.server;
import static com.android.internal.util.ArrayUtils.appendInt;
+import android.annotation.NonNull;
import android.app.ActivityManager;
import android.content.ComponentName;
import android.content.pm.FeatureInfo;
@@ -221,6 +222,12 @@ public class SystemConfig {
private ArrayMap<String, Set<String>> mPackageToUserTypeWhitelist = new ArrayMap<>();
private ArrayMap<String, Set<String>> mPackageToUserTypeBlacklist = new ArrayMap<>();
+ /**
+ * Map of system pre-defined, uniquely named actors; keys are namespace,
+ * value maps actor name to package name.
+ */
+ private ArrayMap<String, ArrayMap<String, String>> mNamedActors = null;
+
public static SystemConfig getInstance() {
if (!isSystemProcess()) {
Slog.wtf(TAG, "SystemConfig is being accessed by a process other than "
@@ -398,12 +405,17 @@ public class SystemConfig {
return r;
}
+ @NonNull
+ public Map<String, ? extends Map<String, String>> getNamedActors() {
+ return mNamedActors != null ? mNamedActors : Collections.emptyMap();
+ }
+
/**
* Only use for testing. Do NOT use in production code.
* @param readPermissions false to create an empty SystemConfig; true to read the permissions.
*/
@VisibleForTesting
- protected SystemConfig(boolean readPermissions) {
+ public SystemConfig(boolean readPermissions) {
if (readPermissions) {
Slog.w(TAG, "Constructing a test SystemConfig");
readAllPermissions();
@@ -1028,6 +1040,44 @@ public class SystemConfig {
readInstallInUserType(parser,
mPackageToUserTypeWhitelist, mPackageToUserTypeBlacklist);
} break;
+ case "named-actor": {
+ String namespace = TextUtils.safeIntern(
+ parser.getAttributeValue(null, "namespace"));
+ String actorName = parser.getAttributeValue(null, "name");
+ String pkgName = TextUtils.safeIntern(
+ parser.getAttributeValue(null, "package"));
+ if (TextUtils.isEmpty(namespace)) {
+ Slog.wtf(TAG, "<" + name + "> without namespace in " + permFile
+ + " at " + parser.getPositionDescription());
+ } else if (TextUtils.isEmpty(actorName)) {
+ Slog.wtf(TAG, "<" + name + "> without actor name in " + permFile
+ + " at " + parser.getPositionDescription());
+ } else if (TextUtils.isEmpty(pkgName)) {
+ Slog.wtf(TAG, "<" + name + "> without package name in " + permFile
+ + " at " + parser.getPositionDescription());
+ } else if ("android".equalsIgnoreCase(namespace)) {
+ throw new IllegalStateException("Defining " + actorName + " as "
+ + pkgName + " for the android namespace is not allowed");
+ } else {
+ if (mNamedActors == null) {
+ mNamedActors = new ArrayMap<>();
+ }
+
+ ArrayMap<String, String> nameToPkgMap = mNamedActors.get(namespace);
+ if (nameToPkgMap == null) {
+ nameToPkgMap = new ArrayMap<>();
+ mNamedActors.put(namespace, nameToPkgMap);
+ } else if (nameToPkgMap.containsKey(actorName)) {
+ String existing = nameToPkgMap.get(actorName);
+ throw new IllegalStateException("Duplicate actor definition for "
+ + namespace + "/" + actorName
+ + "; defined as both " + existing + " and " + pkgName);
+ }
+
+ nameToPkgMap.put(actorName, pkgName);
+ }
+ XmlUtils.skipCurrentTag(parser);
+ } break;
default: {
Slog.w(TAG, "Tag " + name + " is unknown in "
+ permFile + " at " + parser.getPositionDescription());
diff --git a/core/jni/android_content_res_ApkAssets.cpp b/core/jni/android_content_res_ApkAssets.cpp
index 637025329e37..f3a626e1e193 100644
--- a/core/jni/android_content_res_ApkAssets.cpp
+++ b/core/jni/android_content_res_ApkAssets.cpp
@@ -194,6 +194,59 @@ static jlong NativeOpenXml(JNIEnv* env, jclass /*clazz*/, jlong ptr, jstring fil
return reinterpret_cast<jlong>(xml_tree.release());
}
+static jobject NativeGetOverlayableInfo(JNIEnv* env, jclass /*clazz*/, jlong ptr,
+ jstring overlayable_name) {
+ const ApkAssets* apk_assets = reinterpret_cast<const ApkAssets*>(ptr);
+
+ const auto& packages = apk_assets->GetLoadedArsc()->GetPackages();
+ if (packages.empty()) {
+ jniThrowException(env, "java/io/IOException", "Error reading overlayable from APK");
+ return 0;
+ }
+
+ // TODO(b/119899133): Convert this to a search for the info rather than assuming it's at index 0
+ const auto& overlayable_map = packages[0]->GetOverlayableMap();
+ if (overlayable_map.empty()) {
+ return nullptr;
+ }
+
+ auto overlayable_name_native = std::string(env->GetStringUTFChars(overlayable_name, NULL));
+ auto actor = overlayable_map.find(overlayable_name_native);
+ if (actor == overlayable_map.end()) {
+ return nullptr;
+ }
+
+ jstring actor_string = env->NewStringUTF(actor->first.c_str());
+ if (env->ExceptionCheck() || actor_string == nullptr) {
+ jniThrowException(env, "java/io/IOException", "Error reading overlayable from APK");
+ return 0;
+ }
+
+ jclass overlayable_class = env->FindClass("android/content/om/OverlayableInfo");
+ jmethodID overlayable_constructor = env->GetMethodID(overlayable_class, "<init>",
+ "(Ljava/lang/String;Ljava/lang/String;I)V");
+ return env->NewObject(
+ overlayable_class,
+ overlayable_constructor,
+ overlayable_name,
+ actor_string
+ );
+}
+
+static jboolean NativeDefinesOverlayable(JNIEnv* env, jclass /*clazz*/, jlong ptr) {
+ const ApkAssets* apk_assets = reinterpret_cast<const ApkAssets*>(ptr);
+
+ const auto& packages = apk_assets->GetLoadedArsc()->GetPackages();
+ if (packages.empty()) {
+ // Must throw to prevent bypass by returning false
+ jniThrowException(env, "java/io/IOException", "Error reading overlayable from APK");
+ return 0;
+ }
+
+ const auto& overlayable_infos = packages[0]->GetOverlayableMap();
+ return overlayable_infos.empty() ? JNI_FALSE : JNI_TRUE;
+}
+
// JNI registration.
static const JNINativeMethod gApkAssetsMethods[] = {
{"nativeLoad", "(Ljava/lang/String;ZZZZ)J", (void*)NativeLoad},
@@ -208,6 +261,9 @@ static const JNINativeMethod gApkAssetsMethods[] = {
{"nativeGetStringBlock", "(J)J", (void*)NativeGetStringBlock},
{"nativeIsUpToDate", "(J)Z", (void*)NativeIsUpToDate},
{"nativeOpenXml", "(JLjava/lang/String;)J", (void*)NativeOpenXml},
+ {"nativeGetOverlayableInfo", "(JLjava/lang/String;)Landroid/content/om/OverlayableInfo;",
+ (void*)NativeGetOverlayableInfo},
+ {"nativeDefinesOverlayable", "(J)Z", (void*)NativeDefinesOverlayable},
};
int register_android_content_res_ApkAssets(JNIEnv* env) {
diff --git a/libs/androidfw/include/androidfw/LoadedArsc.h b/libs/androidfw/include/androidfw/LoadedArsc.h
index 6cbda07b6950..b5d3a1fc6c1f 100644
--- a/libs/androidfw/include/androidfw/LoadedArsc.h
+++ b/libs/androidfw/include/androidfw/LoadedArsc.h
@@ -273,6 +273,8 @@ class LoadedPackage {
ByteBucketArray<uint32_t> resource_ids_;
std::vector<DynamicPackageEntry> dynamic_package_map_;
std::vector<const std::pair<OverlayableInfo, std::unordered_set<uint32_t>>> overlayable_infos_;
+
+ // A map of overlayable name to actor
std::unordered_map<std::string, std::string> overlayable_map_;
};
diff --git a/services/core/java/com/android/server/om/OverlayActorEnforcer.java b/services/core/java/com/android/server/om/OverlayActorEnforcer.java
new file mode 100644
index 000000000000..e05511681ba8
--- /dev/null
+++ b/services/core/java/com/android/server/om/OverlayActorEnforcer.java
@@ -0,0 +1,261 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.om;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.om.OverlayInfo;
+import android.content.om.OverlayableInfo;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.net.Uri;
+import android.os.Process;
+import android.os.RemoteException;
+import android.text.TextUtils;
+
+import com.android.internal.util.ArrayUtils;
+import com.android.internal.util.CollectionUtils;
+import com.android.server.SystemConfig;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Performs verification that a calling UID can act on a target package's overlayable.
+ *
+ * @hide
+ */
+public class OverlayActorEnforcer {
+
+ private final VerifyCallback mVerifyCallback;
+
+ public OverlayActorEnforcer(@NonNull VerifyCallback verifyCallback) {
+ mVerifyCallback = verifyCallback;
+ }
+
+ void enforceActor(@NonNull OverlayInfo overlayInfo, @NonNull String methodName,
+ int callingUid, int userId) throws SecurityException {
+ ActorState actorState = isAllowedActor(methodName, overlayInfo, callingUid, userId);
+ if (actorState == ActorState.ALLOWED) {
+ return;
+ }
+
+ String targetOverlayableName = overlayInfo.targetOverlayableName;
+ throw new SecurityException("UID" + callingUid + " is not allowed to call "
+ + methodName + " for "
+ + (TextUtils.isEmpty(targetOverlayableName) ? "" : (targetOverlayableName + " in "))
+ + overlayInfo.targetPackageName + " because " + actorState
+ );
+ }
+
+ /**
+ * An actor is valid if any of the following is true:
+ * - is {@link Process#ROOT_UID}, {@link Process#SYSTEM_UID}
+ * - is the target overlay package
+ * - has the CHANGE_OVERLAY_PACKAGES permission and an actor is not defined
+ * - is the same the as the package defined in {@link SystemConfig#getNamedActors()} for a given
+ * namespace and actor name
+ *
+ * @return true if the actor is allowed to act on the target overlayInfo
+ */
+ private ActorState isAllowedActor(String methodName, OverlayInfo overlayInfo,
+ int callingUid, int userId) {
+ switch (callingUid) {
+ case Process.ROOT_UID:
+ case Process.SYSTEM_UID:
+ return ActorState.ALLOWED;
+ }
+
+ String[] callingPackageNames = mVerifyCallback.getPackagesForUid(callingUid);
+ if (ArrayUtils.isEmpty(callingPackageNames)) {
+ return ActorState.NO_PACKAGES_FOR_UID;
+ }
+
+ // A target is always an allowed actor for itself
+ String targetPackageName = overlayInfo.targetPackageName;
+ if (ArrayUtils.contains(callingPackageNames, targetPackageName)) {
+ return ActorState.ALLOWED;
+ }
+
+ String targetOverlayableName = overlayInfo.targetOverlayableName;
+
+ if (TextUtils.isEmpty(targetOverlayableName)) {
+ try {
+ if (mVerifyCallback.doesTargetDefineOverlayable(targetPackageName, userId)) {
+ return ActorState.MISSING_TARGET_OVERLAYABLE_NAME;
+ } else {
+ // If there's no overlayable defined, fallback to the legacy permission check
+ try {
+ mVerifyCallback.enforcePermission(
+ android.Manifest.permission.CHANGE_OVERLAY_PACKAGES, methodName);
+
+ // If the previous method didn't throw, check passed
+ return ActorState.ALLOWED;
+ } catch (SecurityException e) {
+ return ActorState.MISSING_LEGACY_PERMISSION;
+ }
+ }
+ } catch (RemoteException | IOException e) {
+ return ActorState.ERROR_READING_OVERLAYABLE;
+ }
+ }
+
+ OverlayableInfo targetOverlayable;
+ try {
+ targetOverlayable = mVerifyCallback.getOverlayableForTarget(targetPackageName,
+ targetOverlayableName, userId);
+ } catch (IOException e) {
+ return ActorState.UNABLE_TO_GET_TARGET;
+ }
+
+ if (targetOverlayable == null) {
+ return ActorState.MISSING_OVERLAYABLE;
+ }
+
+ String actor = targetOverlayable.actor;
+ if (TextUtils.isEmpty(actor)) {
+ // If there's no actor defined, fallback to the legacy permission check
+ try {
+ mVerifyCallback.enforcePermission(
+ android.Manifest.permission.CHANGE_OVERLAY_PACKAGES, methodName);
+
+ // If the previous method didn't throw, check passed
+ return ActorState.ALLOWED;
+ } catch (SecurityException e) {
+ return ActorState.MISSING_LEGACY_PERMISSION;
+ }
+ }
+
+ Map<String, ? extends Map<String, String>> namedActors = mVerifyCallback.getNamedActors();
+ if (namedActors.isEmpty()) {
+ return ActorState.NO_NAMED_ACTORS;
+ }
+
+ Uri actorUri = Uri.parse(actor);
+
+ String actorScheme = actorUri.getScheme();
+ List<String> actorPathSegments = actorUri.getPathSegments();
+ if (!"overlay".equals(actorScheme) || CollectionUtils.size(actorPathSegments) != 1) {
+ return ActorState.INVALID_OVERLAYABLE_ACTOR_NAME;
+ }
+
+ String actorNamespace = actorUri.getAuthority();
+ Map<String, String> namespace = namedActors.get(actorNamespace);
+ if (namespace == null) {
+ return ActorState.MISSING_NAMESPACE;
+ }
+
+ String actorName = actorPathSegments.get(0);
+ String packageName = namespace.get(actorName);
+ if (TextUtils.isEmpty(packageName)) {
+ return ActorState.MISSING_ACTOR_NAME;
+ }
+
+ PackageInfo packageInfo = mVerifyCallback.getPackageInfo(packageName, userId);
+ if (packageInfo == null) {
+ return ActorState.MISSING_APP_INFO;
+ }
+
+ ApplicationInfo appInfo = packageInfo.applicationInfo;
+ if (appInfo == null) {
+ return ActorState.MISSING_APP_INFO;
+ }
+
+ // Currently only pre-installed apps can be actors
+ if (!appInfo.isSystemApp() && !appInfo.isUpdatedSystemApp()) {
+ return ActorState.ACTOR_NOT_PREINSTALLED;
+ }
+
+ if (ArrayUtils.contains(callingPackageNames, packageName)) {
+ return ActorState.ALLOWED;
+ }
+
+ return ActorState.INVALID_ACTOR;
+ }
+
+ /**
+ * For easier logging/debugging, a set of all possible failure/success states when running
+ * enforcement.
+ */
+ private enum ActorState {
+ ALLOWED,
+ INVALID_ACTOR,
+ MISSING_NAMESPACE,
+ MISSING_PACKAGE,
+ MISSING_APP_INFO,
+ ACTOR_NOT_PREINSTALLED,
+ NO_PACKAGES_FOR_UID,
+ MISSING_ACTOR_NAME,
+ ERROR_READING_OVERLAYABLE,
+ MISSING_TARGET_OVERLAYABLE_NAME,
+ MISSING_OVERLAYABLE,
+ INVALID_OVERLAYABLE_ACTOR_NAME,
+ NO_NAMED_ACTORS,
+ UNABLE_TO_GET_TARGET,
+ MISSING_LEGACY_PERMISSION
+ }
+
+ /**
+ * Delegate to the system for querying information about packages.
+ */
+ public interface VerifyCallback {
+
+ /**
+ * Read from the APK and AndroidManifest of a package to return the overlayable defined for
+ * a given name.
+ *
+ * @throws IOException if the target can't be read
+ */
+ @Nullable
+ OverlayableInfo getOverlayableForTarget(@NonNull String packageName,
+ @Nullable String targetOverlayableName, int userId)
+ throws IOException;
+
+ /**
+ * @see android.content.pm.PackageManager#getPackagesForUid(int)
+ */
+ @Nullable
+ String[] getPackagesForUid(int uid);
+
+ /**
+ * @param userId user to filter package visibility by
+ * @see android.content.pm.PackageManager#getPackageInfo(String, int)
+ */
+ @Nullable
+ PackageInfo getPackageInfo(@NonNull String packageName, int userId);
+
+ /**
+ * @return map of system pre-defined, uniquely named actors; keys are namespace,
+ * value maps actor name to package name
+ */
+ @NonNull
+ Map<String, ? extends Map<String, String>> getNamedActors();
+
+ /**
+ * @return true if the target package has declared an overlayable
+ */
+ boolean doesTargetDefineOverlayable(String targetPackageName, int userId)
+ throws RemoteException, IOException;
+
+ /**
+ * @throws SecurityException containing message if the caller doesn't have the given
+ * permission
+ */
+ void enforcePermission(String permission, String message) throws SecurityException;
+ }
+}
diff --git a/services/core/java/com/android/server/om/OverlayManagerService.java b/services/core/java/com/android/server/om/OverlayManagerService.java
index 5f3e50320752..63de61c9782f 100644
--- a/services/core/java/com/android/server/om/OverlayManagerService.java
+++ b/services/core/java/com/android/server/om/OverlayManagerService.java
@@ -39,10 +39,12 @@ import android.content.Intent;
import android.content.IntentFilter;
import android.content.om.IOverlayManager;
import android.content.om.OverlayInfo;
+import android.content.om.OverlayableInfo;
import android.content.pm.IPackageManager;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManagerInternal;
import android.content.pm.UserInfo;
+import android.content.res.ApkAssets;
import android.net.Uri;
import android.os.Binder;
import android.os.Environment;
@@ -63,6 +65,7 @@ import android.util.SparseArray;
import com.android.server.FgThread;
import com.android.server.IoThread;
import com.android.server.LocalServices;
+import com.android.server.SystemConfig;
import com.android.server.SystemService;
import com.android.server.pm.UserManagerService;
@@ -229,6 +232,8 @@ public final class OverlayManagerService extends SystemService {
private final OverlayManagerServiceImpl mImpl;
+ private final OverlayActorEnforcer mActorEnforcer;
+
private final AtomicBoolean mPersistSettingsScheduled = new AtomicBoolean(false);
public OverlayManagerService(@NonNull final Context context) {
@@ -237,12 +242,13 @@ public final class OverlayManagerService extends SystemService {
traceBegin(TRACE_TAG_RRO, "OMS#OverlayManagerService");
mSettingsFile = new AtomicFile(
new File(Environment.getDataSystemDirectory(), "overlays.xml"), "overlays");
- mPackageManager = new PackageManagerHelper();
+ mPackageManager = new PackageManagerHelper(context);
mUserManager = UserManagerService.getInstance();
IdmapManager im = new IdmapManager(mPackageManager);
mSettings = new OverlayManagerSettings();
mImpl = new OverlayManagerServiceImpl(mPackageManager, im, mSettings,
getDefaultOverlayPackages(), new OverlayChangeListener());
+ mActorEnforcer = new OverlayActorEnforcer(mPackageManager);
final IntentFilter packageFilter = new IntentFilter();
packageFilter.addAction(ACTION_PACKAGE_ADDED);
@@ -581,7 +587,7 @@ public final class OverlayManagerService extends SystemService {
int userId) throws RemoteException {
try {
traceBegin(TRACE_TAG_RRO, "OMS#setEnabled " + packageName + " " + enable);
- enforceChangeOverlayPackagesPermission("setEnabled");
+ enforceActor(packageName, "setEnabled", userId);
userId = handleIncomingUser(userId, "setEnabled");
if (packageName == null) {
return false;
@@ -605,7 +611,7 @@ public final class OverlayManagerService extends SystemService {
int userId) throws RemoteException {
try {
traceBegin(TRACE_TAG_RRO, "OMS#setEnabledExclusive " + packageName + " " + enable);
- enforceChangeOverlayPackagesPermission("setEnabledExclusive");
+ enforceActor(packageName, "setEnabledExclusive", userId);
userId = handleIncomingUser(userId, "setEnabledExclusive");
if (packageName == null || !enable) {
return false;
@@ -630,7 +636,7 @@ public final class OverlayManagerService extends SystemService {
throws RemoteException {
try {
traceBegin(TRACE_TAG_RRO, "OMS#setEnabledExclusiveInCategory " + packageName);
- enforceChangeOverlayPackagesPermission("setEnabledExclusiveInCategory");
+ enforceActor(packageName, "setEnabledExclusiveInCategory", userId);
userId = handleIncomingUser(userId, "setEnabledExclusiveInCategory");
if (packageName == null) {
return false;
@@ -656,7 +662,7 @@ public final class OverlayManagerService extends SystemService {
try {
traceBegin(TRACE_TAG_RRO, "OMS#setPriority " + packageName + " "
+ parentPackageName);
- enforceChangeOverlayPackagesPermission("setPriority");
+ enforceActor(packageName, "setPriority", userId);
userId = handleIncomingUser(userId, "setPriority");
if (packageName == null || parentPackageName == null) {
return false;
@@ -680,7 +686,7 @@ public final class OverlayManagerService extends SystemService {
throws RemoteException {
try {
traceBegin(TRACE_TAG_RRO, "OMS#setHighestPriority " + packageName);
- enforceChangeOverlayPackagesPermission("setHighestPriority");
+ enforceActor(packageName, "setHighestPriority", userId);
userId = handleIncomingUser(userId, "setHighestPriority");
if (packageName == null) {
return false;
@@ -704,7 +710,7 @@ public final class OverlayManagerService extends SystemService {
throws RemoteException {
try {
traceBegin(TRACE_TAG_RRO, "OMS#setLowestPriority " + packageName);
- enforceChangeOverlayPackagesPermission("setLowestPriority");
+ enforceActor(packageName, "setLowestPriority", userId);
userId = handleIncomingUser(userId, "setLowestPriority");
if (packageName == null) {
return false;
@@ -750,7 +756,7 @@ public final class OverlayManagerService extends SystemService {
return;
}
- enforceChangeOverlayPackagesPermission("invalidateCachesForOverlay");
+ enforceActor(packageName, "invalidateCachesForOverlay", userId);
userId = handleIncomingUser(userId, "invalidateCachesForOverlay");
final long ident = Binder.clearCallingIdentity();
try {
@@ -861,18 +867,6 @@ public final class OverlayManagerService extends SystemService {
}
/**
- * Enforce that the caller holds the CHANGE_OVERLAY_PACKAGES permission (or is
- * system or root).
- *
- * @param message used as message if SecurityException is thrown
- * @throws SecurityException if the permission check fails
- */
- private void enforceChangeOverlayPackagesPermission(@NonNull final String message) {
- getContext().enforceCallingOrSelfPermission(
- android.Manifest.permission.CHANGE_OVERLAY_PACKAGES, message);
- }
-
- /**
* Enforce that the caller holds the DUMP permission (or is system or root).
*
* @param message used as message if SecurityException is thrown
@@ -881,6 +875,13 @@ public final class OverlayManagerService extends SystemService {
private void enforceDumpPermission(@NonNull final String message) {
getContext().enforceCallingOrSelfPermission(android.Manifest.permission.DUMP, message);
}
+
+ private void enforceActor(String packageName, String methodName, int userId)
+ throws SecurityException {
+ OverlayInfo overlayInfo = mImpl.getOverlayInfo(packageName, userId);
+ int callingUid = Binder.getCallingUid();
+ mActorEnforcer.enforceActor(overlayInfo, methodName, callingUid, userId);
+ }
};
private final class OverlayChangeListener
@@ -1035,9 +1036,16 @@ public final class OverlayManagerService extends SystemService {
}
}
- private static final class PackageManagerHelper implements
- OverlayManagerServiceImpl.PackageManagerHelper {
+ /**
+ * Delegate for {@link android.content.pm.PackageManager} and {@link PackageManagerInternal}
+ * functionality, separated for easy testing.
+ *
+ * @hide
+ */
+ public static final class PackageManagerHelper implements
+ OverlayManagerServiceImpl.PackageManagerHelper, OverlayActorEnforcer.VerifyCallback {
+ private final Context mContext;
private final IPackageManager mPackageManager;
private final PackageManagerInternal mPackageManagerInternal;
@@ -1048,11 +1056,14 @@ public final class OverlayManagerService extends SystemService {
// behind until all pending intents have been processed.
private final SparseArray<HashMap<String, PackageInfo>> mCache = new SparseArray<>();
- PackageManagerHelper() {
+ PackageManagerHelper(Context context) {
+ mContext = context;
mPackageManager = getPackageManager();
mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class);
}
+ // TODO(b/143096091): Remove PackageInfo cache so that PackageManager is always queried
+ // to enforce visibility/other permission checks
public PackageInfo getPackageInfo(@NonNull final String packageName, final int userId,
final boolean useCache) {
if (useCache) {
@@ -1075,7 +1086,19 @@ public final class OverlayManagerService extends SystemService {
@Override
public PackageInfo getPackageInfo(@NonNull final String packageName, final int userId) {
- return getPackageInfo(packageName, userId, true);
+ // TODO(b/143096091): Remove clearing calling ID
+ long callingIdentity = Binder.clearCallingIdentity();
+ try {
+ return getPackageInfo(packageName, userId, true);
+ } finally {
+ Binder.restoreCallingIdentity(callingIdentity);
+ }
+ }
+
+ @NonNull
+ @Override
+ public Map<String, ? extends Map<String, String>> getNamedActors() {
+ return SystemConfig.getInstance().getNamedActors();
}
@Override
@@ -1097,6 +1120,70 @@ public final class OverlayManagerService extends SystemService {
return mPackageManagerInternal.getOverlayPackages(userId);
}
+ @Nullable
+ @Override
+ public OverlayableInfo getOverlayableForTarget(@NonNull String packageName,
+ @Nullable String targetOverlayableName, int userId)
+ throws IOException {
+ // TODO(b/143096091): Remove clearing calling ID
+ long callingIdentity = Binder.clearCallingIdentity();
+ try {
+ PackageInfo packageInfo = getPackageInfo(packageName, userId);
+ if (packageInfo == null) {
+ throw new IOException("Unable to get target package");
+ }
+
+ String baseCodePath = packageInfo.applicationInfo.getBaseCodePath();
+
+ ApkAssets apkAssets = null;
+ try {
+ apkAssets = ApkAssets.loadFromPath(baseCodePath);
+ return apkAssets.getOverlayableInfo(targetOverlayableName);
+ } finally {
+ if (apkAssets != null) {
+ try {
+ apkAssets.close();
+ } catch (Throwable ignored) {
+ }
+ }
+ }
+ } finally {
+ Binder.restoreCallingIdentity(callingIdentity);
+ }
+ }
+
+ @Override
+ public boolean doesTargetDefineOverlayable(String targetPackageName, int userId)
+ throws RemoteException, IOException {
+ // TODO(b/143096091): Remove clearing calling ID
+ long callingIdentity = Binder.clearCallingIdentity();
+ try {
+ PackageInfo packageInfo = mPackageManager.getPackageInfo(targetPackageName, 0,
+ userId);
+ String baseCodePath = packageInfo.applicationInfo.getBaseCodePath();
+
+ ApkAssets apkAssets = null;
+ try {
+ apkAssets = ApkAssets.loadFromPath(baseCodePath);
+ return apkAssets.definesOverlayable();
+ } finally {
+ if (apkAssets != null) {
+ try {
+ apkAssets.close();
+ } catch (Throwable ignored) {
+ }
+ }
+ }
+ } finally {
+ Binder.restoreCallingIdentity(callingIdentity);
+ }
+ }
+
+ @Override
+ public void enforcePermission(String permission, String message) throws SecurityException {
+ mContext.enforceCallingOrSelfPermission(permission, message);
+ }
+
public PackageInfo getCachedPackageInfo(@NonNull final String packageName,
final int userId) {
final HashMap<String, PackageInfo> map = mCache.get(userId);
@@ -1128,6 +1215,22 @@ public final class OverlayManagerService extends SystemService {
mCache.delete(userId);
}
+ @Nullable
+ @Override
+ public String[] getPackagesForUid(int uid) {
+ // TODO(b/143096091): Remove clearing calling ID
+ long callingIdentity = Binder.clearCallingIdentity();
+ try {
+ try {
+ return mPackageManager.getPackagesForUid(uid);
+ } catch (RemoteException ignored) {
+ return null;
+ }
+ } finally {
+ Binder.restoreCallingIdentity(callingIdentity);
+ }
+ }
+
private static final String TAB1 = " ";
private static final String TAB2 = TAB1 + TAB1;
diff --git a/services/tests/servicestests/Android.bp b/services/tests/servicestests/Android.bp
index 30ccb717e8a2..52fb69eb99be 100644
--- a/services/tests/servicestests/Android.bp
+++ b/services/tests/servicestests/Android.bp
@@ -8,6 +8,7 @@ android_test {
// Include all test java files.
srcs: [
"src/**/*.java",
+ "src/**/*.kt",
"aidl/com/android/servicestests/aidl/INetworkStateObserver.aidl",
"aidl/com/android/servicestests/aidl/ICmdReceiverService.aidl",
diff --git a/services/tests/servicestests/src/com/android/server/om/OverlayActorEnforcerTests.kt b/services/tests/servicestests/src/com/android/server/om/OverlayActorEnforcerTests.kt
new file mode 100644
index 000000000000..233e16c297a3
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/om/OverlayActorEnforcerTests.kt
@@ -0,0 +1,197 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.om
+
+import android.content.om.OverlayInfo
+import android.content.om.OverlayableInfo
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageInfo
+import android.os.Process
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.ExpectedException
+
+class OverlayActorEnforcerTests {
+ companion object {
+ private const val NAMESPACE = "testnamespace"
+ private const val ACTOR_NAME = "testactor"
+ private const val ACTOR_PKG_NAME = "com.test.actor.one"
+ private const val OVERLAYABLE_NAME = "TestOverlayable"
+ private const val UID = 3536
+ private const val USER_ID = 55
+ }
+
+ @get:Rule
+ val expectedException = ExpectedException.none()!!
+
+ @Test
+ fun isRoot() {
+ verify(callingUid = Process.ROOT_UID)
+ }
+
+ @Test(expected = SecurityException::class)
+ fun isShell() {
+ verify(callingUid = Process.SHELL_UID)
+ }
+
+ @Test
+ fun isSystem() {
+ verify(callingUid = Process.SYSTEM_UID)
+ }
+
+ @Test(expected = SecurityException::class)
+ fun noOverlayable_noTarget() {
+ verify(targetOverlayableName = null)
+ }
+
+ @Test
+ fun noOverlayable_noTarget_withPermission() {
+ verify(targetOverlayableName = null, hasPermission = true)
+ }
+
+ @Test(expected = SecurityException::class)
+ fun noOverlayable_withTarget() {
+ verify(targetOverlayableName = OVERLAYABLE_NAME)
+ }
+
+ @Test(expected = SecurityException::class)
+ fun withOverlayable_noTarget() {
+ verify(
+ targetOverlayableName = null,
+ overlayableInfo = OverlayableInfo(OVERLAYABLE_NAME, null)
+ )
+ }
+
+ @Test(expected = SecurityException::class)
+ fun withOverlayable_noActor() {
+ verify(
+ overlayableInfo = OverlayableInfo(OVERLAYABLE_NAME, null)
+ )
+ }
+
+ @Test
+ fun withOverlayable_noActor_withPermission() {
+ verify(
+ hasPermission = true,
+ overlayableInfo = OverlayableInfo(OVERLAYABLE_NAME, null)
+ )
+ }
+
+ @Test(expected = SecurityException::class)
+ fun withOverlayable_withActor_notActor() {
+ verify(
+ isActor = false,
+ overlayableInfo = OverlayableInfo(OVERLAYABLE_NAME,
+ "overlay://$NAMESPACE/$ACTOR_NAME")
+ )
+ }
+
+ @Test(expected = SecurityException::class)
+ fun withOverlayable_withActor_isActor_notPreInstalled() {
+ verify(
+ isActor = true,
+ isPreInstalled = false,
+ overlayableInfo = OverlayableInfo(OVERLAYABLE_NAME,
+ "overlay://$NAMESPACE/$ACTOR_NAME")
+ )
+ }
+
+ @Test
+ fun withOverlayable_withActor_isActor_isPreInstalled() {
+ verify(
+ isActor = true,
+ isPreInstalled = true,
+ overlayableInfo = OverlayableInfo(OVERLAYABLE_NAME,
+ "overlay://$NAMESPACE/$ACTOR_NAME")
+ )
+ }
+
+ @Test(expected = SecurityException::class)
+ fun withOverlayable_invalidActor() {
+ verify(
+ isActor = true,
+ isPreInstalled = true,
+ overlayableInfo = OverlayableInfo(OVERLAYABLE_NAME, "notValidActor")
+ )
+ }
+
+ private fun verify(
+ isActor: Boolean = false,
+ isPreInstalled: Boolean = false,
+ hasPermission: Boolean = false,
+ overlayableInfo: OverlayableInfo? = null,
+ callingUid: Int = UID,
+ targetOverlayableName: String? = OVERLAYABLE_NAME
+ ) {
+ val callback = MockCallback(
+ isActor = isActor,
+ isPreInstalled = isPreInstalled,
+ hasPermission = hasPermission,
+ overlayableInfo = overlayableInfo
+ )
+
+ val overlayInfo = overlayInfo(targetOverlayableName)
+ OverlayActorEnforcer(callback)
+ .enforceActor(overlayInfo, "test", callingUid, USER_ID)
+ }
+
+ private fun overlayInfo(targetOverlayableName: String?) = OverlayInfo("com.test.overlay",
+ "com.test.target", targetOverlayableName, null, "/path", OverlayInfo.STATE_UNKNOWN, 0,
+ 0, false)
+
+ private class MockCallback(
+ private val isActor: Boolean = false,
+ private val isPreInstalled: Boolean = false,
+ private val hasPermission: Boolean = false,
+ private val overlayableInfo: OverlayableInfo? = null,
+ private vararg val packageNames: String = arrayOf("com.test.actor.one")
+ ) : OverlayActorEnforcer.VerifyCallback {
+
+ override fun getNamedActors() = if (isActor) {
+ mapOf(NAMESPACE to mapOf(ACTOR_NAME to ACTOR_PKG_NAME))
+ } else {
+ emptyMap()
+ }
+
+ override fun getOverlayableForTarget(
+ packageName: String,
+ targetOverlayableName: String?,
+ userId: Int
+ ) = overlayableInfo
+
+ override fun getPackagesForUid(uid: Int) = when (uid) {
+ UID -> packageNames
+ else -> null
+ }
+
+ override fun getPackageInfo(packageName: String, userId: Int) = PackageInfo().apply {
+ applicationInfo = ApplicationInfo().apply {
+ flags = if (isPreInstalled) ApplicationInfo.FLAG_SYSTEM else 0
+ }
+ }
+
+ override fun doesTargetDefineOverlayable(targetPackageName: String?, userId: Int): Boolean {
+ return overlayableInfo != null
+ }
+
+ override fun enforcePermission(permission: String?, message: String?) {
+ if (!hasPermission) {
+ throw SecurityException()
+ }
+ }
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/systemconfig/SystemConfigNamedActorTest.kt b/services/tests/servicestests/src/com/android/server/systemconfig/SystemConfigNamedActorTest.kt
new file mode 100644
index 000000000000..b7199d4a2443
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/systemconfig/SystemConfigNamedActorTest.kt
@@ -0,0 +1,232 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.systemconfig
+
+import android.content.Context
+import androidx.test.InstrumentationRegistry
+import com.android.server.SystemConfig
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.ExpectedException
+import org.junit.rules.TemporaryFolder
+
+class SystemConfigNamedActorTest {
+
+ companion object {
+ private const val NAMESPACE_TEST = "someTestNamespace"
+ private const val NAMESPACE_ANDROID = "android"
+ private const val ACTOR_ONE = "iconShaper"
+ private const val ACTOR_TWO = "colorChanger"
+ private const val PACKAGE_ONE = "com.test.actor.one"
+ private const val PACKAGE_TWO = "com.test.actor.two"
+ }
+
+ private val context: Context = InstrumentationRegistry.getContext()
+
+ @get:Rule
+ val tempFolder = TemporaryFolder(context.filesDir)
+
+ @get:Rule
+ val expected = ExpectedException.none()
+
+ private var uniqueCounter = 0
+
+ @Test
+ fun twoUnique() {
+ """
+ <config>
+ <named-actor
+ namespace="$NAMESPACE_TEST"
+ name="$ACTOR_ONE"
+ package="$PACKAGE_ONE"
+ />
+ <named-actor
+ namespace="$NAMESPACE_TEST"
+ name="$ACTOR_TWO"
+ package="$PACKAGE_TWO"
+ />
+ </config>
+ """.write()
+
+ assertPermissions().containsExactlyEntriesIn(
+ mapOf(
+ NAMESPACE_TEST to mapOf(
+ ACTOR_ONE to PACKAGE_ONE,
+ ACTOR_TWO to PACKAGE_TWO
+ )
+ )
+ )
+ }
+
+ @Test
+ fun twoSamePackage() {
+ """
+ <config>
+ <named-actor
+ namespace="$NAMESPACE_TEST"
+ name="$ACTOR_ONE"
+ package="$PACKAGE_ONE"
+ />
+ <named-actor
+ namespace="$NAMESPACE_TEST"
+ name="$ACTOR_TWO"
+ package="$PACKAGE_ONE"
+ />
+ </config>
+ """.write()
+
+ assertPermissions().containsExactlyEntriesIn(
+ mapOf(
+ NAMESPACE_TEST to mapOf(
+ ACTOR_ONE to PACKAGE_ONE,
+ ACTOR_TWO to PACKAGE_ONE
+ )
+ )
+ )
+ }
+
+ @Test
+ fun missingNamespace() {
+ """
+ <config>
+ <named-actor
+ name="$ACTOR_ONE"
+ package="$PACKAGE_ONE"
+ />
+ <named-actor
+ namespace="$NAMESPACE_TEST"
+ name="$ACTOR_TWO"
+ package="$PACKAGE_TWO"
+ />
+ </config>
+ """.write()
+
+ assertPermissions().containsExactlyEntriesIn(
+ mapOf(
+ NAMESPACE_TEST to mapOf(
+ ACTOR_TWO to PACKAGE_TWO
+ )
+ )
+ )
+ }
+
+ @Test
+ fun missingName() {
+ """
+ <config>
+ <named-actor
+ namespace="$NAMESPACE_TEST"
+ package="$PACKAGE_ONE"
+ />
+ <named-actor
+ namespace="$NAMESPACE_TEST"
+ name="$ACTOR_TWO"
+ package="$PACKAGE_TWO"
+ />
+ </config>
+ """.write()
+
+ assertPermissions().containsExactlyEntriesIn(
+ mapOf(
+ NAMESPACE_TEST to mapOf(
+ ACTOR_TWO to PACKAGE_TWO
+ )
+ )
+ )
+ }
+
+ @Test
+ fun missingPackage() {
+ """
+ <config>
+ <named-actor
+ namespace="$NAMESPACE_TEST"
+ name="$ACTOR_ONE"
+ />
+ <named-actor
+ namespace="$NAMESPACE_TEST"
+ name="$ACTOR_TWO"
+ package="$PACKAGE_TWO"
+ />
+ </config>
+ """.write()
+
+ assertPermissions().containsExactlyEntriesIn(
+ mapOf(
+ NAMESPACE_TEST to mapOf(
+ ACTOR_TWO to PACKAGE_TWO
+ )
+ )
+ )
+ }
+
+ @Test
+ fun androidNamespaceThrows() {
+ """
+ <config>
+ <named-actor
+ namespace="$NAMESPACE_TEST"
+ name="$ACTOR_ONE"
+ package="$PACKAGE_ONE"
+ />
+ <named-actor
+ namespace="$NAMESPACE_ANDROID"
+ name="$ACTOR_ONE"
+ package="$PACKAGE_ONE"
+ />
+ </config>
+ """.write()
+
+ expected.expect(IllegalStateException::class.java)
+ expected.expectMessage("Defining $ACTOR_ONE as $PACKAGE_ONE " +
+ "for the android namespace is not allowed")
+
+ assertPermissions()
+ }
+
+ @Test
+ fun duplicateActorNameThrows() {
+ """
+ <config>
+ <named-actor
+ namespace="$NAMESPACE_TEST"
+ name="$ACTOR_ONE"
+ package="$PACKAGE_ONE"
+ />
+ <named-actor
+ namespace="$NAMESPACE_TEST"
+ name="$ACTOR_ONE"
+ package="$PACKAGE_TWO"
+ />
+ </config>
+ """.write()
+
+ expected.expect(IllegalStateException::class.java)
+ expected.expectMessage("Duplicate actor definition for $NAMESPACE_TEST/$ACTOR_ONE;" +
+ " defined as both $PACKAGE_ONE and $PACKAGE_TWO")
+
+ assertPermissions()
+ }
+
+ private fun String.write() = tempFolder.root.resolve("${uniqueCounter++}.xml")
+ .writeText(this.trimIndent())
+
+ private fun assertPermissions() = SystemConfig(false).apply {
+ readPermissions(tempFolder.root, 0)
+ }. let { assertThat(it.namedActors) }
+}
diff --git a/services/tests/servicestests/src/com/android/server/SystemConfigTest.java b/services/tests/servicestests/src/com/android/server/systemconfig/SystemConfigTest.java
index ff03391ea031..fde0ddffa365 100644
--- a/services/tests/servicestests/src/com/android/server/SystemConfigTest.java
+++ b/services/tests/servicestests/src/com/android/server/systemconfig/SystemConfigTest.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.server;
+package com.android.server.systemconfig;
import static org.junit.Assert.assertEquals;
@@ -25,6 +25,8 @@ import android.util.Log;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
+import com.android.server.SystemConfig;
+
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;