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