summaryrefslogtreecommitdiff
path: root/common/framework/com/android/net/module/util/LocationPermissionChecker.java
blob: cd1f31cbc5273208c344bb7544ff880228ad91a0 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
/*
 * Copyright (C) 2020 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.net.module.util;

import android.Manifest;
import android.annotation.IntDef;
import android.annotation.Nullable;
import android.app.ActivityManager;
import android.app.AppOpsManager;
import android.content.Context;
import android.content.pm.PackageManager;
import android.location.LocationManager;
import android.net.NetworkStack;
import android.os.Binder;
import android.os.Build;
import android.os.UserHandle;
import android.util.Log;

import androidx.annotation.RequiresApi;

import com.android.internal.annotations.VisibleForTesting;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;


/**
 * The methods used for location permission and location mode checking.
 *
 * @hide
 */
@RequiresApi(Build.VERSION_CODES.R)
public class LocationPermissionChecker {

    private static final String TAG = "LocationPermissionChecker";

    @Retention(RetentionPolicy.SOURCE)
    @IntDef(prefix = {"LOCATION_PERMISSION_CHECK_STATUS_"}, value = {
        SUCCEEDED,
        ERROR_LOCATION_MODE_OFF,
        ERROR_LOCATION_PERMISSION_MISSING,
    })
    public @interface LocationPermissionCheckStatus{}

    // The location permission check succeeded.
    public static final int SUCCEEDED = 0;
    // The location mode turns off for the caller.
    public static final int ERROR_LOCATION_MODE_OFF = 1;
    // The location permission isn't granted for the caller.
    public static final int ERROR_LOCATION_PERMISSION_MISSING = 2;

    private final Context mContext;
    private final AppOpsManager mAppOpsManager;

    public LocationPermissionChecker(Context context) {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
            throw new UnsupportedOperationException("This utility is not supported before R");
        }

