Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 47c1de2f authored by Hai Zhang's avatar Hai Zhang
Browse files

Move roles persistence into APEX.

Bug: 136503238
Test: presubmit
Change-Id: Id11842ad7653317d5f0ebf2df0f4c315d0018440
parent 76f0defe
Loading
Loading
Loading
Loading
+72 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2020 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.role.persistence;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SystemApi;
import android.os.UserHandle;

/**
 * Persistence for roles.
 *
 * TODO(b/147914847): Remove @hide when it becomes the default.
 * @hide
 */
@SystemApi(client = SystemApi.Client.MODULE_LIBRARIES, process = SystemApi.Process.SYSTEM_SERVER)
public interface RolesPersistence {

    /**
     * Read the roles from persistence.
     *
     * This will perform I/O operations synchronously.
     *
     * @param user the user to read for
     * @return the roles read
     */
    @Nullable
    RolesState read(@NonNull UserHandle user);

    /**
     * Write the roles to persistence.
     *
     * This will perform I/O operations synchronously.
     *
     * @param roles the roles to write
     * @param user the user to write for
     */
    void write(@NonNull RolesState roles, @NonNull UserHandle user);

    /**
     * Delete the roles from persistence.
     *
     * This will perform I/O operations synchronously.
     *
     * @param user the user to delete for
     */
    void delete(@NonNull UserHandle user);

    /**
     * Create a new instance of {@link RolesPersistence} implementation.
     *
     * @return the new instance.
     */
    @NonNull
    static RolesPersistence createInstance() {
        return new RolesPersistenceImpl();
    }
}
+217 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2020 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.role.persistence;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.os.UserHandle;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.AtomicFile;
import android.util.Log;
import android.util.Xml;

import com.android.permission.persistence.IoUtils;

import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlSerializer;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.Set;

/**
 * Persistence implementation for roles.
 *
 * TODO(b/147914847): Remove @hide when it becomes the default.
 * @hide
 */
public class RolesPersistenceImpl implements RolesPersistence {

    private static final String LOG_TAG = RolesPersistenceImpl.class.getSimpleName();

    private static final String ROLES_FILE_NAME = "roles.xml";

    private static final String TAG_ROLES = "roles";
    private static final String TAG_ROLE = "role";
    private static final String TAG_HOLDER = "holder";

    private static final String ATTRIBUTE_VERSION = "version";
    private static final String ATTRIBUTE_NAME = "name";
    private static final String ATTRIBUTE_PACKAGES_HASH = "packagesHash";

    @Nullable
    @Override
    public RolesState read(@NonNull UserHandle user) {
        File file = getFile(user);
        try (FileInputStream inputStream = new AtomicFile(file).openRead()) {
            XmlPullParser parser = Xml.newPullParser();
            parser.setInput(inputStream, null);
            return parseXml(parser);
        } catch (FileNotFoundException e) {
            Log.i(LOG_TAG, "roles.xml not found");
            return null;
        } catch (XmlPullParserException | IOException e) {
            throw new IllegalStateException("Failed to read roles.xml: " + file , e);
        }
    }

