/*
|
* 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).sendToTarget();
|
}
|
|
public String[] getAllPackagesGranted(String pkg) {
|
ArraySet<String> ret = new ArraySet<>();
|
for (SliceAuthority authority : getProvider(new PkgUser(pkg, 0)).getAuthorities()) {
|
for (PkgUser pkgUser : authority.getPkgs()) {
|
ret.add(pkgUser.mPkg);
|
}
|
}
|
return ret.toArray(new String[ret.size()]);
|
}
|
|
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()) {
|
try (ParserHolder parser = getParser(file)) {
|
Persistable p = null;
|
while (parser.parser.getEventType() != XmlPullParser.END_DOCUMENT) {
|
if (parser.parser.getEventType() == XmlPullParser.START_TAG) {
|
if (SliceClientPermissions.TAG_CLIENT.equals(parser.parser.getName())) {
|
p = SliceClientPermissions.createFrom(parser.parser, tracker);
|
} else {
|
p = SliceProviderPermissions.createFrom(parser.parser, tracker);
|
}
|
break;
|
}
|
parser.parser.next();
|
}
|
if (p != null) {
|
p.writeTo(out);
|
} else {
|
Slog.w(TAG, "Invalid or empty slice permissions file: " + file);
|
}
|
}
|
}
|
|
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);
|
synchronized (mCachedClients) {
|
mCachedClients.put(pkgUser, client);
|
}
|
}
|
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);
|
synchronized (mCachedProviders) {
|
mCachedProviders.put(pkgUser, provider);
|
}
|
}
|
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));
|
}
|
|
@VisibleForTesting
|
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 | RuntimeException e) {
|
Slog.w(TAG, "Failed to save access file, restoring backup", e);
|
file.failWrite(stream);
|
}
|
}
|
mDirty.clear();
|
}
|
}
|
|
// use addPersistableDirty(); this is just for tests
|
@VisibleForTesting
|
void addDirtyImmediate(Persistable obj) {
|
mDirty.add(obj);
|
}
|
|
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();
|
}
|
}
|
}
|