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 
SliceClientPermissions(@onNull PkgUser pkg, @NonNull DirtyTracker tracker)59     public SliceClientPermissions(@NonNull PkgUser pkg, @NonNull DirtyTracker tracker) {
60         mPkg = pkg;
61         mTracker = tracker;
62     }
63 
getPkg()64     public PkgUser getPkg() {
65         return mPkg;
66     }
67 
getAuthorities()68     public synchronized Collection<SliceAuthority> getAuthorities() {
69         return new ArrayList<>(mAuths.values());
70     }
71 
getOrCreateAuthority(PkgUser authority, PkgUser provider)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 
getAuthority(PkgUser authority)82     public synchronized SliceAuthority getAuthority(PkgUser authority) {
83         return mAuths.get(authority);
84     }
85 
hasFullAccess()86     public boolean hasFullAccess() {
87         return mHasFullAccess;
88     }
89 
setHasFullAccess(boolean hasFullAccess)90     public void setHasFullAccess(boolean hasFullAccess) {
91         if (mHasFullAccess == hasFullAccess) return;
92         mHasFullAccess = hasFullAccess;
93         mTracker.onPersistableDirty(this);
94     }
95 
removeAuthority(String authority, int userId)96     public void removeAuthority(String authority, int userId) {
97         if (mAuths.remove(new PkgUser(authority, userId)) != null) {
98             mTracker.onPersistableDirty(this);
99         }
100     }
101 
hasPermission(Uri uri, int userId)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 
grantUri(Uri uri, PkgUser providerPkg)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 
revokeUri(Uri uri, PkgUser providerPkg)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 
clear()122     public void clear() {
123         if (!mHasFullAccess && mAuths.isEmpty()) return;
124         mHasFullAccess = false;
125         mAuths.clear();
126         onPersistableDirty(this);
127     }
128 
129     @Override
onPersistableDirty(Persistable obj)130     public void onPersistableDirty(Persistable obj) {
131         mTracker.onPersistableDirty(this);
132     }
133 
134     @Override
getFileName()135     public String getFileName() {
136         return getFileName(mPkg);
137     }
138 
writeTo(XmlSerializer out)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 
createFrom(XmlPullParser parser, DirtyTracker tracker)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             if (parser.getEventType() == XmlPullParser.END_DOCUMENT) {
164                 throw new XmlPullParserException("Can't find client tag in xml");
165             }
166             parser.next();
167         }
168         int depth = parser.getDepth();
169         PkgUser pkgUser = new PkgUser(parser.getAttributeValue(NAMESPACE, ATTR_PKG));
170         SliceClientPermissions provider = new SliceClientPermissions(pkgUser, tracker);
171         String fullAccess = parser.getAttributeValue(NAMESPACE, ATTR_FULL_ACCESS);
172         if (fullAccess == null) {
173             fullAccess = "0";
174         }
175         provider.mHasFullAccess = Integer.parseInt(fullAccess) != 0;
176         parser.next();
177 
178         while (parser.getDepth() > depth) {
179             if (parser.getEventType() == XmlPullParser.END_DOCUMENT) {
180                 return provider;
181             }
182             if (parser.getEventType() == XmlPullParser.START_TAG
183                     && TAG_AUTHORITY.equals(parser.getName())) {
184                 try {
185                     PkgUser pkg = new PkgUser(parser.getAttributeValue(NAMESPACE, ATTR_PKG));
186                     SliceAuthority authority = new SliceAuthority(
187                             parser.getAttributeValue(NAMESPACE, ATTR_AUTHORITY), pkg, provider);
188                     authority.readFrom(parser);
189                     provider.mAuths.put(new PkgUser(authority.getAuthority(), pkg.getUserId()),
190                             authority);
191                 } catch (IllegalArgumentException e) {
192                     Slog.e(TAG, "Couldn't read PkgUser", e);
193                 }
194             }
195 
196             parser.next();
197         }
198         return provider;
199     }
200 
getFileName(PkgUser pkg)201     public static String getFileName(PkgUser pkg) {
202         return String.format("client_%s", pkg.toString());
203     }
204 
205     public static class SliceAuthority implements Persistable {
206         public static final String DELIMITER = "/";
207         private final String mAuthority;
208         private final DirtyTracker mTracker;
209         private final PkgUser mPkg;
210         private final ArraySet<String[]> mPaths = new ArraySet<>();
211 
SliceAuthority(String authority, PkgUser pkg, DirtyTracker tracker)212         public SliceAuthority(String authority, PkgUser pkg, DirtyTracker tracker) {
213             mAuthority = authority;
214             mPkg = pkg;
215             mTracker = tracker;
216         }
217 
getAuthority()218         public String getAuthority() {
219             return mAuthority;
220         }
221 
getPkg()222         public PkgUser getPkg() {
223             return mPkg;
224         }
225 
addPath(List<String> path)226         void addPath(List<String> path) {
227             String[] pathSegs = path.toArray(new String[path.size()]);
228             for (int i = mPaths.size() - 1; i >= 0; i--) {
229                 String[] existing = mPaths.valueAt(i);
230                 if (isPathPrefixMatch(existing, pathSegs)) {
231                     // Nothing to add here.
232                     return;
233                 }
234                 if (isPathPrefixMatch(pathSegs, existing)) {
235                     mPaths.removeAt(i);
236                 }
237             }
238             mPaths.add(pathSegs);
239             mTracker.onPersistableDirty(this);
240         }
241 
removePath(List<String> path)242         void removePath(List<String> path) {
243             boolean changed = false;
244             String[] pathSegs = path.toArray(new String[path.size()]);
245             for (int i = mPaths.size() - 1; i >= 0; i--) {
246                 String[] existing = mPaths.valueAt(i);
247                 if (isPathPrefixMatch(pathSegs, existing)) {
248                     changed = true;
249                     mPaths.removeAt(i);
250                 }
251             }
252             if (changed) {
253                 mTracker.onPersistableDirty(this);
254             }
255         }
256 
getPaths()257         public synchronized Collection<String[]> getPaths() {
258             return new ArraySet<>(mPaths);
259         }
260 
hasPermission(List<String> path)261         public boolean hasPermission(List<String> path) {
262             for (String[] p : mPaths) {
263                 if (isPathPrefixMatch(p, path.toArray(new String[path.size()]))) {
264                     return true;
265                 }
266             }
267             return false;
268         }
269 
isPathPrefixMatch(String[] prefix, String[] path)270         private boolean isPathPrefixMatch(String[] prefix, String[] path) {
271             final int prefixSize = prefix.length;
272             if (path.length < prefixSize) return false;
273 
274             for (int i = 0; i < prefixSize; i++) {
275                 if (!Objects.equals(path[i], prefix[i])) {
276                     return false;
277                 }
278             }
279 
280             return true;
281         }
282 
283         @Override
getFileName()284         public String getFileName() {
285             return null;
286         }
287 
writeTo(XmlSerializer out)288         public synchronized void writeTo(XmlSerializer out) throws IOException {
289             final int N = mPaths.size();
290             for (int i = 0; i < N; i++) {
291                 final String[] segments = mPaths.valueAt(i);
292                 if (segments != null) {
293                     out.startTag(NAMESPACE, TAG_PATH);
294                     out.text(encodeSegments(segments));
295                     out.endTag(NAMESPACE, TAG_PATH);
296                 }
297             }
298         }
299 
readFrom(XmlPullParser parser)300         public synchronized void readFrom(XmlPullParser parser)
301                 throws IOException, XmlPullParserException {
302             parser.next();
303             int depth = parser.getDepth();
304             while (parser.getDepth() >= depth) {
305                 if (parser.getEventType() == XmlPullParser.START_TAG
306                         && TAG_PATH.equals(parser.getName())) {
307                     mPaths.add(decodeSegments(parser.nextText()));
308                 }
309                 parser.next();
310             }
311         }
312 
encodeSegments(String[] s)313         private String encodeSegments(String[] s) {
314             String[] out = new String[s.length];
315             for (int i = 0; i < s.length; i++) {
316                 out[i] = Uri.encode(s[i]);
317             }
318             return TextUtils.join(DELIMITER, out);
319         }
320 
decodeSegments(String s)321         private String[] decodeSegments(String s) {
322             String[] sets = s.split(DELIMITER, -1);
323             for (int i = 0; i < sets.length; i++) {
324                 sets[i] = Uri.decode(sets[i]);
325             }
326             return sets;
327         }
328 
329         /**
330          * Only for testing, no deep equality of these are done normally.
331          */
332         @Override
equals(Object obj)333         public boolean equals(Object obj) {
334             if (!getClass().equals(obj != null ? obj.getClass() : null)) return false;
335             SliceAuthority other = (SliceAuthority) obj;
336             if (mPaths.size() != other.mPaths.size()) return false;
337             ArrayList<String[]> p1 = new ArrayList<>(mPaths);
338             ArrayList<String[]> p2 = new ArrayList<>(other.mPaths);
339             p1.sort(Comparator.comparing(o -> TextUtils.join(",", o)));
340             p2.sort(Comparator.comparing(o -> TextUtils.join(",", o)));
341             for (int i = 0; i < p1.size(); i++) {
342                 String[] a1 = p1.get(i);
343                 String[] a2 = p2.get(i);
344                 if (a1.length != a2.length) return false;
345                 for (int j = 0; j < a1.length; j++) {
346                     if (!Objects.equals(a1[j], a2[j])) return false;
347                 }
348             }
349             return Objects.equals(mAuthority, other.mAuthority)
350                     && Objects.equals(mPkg, other.mPkg);
351         }
352 
353         @Override
toString()354         public String toString() {
355             return String.format("(%s, %s: %s)", mAuthority, mPkg.toString(), pathToString(mPaths));
356         }
357 
pathToString(ArraySet<String[]> paths)358         private String pathToString(ArraySet<String[]> paths) {
359             return TextUtils.join(", ", paths.stream().map(s -> TextUtils.join("/", s))
360                     .collect(Collectors.toList()));
361         }
362     }
363 }
364