    @NonNull
    private static RolesState parseXml(@NonNull XmlPullParser parser)
            throws IOException, XmlPullParserException {
        int type;
        int depth;
        int innerDepth = parser.getDepth() + 1;
        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
                && ((depth = parser.getDepth()) >= innerDepth || type != XmlPullParser.END_TAG)) {
            if (depth > innerDepth || type != XmlPullParser.START_TAG) {
                continue;
            }

            if (parser.getName().equals(TAG_ROLES)) {
                return parseRoles(parser);
            }
        }
        throw new IllegalStateException("Missing <" + TAG_ROLES + "> in roles.xml");
    }

    @NonNull
    private static RolesState parseRoles(@NonNull XmlPullParser parser)
            throws IOException, XmlPullParserException {
        int version = Integer.parseInt(parser.getAttributeValue(null, ATTRIBUTE_VERSION));
        String packagesHash = parser.getAttributeValue(null, ATTRIBUTE_PACKAGES_HASH);

        Map<String, Set<String>> roles = new ArrayMap<>();
        int type;
        int depth;
        int innerDepth = parser.getDepth() + 1;
        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
                && ((depth = parser.getDepth()) >= innerDepth || type != XmlPullParser.END_TAG)) {
            if (depth > innerDepth || type != XmlPullParser.START_TAG) {
                continue;
            }

            if (parser.getName().equals(TAG_ROLE)) {
                String roleName = parser.getAttributeValue(null, ATTRIBUTE_NAME);
                Set<String> roleHolders = parseRoleHolders(parser);
                roles.put(roleName, roleHolders);
            }
        }

        return new RolesState(version, packagesHash, roles);
    }

    @NonNull
    private static Set<String> parseRoleHolders(@NonNull XmlPullParser parser)
            throws IOException, XmlPullParserException {
        Set<String> roleHolders = new ArraySet<>();
        int type;
        int depth;
        int innerDepth = parser.getDepth() + 1;
        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
                && ((depth = parser.getDepth()) >= innerDepth || type != XmlPullParser.END_TAG)) {
            if (depth > innerDepth || type != XmlPullParser.START_TAG) {
                continue;
            }

            if (parser.getName().equals(TAG_HOLDER)) {
                String roleHolder = parser.getAttributeValue(null, ATTRIBUTE_NAME);
                roleHolders.add(roleHolder);
            }
        }
        return roleHolders;
    }

    @Override
    public void write(@NonNull RolesState roles,
            @NonNull UserHandle user) {
        File file = getFile(user);
        AtomicFile atomicFile = new AtomicFile(file);
        FileOutputStream outputStream = null;
        try {
            outputStream = atomicFile.startWrite();

            XmlSerializer serializer = Xml.newSerializer();
            serializer.setOutput(outputStream, StandardCharsets.UTF_8.name());
            serializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
            serializer.startDocument(null, true);

            serializeRoles(serializer, roles);

            serializer.endDocument();
            atomicFile.finishWrite(outputStream);
        } catch (Exception e) {
            Log.wtf(LOG_TAG, "Failed to write roles.xml, restoring backup: " + file,
                    e);
            atomicFile.failWrite(outputStream);
        } finally {
            IoUtils.closeQuietly(outputStream);
        }
    }

    private static void serializeRoles(@NonNull XmlSerializer serializer,
            @NonNull RolesState roles) throws IOException {
        serializer.startTag(null, TAG_ROLES);

        int version = roles.getVersion();
        serializer.attribute(null, ATTRIBUTE_VERSION, Integer.toString(version));
        String packagesHash = roles.getPackagesHash();
        if (packagesHash != null) {
            serializer.attribute(null, ATTRIBUTE_PACKAGES_HASH, packagesHash);
        }

        for (Map.Entry<String, Set<String>> entry : roles.getRoles().entrySet()) {
            String roleName = entry.getKey();
            Set<String> roleHolders = entry.getValue();

            serializer.startTag(null, TAG_ROLE);
            serializer.attribute(null, ATTRIBUTE_NAME, roleName);
            serializeRoleHolders(serializer, roleHolders);
            serializer.endTag(null, TAG_ROLE);
        }

        serializer.endTag(null, TAG_ROLES);
    }

    private static void serializeRoleHolders(@NonNull XmlSerializer serializer,
            @NonNull Set<String> roleHolders) throws IOException {
        for (String roleHolder : roleHolders) {
            serializer.startTag(null, TAG_HOLDER);
            serializer.attribute(null, ATTRIBUTE_NAME, roleHolder);
            serializer.endTag(null, TAG_HOLDER);
        }
    }

    @Override
    public void delete(@NonNull UserHandle user) {
        getFile(user).delete();
    }

    @NonNull
    private static File getFile(@NonNull UserHandle user) {
        // TODO: Use an API for this.
        File dataDirectory = new File("/data/misc_de/" + user.getIdentifier()
                + "/apexdata/com.android.permission");
        return new File(dataDirectory, ROLES_FILE_NAME);
    }
}
+72 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2020 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.role.persistence;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SystemApi;

