diff options
Diffstat (limited to 'com/android/server/slice/SlicePermissionManager.java')
-rw-r--r-- | com/android/server/slice/SlicePermissionManager.java | 432 |
1 files changed, 432 insertions, 0 deletions
diff --git a/com/android/server/slice/SlicePermissionManager.java b/com/android/server/slice/SlicePermissionManager.java new file mode 100644 index 00000000..d25ec89e --- /dev/null +++ b/com/android/server/slice/SlicePermissionManager.java @@ -0,0 +1,432 @@ +/* + * Copyright (C) 2018 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.slice; + +import android.content.ContentProvider; +import android.content.Context; +import android.net.Uri; +import android.os.Environment; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.text.format.DateUtils; +import android.util.ArrayMap; +import android.util.ArraySet; +import android.util.AtomicFile; +import android.util.Log; +import android.util.Slog; +import android.util.Xml.Encoding; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.XmlUtils; +import com.android.server.slice.SliceProviderPermissions.SliceAuthority; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlPullParserFactory; +import org.xmlpull.v1.XmlSerializer; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Objects; + +public class SlicePermissionManager implements DirtyTracker { + + private static final String TAG = "SlicePermissionManager"; + + /** + * The amount of time we'll cache a SliceProviderPermissions or SliceClientPermissions + * in case they are used again. + */ + private static final long PERMISSION_CACHE_PERIOD = 5 * DateUtils.MINUTE_IN_MILLIS; + + /** + * The amount of time we delay flushing out permission changes to disk because they usually + * come in short bursts. + */ + private static final long WRITE_GRACE_PERIOD = 500; + + private static final String SLICE_DIR = "slice"; + + // If/when this bumps again we'll need to write it out in the disk somewhere. + // Currently we don't have a central file for this in version 2 and there is no + // reason to add one until we actually have incompatible version bumps. + // This does however block us from reading backups from P-DP1 which may contain + // a very different XML format for perms. + static final int DB_VERSION = 2; + + private static final String TAG_LIST = "slice-access-list"; + private final String ATT_VERSION = "version"; + + private final File mSliceDir; + private final Context mContext; + private final Handler mHandler; + private final ArrayMap<PkgUser, SliceProviderPermissions> mCachedProviders = new ArrayMap<>(); + private final ArrayMap<PkgUser, SliceClientPermissions> mCachedClients = new ArrayMap<>(); + private final ArraySet<Persistable> mDirty = new ArraySet<>(); + + @VisibleForTesting + SlicePermissionManager(Context context, Looper looper, File sliceDir) { + mContext = context; + mHandler = new H(looper); + mSliceDir = sliceDir; + } + + public SlicePermissionManager(Context context, Looper looper) { + this(context, looper, new File(Environment.getDataDirectory(), "system/" + SLICE_DIR)); + } + + public void grantFullAccess(String pkg, int userId) { + PkgUser pkgUser = new PkgUser(pkg, userId); + SliceClientPermissions client = getClient(pkgUser); + client.setHasFullAccess(true); + } + + public void grantSliceAccess(String pkg, int userId, String providerPkg, int providerUser, + Uri uri) { + PkgUser pkgUser = new PkgUser(pkg, userId); + PkgUser providerPkgUser = new PkgUser(providerPkg, providerUser); + + SliceClientPermissions client = getClient(pkgUser); + client.grantUri(uri, providerPkgUser); + + SliceProviderPermissions provider = getProvider(providerPkgUser); + provider.getOrCreateAuthority(ContentProvider.getUriWithoutUserId(uri).getAuthority()) + .addPkg(pkgUser); + } + + public void revokeSliceAccess(String pkg, int userId, String providerPkg, int providerUser, + Uri uri) { + PkgUser pkgUser = new PkgUser(pkg, userId); + PkgUser providerPkgUser = new PkgUser(providerPkg, providerUser); + + SliceClientPermissions client = getClient(pkgUser); + client.revokeUri(uri, providerPkgUser); + } + + public void removePkg(String pkg, int userId) { + PkgUser pkgUser = new PkgUser(pkg, userId); + SliceProviderPermissions provider = getProvider(pkgUser); + + for (SliceAuthority authority : provider.getAuthorities()) { + for (PkgUser p : authority.getPkgs()) { + getClient(p).removeAuthority(authority.getAuthority(), userId); + } + } + SliceClientPermissions client = getClient(pkgUser); + client.clear(); + mHandler.obtainMessage(H.MSG_REMOVE, pkgUser); + } + + public boolean hasFullAccess(String pkg, int userId) { + PkgUser pkgUser = new PkgUser(pkg, userId); + return getClient(pkgUser).hasFullAccess(); + } + + public boolean hasPermission(String pkg, int userId, Uri uri) { + PkgUser pkgUser = new PkgUser(pkg, userId); + SliceClientPermissions client = getClient(pkgUser); + int providerUserId = ContentProvider.getUserIdFromUri(uri, userId); + return client.hasFullAccess() + || client.hasPermission(ContentProvider.getUriWithoutUserId(uri), providerUserId); + } + + @Override + public void onPersistableDirty(Persistable obj) { + mHandler.removeMessages(H.MSG_PERSIST); + mHandler.obtainMessage(H.MSG_ADD_DIRTY, obj).sendToTarget(); + mHandler.sendEmptyMessageDelayed(H.MSG_PERSIST, WRITE_GRACE_PERIOD); + } + + public void writeBackup(XmlSerializer out) throws IOException, XmlPullParserException { + synchronized (this) { + out.startTag(null, TAG_LIST); + out.attribute(null, ATT_VERSION, String.valueOf(DB_VERSION)); + + // Don't do anything with changes from the backup, because there shouldn't be any. + DirtyTracker tracker = obj -> { }; + if (mHandler.hasMessages(H.MSG_PERSIST)) { + mHandler.removeMessages(H.MSG_PERSIST); + handlePersist(); + } + for (String file : new File(mSliceDir.getAbsolutePath()).list()) { + if (file.isEmpty()) continue; + try (ParserHolder parser = getParser(file)) { + Persistable p; + while (parser.parser.getEventType() != XmlPullParser.START_TAG) { + parser.parser.next(); + } + if (SliceClientPermissions.TAG_CLIENT.equals(parser.parser.getName())) { + p = SliceClientPermissions.createFrom(parser.parser, tracker); + } else { + p = SliceProviderPermissions.createFrom(parser.parser, tracker); + } + p.writeTo(out); + } + } + + out.endTag(null, TAG_LIST); + } + } + + public void readRestore(XmlPullParser parser) throws IOException, XmlPullParserException { + synchronized (this) { + while ((parser.getEventType() != XmlPullParser.START_TAG + || !TAG_LIST.equals(parser.getName())) + && parser.getEventType() != XmlPullParser.END_DOCUMENT) { + parser.next(); + } + int xmlVersion = XmlUtils.readIntAttribute(parser, ATT_VERSION, 0); + if (xmlVersion < DB_VERSION) { + // No conversion support right now. + return; + } + while (parser.getEventType() != XmlPullParser.END_DOCUMENT) { + if (parser.getEventType() == XmlPullParser.START_TAG) { + if (SliceClientPermissions.TAG_CLIENT.equals(parser.getName())) { + SliceClientPermissions client = SliceClientPermissions.createFrom(parser, + this); + synchronized (mCachedClients) { + mCachedClients.put(client.getPkg(), client); + } + onPersistableDirty(client); + mHandler.sendMessageDelayed( + mHandler.obtainMessage(H.MSG_CLEAR_CLIENT, client.getPkg()), + PERMISSION_CACHE_PERIOD); + } else if (SliceProviderPermissions.TAG_PROVIDER.equals(parser.getName())) { + SliceProviderPermissions provider = SliceProviderPermissions.createFrom( + parser, this); + synchronized (mCachedProviders) { + mCachedProviders.put(provider.getPkg(), provider); + } + onPersistableDirty(provider); + mHandler.sendMessageDelayed( + mHandler.obtainMessage(H.MSG_CLEAR_PROVIDER, provider.getPkg()), + PERMISSION_CACHE_PERIOD); + } else { + parser.next(); + } + } else { + parser.next(); + } + } + } + } + + private SliceClientPermissions getClient(PkgUser pkgUser) { + SliceClientPermissions client; + synchronized (mCachedClients) { + client = mCachedClients.get(pkgUser); + } + if (client == null) { + try (ParserHolder parser = getParser(SliceClientPermissions.getFileName(pkgUser))) { + client = SliceClientPermissions.createFrom(parser.parser, this); + synchronized (mCachedClients) { + mCachedClients.put(pkgUser, client); + } + mHandler.sendMessageDelayed(mHandler.obtainMessage(H.MSG_CLEAR_CLIENT, pkgUser), + PERMISSION_CACHE_PERIOD); + return client; + } catch (FileNotFoundException e) { + // No client exists yet. + } catch (IOException e) { + Log.e(TAG, "Can't read client", e); + } catch (XmlPullParserException e) { + Log.e(TAG, "Can't read client", e); + } + // Can't read or no permissions exist, create a clean object. + client = new SliceClientPermissions(pkgUser, this); + } + return client; + } + + private SliceProviderPermissions getProvider(PkgUser pkgUser) { + SliceProviderPermissions provider; + synchronized (mCachedProviders) { + provider = mCachedProviders.get(pkgUser); + } + if (provider == null) { + try (ParserHolder parser = getParser(SliceProviderPermissions.getFileName(pkgUser))) { + provider = SliceProviderPermissions.createFrom(parser.parser, this); + synchronized (mCachedProviders) { + mCachedProviders.put(pkgUser, provider); + } + mHandler.sendMessageDelayed(mHandler.obtainMessage(H.MSG_CLEAR_PROVIDER, pkgUser), + PERMISSION_CACHE_PERIOD); + return provider; + } catch (FileNotFoundException e) { + // No provider exists yet. + } catch (IOException e) { + Log.e(TAG, "Can't read provider", e); + } catch (XmlPullParserException e) { + Log.e(TAG, "Can't read provider", e); + } + // Can't read or no permissions exist, create a clean object. + provider = new SliceProviderPermissions(pkgUser, this); + } + return provider; + } + + private ParserHolder getParser(String fileName) + throws FileNotFoundException, XmlPullParserException { + AtomicFile file = getFile(fileName); + ParserHolder holder = new ParserHolder(); + holder.input = file.openRead(); + holder.parser = XmlPullParserFactory.newInstance().newPullParser(); + holder.parser.setInput(holder.input, Encoding.UTF_8.name()); + return holder; + } + + private AtomicFile getFile(String fileName) { + if (!mSliceDir.exists()) { + mSliceDir.mkdir(); + } + return new AtomicFile(new File(mSliceDir, fileName)); + } + + private void handlePersist() { + synchronized (this) { + for (Persistable persistable : mDirty) { + AtomicFile file = getFile(persistable.getFileName()); + final FileOutputStream stream; + try { + stream = file.startWrite(); + } catch (IOException e) { + Slog.w(TAG, "Failed to save access file", e); + return; + } + + try { + XmlSerializer out = XmlPullParserFactory.newInstance().newSerializer(); + out.setOutput(stream, Encoding.UTF_8.name()); + + persistable.writeTo(out); + + out.flush(); + file.finishWrite(stream); + } catch (IOException | XmlPullParserException e) { + Slog.w(TAG, "Failed to save access file, restoring backup", e); + file.failWrite(stream); + } + } + mDirty.clear(); + } + } + + private void handleRemove(PkgUser pkgUser) { + getFile(SliceClientPermissions.getFileName(pkgUser)).delete(); + getFile(SliceProviderPermissions.getFileName(pkgUser)).delete(); + mDirty.remove(mCachedClients.remove(pkgUser)); + mDirty.remove(mCachedProviders.remove(pkgUser)); + } + + private final class H extends Handler { + private static final int MSG_ADD_DIRTY = 1; + private static final int MSG_PERSIST = 2; + private static final int MSG_REMOVE = 3; + private static final int MSG_CLEAR_CLIENT = 4; + private static final int MSG_CLEAR_PROVIDER = 5; + + public H(Looper looper) { + super(looper); + } + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_ADD_DIRTY: + mDirty.add((Persistable) msg.obj); + break; + case MSG_PERSIST: + handlePersist(); + break; + case MSG_REMOVE: + handleRemove((PkgUser) msg.obj); + break; + case MSG_CLEAR_CLIENT: + synchronized (mCachedClients) { + mCachedClients.remove(msg.obj); + } + break; + case MSG_CLEAR_PROVIDER: + synchronized (mCachedProviders) { + mCachedProviders.remove(msg.obj); + } + break; + } + } + } + + public static class PkgUser { + private static final String SEPARATOR = "@"; + private static final String FORMAT = "%s" + SEPARATOR + "%d"; + private final String mPkg; + private final int mUserId; + + public PkgUser(String pkg, int userId) { + mPkg = pkg; + mUserId = userId; + } + + public PkgUser(String pkgUserStr) throws IllegalArgumentException { + try { + String[] vals = pkgUserStr.split(SEPARATOR, 2); + mPkg = vals[0]; + mUserId = Integer.parseInt(vals[1]); + } catch (Exception e) { + throw new IllegalArgumentException(e); + } + } + + public String getPkg() { + return mPkg; + } + + public int getUserId() { + return mUserId; + } + + @Override + public int hashCode() { + return mPkg.hashCode() + mUserId; + } + + @Override + public boolean equals(Object obj) { + if (!getClass().equals(obj != null ? obj.getClass() : null)) return false; + PkgUser other = (PkgUser) obj; + return Objects.equals(other.mPkg, mPkg) && other.mUserId == mUserId; + } + + @Override + public String toString() { + return String.format(FORMAT, mPkg, mUserId); + } + } + + private class ParserHolder implements AutoCloseable { + + private InputStream input; + private XmlPullParser parser; + + @Override + public void close() throws IOException { + input.close(); + } + } +} |