Home | History | Annotate | Download | only in slice
      1 /*
      2  * Copyright (C) 2018 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
      5  * except in compliance with the License. You may obtain a copy of the License at
      6  *
      7  *      http://www.apache.org/licenses/LICENSE-2.0
      8  *
      9  * Unless required by applicable law or agreed to in writing, software distributed under the
     10  * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
     11  * KIND, either express or implied. See the License for the specific language governing
     12  * permissions and limitations under the License.
     13  */
     14 
     15 package com.android.server.slice;
     16 
     17 import android.content.ContentProvider;
     18 import android.content.Context;
     19 import android.net.Uri;
     20 import android.os.Environment;
     21 import android.os.Handler;
     22 import android.os.Looper;
     23 import android.os.Message;
     24 import android.text.format.DateUtils;
     25 import android.util.ArrayMap;
     26 import android.util.ArraySet;
     27 import android.util.AtomicFile;
     28 import android.util.Log;
     29 import android.util.Slog;
     30 import android.util.Xml.Encoding;
     31 
     32 import com.android.internal.annotations.VisibleForTesting;
     33 import com.android.internal.util.XmlUtils;
     34 import com.android.server.slice.SliceProviderPermissions.SliceAuthority;
     35 
     36 import org.xmlpull.v1.XmlPullParser;
     37 import org.xmlpull.v1.XmlPullParserException;
     38 import org.xmlpull.v1.XmlPullParserFactory;
     39 import org.xmlpull.v1.XmlSerializer;
     40 
     41 import java.io.File;
     42 import java.io.FileNotFoundException;
     43 import java.io.FileOutputStream;
     44 import java.io.IOException;
     45 import java.io.InputStream;
     46 import java.util.Objects;
     47 
     48 public class SlicePermissionManager implements DirtyTracker {
     49 
     50     private static final String TAG = "SlicePermissionManager";
     51 
     52     /**
     53      * The amount of time we'll cache a SliceProviderPermissions or SliceClientPermissions
     54      * in case they are used again.
     55      */
     56     private static final long PERMISSION_CACHE_PERIOD = 5 * DateUtils.MINUTE_IN_MILLIS;
     57 
     58     /**
     59      * The amount of time we delay flushing out permission changes to disk because they usually
     60      * come in short bursts.
     61      */
     62     private static final long WRITE_GRACE_PERIOD = 500;
     63 
     64     private static final String SLICE_DIR = "slice";
     65 
     66     // If/when this bumps again we'll need to write it out in the disk somewhere.
     67     // Currently we don't have a central file for this in version 2 and there is no
     68     // reason to add one until we actually have incompatible version bumps.
     69     // This does however block us from reading backups from P-DP1 which may contain
     70     // a very different XML format for perms.
     71     static final int DB_VERSION = 2;
     72 
     73     private static final String TAG_LIST = "slice-access-list";
     74     private final String ATT_VERSION = "version";
     75 
     76     private final File mSliceDir;
     77     private final Context mContext;
     78     private final Handler mHandler;
     79     private final ArrayMap<PkgUser, SliceProviderPermissions> mCachedProviders = new ArrayMap<>();
     80     private final ArrayMap<PkgUser, SliceClientPermissions> mCachedClients = new ArrayMap<>();
     81     private final ArraySet<Persistable> mDirty = new ArraySet<>();
     82 
     83     @VisibleForTesting
     84     SlicePermissionManager(Context context, Looper looper, File sliceDir) {
     85         mContext = context;
     86         mHandler = new H(looper);
     87         mSliceDir = sliceDir;
     88     }
     89 
     90     public SlicePermissionManager(Context context, Looper looper) {
     91         this(context, looper, new File(Environment.getDataDirectory(), "system/" + SLICE_DIR));
     92     }
     93 
     94     public void grantFullAccess(String pkg, int userId) {
     95         PkgUser pkgUser = new PkgUser(pkg, userId);
     96         SliceClientPermissions client = getClient(pkgUser);
     97         client.setHasFullAccess(true);
     98     }
     99 
    100     public void grantSliceAccess(String pkg, int userId, String providerPkg, int providerUser,
    101             Uri uri) {
    102         PkgUser pkgUser = new PkgUser(pkg, userId);
    103         PkgUser providerPkgUser = new PkgUser(providerPkg, providerUser);
    104 
    105         SliceClientPermissions client = getClient(pkgUser);
    106         client.grantUri(uri, providerPkgUser);
    107 
    108         SliceProviderPermissions provider = getProvider(providerPkgUser);
    109         provider.getOrCreateAuthority(ContentProvider.getUriWithoutUserId(uri).getAuthority())
    110                 .addPkg(pkgUser);
    111     }
    112 
    113     public void revokeSliceAccess(String pkg, int userId, String providerPkg, int providerUser,
    114             Uri uri) {
    115         PkgUser pkgUser = new PkgUser(pkg, userId);
    116         PkgUser providerPkgUser = new PkgUser(providerPkg, providerUser);
    117 
    118         SliceClientPermissions client = getClient(pkgUser);
    119         client.revokeUri(uri, providerPkgUser);
    120     }
    121 
    122     public void removePkg(String pkg, int userId) {
    123         PkgUser pkgUser = new PkgUser(pkg, userId);
    124         SliceProviderPermissions provider = getProvider(pkgUser);
    125 
    126         for (SliceAuthority authority : provider.getAuthorities()) {
    127             for (PkgUser p : authority.getPkgs()) {
    128                 getClient(p).removeAuthority(authority.getAuthority(), userId);
    129             }
    130         }
    131         SliceClientPermissions client = getClient(pkgUser);
    132         client.clear();
    133         mHandler.obtainMessage(H.MSG_REMOVE, pkgUser);
    134     }
    135 
    136     public String[] getAllPackagesGranted(String pkg) {
    137         ArraySet<String> ret = new ArraySet<>();
    138         for (SliceAuthority authority : getProvider(new PkgUser(pkg, 0)).getAuthorities()) {
    139             for (PkgUser pkgUser : authority.getPkgs()) {
    140                 ret.add(pkgUser.mPkg);
    141             }
    142         }
    143         return ret.toArray(new String[ret.size()]);
    144     }
    145 
    146     public boolean hasFullAccess(String pkg, int userId) {
    147         PkgUser pkgUser = new PkgUser(pkg, userId);
    148         return getClient(pkgUser).hasFullAccess();
    149     }
    150 
    151     public boolean hasPermission(String pkg, int userId, Uri uri) {
    152         PkgUser pkgUser = new PkgUser(pkg, userId);
    153         SliceClientPermissions client = getClient(pkgUser);
    154         int providerUserId = ContentProvider.getUserIdFromUri(uri, userId);
    155         return client.hasFullAccess()
    156                 || client.hasPermission(ContentProvider.getUriWithoutUserId(uri), providerUserId);
    157     }
    158 
    159     @Override
    160     public void onPersistableDirty(Persistable obj) {
    161         mHandler.removeMessages(H.MSG_PERSIST);
    162         mHandler.obtainMessage(H.MSG_ADD_DIRTY, obj).sendToTarget();
    163         mHandler.sendEmptyMessageDelayed(H.MSG_PERSIST, WRITE_GRACE_PERIOD);
    164     }
    165 
    166     public void writeBackup(XmlSerializer out) throws IOException, XmlPullParserException {
    167         synchronized (this) {
    168             out.startTag(null, TAG_LIST);
    169             out.attribute(null, ATT_VERSION, String.valueOf(DB_VERSION));
    170 
    171             // Don't do anything with changes from the backup, because there shouldn't be any.
    172             DirtyTracker tracker = obj -> { };
    173             if (mHandler.hasMessages(H.MSG_PERSIST)) {
    174                 mHandler.removeMessages(H.MSG_PERSIST);
    175                 handlePersist();
    176             }
    177             for (String file : new File(mSliceDir.getAbsolutePath()).list()) {
    178                 if (file.isEmpty()) continue;
    179                 try (ParserHolder parser = getParser(file)) {
    180                     Persistable p;
    181                     while (parser.parser.getEventType() != XmlPullParser.START_TAG) {
    182                         parser.parser.next();
    183                     }
    184                     if (SliceClientPermissions.TAG_CLIENT.equals(parser.parser.getName())) {
    185                         p = SliceClientPermissions.createFrom(parser.parser, tracker);
    186                     } else {
    187                         p = SliceProviderPermissions.createFrom(parser.parser, tracker);
    188                     }
    189                     p.writeTo(out);
    190                 }
    191             }
    192 
    193             out.endTag(null, TAG_LIST);
    194         }
    195     }
    196 
    197     public void readRestore(XmlPullParser parser) throws IOException, XmlPullParserException {
    198         synchronized (this) {
    199             while ((parser.getEventType() != XmlPullParser.START_TAG
    200                     || !TAG_LIST.equals(parser.getName()))
    201                     && parser.getEventType() != XmlPullParser.END_DOCUMENT) {
    202                 parser.next();
    203             }
    204             int xmlVersion = XmlUtils.readIntAttribute(parser, ATT_VERSION, 0);
    205             if (xmlVersion < DB_VERSION) {
    206                 // No conversion support right now.
    207                 return;
    208             }
    209             while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
    210                 if (parser.getEventType() == XmlPullParser.START_TAG) {
    211                     if (SliceClientPermissions.TAG_CLIENT.equals(parser.getName())) {
    212                         SliceClientPermissions client = SliceClientPermissions.createFrom(parser,
    213                                 this);
    214                         synchronized (mCachedClients) {
    215                             mCachedClients.put(client.getPkg(), client);
    216                         }
    217                         onPersistableDirty(client);
    218                         mHandler.sendMessageDelayed(
    219                                 mHandler.obtainMessage(H.MSG_CLEAR_CLIENT, client.getPkg()),
    220                                 PERMISSION_CACHE_PERIOD);
    221                     } else if (SliceProviderPermissions.TAG_PROVIDER.equals(parser.getName())) {
    222                         SliceProviderPermissions provider = SliceProviderPermissions.createFrom(
    223                                 parser, this);
    224                         synchronized (mCachedProviders) {
    225                             mCachedProviders.put(provider.getPkg(), provider);
    226                         }
    227                         onPersistableDirty(provider);
    228                         mHandler.sendMessageDelayed(
    229                                 mHandler.obtainMessage(H.MSG_CLEAR_PROVIDER, provider.getPkg()),
    230                                 PERMISSION_CACHE_PERIOD);
    231                     } else {
    232                         parser.next();
    233                     }
    234                 } else {
    235                     parser.next();
    236                 }
    237             }
    238         }
    239     }
    240 
    241     private SliceClientPermissions getClient(PkgUser pkgUser) {
    242         SliceClientPermissions client;
    243         synchronized (mCachedClients) {
    244             client = mCachedClients.get(pkgUser);
    245         }
    246         if (client == null) {
    247             try (ParserHolder parser = getParser(SliceClientPermissions.getFileName(pkgUser))) {
    248                 client = SliceClientPermissions.createFrom(parser.parser, this);
    249                 synchronized (mCachedClients) {
    250                     mCachedClients.put(pkgUser, client);
    251                 }
    252                 mHandler.sendMessageDelayed(mHandler.obtainMessage(H.MSG_CLEAR_CLIENT, pkgUser),
    253                         PERMISSION_CACHE_PERIOD);
    254                 return client;
    255             } catch (FileNotFoundException e) {
    256                 // No client exists yet.
    257             } catch (IOException e) {
    258                 Log.e(TAG, "Can't read client", e);
    259             } catch (XmlPullParserException e) {
    260                 Log.e(TAG, "Can't read client", e);
    261             }
    262             // Can't read or no permissions exist, create a clean object.
    263             client = new SliceClientPermissions(pkgUser, this);
    264             synchronized (mCachedClients) {
    265                 mCachedClients.put(pkgUser, client);
    266             }
    267         }
    268         return client;
    269     }
    270 
    271     private SliceProviderPermissions getProvider(PkgUser pkgUser) {
    272         SliceProviderPermissions provider;
    273         synchronized (mCachedProviders) {
    274             provider = mCachedProviders.get(pkgUser);
    275         }
    276         if (provider == null) {
    277             try (ParserHolder parser = getParser(SliceProviderPermissions.getFileName(pkgUser))) {
    278                 provider = SliceProviderPermissions.createFrom(parser.parser, this);
    279                 synchronized (mCachedProviders) {
    280                     mCachedProviders.put(pkgUser, provider);
    281                 }
    282                 mHandler.sendMessageDelayed(mHandler.obtainMessage(H.MSG_CLEAR_PROVIDER, pkgUser),
    283                         PERMISSION_CACHE_PERIOD);
    284                 return provider;
    285             } catch (FileNotFoundException e) {
    286                 // No provider exists yet.
    287             } catch (IOException e) {
    288                 Log.e(TAG, "Can't read provider", e);
    289             } catch (XmlPullParserException e) {
    290                 Log.e(TAG, "Can't read provider", e);
    291             }
    292             // Can't read or no permissions exist, create a clean object.
    293             provider = new SliceProviderPermissions(pkgUser, this);
    294             synchronized (mCachedProviders) {
    295                 mCachedProviders.put(pkgUser, provider);
    296             }
    297         }
    298         return provider;
    299     }
    300 
    301     private ParserHolder getParser(String fileName)
    302             throws FileNotFoundException, XmlPullParserException {
    303         AtomicFile file = getFile(fileName);
    304         ParserHolder holder = new ParserHolder();
    305         holder.input = file.openRead();
    306         holder.parser = XmlPullParserFactory.newInstance().newPullParser();
    307         holder.parser.setInput(holder.input, Encoding.UTF_8.name());
    308         return holder;
    309     }
    310 
    311     private AtomicFile getFile(String fileName) {
    312         if (!mSliceDir.exists()) {
    313             mSliceDir.mkdir();
    314         }
    315         return new AtomicFile(new File(mSliceDir, fileName));
    316     }
    317 
    318     private void handlePersist() {
    319         synchronized (this) {
    320             for (Persistable persistable : mDirty) {
    321                 AtomicFile file = getFile(persistable.getFileName());
    322                 final FileOutputStream stream;
    323                 try {
    324                     stream = file.startWrite();
    325                 } catch (IOException e) {
    326                     Slog.w(TAG, "Failed to save access file", e);
    327                     return;
    328                 }
    329 
    330                 try {
    331                     XmlSerializer out = XmlPullParserFactory.newInstance().newSerializer();
    332                     out.setOutput(stream, Encoding.UTF_8.name());
    333 
    334                     persistable.writeTo(out);
    335 
    336                     out.flush();
    337                     file.finishWrite(stream);
    338                 } catch (IOException | XmlPullParserException e) {
    339                     Slog.w(TAG, "Failed to save access file, restoring backup", e);
    340                     file.failWrite(stream);
    341                 }
    342             }
    343             mDirty.clear();
    344         }
    345     }
    346 
    347     private void handleRemove(PkgUser pkgUser) {
    348         getFile(SliceClientPermissions.getFileName(pkgUser)).delete();
    349         getFile(SliceProviderPermissions.getFileName(pkgUser)).delete();
    350         mDirty.remove(mCachedClients.remove(pkgUser));
    351         mDirty.remove(mCachedProviders.remove(pkgUser));
    352     }
    353 
    354     private final class H extends Handler {
    355         private static final int MSG_ADD_DIRTY = 1;
    356         private static final int MSG_PERSIST = 2;
    357         private static final int MSG_REMOVE = 3;
    358         private static final int MSG_CLEAR_CLIENT = 4;
    359         private static final int MSG_CLEAR_PROVIDER = 5;
    360 
    361         public H(Looper looper) {
    362             super(looper);
    363         }
    364 
    365         @Override
    366         public void handleMessage(Message msg) {
    367             switch (msg.what) {
    368                 case MSG_ADD_DIRTY:
    369                     mDirty.add((Persistable) msg.obj);
    370                     break;
    371                 case MSG_PERSIST:
    372                     handlePersist();
    373                     break;
    374                 case MSG_REMOVE:
    375                     handleRemove((PkgUser) msg.obj);
    376                     break;
    377                 case MSG_CLEAR_CLIENT:
    378                     synchronized (mCachedClients) {
    379                         mCachedClients.remove(msg.obj);
    380                     }
    381                     break;
    382                 case MSG_CLEAR_PROVIDER:
    383                     synchronized (mCachedProviders) {
    384                         mCachedProviders.remove(msg.obj);
    385                     }
    386                     break;
    387             }
    388         }
    389     }
    390 
    391     public static class PkgUser {
    392         private static final String SEPARATOR = "@";
    393         private static final String FORMAT = "%s" + SEPARATOR + "%d";
    394         private final String mPkg;
    395         private final int mUserId;
    396 
    397         public PkgUser(String pkg, int userId) {
    398             mPkg = pkg;
    399             mUserId = userId;
    400         }
    401 
    402         public PkgUser(String pkgUserStr) throws IllegalArgumentException {
    403             try {
    404                 String[] vals = pkgUserStr.split(SEPARATOR, 2);
    405                 mPkg = vals[0];
    406                 mUserId = Integer.parseInt(vals[1]);
    407             } catch (Exception e) {
    408                 throw new IllegalArgumentException(e);
    409             }
    410         }
    411 
    412         public String getPkg() {
    413             return mPkg;
    414         }
    415 
    416         public int getUserId() {
    417             return mUserId;
    418         }
    419 
    420         @Override
    421         public int hashCode() {
    422             return mPkg.hashCode() + mUserId;
    423         }
    424 
    425         @Override
    426         public boolean equals(Object obj) {
    427             if (!getClass().equals(obj != null ? obj.getClass() : null)) return false;
    428             PkgUser other = (PkgUser) obj;
    429             return Objects.equals(other.mPkg, mPkg) && other.mUserId == mUserId;
    430         }
    431 
    432         @Override
    433         public String toString() {
    434             return String.format(FORMAT, mPkg, mUserId);
    435         }
    436     }
    437 
    438     private class ParserHolder implements AutoCloseable {
    439 
    440         private InputStream input;
    441         private XmlPullParser parser;
    442 
    443         @Override
    444         public void close() throws IOException {
    445             input.close();
    446         }
    447     }
    448 }
    449