import java.util.Map;
import java.util.Set;

/**
 * State of all roles.
 *
 * TODO(b/147914847): Remove @hide when it becomes the default.
 * @hide
 */
@SystemApi(client = SystemApi.Client.MODULE_LIBRARIES, process = SystemApi.Process.SYSTEM_SERVER)
public final class RolesState {

    /**
     * The version of the roles.
     */
    private final int mVersion;

    /**
     * The hash of all packages in the system.
     */
    @Nullable
    private final String mPackagesHash;

    /**
     * The roles.
     */
    @NonNull
    private final Map<String, Set<String>> mRoles;

    public RolesState(int version, @Nullable String packagesHash,
            @NonNull Map<String, Set<String>> roles) {
        mVersion = version;
        mPackagesHash = packagesHash;
        mRoles = roles;
    }

    public int getVersion() {
        return mVersion;
    }

    @Nullable
    public String getPackagesHash() {
        return mPackagesHash;
    }

    @NonNull
    public Map<String, Set<String>> getRoles() {
        return mRoles;
    }
}
+18 −0
Original line number Diff line number Diff line
@@ -26,6 +26,24 @@ package com.android.permission.persistence {

}

package com.android.role.persistence {

  public interface RolesPersistence {
    method @NonNull public static com.android.role.persistence.RolesPersistence createInstance();
    method public void delete(@NonNull android.os.UserHandle);
    method @Nullable public com.android.role.persistence.RolesState read(@NonNull android.os.UserHandle);
    method public void write(@NonNull com.android.role.persistence.RolesState, @NonNull android.os.UserHandle);
  }

  public final class RolesState {
    ctor public RolesState(int, @Nullable String, @NonNull java.util.Map<java.lang.String,java.util.Set<java.lang.String>>);
    method @Nullable public String getPackagesHash();
    method @NonNull public java.util.Map<java.lang.String,java.util.Set<java.lang.String>> getRoles();
    method public int getVersion();
  }

}

