1 /*
2  * Copyright (C) 2018 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.providers.calendar.enterprise;
18 
19 import android.app.admin.DevicePolicyManager;
20 import android.content.Context;
21 import android.content.pm.PackageManager;
22 import android.net.Uri;
23 import android.os.UserHandle;
24 import android.provider.CalendarContract;
25 import android.provider.Settings;
26 import android.util.ArraySet;
27 import android.util.Log;
28 
29 import java.util.Set;
30 
31 /**
32  * Helper class for cross profile calendar related policies.
33  */
34 public class CrossProfileCalendarHelper {
35 
36     private static final String LOG_TAG = "CrossProfileCalendarHelper";
37 
38     final private Context mContext;
39 
40     public static final Set<String> EVENTS_TABLE_WHITELIST;
41     public static final Set<String> CALENDARS_TABLE_WHITELIST;
42     public static final Set<String> INSTANCES_TABLE_WHITELIST;
43 
44     static {
45         EVENTS_TABLE_WHITELIST = new ArraySet<>();
46         EVENTS_TABLE_WHITELIST.add(CalendarContract.Events._ID);
47         EVENTS_TABLE_WHITELIST.add(CalendarContract.Events.CALENDAR_ID);
48         EVENTS_TABLE_WHITELIST.add(CalendarContract.Events.TITLE);
49         EVENTS_TABLE_WHITELIST.add(CalendarContract.Events.EVENT_LOCATION);
50         EVENTS_TABLE_WHITELIST.add(CalendarContract.Events.EVENT_COLOR);
51         EVENTS_TABLE_WHITELIST.add(CalendarContract.Events.STATUS);
52         EVENTS_TABLE_WHITELIST.add(CalendarContract.Events.DTSTART);
53         EVENTS_TABLE_WHITELIST.add(CalendarContract.Events.DTEND);
54         EVENTS_TABLE_WHITELIST.add(CalendarContract.Events.EVENT_TIMEZONE);
55         EVENTS_TABLE_WHITELIST.add(CalendarContract.Events.EVENT_END_TIMEZONE);
56         EVENTS_TABLE_WHITELIST.add(CalendarContract.Events.DURATION);
57         EVENTS_TABLE_WHITELIST.add(CalendarContract.Events.ALL_DAY);
58         EVENTS_TABLE_WHITELIST.add(CalendarContract.Events.AVAILABILITY);
59         EVENTS_TABLE_WHITELIST.add(CalendarContract.Events.RRULE);
60         EVENTS_TABLE_WHITELIST.add(CalendarContract.Events.RDATE);
61         EVENTS_TABLE_WHITELIST.add(CalendarContract.Events.EXRULE);
62         EVENTS_TABLE_WHITELIST.add(CalendarContract.Events.EXDATE);
63         EVENTS_TABLE_WHITELIST.add(CalendarContract.Events.LAST_DATE);
64         EVENTS_TABLE_WHITELIST.add(CalendarContract.Events.SELF_ATTENDEE_STATUS);
65         EVENTS_TABLE_WHITELIST.add(CalendarContract.Events.DISPLAY_COLOR);
66 
67         CALENDARS_TABLE_WHITELIST = new ArraySet<>();
68         CALENDARS_TABLE_WHITELIST.add(CalendarContract.Calendars._ID);
69         CALENDARS_TABLE_WHITELIST.add(CalendarContract.Calendars.CALENDAR_COLOR);
70         CALENDARS_TABLE_WHITELIST.add(CalendarContract.Calendars.VISIBLE);
71         CALENDARS_TABLE_WHITELIST.add(CalendarContract.Calendars.CALENDAR_LOCATION);
72         CALENDARS_TABLE_WHITELIST.add(CalendarContract.Calendars.CALENDAR_TIME_ZONE);
73         CALENDARS_TABLE_WHITELIST.add(CalendarContract.Calendars.IS_PRIMARY);
74 
75         INSTANCES_TABLE_WHITELIST = new ArraySet<>();
76         INSTANCES_TABLE_WHITELIST.add(CalendarContract.Instances._ID);
77         INSTANCES_TABLE_WHITELIST.add(CalendarContract.Instances.EVENT_ID);
78         INSTANCES_TABLE_WHITELIST.add(CalendarContract.Instances.BEGIN);
79         INSTANCES_TABLE_WHITELIST.add(CalendarContract.Instances.END);
80         INSTANCES_TABLE_WHITELIST.add(CalendarContract.Instances.START_DAY);
81         INSTANCES_TABLE_WHITELIST.add(CalendarContract.Instances.END_DAY);
82         INSTANCES_TABLE_WHITELIST.add(CalendarContract.Instances.START_MINUTE);
83         INSTANCES_TABLE_WHITELIST.add(CalendarContract.Instances.END_MINUTE);
84 
85         // Add calendar columns.
86         EVENTS_TABLE_WHITELIST.add(CalendarContract.Calendars.CALENDAR_COLOR);
87         EVENTS_TABLE_WHITELIST.add(CalendarContract.Calendars.VISIBLE);
88         EVENTS_TABLE_WHITELIST.add(CalendarContract.Calendars.CALENDAR_TIME_ZONE);
89         EVENTS_TABLE_WHITELIST.add(CalendarContract.Calendars.IS_PRIMARY);
90 
addAll(EVENTS_TABLE_WHITELIST)91         ((ArraySet<String>) INSTANCES_TABLE_WHITELIST).addAll(EVENTS_TABLE_WHITELIST);
92     }
93 
CrossProfileCalendarHelper(Context context)94     public CrossProfileCalendarHelper(Context context) {
95         mContext = context;
96     }
97 
98     /**
99      * @return a context created from the given context for the given user, or null if it fails.
100      */
createPackageContextAsUser(Context context, int userId)101     private Context createPackageContextAsUser(Context context, int userId) {
102         try {
103             return context.createPackageContextAsUser(
104                     context.getPackageName(), 0 /* flags */, UserHandle.of(userId));
105         } catch (PackageManager.NameNotFoundException e) {
106             Log.e(LOG_TAG, "Failed to create user context", e);
107         }
108         return null;
109     }
110 
111     /**
112      * Returns whether a package is allowed to access cross-profile calendar APIs.
113      *
114      * A package is allowed to access cross-profile calendar APIs if it's allowed by the
115      * profile owner of a managed profile to access the managed profile calendar provider,
116      * and the setting {@link Settings.Secure#CROSS_PROFILE_CALENDAR_ENABLED} is turned
117      * on in the managed profile.
118      *
119      * @param packageName  the name of the package
120      * @param managedProfileUserId the user id of the managed profile
121      * @return {@code true} if the package is allowed, {@false} otherwise.
122      */
isPackageAllowedToAccessCalendar(String packageName, int managedProfileUserId)123     public boolean isPackageAllowedToAccessCalendar(String packageName, int managedProfileUserId) {
124         final Context managedProfileUserContext = createPackageContextAsUser(
125                 mContext, managedProfileUserId);
126         final DevicePolicyManager mDpm = managedProfileUserContext.getSystemService(
127                 DevicePolicyManager.class);
128         return mDpm.isPackageAllowedToAccessCalendar(packageName);
129     }
130 
ensureProjectionAllowed(String[] projection, Set<String> validColumnsSet)131     private static void ensureProjectionAllowed(String[] projection, Set<String> validColumnsSet) {
132         for (String column : projection) {
133             if (!validColumnsSet.contains(column)) {
134                 throw new IllegalArgumentException(String.format("Column %s is not "
135                         + "allowed to be accessed from cross profile Uris", column));
136             }
137         }
138     }
139 
140     /**
141      * Returns the calibrated version of projection for a given table.
142      *
143      * If the input projection is empty, return an array of all the whitelisted columns for a
144      * given table. Table is determined by the input uri.
145      *
146      * @param projection the original projection
147      * @param localUri the local uri for the query of the projection
148      * @return the original value of the input projection if it's not empty, otherwise an array of
149      * all the whitelisted columns.
150      * @throws IllegalArgumentException if the input projection contains a column that is not
151      * whitelisted for a given table.
152      */
getCalibratedProjection(String[] projection, Uri localUri)153     public String[] getCalibratedProjection(String[] projection, Uri localUri) {
154         // If projection is not empty, check if it's valid. Otherwise fill it with all
155         // allowed columns.
156         Set<String> validColumnsSet = new ArraySet<String>();
157         if (CalendarContract.Events.CONTENT_URI.equals(localUri)) {
158             validColumnsSet = EVENTS_TABLE_WHITELIST;
159         } else if (CalendarContract.Calendars.CONTENT_URI.equals(localUri)) {
160             validColumnsSet = CALENDARS_TABLE_WHITELIST;
161         } else if (CalendarContract.Instances.CONTENT_URI.equals(localUri)
162                 || CalendarContract.Instances.CONTENT_BY_DAY_URI.equals(localUri)
163                 || CalendarContract.Instances.CONTENT_SEARCH_URI.equals(localUri)
164                 || CalendarContract.Instances.CONTENT_SEARCH_BY_DAY_URI.equals(localUri)) {
165             validColumnsSet = INSTANCES_TABLE_WHITELIST;
166         } else {
167             throw new IllegalArgumentException(String.format("Cross profile version of %d is not "
168                     + "supported", localUri.toSafeString()));
169         }
170 
171         if (projection != null && projection.length > 0) {
172             // If there exists some columns in original projection, check if these columns are
173             // allowed.
174             ensureProjectionAllowed(projection, validColumnsSet);
175             return projection;
176         }
177         // Query of content provider will return cursor that contains all columns if projection is
178         // null or empty. To be consistent with this behavior, we fill projection with all allowed
179         // columns if it's null or empty for cross profile Uris.
180         return validColumnsSet.toArray(new String[validColumnsSet.size()]);
181     }
182 }
183