• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.permissioncontroller.role.model;
18 
19 import android.app.AppOpsManager;
20 import android.content.Context;
21 import android.content.Intent;
22 import android.content.pm.PackageManager;
23 import android.content.pm.PermissionInfo;
24 import android.content.res.XmlResourceParser;
25 import android.os.Build;
26 import android.util.ArrayMap;
27 import android.util.Log;
28 import android.util.Pair;
29 
30 import androidx.annotation.NonNull;
31 import androidx.annotation.Nullable;
32 import androidx.annotation.VisibleForTesting;
33 
34 import com.android.permissioncontroller.R;
35 
36 import org.xmlpull.v1.XmlPullParserException;
37 
38 import java.io.IOException;
39 import java.util.ArrayList;
40 import java.util.Collection;
41 import java.util.Collections;
42 import java.util.List;
43 import java.util.Objects;
44 
45 /**
46  * Parser for {@link Role} definitions.
47  */
48 @VisibleForTesting
49 public class RoleParser {
50 
51     private static final String LOG_TAG = RoleParser.class.getSimpleName();
52 
53     private static final String TAG_ROLES = "roles";
54     private static final String TAG_PERMISSION_SET = "permission-set";
55     private static final String TAG_PERMISSION = "permission";
56     private static final String TAG_ROLE = "role";
57     private static final String TAG_REQUIRED_COMPONENTS = "required-components";
58     private static final String TAG_ACTIVITY = "activity";
59     private static final String TAG_PROVIDER = "provider";
60     private static final String TAG_RECEIVER = "receiver";
61     private static final String TAG_SERVICE = "service";
62     private static final String TAG_INTENT_FILTER = "intent-filter";
63     private static final String TAG_ACTION = "action";
64     private static final String TAG_CATEGORY = "category";
65     private static final String TAG_DATA = "data";
66     private static final String TAG_PERMISSIONS = "permissions";
67     private static final String TAG_APP_OP_PERMISSIONS = "app-op-permissions";
68     private static final String TAG_APP_OP_PERMISSION = "app-op-permission";
69     private static final String TAG_APP_OPS = "app-ops";
70     private static final String TAG_APP_OP = "app-op";
71     private static final String TAG_PREFERRED_ACTIVITIES = "preferred-activities";
72     private static final String TAG_PREFERRED_ACTIVITY = "preferred-activity";
73     private static final String ATTRIBUTE_NAME = "name";
74     private static final String ATTRIBUTE_ALLOW_BYPASSING_QUALIFICATION =
75             "allowBypassingQualification";
76     private static final String ATTRIBUTE_BEHAVIOR = "behavior";
77     private static final String ATTRIBUTE_DEFAULT_HOLDERS = "defaultHolders";
78     private static final String ATTRIBUTE_DESCRIPTION = "description";
79     private static final String ATTRIBUTE_EXCLUSIVE = "exclusive";
80     private static final String ATTRIBUTE_FALL_BACK_TO_DEFAULT_HOLDER = "fallBackToDefaultHolder";
81     private static final String ATTRIBUTE_LABEL = "label";
82     private static final String ATTRIBUTE_MIN_SDK_VERSION = "minSdkVersion";
83     private static final String ATTRIBUTE_OVERRIDE_USER_WHEN_GRANTING = "overrideUserWhenGranting";
84     private static final String ATTRIBUTE_QUERY_FLAGS = "queryFlags";
85     private static final String ATTRIBUTE_REQUEST_TITLE = "requestTitle";
86     private static final String ATTRIBUTE_REQUEST_DESCRIPTION = "requestDescription";
87     private static final String ATTRIBUTE_REQUESTABLE = "requestable";
88     private static final String ATTRIBUTE_SEARCH_KEYWORDS = "searchKeywords";
89     private static final String ATTRIBUTE_SHORT_LABEL = "shortLabel";
90     private static final String ATTRIBUTE_SHOW_NONE = "showNone";
91     private static final String ATTRIBUTE_STATIC = "static";
92     private static final String ATTRIBUTE_SYSTEM_ONLY = "systemOnly";
93     private static final String ATTRIBUTE_VISIBLE = "visible";
94     private static final String ATTRIBUTE_PERMISSION = "permission";
95     private static final String ATTRIBUTE_SCHEME = "scheme";
96     private static final String ATTRIBUTE_MIME_TYPE = "mimeType";
97     private static final String ATTRIBUTE_MAX_TARGET_SDK_VERSION = "maxTargetSdkVersion";
98     private static final String ATTRIBUTE_MODE = "mode";
99 
100     private static final String MODE_NAME_ALLOWED = "allowed";
101     private static final String MODE_NAME_IGNORED = "ignored";
102     private static final String MODE_NAME_ERRORED = "errored";
103     private static final String MODE_NAME_DEFAULT = "default";
104     private static final String MODE_NAME_FOREGROUND = "foreground";
105     private static final ArrayMap<String, Integer> sModeNameToMode = new ArrayMap<>();
106     static {
sModeNameToMode.put(MODE_NAME_ALLOWED, AppOpsManager.MODE_ALLOWED)107         sModeNameToMode.put(MODE_NAME_ALLOWED, AppOpsManager.MODE_ALLOWED);
sModeNameToMode.put(MODE_NAME_IGNORED, AppOpsManager.MODE_IGNORED)108         sModeNameToMode.put(MODE_NAME_IGNORED, AppOpsManager.MODE_IGNORED);
sModeNameToMode.put(MODE_NAME_ERRORED, AppOpsManager.MODE_ERRORED)109         sModeNameToMode.put(MODE_NAME_ERRORED, AppOpsManager.MODE_ERRORED);
sModeNameToMode.put(MODE_NAME_DEFAULT, AppOpsManager.MODE_DEFAULT)110         sModeNameToMode.put(MODE_NAME_DEFAULT, AppOpsManager.MODE_DEFAULT);
sModeNameToMode.put(MODE_NAME_FOREGROUND, AppOpsManager.MODE_FOREGROUND)111         sModeNameToMode.put(MODE_NAME_FOREGROUND, AppOpsManager.MODE_FOREGROUND);
112     }
113 
114     @NonNull
115     private final Context mContext;
116 
117     private final boolean mValidationEnabled;
118 
RoleParser(@onNull Context context)119     public RoleParser(@NonNull Context context) {
120         this(context, false);
121     }
122 
RoleParser(@onNull Context context, boolean validationEnabled)123     RoleParser(@NonNull Context context, boolean validationEnabled) {
124         mContext = context;
125         mValidationEnabled = validationEnabled;
126     }
127 
128     /**
129      * Parse the roles defined in {@code roles.xml}.
130      *
131      * @return a map from role name to {@link Role} instances
132      */
133     @NonNull
parse()134     public ArrayMap<String, Role> parse() {
135         try (XmlResourceParser parser = mContext.getResources().getXml(R.xml.roles)) {
136             Pair<ArrayMap<String, PermissionSet>, ArrayMap<String, Role>> xml = parseXml(parser);
137             if (xml == null) {
138                 return new ArrayMap<>();
139             }
140             ArrayMap<String, PermissionSet> permissionSets = xml.first;
141             ArrayMap<String, Role> roles = xml.second;
142             validateResult(permissionSets, roles);
143             return roles;
144         } catch (XmlPullParserException | IOException e) {
145             throwOrLogMessage("Unable to parse roles.xml", e);
146             return new ArrayMap<>();
147         }
148     }
149 
150     @Nullable
parseXml( @onNull XmlResourceParser parser)151     private Pair<ArrayMap<String, PermissionSet>, ArrayMap<String, Role>> parseXml(
152             @NonNull XmlResourceParser parser) throws IOException, XmlPullParserException {
153         Pair<ArrayMap<String, PermissionSet>, ArrayMap<String, Role>> xml = null;
154 
155         int type;
156         int depth;
157         int innerDepth = parser.getDepth() + 1;
158         while ((type = parser.next()) != XmlResourceParser.END_DOCUMENT
159                 && ((depth = parser.getDepth()) >= innerDepth
160                 || type != XmlResourceParser.END_TAG)) {
161             if (depth > innerDepth || type != XmlResourceParser.START_TAG) {
162                 continue;
163             }
164 
165             if (parser.getName().equals(TAG_ROLES)) {
166                 if (xml != null) {
167                     throwOrLogMessage("Duplicate <roles>");
168                     skipCurrentTag(parser);
169                     continue;
170                 }
171                 xml = parseRoles(parser);
172             } else {
173                 throwOrLogForUnknownTag(parser);
174                 skipCurrentTag(parser);
175             }
176         }
177 
178         if (xml == null) {
179             throwOrLogMessage("Missing <roles>");
180         }
181         return xml;
182     }
183 
184     @NonNull
parseRoles( @onNull XmlResourceParser parser)185     private Pair<ArrayMap<String, PermissionSet>, ArrayMap<String, Role>> parseRoles(
186             @NonNull XmlResourceParser parser) throws IOException, XmlPullParserException {
187         ArrayMap<String, PermissionSet> permissionSets = new ArrayMap<>();
188         ArrayMap<String, Role> roles = new ArrayMap<>();
189 
190         int type;
191         int depth;
192         int innerDepth = parser.getDepth() + 1;
193         while ((type = parser.next()) != XmlResourceParser.END_DOCUMENT
194                 && ((depth = parser.getDepth()) >= innerDepth
195                 || type != XmlResourceParser.END_TAG)) {
196             if (depth > innerDepth || type != XmlResourceParser.START_TAG) {
197                 continue;
198             }
199 
200             switch (parser.getName()) {
201                 case TAG_PERMISSION_SET: {
202                     PermissionSet permissionSet = parsePermissionSet(parser);
203                     if (permissionSet == null) {
204                         continue;
205                     }
206                     validateNoDuplicateElement(permissionSet.getName(), permissionSets.keySet(),
207                             "permission set");
208                     permissionSets.put(permissionSet.getName(), permissionSet);
209                     break;
210                 }
211                 case TAG_ROLE: {
212                     Role role = parseRole(parser, permissionSets);
213                     if (role == null) {
214                         continue;
215                     }
216                     validateNoDuplicateElement(role.getName(), roles.keySet(), "role");
217                     roles.put(role.getName(), role);
218                     break;
219                 }
220                 default:
221                     throwOrLogForUnknownTag(parser);
222                     skipCurrentTag(parser);
223             }
224         }
225 
226         return new Pair<>(permissionSets, roles);
227     }
228 
229     @Nullable
parsePermissionSet(@onNull XmlResourceParser parser)230     private PermissionSet parsePermissionSet(@NonNull XmlResourceParser parser)
231             throws IOException, XmlPullParserException {
232         String name = requireAttributeValue(parser, ATTRIBUTE_NAME, TAG_PERMISSION_SET);
233         if (name == null) {
234             skipCurrentTag(parser);
235             return null;
236         }
237 
238         List<Permission> permissions = new ArrayList<>();
239 
240         int type;
241         int depth;
242         int innerDepth = parser.getDepth() + 1;
243         while ((type = parser.next()) != XmlResourceParser.END_DOCUMENT
244                 && ((depth = parser.getDepth()) >= innerDepth
245                 || type != XmlResourceParser.END_TAG)) {
246             if (depth > innerDepth || type != XmlResourceParser.START_TAG) {
247                 continue;
248             }
249 
250             if (parser.getName().equals(TAG_PERMISSION)) {
251                 Permission permission = parsePermission(parser);
252                 if (permission == null) {
253                     continue;
254                 }
255                 validateNoDuplicateElement(permission, permissions, "permission");
256                 permissions.add(permission);
257             } else {
258                 throwOrLogForUnknownTag(parser);
259                 skipCurrentTag(parser);
260             }
261         }
262 
263         return new PermissionSet(name, permissions);
264     }
265 
266     @Nullable
parsePermission(@onNull XmlResourceParser parser)267     private Permission parsePermission(@NonNull XmlResourceParser parser) throws IOException,
268             XmlPullParserException {
269         String name = requireAttributeValue(parser, ATTRIBUTE_NAME, TAG_PERMISSION);
270         if (name == null) {
271             skipCurrentTag(parser);
272             return null;
273         }
274         int minSdkVersion = getAttributeIntValue(parser, ATTRIBUTE_MIN_SDK_VERSION,
275                 Build.VERSION_CODES.BASE);
276         return new Permission(name, minSdkVersion);
277     }
278 
279     @Nullable
parseRole(@onNull XmlResourceParser parser, @NonNull ArrayMap<String, PermissionSet> permissionSets)280     private Role parseRole(@NonNull XmlResourceParser parser,
281             @NonNull ArrayMap<String, PermissionSet> permissionSets) throws IOException,
282             XmlPullParserException {
283         String name = requireAttributeValue(parser, ATTRIBUTE_NAME, TAG_ROLE);
284         if (name == null) {
285             skipCurrentTag(parser);
286             return null;
287         }
288 
289         boolean allowBypassingQualification = getAttributeBooleanValue(parser,
290                 ATTRIBUTE_ALLOW_BYPASSING_QUALIFICATION, false);
291 
292         String behaviorClassSimpleName = getAttributeValue(parser, ATTRIBUTE_BEHAVIOR);
293         RoleBehavior behavior;
294         if (behaviorClassSimpleName != null) {
295             String behaviorClassName = RoleParser.class.getPackage().getName() + '.'
296                     + behaviorClassSimpleName;
297             try {
298                 behavior = (RoleBehavior) Class.forName(behaviorClassName).newInstance();
299             } catch (ClassNotFoundException | IllegalAccessException | InstantiationException e) {
300                 throwOrLogMessage("Unable to instantiate behavior: " + behaviorClassName, e);
301                 skipCurrentTag(parser);
302                 return null;
303             }
304         } else {
305             behavior = null;
306         }
307 
308         String defaultHoldersResourceName = getAttributeValue(parser, ATTRIBUTE_DEFAULT_HOLDERS);
309 
310         int descriptionResource = getAttributeResourceValue(parser, ATTRIBUTE_DESCRIPTION, 0);
311 
312         boolean visible = getAttributeBooleanValue(parser, ATTRIBUTE_VISIBLE, true);
313         Integer labelResource;
314         Integer shortLabelResource;
315         if (visible) {
316             if (descriptionResource == 0) {
317                 skipCurrentTag(parser);
318                 return null;
319             }
320 
321             labelResource = requireAttributeResourceValue(parser, ATTRIBUTE_LABEL, 0, TAG_ROLE);
322             if (labelResource == null) {
323                 skipCurrentTag(parser);
324                 return null;
325             }
326 
327             shortLabelResource = requireAttributeResourceValue(parser, ATTRIBUTE_SHORT_LABEL, 0,
328                     TAG_ROLE);
329             if (shortLabelResource == null) {
330                 skipCurrentTag(parser);
331                 return null;
332             }
333         } else {
334             labelResource = 0;
335             shortLabelResource = 0;
336         }
337 
338         Boolean exclusive = requireAttributeBooleanValue(parser, ATTRIBUTE_EXCLUSIVE, true,
339                 TAG_ROLE);
340         if (exclusive == null) {
341             skipCurrentTag(parser);
342             return null;
343         }
344 
345         boolean fallBackToDefaultHolder = getAttributeBooleanValue(parser,
346                 ATTRIBUTE_FALL_BACK_TO_DEFAULT_HOLDER, false);
347 
348         int minSdkVersion = getAttributeIntValue(parser, ATTRIBUTE_MIN_SDK_VERSION,
349                 Build.VERSION_CODES.BASE);
350 
351         boolean overrideUserWhenGranting = getAttributeBooleanValue(parser,
352                 ATTRIBUTE_OVERRIDE_USER_WHEN_GRANTING, true);
353 
354         boolean requestable = getAttributeBooleanValue(parser, ATTRIBUTE_REQUESTABLE, visible);
355         Integer requestDescriptionResource;
356         Integer requestTitleResource;
357         if (requestable) {
358             requestDescriptionResource = requireAttributeResourceValue(parser,
359                     ATTRIBUTE_REQUEST_DESCRIPTION, 0, TAG_ROLE);
360             if (requestDescriptionResource == null) {
361                 skipCurrentTag(parser);
362                 return null;
363             }
364 
365             requestTitleResource = requireAttributeResourceValue(parser, ATTRIBUTE_REQUEST_TITLE, 0,
366                     TAG_ROLE);
367             if (requestTitleResource == null) {
368                 skipCurrentTag(parser);
369                 return null;
370             }
371         } else {
372             requestDescriptionResource = 0;
373             requestTitleResource = 0;
374         }
375 
376         int searchKeywordsResource = getAttributeResourceValue(parser, ATTRIBUTE_SEARCH_KEYWORDS,
377                 0);
378 
379         boolean showNone = getAttributeBooleanValue(parser, ATTRIBUTE_SHOW_NONE, false);
380         if (showNone && !exclusive) {
381             throwOrLogMessage("showNone=\"true\" is invalid for a non-exclusive role: " + name);
382             skipCurrentTag(parser);
383             return null;
384         }
385 
386         boolean statik = getAttributeBooleanValue(parser, ATTRIBUTE_STATIC, false);
387         if (statik && (visible || requestable)) {
388             throwOrLogMessage("static=\"true\" is invalid for a visible or requestable role: "
389                     + name);
390             skipCurrentTag(parser);
391             return null;
392         }
393 
394         boolean systemOnly = getAttributeBooleanValue(parser, ATTRIBUTE_SYSTEM_ONLY, false);
395 
396         List<RequiredComponent> requiredComponents = null;
397         List<Permission> permissions = null;
398         List<String> appOpPermissions = null;
399         List<AppOp> appOps = null;
400         List<PreferredActivity> preferredActivities = null;
401 
402         int type;
403         int depth;
404         int innerDepth = parser.getDepth() + 1;
405         while ((type = parser.next()) != XmlResourceParser.END_DOCUMENT
406                 && ((depth = parser.getDepth()) >= innerDepth
407                 || type != XmlResourceParser.END_TAG)) {
408             if (depth > innerDepth || type != XmlResourceParser.START_TAG) {
409                 continue;
410             }
411 
412             switch (parser.getName()) {
413                 case TAG_REQUIRED_COMPONENTS:
414                     if (requiredComponents != null) {
415                         throwOrLogMessage("Duplicate <required-components> in role: " + name);
416                         skipCurrentTag(parser);
417                         continue;
418                     }
419                     requiredComponents = parseRequiredComponents(parser);
420                     break;
421                 case TAG_PERMISSIONS:
422                     if (permissions != null) {
423                         throwOrLogMessage("Duplicate <permissions> in role: " + name);
424                         skipCurrentTag(parser);
425                         continue;
426                     }
427                     permissions = parsePermissions(parser, permissionSets);
428                     break;
429                 case TAG_APP_OP_PERMISSIONS:
430                     if (appOpPermissions != null) {
431                         throwOrLogMessage("Duplicate <app-op-permissions> in role: " + name);
432                         skipCurrentTag(parser);
433                         continue;
434                     }
435                     appOpPermissions = parseAppOpPermissions(parser);
436                     break;
437                 case TAG_APP_OPS:
438                     if (appOps != null) {
439                         throwOrLogMessage("Duplicate <app-ops> in role: " + name);
440                         skipCurrentTag(parser);
441                         continue;
442                     }
443                     appOps = parseAppOps(parser);
444                     break;
445                 case TAG_PREFERRED_ACTIVITIES:
446                     if (preferredActivities != null) {
447                         throwOrLogMessage("Duplicate <preferred-activities> in role: " + name);
448                         skipCurrentTag(parser);
449                         continue;
450                     }
451                     preferredActivities = parsePreferredActivities(parser);
452                     break;
453                 default:
454                     throwOrLogForUnknownTag(parser);
455                     skipCurrentTag(parser);
456             }
457         }
458 
459         if (requiredComponents == null) {
460             requiredComponents = Collections.emptyList();
461         }
462         if (permissions == null) {
463             permissions = Collections.emptyList();
464         }
465         if (appOpPermissions == null) {
466             appOpPermissions = Collections.emptyList();
467         }
468         if (appOps == null) {
469             appOps = Collections.emptyList();
470         }
471         if (preferredActivities == null) {
472             preferredActivities = Collections.emptyList();
473         }
474         return new Role(name, allowBypassingQualification, behavior, defaultHoldersResourceName,
475                 descriptionResource, exclusive, fallBackToDefaultHolder, labelResource,
476                 minSdkVersion, overrideUserWhenGranting, requestDescriptionResource,
477                 requestTitleResource, requestable, searchKeywordsResource, shortLabelResource,
478                 showNone, statik, systemOnly, visible, requiredComponents, permissions,
479                 appOpPermissions, appOps, preferredActivities);
480     }
481 
482     @NonNull
parseRequiredComponents( @onNull XmlResourceParser parser)483     private List<RequiredComponent> parseRequiredComponents(
484             @NonNull XmlResourceParser parser) throws IOException, XmlPullParserException {
485         List<RequiredComponent> requiredComponents = new ArrayList<>();
486 
487         int type;
488         int depth;
489         int innerDepth = parser.getDepth() + 1;
490         while ((type = parser.next()) != XmlResourceParser.END_DOCUMENT
491                 && ((depth = parser.getDepth()) >= innerDepth
492                 || type != XmlResourceParser.END_TAG)) {
493             if (depth > innerDepth || type != XmlResourceParser.START_TAG) {
494                 continue;
495             }
496 
497             String name = parser.getName();
498             switch (name) {
499                 case TAG_ACTIVITY:
500                 case TAG_PROVIDER:
501                 case TAG_RECEIVER:
502                 case TAG_SERVICE: {
503                     RequiredComponent requiredComponent = parseRequiredComponent(parser, name);
504                     if (requiredComponent == null) {
505                         continue;
506                     }
507                     validateNoDuplicateElement(requiredComponent, requiredComponents,
508                             "require component");
509                     requiredComponents.add(requiredComponent);
510                     break;
511                 }
512                 default:
513                     throwOrLogForUnknownTag(parser);
514                     skipCurrentTag(parser);
515             }
516         }
517 
518         return requiredComponents;
519     }
520 
521     @Nullable
parseRequiredComponent(@onNull XmlResourceParser parser, @NonNull String name)522     private RequiredComponent parseRequiredComponent(@NonNull XmlResourceParser parser,
523             @NonNull String name) throws IOException, XmlPullParserException {
524         String permission = getAttributeValue(parser, ATTRIBUTE_PERMISSION);
525         int queryFlags = getAttributeIntValue(parser, ATTRIBUTE_QUERY_FLAGS, 0);
526         IntentFilterData intentFilterData = null;
527 
528         int type;
529         int depth;
530         int innerDepth = parser.getDepth() + 1;
531         while ((type = parser.next()) != XmlResourceParser.END_DOCUMENT
532                 && ((depth = parser.getDepth()) >= innerDepth
533                 || type != XmlResourceParser.END_TAG)) {
534             if (depth > innerDepth || type != XmlResourceParser.START_TAG) {
535                 continue;
536             }
537 
538             switch (parser.getName()) {
539                 case TAG_INTENT_FILTER:
540                     if (intentFilterData != null) {
541                         throwOrLogMessage("Duplicate <intent-filter> in <" + name + ">");
542                         skipCurrentTag(parser);
543                         continue;
544                     }
545                     intentFilterData = parseIntentFilterData(parser);
546                     break;
547                 default:
548                     throwOrLogForUnknownTag(parser);
549                     skipCurrentTag(parser);
550             }
551         }
552 
553         if (intentFilterData == null) {
554             throwOrLogMessage("Missing <intent-filter> in <" + name + ">");
555             return null;
556         }
557         switch (name) {
558             case TAG_ACTIVITY:
559                 return new RequiredActivity(intentFilterData, permission, queryFlags);
560             case TAG_PROVIDER:
561                 return new RequiredContentProvider(intentFilterData, permission, queryFlags);
562             case TAG_RECEIVER:
563                 return new RequiredBroadcastReceiver(intentFilterData, permission, queryFlags);
564             case TAG_SERVICE:
565                 return new RequiredService(intentFilterData, permission, queryFlags);
566             default:
567                 throwOrLogMessage("Unknown tag <" + name + ">");
568                 return null;
569         }
570     }
571 
572     @Nullable
parseIntentFilterData(@onNull XmlResourceParser parser)573     private IntentFilterData parseIntentFilterData(@NonNull XmlResourceParser parser)
574             throws IOException, XmlPullParserException {
575         String action = null;
576         List<String> categories = new ArrayList<>();
577         boolean hasData = false;
578         String dataScheme = null;
579         String dataType = null;
580 
581         int type;
582         int depth;
583         int innerDepth = parser.getDepth() + 1;
584         while ((type = parser.next()) != XmlResourceParser.END_DOCUMENT
585                 && ((depth = parser.getDepth()) >= innerDepth
586                 || type != XmlResourceParser.END_TAG)) {
587             if (depth > innerDepth || type != XmlResourceParser.START_TAG) {
588                 continue;
589             }
590 
591             switch (parser.getName()) {
592                 case TAG_ACTION:
593                     if (action != null) {
594                         throwOrLogMessage("Duplicate <action> in <intent-filter>");
595                         skipCurrentTag(parser);
596                         continue;
597                     }
598                     action = requireAttributeValue(parser, ATTRIBUTE_NAME, TAG_ACTION);
599                     break;
600                 case TAG_CATEGORY: {
601                     String category = requireAttributeValue(parser, ATTRIBUTE_NAME, TAG_CATEGORY);
602                     if (category == null) {
603                         continue;
604                     }
605                     validateIntentFilterCategory(category);
606                     validateNoDuplicateElement(category, categories, "category");
607                     categories.add(category);
608                     break;
609                 }
610                 case TAG_DATA:
611                     if (!hasData) {
612                         hasData = true;
613                     } else {
614                         throwOrLogMessage("Duplicate <data> in <intent-filter>");
615                         skipCurrentTag(parser);
616                         continue;
617                     }
618                     dataScheme = getAttributeValue(parser, ATTRIBUTE_SCHEME);
619                     dataType = getAttributeValue(parser, ATTRIBUTE_MIME_TYPE);
620                     if (dataType != null) {
621                         validateIntentFilterDataType(dataType);
622                     }
623                     break;
624                 default:
625                     throwOrLogForUnknownTag(parser);
626                     skipCurrentTag(parser);
627             }
628         }
629 
630         if (action == null) {
631             throwOrLogMessage("Missing <action> in <intent-filter>");
632             return null;
633         }
634         return new IntentFilterData(action, categories, dataScheme, dataType);
635     }
636 
validateIntentFilterCategory(@onNull String category)637     private void validateIntentFilterCategory(@NonNull String category) {
638         if (Objects.equals(category, Intent.CATEGORY_DEFAULT)) {
639             throwOrLogMessage("<category> should not include " + Intent.CATEGORY_DEFAULT);
640         }
641     }
642 
643     /**
644      * Validates the data type with the same logic in {@link
645      * android.content.IntentFilter#addDataType(String)} to prevent the {@code
646      * MalformedMimeTypeException}.
647      */
validateIntentFilterDataType(@onNull String type)648     private void validateIntentFilterDataType(@NonNull String type) {
649         int slashIndex = type.indexOf('/');
650         if (slashIndex <= 0 || type.length() < slashIndex + 2) {
651             throwOrLogMessage("Invalid attribute \"mimeType\" value on <data>: " + type);
652         }
653     }
654 
655     @NonNull
parsePermissions(@onNull XmlResourceParser parser, @NonNull ArrayMap<String, PermissionSet> permissionSets)656     private List<Permission> parsePermissions(@NonNull XmlResourceParser parser,
657             @NonNull ArrayMap<String, PermissionSet> permissionSets) throws IOException,
658             XmlPullParserException {
659         List<Permission> permissions = new ArrayList<>();
660 
661         int type;
662         int depth;
663         int innerDepth = parser.getDepth() + 1;
664         while ((type = parser.next()) != XmlResourceParser.END_DOCUMENT
665                 && ((depth = parser.getDepth()) >= innerDepth
666                 || type != XmlResourceParser.END_TAG)) {
667             if (depth > innerDepth || type != XmlResourceParser.START_TAG) {
668                 continue;
669             }
670 
671             switch (parser.getName()) {
672                 case TAG_PERMISSION_SET: {
673                     String permissionSetName = requireAttributeValue(parser, ATTRIBUTE_NAME,
674                             TAG_PERMISSION_SET);
675                     if (permissionSetName == null) {
676                         continue;
677                     }
678                     if (!permissionSets.containsKey(permissionSetName)) {
679                         throwOrLogMessage("Unknown permission set:" + permissionSetName);
680                         continue;
681                     }
682                     PermissionSet permissionSet = permissionSets.get(permissionSetName);
683                     // We do allow intersection between permission sets.
684                     permissions.addAll(permissionSet.getPermissions());
685                     break;
686                 }
687                 case TAG_PERMISSION: {
688                     Permission permission = parsePermission(parser);
689                     if (permission == null) {
690                         continue;
691                     }
692                     validateNoDuplicateElement(permission, permissions, "permission");
693                     permissions.add(permission);
694                     break;
695                 }
696                 default:
697                     throwOrLogForUnknownTag(parser);
698                     skipCurrentTag(parser);
699             }
700         }
701 
702         return permissions;
703     }
704 
705     @NonNull
parseAppOpPermissions(@onNull XmlResourceParser parser)706     private List<String> parseAppOpPermissions(@NonNull XmlResourceParser parser)
707             throws IOException, XmlPullParserException {
708         List<String> appOpPermissions = new ArrayList<>();
709 
710         int type;
711         int depth;
712         int innerDepth = parser.getDepth() + 1;
713         while ((type = parser.next()) != XmlResourceParser.END_DOCUMENT
714                 && ((depth = parser.getDepth()) >= innerDepth
715                 || type != XmlResourceParser.END_TAG)) {
716             if (depth > innerDepth || type != XmlResourceParser.START_TAG) {
717                 continue;
718             }
719 
720             if (parser.getName().equals(TAG_APP_OP_PERMISSION)) {
721                 String appOpPermission = requireAttributeValue(parser, ATTRIBUTE_NAME,
722                         TAG_APP_OP_PERMISSION);
723                 if (appOpPermission == null) {
724                     continue;
725                 }
726                 validateNoDuplicateElement(appOpPermission, appOpPermissions, "app op permission");
727                 appOpPermissions.add(appOpPermission);
728             } else {
729                 throwOrLogForUnknownTag(parser);
730                 skipCurrentTag(parser);
731             }
732         }
733 
734         return appOpPermissions;
735     }
736 
737     @NonNull
parseAppOps(@onNull XmlResourceParser parser)738     private List<AppOp> parseAppOps(@NonNull XmlResourceParser parser) throws IOException,
739             XmlPullParserException {
740         List<String> appOpNames = new ArrayList<>();
741         List<AppOp> appOps = new ArrayList<>();
742 
743         int type;
744         int depth;
745         int innerDepth = parser.getDepth() + 1;
746         while ((type = parser.next()) != XmlResourceParser.END_DOCUMENT
747                 && ((depth = parser.getDepth()) >= innerDepth
748                 || type != XmlResourceParser.END_TAG)) {
749             if (depth > innerDepth || type != XmlResourceParser.START_TAG) {
750                 continue;
751             }
752 
753             if (parser.getName().equals(TAG_APP_OP)) {
754                 String name = requireAttributeValue(parser, ATTRIBUTE_NAME, TAG_APP_OP);
755                 if (name == null) {
756                     continue;
757                 }
758                 validateNoDuplicateElement(name, appOpNames, "app op");
759                 appOpNames.add(name);
760                 Integer maxTargetSdkVersion = getAttributeIntValue(parser,
761                         ATTRIBUTE_MAX_TARGET_SDK_VERSION, Integer.MIN_VALUE);
762                 if (maxTargetSdkVersion == Integer.MIN_VALUE) {
763                     maxTargetSdkVersion = null;
764                 }
765                 if (maxTargetSdkVersion != null && maxTargetSdkVersion < Build.VERSION_CODES.BASE) {
766                     throwOrLogMessage("Invalid value for \"maxTargetSdkVersion\": "
767                             + maxTargetSdkVersion);
768                 }
769                 String modeName = requireAttributeValue(parser, ATTRIBUTE_MODE, TAG_APP_OP);
770                 if (modeName == null) {
771                     continue;
772                 }
773                 int modeIndex = sModeNameToMode.indexOfKey(modeName);
774                 if (modeIndex < 0) {
775                     throwOrLogMessage("Unknown value for \"mode\" on <app-op>: " + modeName);
776                     continue;
777                 }
778                 int mode = sModeNameToMode.valueAt(modeIndex);
779                 AppOp appOp = new AppOp(name, maxTargetSdkVersion, mode);
780                 appOps.add(appOp);
781             } else {
782                 throwOrLogForUnknownTag(parser);
783                 skipCurrentTag(parser);
784             }
785         }
786 
787         return appOps;
788     }
789 
790     @NonNull
parsePreferredActivities( @onNull XmlResourceParser parser)791     private List<PreferredActivity> parsePreferredActivities(
792             @NonNull XmlResourceParser parser) throws IOException, XmlPullParserException {
793         List<PreferredActivity> preferredActivities = new ArrayList<>();
794 
795         int type;
796         int depth;
797         int innerDepth = parser.getDepth() + 1;
798         while ((type = parser.next()) != XmlResourceParser.END_DOCUMENT
799                 && ((depth = parser.getDepth()) >= innerDepth
800                 || type != XmlResourceParser.END_TAG)) {
801             if (depth > innerDepth || type != XmlResourceParser.START_TAG) {
802                 continue;
803             }
804 
805             if (parser.getName().equals(TAG_PREFERRED_ACTIVITY)) {
806                 PreferredActivity preferredActivity = parsePreferredActivity(parser);
807                 if (preferredActivity == null) {
808                     continue;
809                 }
810                 validateNoDuplicateElement(preferredActivity, preferredActivities,
811                         "preferred activity");
812                 preferredActivities.add(preferredActivity);
813             } else {
814                 throwOrLogForUnknownTag(parser);
815                 skipCurrentTag(parser);
816             }
817         }
818 
819         return preferredActivities;
820     }
821 
822     @Nullable
parsePreferredActivity(@onNull XmlResourceParser parser)823     private PreferredActivity parsePreferredActivity(@NonNull XmlResourceParser parser)
824             throws IOException, XmlPullParserException {
825         RequiredActivity activity = null;
826         List<IntentFilterData> intentFilterDatas = new ArrayList<>();
827 
828         int type;
829         int depth;
830         int innerDepth = parser.getDepth() + 1;
831         while ((type = parser.next()) != XmlResourceParser.END_DOCUMENT
832                 && ((depth = parser.getDepth()) >= innerDepth
833                 || type != XmlResourceParser.END_TAG)) {
834             if (depth > innerDepth || type != XmlResourceParser.START_TAG) {
835                 continue;
836             }
837 
838             switch (parser.getName()) {
839                 case TAG_ACTIVITY:
840                     if (activity != null) {
841                         throwOrLogMessage("Duplicate <activity> in <preferred-activity>");
842                         skipCurrentTag(parser);
843                         continue;
844                     }
845                     activity = (RequiredActivity) parseRequiredComponent(parser, TAG_ACTIVITY);
846                     break;
847                 case TAG_INTENT_FILTER:
848                     IntentFilterData intentFilterData = parseIntentFilterData(parser);
849                     if (intentFilterData == null) {
850                         continue;
851                     }
852                     validateNoDuplicateElement(intentFilterData, intentFilterDatas,
853                             "intent filter");
854                     if (intentFilterData.getDataType() != null) {
855                         throwOrLogMessage("mimeType in <data> is not supported when setting a"
856                                 + " preferred activity");
857                     }
858                     intentFilterDatas.add(intentFilterData);
859                     break;
860                 default:
861                     throwOrLogForUnknownTag(parser);
862                     skipCurrentTag(parser);
863             }
864         }
865 
866         if (activity == null) {
867             throwOrLogMessage("Missing <activity> in <preferred-activity>");
868             return null;
869         }
870         if (intentFilterDatas.isEmpty()) {
871             throwOrLogMessage("Missing <intent-filter> in <preferred-activity>");
872             return null;
873         }
874         return new PreferredActivity(activity, intentFilterDatas);
875     }
876 
skipCurrentTag(@onNull XmlResourceParser parser)877     private void skipCurrentTag(@NonNull XmlResourceParser parser)
878             throws XmlPullParserException, IOException {
879         int type;
880         int innerDepth = parser.getDepth() + 1;
881         while ((type = parser.next()) != XmlResourceParser.END_DOCUMENT
882                 && (parser.getDepth() >= innerDepth || type != XmlResourceParser.END_TAG)) {
883             // Do nothing
884         }
885     }
886 
887     @Nullable
getAttributeValue(@onNull XmlResourceParser parser, @NonNull String name)888     private String getAttributeValue(@NonNull XmlResourceParser parser,
889             @NonNull String name) {
890         return parser.getAttributeValue(null, name);
891     }
892 
893     @Nullable
requireAttributeValue(@onNull XmlResourceParser parser, @NonNull String name, @NonNull String tagName)894     private String requireAttributeValue(@NonNull XmlResourceParser parser,
895             @NonNull String name, @NonNull String tagName) {
896         String value = getAttributeValue(parser, name);
897         if (value == null) {
898             throwOrLogMessage("Missing attribute \"" + name + "\" on <" + tagName + ">");
899         }
900         return value;
901     }
902 
getAttributeBooleanValue(@onNull XmlResourceParser parser, @NonNull String name, boolean defaultValue)903     private boolean getAttributeBooleanValue(@NonNull XmlResourceParser parser,
904             @NonNull String name, boolean defaultValue) {
905         return parser.getAttributeBooleanValue(null, name, defaultValue);
906     }
907 
908     @Nullable
requireAttributeBooleanValue(@onNull XmlResourceParser parser, @NonNull String name, boolean defaultValue, @NonNull String tagName)909     private Boolean requireAttributeBooleanValue(@NonNull XmlResourceParser parser,
910             @NonNull String name, boolean defaultValue, @NonNull String tagName) {
911         String value = requireAttributeValue(parser, name, tagName);
912         if (value == null) {
913             return null;
914         }
915         return getAttributeBooleanValue(parser, name, defaultValue);
916     }
917 
getAttributeIntValue(@onNull XmlResourceParser parser, @NonNull String name, int defaultValue)918     private int getAttributeIntValue(@NonNull XmlResourceParser parser,
919             @NonNull String name, int defaultValue) {
920         return parser.getAttributeIntValue(null, name, defaultValue);
921     }
922 
getAttributeResourceValue(@onNull XmlResourceParser parser, @NonNull String name, int defaultValue)923     private int getAttributeResourceValue(@NonNull XmlResourceParser parser,
924             @NonNull String name, int defaultValue) {
925         return parser.getAttributeResourceValue(null, name, defaultValue);
926     }
927 
928     @Nullable
requireAttributeResourceValue(@onNull XmlResourceParser parser, @NonNull String name, int defaultValue, @NonNull String tagName)929     private Integer requireAttributeResourceValue(@NonNull XmlResourceParser parser,
930             @NonNull String name, int defaultValue, @NonNull String tagName) {
931         String value = requireAttributeValue(parser, name, tagName);
932         if (value == null) {
933             return null;
934         }
935         return getAttributeResourceValue(parser, name, defaultValue);
936     }
937 
validateNoDuplicateElement(@onNull T element, @NonNull Collection<T> collection, @NonNull String name)938     private <T> void validateNoDuplicateElement(@NonNull T element,
939             @NonNull Collection<T> collection, @NonNull String name) {
940         if (collection.contains(element)) {
941             throwOrLogMessage("Duplicate " + name + ": " + element);
942         }
943     }
944 
throwOrLogMessage(String message)945     private void throwOrLogMessage(String message) {
946         if (mValidationEnabled) {
947             throw new IllegalArgumentException(message);
948         } else {
949             Log.wtf(LOG_TAG, message);
950         }
951     }
952 
throwOrLogMessage(String message, Throwable cause)953     private void throwOrLogMessage(String message, Throwable cause) {
954         if (mValidationEnabled) {
955             throw new IllegalArgumentException(message, cause);
956         } else {
957             Log.wtf(LOG_TAG, message, cause);
958         }
959     }
960 
throwOrLogForUnknownTag(@onNull XmlResourceParser parser)961     private void throwOrLogForUnknownTag(@NonNull XmlResourceParser parser) {
962         throwOrLogMessage("Unknown tag: " + parser.getName());
963     }
964 
965     /**
966      * Validates the permission names with {@code PackageManager} and ensures that all app ops with
967      * a permission in {@code AppOpsManager} have declared that permission in its role and ensures
968      * that all preferred activities are listed in the required components.
969      */
validateResult(@onNull ArrayMap<String, PermissionSet> permissionSets, @NonNull ArrayMap<String, Role> roles)970     private void validateResult(@NonNull ArrayMap<String, PermissionSet> permissionSets,
971             @NonNull ArrayMap<String, Role> roles) {
972         if (!mValidationEnabled) {
973             return;
974         }
975 
976         int permissionSetsSize = permissionSets.size();
977         for (int permissionSetsIndex = 0; permissionSetsIndex < permissionSetsSize;
978                 permissionSetsIndex++) {
979             PermissionSet permissionSet = permissionSets.valueAt(permissionSetsIndex);
980 
981             List<Permission> permissions = permissionSet.getPermissions();
982             int permissionsSize = permissions.size();
983             for (int permissionsIndex = 0; permissionsIndex < permissionsSize; permissionsIndex++) {
984                 Permission permission = permissions.get(permissionsIndex);
985 
986                 validatePermission(permission);
987             }
988         }
989 
990         int rolesSize = roles.size();
991         for (int rolesIndex = 0; rolesIndex < rolesSize; rolesIndex++) {
992             Role role = roles.valueAt(rolesIndex);
993 
994             if (!role.isAvailableBySdkVersion()) {
995                 continue;
996             }
997 
998             List<RequiredComponent> requiredComponents = role.getRequiredComponents();
999             int requiredComponentsSize = requiredComponents.size();
1000             for (int requiredComponentsIndex = 0; requiredComponentsIndex < requiredComponentsSize;
1001                     requiredComponentsIndex++) {
1002                 RequiredComponent requiredComponent = requiredComponents.get(
1003                         requiredComponentsIndex);
1004 
1005                 String permission = requiredComponent.getPermission();
1006                 if (permission != null) {
1007                     validatePermission(permission);
1008                 }
1009             }
1010 
1011             List<Permission> permissions = role.getPermissions();
1012             int permissionsSize = permissions.size();
1013             for (int i = 0; i < permissionsSize; i++) {
1014                 Permission permission = permissions.get(i);
1015 
1016                 validatePermission(permission);
1017             }
1018 
1019             List<AppOp> appOps = role.getAppOps();
1020             int appOpsSize = appOps.size();
1021             for (int i = 0; i < appOpsSize; i++) {
1022                 AppOp appOp = appOps.get(i);
1023 
1024                 validateAppOp(appOp);
1025             }
1026 
1027             List<String> appOpPermissions = role.getAppOpPermissions();
1028             int appOpPermissionsSize = appOpPermissions.size();
1029             for (int i = 0; i < appOpPermissionsSize; i++) {
1030                 String appOpPermission = appOpPermissions.get(i);
1031 
1032                 validateAppOpPermission(appOpPermission);
1033             }
1034 
1035             List<PreferredActivity> preferredActivities = role.getPreferredActivities();
1036             int preferredActivitiesSize = preferredActivities.size();
1037             for (int preferredActivitiesIndex = 0;
1038                     preferredActivitiesIndex < preferredActivitiesSize;
1039                     preferredActivitiesIndex++) {
1040                 PreferredActivity preferredActivity = preferredActivities.get(
1041                         preferredActivitiesIndex);
1042 
1043                 if (!role.getRequiredComponents().contains(preferredActivity.getActivity())) {
1044                     throw new IllegalArgumentException("<activity> of <preferred-activity> not"
1045                             + " required in <required-components>, role: " + role.getName()
1046                             + ", preferred activity: " + preferredActivity);
1047                 }
1048             }
1049         }
1050     }
1051 
validatePermission(@onNull Permission permission)1052     private void validatePermission(@NonNull Permission permission) {
1053         if (!permission.isAvailable()) {
1054             return;
1055         }
1056         validatePermission(permission.getName(), true);
1057     }
1058 
validatePermission(@onNull String permission)1059     private void validatePermission(@NonNull String permission) {
1060         validatePermission(permission, false);
1061     }
1062 
validatePermission(@onNull String permission, boolean enforceIsRuntimeOrRole)1063     private void validatePermission(@NonNull String permission, boolean enforceIsRuntimeOrRole) {
1064         PackageManager packageManager = mContext.getPackageManager();
1065         boolean isAutomotive = packageManager.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE);
1066         // Skip validation for car permissions which may not be available on all build targets.
1067         if (!isAutomotive && permission.startsWith("android.car")) {
1068             return;
1069         }
1070 
1071         PermissionInfo permissionInfo;
1072         try {
1073             permissionInfo = packageManager.getPermissionInfo(permission, 0);
1074         } catch (PackageManager.NameNotFoundException e) {
1075             throw new IllegalArgumentException("Unknown permission: " + permission, e);
1076         }
1077 
1078         if (enforceIsRuntimeOrRole) {
1079             if (!(permissionInfo.getProtection() == PermissionInfo.PROTECTION_DANGEROUS
1080                     || (permissionInfo.getProtectionFlags() & PermissionInfo.PROTECTION_FLAG_ROLE)
1081                             == PermissionInfo.PROTECTION_FLAG_ROLE)) {
1082                 throw new IllegalArgumentException(
1083                         "Permission is not a runtime or role permission: " + permission);
1084             }
1085         }
1086     }
1087 
validateAppOpPermission(@onNull String appOpPermission)1088     private void validateAppOpPermission(@NonNull String appOpPermission) {
1089         PackageManager packageManager = mContext.getPackageManager();
1090         PermissionInfo permissionInfo;
1091         try {
1092             permissionInfo = packageManager.getPermissionInfo(appOpPermission, 0);
1093         } catch (PackageManager.NameNotFoundException e) {
1094             throw new IllegalArgumentException("Unknown app op permission: " + appOpPermission, e);
1095         }
1096         if ((permissionInfo.getProtectionFlags() & PermissionInfo.PROTECTION_FLAG_APPOP)
1097                 != PermissionInfo.PROTECTION_FLAG_APPOP) {
1098             throw new IllegalArgumentException("Permission is not an app op permission: "
1099                     + appOpPermission);
1100         }
1101     }
1102 
validateAppOp(@onNull AppOp appOp)1103     private void validateAppOp(@NonNull AppOp appOp) {
1104         // This throws IllegalArgumentException if app op is unknown.
1105         String permission = AppOpsManager.opToPermission(appOp.getName());
1106         if (permission != null) {
1107             PackageManager packageManager = mContext.getPackageManager();
1108             PermissionInfo permissionInfo;
1109             try {
1110                 permissionInfo = packageManager.getPermissionInfo(permission, 0);
1111             } catch (PackageManager.NameNotFoundException e) {
1112                 throw new RuntimeException(e);
1113             }
1114             if (permissionInfo.getProtection() == PermissionInfo.PROTECTION_DANGEROUS) {
1115                 throw new IllegalArgumentException("App op has an associated runtime permission: "
1116                         + appOp.getName());
1117             }
1118         }
1119     }
1120 }
1121