package com.android.server {

  public abstract class SystemService {
+39 −84
Original line number Diff line number Diff line
@@ -23,6 +23,7 @@ import android.annotation.UserIdInt;
import android.annotation.WorkerThread;
import android.os.Environment;
import android.os.Handler;
import android.os.UserHandle;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.AtomicFile;
@@ -34,22 +35,21 @@ import com.android.internal.os.BackgroundThread;
import com.android.internal.util.CollectionUtils;
import com.android.internal.util.dump.DualDumpOutputStream;
import com.android.internal.util.function.pooled.PooledLambda;

import libcore.io.IoUtils;
import com.android.role.persistence.RolesPersistence;
import com.android.role.persistence.RolesState;

import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlSerializer;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

/**
 * Stores the state of roles for a user.
@@ -71,6 +71,8 @@ public class RoleUserState {
    private static final String ATTRIBUTE_NAME = "name";
    private static final String ATTRIBUTE_PACKAGES_HASH = "packagesHash";

    private final RolesPersistence mPersistence = RolesPersistence.createInstance();

    @UserIdInt
    private final int mUserId;

@@ -350,9 +352,7 @@ public class RoleUserState {

    @WorkerThread
    private void writeFile() {
        int version;
        String packagesHash;
        ArrayMap<String, ArraySet<String>> roles;
        RolesState roles;
        synchronized (mLock) {
            if (mDestroyed) {
                return;
@@ -360,79 +360,35 @@ public class RoleUserState {

            mWriteScheduled = false;

            version = mVersion;
            packagesHash = mPackagesHash;
            roles = snapshotRolesLocked();
            roles = new RolesState(mVersion, mPackagesHash,
                    (Map<String, Set<String>>) (Map<String, ?>) snapshotRolesLocked());
        }

        AtomicFile atomicFile = new AtomicFile(getFile(mUserId), "roles-" + mUserId);
        FileOutputStream out = null;
        try {
            out = atomicFile.startWrite();

            XmlSerializer serializer = Xml.newSerializer();
            serializer.setOutput(out, StandardCharsets.UTF_8.name());
            serializer.setFeature(
                    "http://xmlpull.org/v1/doc/features.html#indent-output", true);
            serializer.startDocument(null, true);

            serializeRoles(serializer, version, packagesHash, roles);

            serializer.endDocument();
            atomicFile.finishWrite(out);
            Slog.i(LOG_TAG, "Wrote roles.xml successfully");
        } catch (IllegalArgumentException | IllegalStateException | IOException e) {
            Slog.wtf(LOG_TAG, "Failed to write roles.xml, restoring backup", e);
            if (out != null) {
                atomicFile.failWrite(out);
            }
        } finally {
            IoUtils.closeQuietly(out);
        mPersistence.write(roles, UserHandle.of(mUserId));
    }
    }

    @WorkerThread
    private void serializeRoles(@NonNull XmlSerializer serializer, int version,
            @Nullable String packagesHash, @NonNull ArrayMap<String, ArraySet<String>> roles)
            throws IOException {
        serializer.startTag(null, TAG_ROLES);

        serializer.attribute(null, ATTRIBUTE_VERSION, Integer.toString(version));

        if (packagesHash != null) {
            serializer.attribute(null, ATTRIBUTE_PACKAGES_HASH, packagesHash);
    private void readFile() {
        synchronized (mLock) {
            RolesState roles = mPersistence.read(UserHandle.of(mUserId));
            if (roles == null) {
                readLegacyFileLocked();
                scheduleWriteFileLocked();
                return;
            }

        for (int i = 0, size = roles.size(); i < size; ++i) {
            String roleName = roles.keyAt(i);
            ArraySet<String> roleHolders = roles.valueAt(i);
            mVersion = roles.getVersion();
            mPackagesHash = roles.getPackagesHash();

            serializer.startTag(null, TAG_ROLE);
            serializer.attribute(null, ATTRIBUTE_NAME, roleName);
            serializeRoleHolders(serializer, roleHolders);
            serializer.endTag(null, TAG_ROLE);
        }

        serializer.endTag(null, TAG_ROLES);
            mRoles.clear();
            for (Map.Entry<String, Set<String>> entry : roles.getRoles().entrySet()) {
                String roleName = entry.getKey();
                ArraySet<String> roleHolders = new ArraySet<>(entry.getValue());
                mRoles.put(roleName, roleHolders);
            }

    @WorkerThread
    private void serializeRoleHolders(@NonNull XmlSerializer serializer,
            @NonNull ArraySet<String> roleHolders) throws IOException {
        for (int i = 0, size = roleHolders.size(); i < size; ++i) {
            String roleHolder = roleHolders.valueAt(i);

            serializer.startTag(null, TAG_HOLDER);
            serializer.attribute(null, ATTRIBUTE_NAME, roleHolder);
            serializer.endTag(null, TAG_HOLDER);
        }
    }

    /**
     * Read the state from file.
     */
    private void readFile() {
        synchronized (mLock) {
    private void readLegacyFileLocked() {
        File file = getFile(mUserId);
        try (FileInputStream in = new AtomicFile(file).openRead()) {
            XmlPullParser parser = Xml.newPullParser();
@@ -445,7 +401,6 @@ public class RoleUserState {
            throw new IllegalStateException("Failed to parse roles.xml: " + file, e);
        }
    }
    }

    private void parseXmlLocked(@NonNull XmlPullParser parser) throws IOException,
            XmlPullParserException {
@@ -590,7 +545,7 @@ public class RoleUserState {
                throw new IllegalStateException("This RoleUserState has already been destroyed");
            }
            mWriteHandler.removeCallbacksAndMessages(null);
            getFile(mUserId).delete();
            mPersistence.delete(UserHandle.of(mUserId));
            mDestroyed = true;
        }
    }