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