Jason Monk | bf3eedc | 2018-04-05 20:56:42 -0400 | [diff] [blame] | 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 | |
Jason Monk | 7f01f3b | 2018-05-15 15:46:26 -0400 | [diff] [blame] | 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 | |
Jason Monk | bf3eedc | 2018-04-05 20:56:42 -0400 | [diff] [blame] | 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()) { |
Jason Monk | bf3eedc | 2018-04-05 20:56:42 -0400 | [diff] [blame] | 178 | try (ParserHolder parser = getParser(file)) { |
Dan Sandler | e731681 | 2019-01-10 11:24:19 -0500 | [diff] [blame] | 179 | Persistable p = null; |
| 180 | while (parser.parser.getEventType() != XmlPullParser.END_DOCUMENT) { |
| 181 | if (parser.parser.getEventType() == XmlPullParser.START_TAG) { |
| 182 | if (SliceClientPermissions.TAG_CLIENT.equals(parser.parser.getName())) { |
| 183 | p = SliceClientPermissions.createFrom(parser.parser, tracker); |
| 184 | } else { |
| 185 | p = SliceProviderPermissions.createFrom(parser.parser, tracker); |
| 186 | } |
| 187 | break; |
| 188 | } |
Jason Monk | bf3eedc | 2018-04-05 20:56:42 -0400 | [diff] [blame] | 189 | parser.parser.next(); |
| 190 | } |
Dan Sandler | e731681 | 2019-01-10 11:24:19 -0500 | [diff] [blame] | 191 | if (p != null) { |
| 192 | p.writeTo(out); |
Jason Monk | bf3eedc | 2018-04-05 20:56:42 -0400 | [diff] [blame] | 193 | } else { |
Dan Sandler | e731681 | 2019-01-10 11:24:19 -0500 | [diff] [blame] | 194 | Slog.w(TAG, "Invalid or empty slice permissions file: " + file); |
Jason Monk | bf3eedc | 2018-04-05 20:56:42 -0400 | [diff] [blame] | 195 | } |
Jason Monk | bf3eedc | 2018-04-05 20:56:42 -0400 | [diff] [blame] | 196 | } |
| 197 | } |
| 198 | |
| 199 | out.endTag(null, TAG_LIST); |
| 200 | } |
| 201 | } |
| 202 | |
| 203 | public void readRestore(XmlPullParser parser) throws IOException, XmlPullParserException { |
| 204 | synchronized (this) { |
| 205 | while ((parser.getEventType() != XmlPullParser.START_TAG |
| 206 | || !TAG_LIST.equals(parser.getName())) |
| 207 | && parser.getEventType() != XmlPullParser.END_DOCUMENT) { |
| 208 | parser.next(); |
| 209 | } |
| 210 | int xmlVersion = XmlUtils.readIntAttribute(parser, ATT_VERSION, 0); |
| 211 | if (xmlVersion < DB_VERSION) { |
| 212 | // No conversion support right now. |
| 213 | return; |
| 214 | } |
| 215 | while (parser.getEventType() != XmlPullParser.END_DOCUMENT) { |
| 216 | if (parser.getEventType() == XmlPullParser.START_TAG) { |
| 217 | if (SliceClientPermissions.TAG_CLIENT.equals(parser.getName())) { |
| 218 | SliceClientPermissions client = SliceClientPermissions.createFrom(parser, |
| 219 | this); |
| 220 | synchronized (mCachedClients) { |
| 221 | mCachedClients.put(client.getPkg(), client); |
| 222 | } |
| 223 | onPersistableDirty(client); |
| 224 | mHandler.sendMessageDelayed( |
| 225 | mHandler.obtainMessage(H.MSG_CLEAR_CLIENT, client.getPkg()), |
| 226 | PERMISSION_CACHE_PERIOD); |
| 227 | } else if (SliceProviderPermissions.TAG_PROVIDER.equals(parser.getName())) { |
| 228 | SliceProviderPermissions provider = SliceProviderPermissions.createFrom( |
| 229 | parser, this); |
| 230 | synchronized (mCachedProviders) { |
| 231 | mCachedProviders.put(provider.getPkg(), provider); |
| 232 | } |
| 233 | onPersistableDirty(provider); |
| 234 | mHandler.sendMessageDelayed( |
| 235 | mHandler.obtainMessage(H.MSG_CLEAR_PROVIDER, provider.getPkg()), |
| 236 | PERMISSION_CACHE_PERIOD); |
| 237 | } else { |
| 238 | parser.next(); |
| 239 | } |
| 240 | } else { |
| 241 | parser.next(); |
| 242 | } |
| 243 | } |
| 244 | } |
| 245 | } |
| 246 | |
| 247 | private SliceClientPermissions getClient(PkgUser pkgUser) { |
| 248 | SliceClientPermissions client; |
| 249 | synchronized (mCachedClients) { |
| 250 | client = mCachedClients.get(pkgUser); |
| 251 | } |
| 252 | if (client == null) { |
| 253 | try (ParserHolder parser = getParser(SliceClientPermissions.getFileName(pkgUser))) { |
| 254 | client = SliceClientPermissions.createFrom(parser.parser, this); |
| 255 | synchronized (mCachedClients) { |
| 256 | mCachedClients.put(pkgUser, client); |
| 257 | } |
| 258 | mHandler.sendMessageDelayed(mHandler.obtainMessage(H.MSG_CLEAR_CLIENT, pkgUser), |
| 259 | PERMISSION_CACHE_PERIOD); |
| 260 | return client; |
| 261 | } catch (FileNotFoundException e) { |
| 262 | // No client exists yet. |
| 263 | } catch (IOException e) { |
| 264 | Log.e(TAG, "Can't read client", e); |
| 265 | } catch (XmlPullParserException e) { |
| 266 | Log.e(TAG, "Can't read client", e); |
| 267 | } |
| 268 | // Can't read or no permissions exist, create a clean object. |
| 269 | client = new SliceClientPermissions(pkgUser, this); |
Jason Monk | 9c03ef4 | 2018-05-11 09:26:51 -0700 | [diff] [blame] | 270 | synchronized (mCachedClients) { |
| 271 | mCachedClients.put(pkgUser, client); |
| 272 | } |
Jason Monk | bf3eedc | 2018-04-05 20:56:42 -0400 | [diff] [blame] | 273 | } |
| 274 | return client; |
| 275 | } |
| 276 | |
| 277 | private SliceProviderPermissions getProvider(PkgUser pkgUser) { |
| 278 | SliceProviderPermissions provider; |
| 279 | synchronized (mCachedProviders) { |
| 280 | provider = mCachedProviders.get(pkgUser); |
| 281 | } |
| 282 | if (provider == null) { |
| 283 | try (ParserHolder parser = getParser(SliceProviderPermissions.getFileName(pkgUser))) { |
| 284 | provider = SliceProviderPermissions.createFrom(parser.parser, this); |
| 285 | synchronized (mCachedProviders) { |
| 286 | mCachedProviders.put(pkgUser, provider); |
| 287 | } |
| 288 | mHandler.sendMessageDelayed(mHandler.obtainMessage(H.MSG_CLEAR_PROVIDER, pkgUser), |
| 289 | PERMISSION_CACHE_PERIOD); |
| 290 | return provider; |
| 291 | } catch (FileNotFoundException e) { |
| 292 | // No provider exists yet. |
| 293 | } catch (IOException e) { |
| 294 | Log.e(TAG, "Can't read provider", e); |
| 295 | } catch (XmlPullParserException e) { |
| 296 | Log.e(TAG, "Can't read provider", e); |
| 297 | } |
| 298 | // Can't read or no permissions exist, create a clean object. |
| 299 | provider = new SliceProviderPermissions(pkgUser, this); |
Jason Monk | 9c03ef4 | 2018-05-11 09:26:51 -0700 | [diff] [blame] | 300 | synchronized (mCachedProviders) { |
| 301 | mCachedProviders.put(pkgUser, provider); |
| 302 | } |
Jason Monk | bf3eedc | 2018-04-05 20:56:42 -0400 | [diff] [blame] | 303 | } |
| 304 | return provider; |
| 305 | } |
| 306 | |
| 307 | private ParserHolder getParser(String fileName) |
| 308 | throws FileNotFoundException, XmlPullParserException { |
| 309 | AtomicFile file = getFile(fileName); |
| 310 | ParserHolder holder = new ParserHolder(); |
| 311 | holder.input = file.openRead(); |
| 312 | holder.parser = XmlPullParserFactory.newInstance().newPullParser(); |
| 313 | holder.parser.setInput(holder.input, Encoding.UTF_8.name()); |
| 314 | return holder; |
| 315 | } |
| 316 | |
| 317 | private AtomicFile getFile(String fileName) { |
| 318 | if (!mSliceDir.exists()) { |
| 319 | mSliceDir.mkdir(); |
| 320 | } |
| 321 | return new AtomicFile(new File(mSliceDir, fileName)); |
| 322 | } |
| 323 | |
Dan Sandler | 2d8e3d1 | 2018-12-13 15:32:13 -0500 | [diff] [blame] | 324 | @VisibleForTesting |
| 325 | void handlePersist() { |
Jason Monk | bf3eedc | 2018-04-05 20:56:42 -0400 | [diff] [blame] | 326 | synchronized (this) { |
| 327 | for (Persistable persistable : mDirty) { |
| 328 | AtomicFile file = getFile(persistable.getFileName()); |
| 329 | final FileOutputStream stream; |
| 330 | try { |
| 331 | stream = file.startWrite(); |
| 332 | } catch (IOException e) { |
| 333 | Slog.w(TAG, "Failed to save access file", e); |
| 334 | return; |
| 335 | } |
| 336 | |
| 337 | try { |
| 338 | XmlSerializer out = XmlPullParserFactory.newInstance().newSerializer(); |
| 339 | out.setOutput(stream, Encoding.UTF_8.name()); |
| 340 | |
| 341 | persistable.writeTo(out); |
| 342 | |
| 343 | out.flush(); |
| 344 | file.finishWrite(stream); |
Dan Sandler | 2d8e3d1 | 2018-12-13 15:32:13 -0500 | [diff] [blame] | 345 | } catch (IOException | XmlPullParserException | RuntimeException e) { |
Jason Monk | bf3eedc | 2018-04-05 20:56:42 -0400 | [diff] [blame] | 346 | Slog.w(TAG, "Failed to save access file, restoring backup", e); |
| 347 | file.failWrite(stream); |
| 348 | } |
| 349 | } |
| 350 | mDirty.clear(); |
| 351 | } |
| 352 | } |
| 353 | |
Dan Sandler | 2d8e3d1 | 2018-12-13 15:32:13 -0500 | [diff] [blame] | 354 | // use addPersistableDirty(); this is just for tests |
| 355 | @VisibleForTesting |
| 356 | void addDirtyImmediate(Persistable obj) { |
| 357 | mDirty.add(obj); |
| 358 | } |
| 359 | |
Jason Monk | bf3eedc | 2018-04-05 20:56:42 -0400 | [diff] [blame] | 360 | private void handleRemove(PkgUser pkgUser) { |
| 361 | getFile(SliceClientPermissions.getFileName(pkgUser)).delete(); |
| 362 | getFile(SliceProviderPermissions.getFileName(pkgUser)).delete(); |
| 363 | mDirty.remove(mCachedClients.remove(pkgUser)); |
| 364 | mDirty.remove(mCachedProviders.remove(pkgUser)); |
| 365 | } |
| 366 | |
| 367 | private final class H extends Handler { |
| 368 | private static final int MSG_ADD_DIRTY = 1; |
| 369 | private static final int MSG_PERSIST = 2; |
| 370 | private static final int MSG_REMOVE = 3; |
| 371 | private static final int MSG_CLEAR_CLIENT = 4; |
| 372 | private static final int MSG_CLEAR_PROVIDER = 5; |
| 373 | |
| 374 | public H(Looper looper) { |
| 375 | super(looper); |
| 376 | } |
| 377 | |
| 378 | @Override |
| 379 | public void handleMessage(Message msg) { |
| 380 | switch (msg.what) { |
| 381 | case MSG_ADD_DIRTY: |
| 382 | mDirty.add((Persistable) msg.obj); |
| 383 | break; |
| 384 | case MSG_PERSIST: |
| 385 | handlePersist(); |
| 386 | break; |
| 387 | case MSG_REMOVE: |
| 388 | handleRemove((PkgUser) msg.obj); |
| 389 | break; |
| 390 | case MSG_CLEAR_CLIENT: |
| 391 | synchronized (mCachedClients) { |
| 392 | mCachedClients.remove(msg.obj); |
| 393 | } |
| 394 | break; |
| 395 | case MSG_CLEAR_PROVIDER: |
| 396 | synchronized (mCachedProviders) { |
| 397 | mCachedProviders.remove(msg.obj); |
| 398 | } |
| 399 | break; |
| 400 | } |
| 401 | } |
| 402 | } |
| 403 | |
| 404 | public static class PkgUser { |
| 405 | private static final String SEPARATOR = "@"; |
| 406 | private static final String FORMAT = "%s" + SEPARATOR + "%d"; |
| 407 | private final String mPkg; |
| 408 | private final int mUserId; |
| 409 | |
| 410 | public PkgUser(String pkg, int userId) { |
| 411 | mPkg = pkg; |
| 412 | mUserId = userId; |
| 413 | } |
| 414 | |
| 415 | public PkgUser(String pkgUserStr) throws IllegalArgumentException { |
| 416 | try { |
| 417 | String[] vals = pkgUserStr.split(SEPARATOR, 2); |
| 418 | mPkg = vals[0]; |
| 419 | mUserId = Integer.parseInt(vals[1]); |
| 420 | } catch (Exception e) { |
| 421 | throw new IllegalArgumentException(e); |
| 422 | } |
| 423 | } |
| 424 | |
| 425 | public String getPkg() { |
| 426 | return mPkg; |
| 427 | } |
| 428 | |
| 429 | public int getUserId() { |
| 430 | return mUserId; |
| 431 | } |
| 432 | |
| 433 | @Override |
| 434 | public int hashCode() { |
| 435 | return mPkg.hashCode() + mUserId; |
| 436 | } |
| 437 | |
| 438 | @Override |
| 439 | public boolean equals(Object obj) { |
| 440 | if (!getClass().equals(obj != null ? obj.getClass() : null)) return false; |
| 441 | PkgUser other = (PkgUser) obj; |
| 442 | return Objects.equals(other.mPkg, mPkg) && other.mUserId == mUserId; |
| 443 | } |
| 444 | |
| 445 | @Override |
| 446 | public String toString() { |
| 447 | return String.format(FORMAT, mPkg, mUserId); |
| 448 | } |
| 449 | } |
| 450 | |
| 451 | private class ParserHolder implements AutoCloseable { |
| 452 | |
| 453 | private InputStream input; |
| 454 | private XmlPullParser parser; |
| 455 | |
| 456 | @Override |
| 457 | public void close() throws IOException { |
| 458 | input.close(); |
| 459 | } |
| 460 | } |
| 461 | } |