        mContext = context;
        mAppOpsManager = mContext.getSystemService(AppOpsManager.class);
    }

    /**
     * Check location permission granted by the caller.
     *
     * This API check if the location mode enabled for the caller and the caller has
     * ACCESS_COARSE_LOCATION permission is targetSDK<29, otherwise, has ACCESS_FINE_LOCATION.
     *
     * @param pkgName package name of the application requesting access
     * @param featureId The feature in the package
     * @param uid The uid of the package
     * @param message A message describing why the permission was checked. Only needed if this is
     *                not inside of a two-way binder call from the data receiver
     *
     * @return {@code true} returns if the caller has location permission and the location mode is
     *         enabled.
     */
    public boolean checkLocationPermission(String pkgName, @Nullable String featureId,
            int uid, @Nullable String message) {
        return checkLocationPermissionInternal(pkgName, featureId, uid, message) == SUCCEEDED;
    }

    /**
     * Check location permission granted by the caller.
     *
     * This API check if the location mode enabled for the caller and the caller has
     * ACCESS_COARSE_LOCATION permission is targetSDK<29, otherwise, has ACCESS_FINE_LOCATION.
     * Compared with {@link #checkLocationPermission(String, String, int, String)}, this API returns
     * the detail information about the checking result, including the reason why it's failed and
     * logs the error for the caller.
     *
     * @param pkgName package name of the application requesting access
     * @param featureId The feature in the package
     * @param uid The uid of the package
     * @param message A message describing why the permission was checked. Only needed if this is
     *                not inside of a two-way binder call from the data receiver
     *
     * @return {@link LocationPermissionCheckStatus} the result of the location permission check.
     */
    public @LocationPermissionCheckStatus int checkLocationPermissionWithDetailInfo(
            String pkgName, @Nullable String featureId, int uid, @Nullable String message) {
        final int result = checkLocationPermissionInternal(pkgName, featureId, uid, message);
        switch (result) {
            case ERROR_LOCATION_MODE_OFF:
                Log.e(TAG, "Location mode is disabled for the device");
                break;
            case ERROR_LOCATION_PERMISSION_MISSING:
                Log.e(TAG, "UID " + uid + " has no location permission");
                break;
        }
        return result;
    }

    /**
     * Enforce the caller has location permission.
     *
     * This API determines if the location mode enabled for the caller and the caller has
     * ACCESS_COARSE_LOCATION permission is targetSDK<29, otherwise, has ACCESS_FINE_LOCATION.
     * SecurityException is thrown if the caller has no permission or the location mode is disabled.
     *
     * @param pkgName package name of the application requesting access
     * @param featureId The feature in the package
     * @param uid The uid of the package
     * @param message A message describing why the permission was checked. Only needed if this is
     *                not inside of a two-way binder call from the data receiver
     */
    public void enforceLocationPermission(String pkgName, @Nullable String featureId, int uid,
            @Nullable String message) throws SecurityException {
        final int result = checkLocationPermissionInternal(pkgName, featureId, uid, message);

        switch (result) {
            case ERROR_LOCATION_MODE_OFF:
                throw new SecurityException("Location mode is disabled for the device");
            case ERROR_LOCATION_PERMISSION_MISSING:
                throw new SecurityException("UID " + uid + " has no location permission");
        }
    }

    private int checkLocationPermissionInternal(String pkgName, @Nullable String featureId,
            int uid, @Nullable String message) {
        checkPackage(uid, pkgName);

        // Apps with NETWORK_SETTINGS, NETWORK_SETUP_WIZARD, NETWORK_STACK & MAINLINE_NETWORK_STACK
        // are granted a bypass.
        if (checkNetworkSettingsPermission(uid) || checkNetworkSetupWizardPermission(uid)
                || checkNetworkStackPermission(uid) || checkMainlineNetworkStackPermission(uid)) {
            return SUCCEEDED;
        }

        // Location mode must be enabled
        if (!isLocationModeEnabled()) {
            return ERROR_LOCATION_MODE_OFF;
        }

        // LocationAccess by App: caller must have Coarse/Fine Location permission to have access to
        // location information.
        if (!checkCallersLocationPermission(pkgName, featureId, uid,
                true /* coarseForTargetSdkLessThanQ */, message)) {
            return ERROR_LOCATION_PERMISSION_MISSING;
        }
        return SUCCEEDED;
    }

    /**
     * Checks that calling process has android.Manifest.permission.ACCESS_FINE_LOCATION or
     * android.Manifest.permission.ACCESS_COARSE_LOCATION (depending on config/targetSDK level)
     * and a corresponding app op is allowed for this package and uid.
     *
     * @param pkgName PackageName of the application requesting access
     * @param featureId The feature in the package
     * @param uid The uid of the package
     * @param coarseForTargetSdkLessThanQ If true and the targetSDK < Q then will check for COARSE
     *                                    else (false or targetSDK >= Q) then will check for FINE
     * @param message A message describing why the permission was checked. Only needed if this is
     *                not inside of a two-way binder call from the data receiver
     */
    public boolean checkCallersLocationPermission(String pkgName, @Nullable String featureId,
            int uid, boolean coarseForTargetSdkLessThanQ, @Nullable String message) {

        boolean isTargetSdkLessThanQ = isTargetSdkLessThan(pkgName, Build.VERSION_CODES.Q, uid);

        String permissionType = Manifest.permission.ACCESS_FINE_LOCATION;
        if (coarseForTargetSdkLessThanQ && isTargetSdkLessThanQ) {
            // Having FINE permission implies having COARSE permission (but not the reverse)
            permissionType = Manifest.permission.ACCESS_COARSE_LOCATION;
        }
        if (getUidPermission(permissionType, uid) == PackageManager.PERMISSION_DENIED) {
            return false;
        }

        // Always checking FINE - even if will not enforce. This will record the request for FINE
        // so that a location request by the app is surfaced to the user.
        boolean isFineLocationAllowed = noteAppOpAllowed(
                AppOpsManager.OPSTR_FINE_LOCATION, pkgName, featureId, uid, message);
        if (isFineLocationAllowed) {
            return true;
        }
        if (coarseForTargetSdkLessThanQ && isTargetSdkLessThanQ) {
            return noteAppOpAllowed(AppOpsManager.OPSTR_COARSE_LOCATION, pkgName, featureId, uid,
                    message);
        }
        return false;
    }

    /**
     * Retrieves a handle to LocationManager (if not already done) and check if location is enabled.
     */
    public boolean isLocationModeEnabled() {
        final LocationManager LocationManager = mContext.getSystemService(LocationManager.class);
        try {
            return LocationManager.isLocationEnabledForUser(UserHandle.of(
                    getCurrentUser()));
        } catch (Exception e) {
            Log.e(TAG, "Failure to get location mode via API, falling back to settings", e);
            return false;
        }
    }

    private boolean isTargetSdkLessThan(String packageName, int versionCode, int callingUid) {
        final long ident = Binder.clearCallingIdentity();
        try {
            if (mContext.getPackageManager().getApplicationInfoAsUser(
                    packageName, 0,
                    UserHandle.getUserHandleForUid(callingUid)).targetSdkVersion
                    < versionCode) {
                return true;
            }
        } catch (PackageManager.NameNotFoundException e) {
            // In case of exception, assume unknown app (more strict checking)
            // Note: This case will never happen since checkPackage is
            // called to verify validity before checking App's version.
        } finally {
            Binder.restoreCallingIdentity(ident);
        }
        return false;
    }

    private boolean noteAppOpAllowed(String op, String pkgName, @Nullable String featureId,
            int uid, @Nullable String message) {
        return mAppOpsManager.noteOp(op, uid, pkgName, featureId, message)
                == AppOpsManager.MODE_ALLOWED;
    }

    private void checkPackage(int uid, String pkgName)
            throws SecurityException {
        if (pkgName == null) {
            throw new SecurityException("Checking UID " + uid + " but Package Name is Null");
        }
        mAppOpsManager.checkPackage(uid, pkgName);
    }

    @VisibleForTesting
    protected int getCurrentUser() {
        return ActivityManager.getCurrentUser();
    }

    private int getUidPermission(String permissionType, int uid) {
        // We don't care about pid, pass in -1
        return mContext.checkPermission(permissionType, -1, uid);
    }

    /**
     * Returns true if the |uid| holds NETWORK_SETTINGS permission.
     */
    public boolean checkNetworkSettingsPermission(int uid) {
        return getUidPermission(android.Manifest.permission.NETWORK_SETTINGS, uid)
                == PackageManager.PERMISSION_GRANTED;
    }

    /**
     * Returns true if the |uid| holds NETWORK_SETUP_WIZARD permission.
     */
    public boolean checkNetworkSetupWizardPermission(int uid) {
        return getUidPermission(android.Manifest.permission.NETWORK_SETUP_WIZARD, uid)
                == PackageManager.PERMISSION_GRANTED;
    }

    /**
     * Returns true if the |uid| holds NETWORK_STACK permission.
     */
    public boolean checkNetworkStackPermission(int uid) {
        return getUidPermission(android.Manifest.permission.NETWORK_STACK, uid)
                == PackageManager.PERMISSION_GRANTED;
    }

    /**
     * Returns true if the |uid| holds MAINLINE_NETWORK_STACK permission.
     */
    public boolean checkMainlineNetworkStackPermission(int uid) {
        return getUidPermission(NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, uid)
                == PackageManager.PERMISSION_GRANTED;
    }

}