| /* |
| * 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(); |
| } |
| } |
| } |