blob: ab94a59c4d9cd3ff23094705e9d3091b3c4049de [file] [log] [blame]
Jason Monkbf3eedc2018-04-05 20:56:42 -04001/*
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
15package com.android.server.slice;
16
17import android.annotation.NonNull;
18import android.content.ContentResolver;
19import android.net.Uri;
20import android.text.TextUtils;
21import android.util.ArrayMap;
22import android.util.ArraySet;
23import android.util.Slog;
24
25import com.android.server.slice.DirtyTracker.Persistable;
26import com.android.server.slice.SlicePermissionManager.PkgUser;
27
28import org.xmlpull.v1.XmlPullParser;
29import org.xmlpull.v1.XmlPullParserException;
30import org.xmlpull.v1.XmlSerializer;
31
32import java.io.IOException;
33import java.util.ArrayList;
34import java.util.Collection;
35import java.util.Comparator;
36import java.util.List;
37import java.util.Objects;
38import java.util.stream.Collectors;
39
40public 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++) {
Dan Sandler2d8e3d12018-12-13 15:32:13 -0500285 final String[] segments = mPaths.valueAt(i);
286 if (segments != null) {
287 out.startTag(NAMESPACE, TAG_PATH);
288 out.text(encodeSegments(segments));
289 out.endTag(NAMESPACE, TAG_PATH);
290 }
Jason Monkbf3eedc2018-04-05 20:56:42 -0400291 }
292 }
293
294 public synchronized void readFrom(XmlPullParser parser)
295 throws IOException, XmlPullParserException {
296 parser.next();
297 int depth = parser.getDepth();
298 while (parser.getDepth() >= depth) {
299 if (parser.getEventType() == XmlPullParser.START_TAG
300 && TAG_PATH.equals(parser.getName())) {
301 mPaths.add(decodeSegments(parser.nextText()));
302 }
303 parser.next();
304 }
305 }
306
307 private String encodeSegments(String[] s) {
308 String[] out = new String[s.length];
309 for (int i = 0; i < s.length; i++) {
310 out[i] = Uri.encode(s[i]);
311 }
312 return TextUtils.join(DELIMITER, out);
313 }
314
315 private String[] decodeSegments(String s) {
316 String[] sets = s.split(DELIMITER, -1);
317 for (int i = 0; i < sets.length; i++) {
318 sets[i] = Uri.decode(sets[i]);
319 }
320 return sets;
321 }
322
323 /**
324 * Only for testing, no deep equality of these are done normally.
325 */
326 @Override
327 public boolean equals(Object obj) {
328 if (!getClass().equals(obj != null ? obj.getClass() : null)) return false;
329 SliceAuthority other = (SliceAuthority) obj;
330 if (mPaths.size() != other.mPaths.size()) return false;
331 ArrayList<String[]> p1 = new ArrayList<>(mPaths);
332 ArrayList<String[]> p2 = new ArrayList<>(other.mPaths);
333 p1.sort(Comparator.comparing(o -> TextUtils.join(",", o)));
334 p2.sort(Comparator.comparing(o -> TextUtils.join(",", o)));
335 for (int i = 0; i < p1.size(); i++) {
336 String[] a1 = p1.get(i);
337 String[] a2 = p2.get(i);
338 if (a1.length != a2.length) return false;
339 for (int j = 0; j < a1.length; j++) {
340 if (!Objects.equals(a1[j], a2[j])) return false;
341 }
342 }
343 return Objects.equals(mAuthority, other.mAuthority)
344 && Objects.equals(mPkg, other.mPkg);
345 }
346
347 @Override
348 public String toString() {
349 return String.format("(%s, %s: %s)", mAuthority, mPkg.toString(), pathToString(mPaths));
350 }
351
352 private String pathToString(ArraySet<String[]> paths) {
353 return TextUtils.join(", ", paths.stream().map(s -> TextUtils.join("/", s))
354 .collect(Collectors.toList()));
355 }
356 }
357}