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.annotation.NonNull; 18 import android.content.ContentResolver; 19 import android.net.Uri; 20 import android.text.TextUtils; 21 import android.util.ArrayMap; 22 import android.util.ArraySet; 23 import android.util.Slog; 24 25 import com.android.server.slice.DirtyTracker.Persistable; 26 import com.android.server.slice.SlicePermissionManager.PkgUser; 27 28 import org.xmlpull.v1.XmlPullParser; 29 import org.xmlpull.v1.XmlPullParserException; 30 import org.xmlpull.v1.XmlSerializer; 31 32 import java.io.IOException; 33 import java.util.ArrayList; 34 import java.util.Collection; 35 import java.util.Comparator; 36 import java.util.List; 37 import java.util.Objects; 38 import java.util.stream.Collectors; 39 40 public class SliceClientPermissions implements DirtyTracker, Persistable { 41 42 private static final String TAG = "SliceClientPermissions"; 43 44 static final String TAG_CLIENT = "client"; 45 private static final String TAG_AUTHORITY = "authority"; 46 private static final String TAG_PATH = "path"; 47 private static final String NAMESPACE = null; 48 49 private static final String ATTR_PKG = "pkg"; 50 private static final String ATTR_AUTHORITY = "authority"; 51 private static final String ATTR_FULL_ACCESS = "fullAccess"; 52 53 private final PkgUser mPkg; 54 // Keyed off (authority, userId) rather than the standard (pkg, userId) 55 private final ArrayMap<PkgUser, SliceAuthority> mAuths = new ArrayMap<>(); 56 private final DirtyTracker mTracker; 57 private boolean mHasFullAccess; 58 59 public SliceClientPermissions(@NonNull PkgUser pkg, @NonNull DirtyTracker tracker) { 60 mPkg = pkg; 61 mTracker = tracker; 62 } 63 64 public PkgUser getPkg() { 65 return mPkg; 66 } 67 68 public synchronized Collection<SliceAuthority> getAuthorities() { 69 return new ArrayList<>(mAuths.values()); 70 } 71 72 public synchronized SliceAuthority getOrCreateAuthority(PkgUser authority, PkgUser provider) { 73 SliceAuthority ret = mAuths.get(authority); 74 if (ret == null) { 75 ret = new SliceAuthority(authority.getPkg(), provider, this); 76 mAuths.put(authority, ret); 77 onPersistableDirty(ret); 78 } 79 return ret; 80 } 81 82 public synchronized SliceAuthority getAuthority(PkgUser authority) { 83 return mAuths.get(authority); 84 } 85 86 public boolean hasFullAccess() { 87 return mHasFullAccess; 88 } 89 90 public void setHasFullAccess(boolean hasFullAccess) { 91 if (mHasFullAccess == hasFullAccess) return; 92 mHasFullAccess = hasFullAccess; 93 mTracker.onPersistableDirty(this); 94 } 95 96 public void removeAuthority(String authority, int userId) { 97 if (mAuths.remove(new PkgUser(authority, userId)) != null) { 98 mTracker.onPersistableDirty(this); 99 } 100 } 101 102 public synchronized boolean hasPermission(Uri uri, int userId) { 103 if (!Objects.equals(ContentResolver.SCHEME_CONTENT, uri.getScheme())) return false; 104 SliceAuthority authority = getAuthority(new PkgUser(uri.getAuthority(), userId)); 105 return authority != null && authority.hasPermission(uri.getPathSegments()); 106 } 107 108 public void grantUri(Uri uri, PkgUser providerPkg) { 109 SliceAuthority authority = getOrCreateAuthority( 110 new PkgUser(uri.getAuthority(), providerPkg.getUserId()), 111 providerPkg); 112 authority.addPath(uri.getPathSegments()); 113 } 114 115 public void revokeUri(Uri uri, PkgUser providerPkg) { 116 SliceAuthority authority = getOrCreateAuthority( 117 new PkgUser(uri.getAuthority(), providerPkg.getUserId()), 118 providerPkg); 119 authority.removePath(uri.getPathSegments()); 120 } 121 122 public void clear() { 123 if (!mHasFullAccess && mAuths.isEmpty()) return; 124 mHasFullAccess = false; 125 mAuths.clear(); 126 onPersistableDirty(this); 127 } 128 129 @Override 130 public void onPersistableDirty(Persistable obj) { 131 mTracker.onPersistableDirty(this); 132 } 133 134 @Override 135 public String getFileName() { 136 return getFileName(mPkg); 137 } 138 139 public synchronized void writeTo(XmlSerializer out) throws IOException { 140 out.startTag(NAMESPACE, TAG_CLIENT); 141 out.attribute(NAMESPACE, ATTR_PKG, mPkg.toString()); 142 out.attribute(NAMESPACE, ATTR_FULL_ACCESS, mHasFullAccess ? "1" : "0"); 143 144 final int N = mAuths.size(); 145 for (int i = 0; i < N; i++) { 146 out.startTag(NAMESPACE, TAG_AUTHORITY); 147 out.attribute(NAMESPACE, ATTR_AUTHORITY, mAuths.valueAt(i).mAuthority); 148 out.attribute(NAMESPACE, ATTR_PKG, mAuths.valueAt(i).mPkg.toString()); 149 150 mAuths.valueAt(i).writeTo(out); 151 152 out.endTag(NAMESPACE, TAG_AUTHORITY); 153 } 154 155 out.endTag(NAMESPACE, TAG_CLIENT); 156 } 157 158 public static SliceClientPermissions createFrom(XmlPullParser parser, DirtyTracker tracker) 159 throws XmlPullParserException, IOException { 160 // Get to the beginning of the provider. 161 while (parser.getEventType() != XmlPullParser.START_TAG 162 || !TAG_CLIENT.equals(parser.getName())) { 163 parser.next(); 164 } 165 int depth = parser.getDepth(); 166 PkgUser pkgUser = new PkgUser(parser.getAttributeValue(NAMESPACE, ATTR_PKG)); 167 SliceClientPermissions provider = new SliceClientPermissions(pkgUser, tracker); 168 String fullAccess = parser.getAttributeValue(NAMESPACE, ATTR_FULL_ACCESS); 169 if (fullAccess == null) { 170 fullAccess = "0"; 171 } 172 provider.mHasFullAccess = Integer.parseInt(fullAccess) != 0; 173 parser.next(); 174 175 while (parser.getDepth() > depth) { 176 if (parser.getEventType() == XmlPullParser.START_TAG 177 && TAG_AUTHORITY.equals(parser.getName())) { 178 try { 179 PkgUser pkg = new PkgUser(parser.getAttributeValue(NAMESPACE, ATTR_PKG)); 180 SliceAuthority authority = new SliceAuthority( 181 parser.getAttributeValue(NAMESPACE, ATTR_AUTHORITY), pkg, provider); 182 authority.readFrom(parser); 183 provider.mAuths.put(new PkgUser(authority.getAuthority(), pkg.getUserId()), 184 authority); 185 } catch (IllegalArgumentException e) { 186 Slog.e(TAG, "Couldn't read PkgUser", e); 187 } 188 } 189 190 parser.next(); 191 } 192 return provider; 193 } 194 195 public static String getFileName(PkgUser pkg) { 196 return String.format("client_%s", pkg.toString()); 197 } 198 199 public static class SliceAuthority implements Persistable { 200 public static final String DELIMITER = "/"; 201 private final String mAuthority; 202 private final DirtyTracker mTracker; 203 private final PkgUser mPkg; 204 private final ArraySet<String[]> mPaths = new ArraySet<>(); 205 206 public SliceAuthority(String authority, PkgUser pkg, DirtyTracker tracker) { 207 mAuthority = authority; 208 mPkg = pkg; 209 mTracker = tracker; 210 } 211 212 public String getAuthority() { 213 return mAuthority; 214 } 215 216 public PkgUser getPkg() { 217 return mPkg; 218 } 219 220 void addPath(List<String> path) { 221 String[] pathSegs = path.toArray(new String[path.size()]); 222 for (int i = mPaths.size() - 1; i >= 0; i--) { 223 String[] existing = mPaths.valueAt(i); 224 if (isPathPrefixMatch(existing, pathSegs)) { 225 // Nothing to add here. 226 return; 227 } 228 if (isPathPrefixMatch(pathSegs, existing)) { 229 mPaths.removeAt(i); 230 } 231 } 232 mPaths.add(pathSegs); 233 mTracker.onPersistableDirty(this); 234 } 235 236 void removePath(List<String> path) { 237 boolean changed = false; 238 String[] pathSegs = path.toArray(new String[path.size()]); 239 for (int i = mPaths.size() - 1; i >= 0; i--) { 240 String[] existing = mPaths.valueAt(i); 241 if (isPathPrefixMatch(pathSegs, existing)) { 242 changed = true; 243 mPaths.removeAt(i); 244 } 245 } 246 if (changed) { 247 mTracker.onPersistableDirty(this); 248 } 249 } 250 251 public synchronized Collection<String[]> getPaths() { 252 return new ArraySet<>(mPaths); 253 } 254 255 public boolean hasPermission(List<String> path) { 256 for (String[] p : mPaths) { 257 if (isPathPrefixMatch(p, path.toArray(new String[path.size()]))) { 258 return true; 259 } 260 } 261 return false; 262 } 263 264 private boolean isPathPrefixMatch(String[] prefix, String[] path) { 265 final int prefixSize = prefix.length; 266 if (path.length < prefixSize) return false; 267 268 for (int i = 0; i < prefixSize; i++) { 269 if (!Objects.equals(path[i], prefix[i])) { 270 return false; 271 } 272 } 273 274 return true; 275 } 276 277 @Override 278 public String getFileName() { 279 return null; 280 } 281 282 public synchronized void writeTo(XmlSerializer out) throws IOException { 283 final int N = mPaths.size(); 284 for (int i = 0; i < N; i++) { 285 out.startTag(NAMESPACE, TAG_PATH); 286 out.text(encodeSegments(mPaths.valueAt(i))); 287 out.endTag(NAMESPACE, TAG_PATH); 288 } 289 } 290 291 public synchronized void readFrom(XmlPullParser parser) 292 throws IOException, XmlPullParserException { 293 parser.next(); 294 int depth = parser.getDepth(); 295 while (parser.getDepth() >= depth) { 296 if (parser.getEventType() == XmlPullParser.START_TAG 297 && TAG_PATH.equals(parser.getName())) { 298 mPaths.add(decodeSegments(parser.nextText())); 299 } 300 parser.next(); 301 } 302 } 303 304 private String encodeSegments(String[] s) { 305 String[] out = new String[s.length]; 306 for (int i = 0; i < s.length; i++) { 307 out[i] = Uri.encode(s[i]); 308 } 309 return TextUtils.join(DELIMITER, out); 310 } 311 312 private String[] decodeSegments(String s) { 313 String[] sets = s.split(DELIMITER, -1); 314 for (int i = 0; i < sets.length; i++) { 315 sets[i] = Uri.decode(sets[i]); 316 } 317 return sets; 318 } 319 320 /** 321 * Only for testing, no deep equality of these are done normally. 322 */ 323 @Override 324 public boolean equals(Object obj) { 325 if (!getClass().equals(obj != null ? obj.getClass() : null)) return false; 326 SliceAuthority other = (SliceAuthority) obj; 327 if (mPaths.size() != other.mPaths.size()) return false; 328 ArrayList<String[]> p1 = new ArrayList<>(mPaths); 329 ArrayList<String[]> p2 = new ArrayList<>(other.mPaths); 330 p1.sort(Comparator.comparing(o -> TextUtils.join(",", o))); 331 p2.sort(Comparator.comparing(o -> TextUtils.join(",", o))); 332 for (int i = 0; i < p1.size(); i++) { 333 String[] a1 = p1.get(i); 334 String[] a2 = p2.get(i); 335 if (a1.length != a2.length) return false; 336 for (int j = 0; j < a1.length; j++) { 337 if (!Objects.equals(a1[j], a2[j])) return false; 338 } 339 } 340 return Objects.equals(mAuthority, other.mAuthority) 341 && Objects.equals(mPkg, other.mPkg); 342 } 343 344 @Override 345 public String toString() { 346 return String.format("(%s, %s: %s)", mAuthority, mPkg.toString(), pathToString(mPaths)); 347 } 348 349 private String pathToString(ArraySet<String[]> paths) { 350 return TextUtils.join(", ", paths.stream().map(s -> TextUtils.join("/", s)) 351 .collect(Collectors.toList())); 352 } 353 } 354 } 355