com.android.tools.lint.checks.PermissionRequirement Maven / Gradle / Ivy
/*
* Copyright (C) 2015 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.tools.lint.checks;
import static com.android.SdkConstants.ATTR_VALUE;
import static com.android.tools.lint.checks.SupportAnnotationDetector.ATTR_ALL_OF;
import static com.android.tools.lint.checks.SupportAnnotationDetector.ATTR_ANY_OF;
import static com.android.tools.lint.checks.SupportAnnotationDetector.ATTR_CONDITIONAL;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.annotations.VisibleForTesting;
import com.android.sdklib.AndroidVersion;
import com.android.tools.lint.detector.api.ConstantEvaluator;
import com.android.tools.lint.detector.api.JavaContext;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.intellij.psi.JavaTokenType;
import com.intellij.psi.PsiAnnotation;
import com.intellij.psi.PsiAnnotationMemberValue;
import com.intellij.psi.PsiArrayInitializerMemberValue;
import com.intellij.psi.tree.IElementType;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Set;
/**
* A permission requirement is a boolean expression of permission names that a
* caller must satisfy for a given Android API.
*/
public abstract class PermissionRequirement {
public static final String ATTR_PROTECTION_LEVEL = "protectionLevel"; //$NON-NLS-1$
public static final String VALUE_DANGEROUS = "dangerous"; //$NON-NLS-1$
protected final PsiAnnotation annotation;
private int firstApi;
private int lastApi;
@SuppressWarnings("ConstantConditions")
public static final PermissionRequirement NONE = new PermissionRequirement(null) {
@Override
public boolean isSatisfied(@NonNull PermissionHolder available) {
return true;
}
@Override
public boolean appliesTo(@NonNull PermissionHolder available) {
return false;
}
@Override
public boolean isConditional() {
return false;
}
@Override
public boolean isRevocable(@NonNull PermissionHolder revocable) {
return false;
}
@Override
public String toString() {
return "None";
}
@Override
protected void addMissingPermissions(@NonNull PermissionHolder available,
@NonNull Set result) {
}
@Override
protected void addRevocablePermissions(@NonNull Set result,
@NonNull PermissionHolder revocable) {
}
@Nullable
@Override
public IElementType getOperator() {
return null;
}
@NonNull
@Override
public Iterable getChildren() {
return Collections.emptyList();
}
};
private PermissionRequirement(@NonNull PsiAnnotation annotation) {
this.annotation = annotation;
}
@NonNull
public static PermissionRequirement create(
@NonNull JavaContext context,
@NonNull PsiAnnotation annotation) {
String value = getAnnotationStringValue(annotation, ATTR_VALUE);
if (value != null && !value.isEmpty()) {
return new Single(annotation, value);
}
String[] anyOf = getAnnotationStringValues(annotation, ATTR_ANY_OF);
if (anyOf != null) {
if (anyOf.length > 1) {
return new Many(annotation, JavaTokenType.OROR, anyOf);
} else if (anyOf.length == 1) {
return new Single(annotation, anyOf[0]);
}
}
String[] allOf = getAnnotationStringValues(annotation, ATTR_ALL_OF);
if (allOf != null) {
if (allOf.length > 1) {
return new Many(annotation, JavaTokenType.ANDAND, allOf);
} else if (allOf.length == 1) {
return new Single(annotation, allOf[0]);
}
}
return NONE;
}
@Nullable
public static Boolean getAnnotationBooleanValue(@Nullable PsiAnnotation annotation,
@NonNull String name) {
if (annotation != null) {
PsiAnnotationMemberValue attributeValue = annotation.findDeclaredAttributeValue(name);
if (attributeValue == null && ATTR_VALUE.equals(name)) {
attributeValue = annotation.findDeclaredAttributeValue(null);
}
// Use constant evaluator since we want to resolve field references as well
if (attributeValue != null) {
Object o = ConstantEvaluator.evaluate(null, attributeValue);
if (o instanceof Boolean) {
return (Boolean) o;
}
}
}
return null;
}
@Nullable
public static Long getAnnotationLongValue(@Nullable PsiAnnotation annotation,
@NonNull String name) {
if (annotation != null) {
PsiAnnotationMemberValue attributeValue = annotation.findDeclaredAttributeValue(name);
if (attributeValue == null && ATTR_VALUE.equals(name)) {
attributeValue = annotation.findDeclaredAttributeValue(null);
}
// Use constant evaluator since we want to resolve field references as well
if (attributeValue != null) {
Object o = ConstantEvaluator.evaluate(null, attributeValue);
if (o instanceof Number) {
return ((Number)o).longValue();
}
}
}
return null;
}
@Nullable
public static Double getAnnotationDoubleValue(@Nullable PsiAnnotation annotation,
@NonNull String name) {
if (annotation != null) {
PsiAnnotationMemberValue attributeValue = annotation.findDeclaredAttributeValue(name);
if (attributeValue == null && ATTR_VALUE.equals(name)) {
attributeValue = annotation.findDeclaredAttributeValue(null);
}
// Use constant evaluator since we want to resolve field references as well
if (attributeValue != null) {
Object o = ConstantEvaluator.evaluate(null, attributeValue);
if (o instanceof Number) {
return ((Number)o).doubleValue();
}
}
}
return null;
}
@Nullable
public static String getAnnotationStringValue(@Nullable PsiAnnotation annotation,
@NonNull String name) {
if (annotation != null) {
PsiAnnotationMemberValue attributeValue = annotation.findDeclaredAttributeValue(name);
if (attributeValue == null && ATTR_VALUE.equals(name)) {
attributeValue = annotation.findDeclaredAttributeValue(null);
}
// Use constant evaluator since we want to resolve field references as well
if (attributeValue != null) {
Object o = ConstantEvaluator.evaluate(null, attributeValue);
if (o instanceof String) {
return (String) o;
}
}
}
return null;
}
@Nullable
public static String[] getAnnotationStringValues(@Nullable PsiAnnotation annotation,
@NonNull String name) {
if (annotation != null) {
PsiAnnotationMemberValue attributeValue = annotation.findDeclaredAttributeValue(name);
if (attributeValue == null && ATTR_VALUE.equals(name)) {
attributeValue = annotation.findDeclaredAttributeValue(null);
}
if (attributeValue instanceof PsiArrayInitializerMemberValue) {
PsiAnnotationMemberValue[] initializers =
((PsiArrayInitializerMemberValue) attributeValue).getInitializers();
List result = Lists.newArrayListWithCapacity(initializers.length);
ConstantEvaluator constantEvaluator = new ConstantEvaluator(null);
for (PsiAnnotationMemberValue element : initializers) {
Object o = constantEvaluator.evaluate(element);
if (o instanceof String) {
result.add((String)o);
}
}
if (result.isEmpty()) {
return null;
} else {
return result.toArray(new String[0]);
}
} else {
// Use constant evaluator since we want to resolve field references as well
if (attributeValue != null) {
Object o = ConstantEvaluator.evaluate(null, attributeValue);
if (o instanceof String) {
return new String[]{(String) o};
} else if (o instanceof String[]) {
return (String[])o;
} else if (o instanceof Object[]) {
Object[] array = (Object[]) o;
List strings = Lists.newArrayListWithCapacity(array.length);
for (Object element : array) {
if (element instanceof String) {
strings.add((String) element);
}
}
return strings.toArray(new String[0]);
}
}
}
}
return null;
}
/**
* Returns false if this permission does not apply given the specified minimum and
* target sdk versions
*
* @param minSdkVersion the minimum SDK version
* @param targetSdkVersion the target SDK version
* @return true if this permission requirement applies for the given versions
*/
/**
* Returns false if this permission does not apply given the specified minimum and target
* sdk versions
*
* @param available the permission holder which also knows the min and target versions
* @return true if this permission requirement applies for the given versions
*/
protected boolean appliesTo(@NonNull PermissionHolder available) {
if (firstApi == 0) { // initialized?
firstApi = -1; // initialized, not specified
// Not initialized
String range = getAnnotationStringValue(annotation, "apis");
if (range != null) {
// Currently only support the syntax "a..b" where a and b are inclusive end points
// and where "a" and "b" are optional
int index = range.indexOf("..");
if (index != -1) {
try {
if (index > 0) {
firstApi = Integer.parseInt(range.substring(0, index));
} else {
firstApi = 1;
}
if (index + 2 < range.length()) {
lastApi = Integer.parseInt(range.substring(index + 2));
} else {
lastApi = Integer.MAX_VALUE;
}
} catch (NumberFormatException ignore) {
}
}
}
}
if (firstApi != -1) {
AndroidVersion minSdkVersion = available.getMinSdkVersion();
if (minSdkVersion.getFeatureLevel() > lastApi) {
return false;
}
AndroidVersion targetSdkVersion = available.getTargetSdkVersion();
if (targetSdkVersion.getFeatureLevel() < firstApi) {
return false;
}
}
return true;
}
/**
* Returns whether this requirement is conditional, meaning that there are
* some circumstances in which the requirement is not necessary. For
* example, consider
* {@code android.app.backup.BackupManager.dataChanged(java.lang.String)} .
* Here the {@code android.permission.BACKUP} is required but only if the
* argument is not your own package.
*
* This is used to handle permissions differently between the "missing" and
* "unused" checks. When checking for missing permissions, we err on the
* side of caution: if you are missing a permission, but the permission is
* conditional, you may not need it so we may not want to complain. However,
* when looking for unused permissions, we don't want to flag the
* conditional permissions as unused since they may be required.
*
* @return true if this requirement is conditional
*/
public boolean isConditional() {
Boolean o = getAnnotationBooleanValue(annotation, ATTR_CONDITIONAL);
if (o != null) {
return o;
}
return false;
}
/**
* Returns whether this requirement is for a single permission (rather than
* a boolean expression such as one permission or another.)
*
* @return true if this requirement is just a simple permission name
*/
public boolean isSingle() {
return true;
}
/**
* Whether the permission requirement is satisfied given the set of granted permissions
*
* @param available the available permissions
* @return true if all permissions specified by this requirement are available
*/
public abstract boolean isSatisfied(@NonNull PermissionHolder available);
/** Describes the missing permissions (e.g. "P1, P2 and P3") */
public String describeMissingPermissions(@NonNull PermissionHolder available) {
return "";
}
/** Returns the missing permissions (e.g. {"P1", "P2", "P3"} */
public Set getMissingPermissions(@NonNull PermissionHolder available) {
Set result = Sets.newHashSet();
addMissingPermissions(available, result);
return result;
}
protected abstract void addMissingPermissions(@NonNull PermissionHolder available,
@NonNull Set result);
/** Returns the permissions in the requirement that are revocable */
public Set getRevocablePermissions(@NonNull PermissionHolder revocable) {
Set result = Sets.newHashSet();
addRevocablePermissions(result, revocable);
return result;
}
protected abstract void addRevocablePermissions(@NonNull Set result,
@NonNull PermissionHolder revocable);
/**
* Returns whether this permission is revocable
*
* @param revocable the set of revocable permissions
* @return true if a user can revoke the permission
*/
public abstract boolean isRevocable(@NonNull PermissionHolder revocable);
/**
* For permission requirements that combine children, the operator to combine them with; null
* for leaf nodes
*/
@Nullable
public abstract IElementType getOperator();
/**
* Returns nested requirements, combined via {@link #getOperator()}
*/
@NonNull
public abstract Iterable getChildren();
/** Require a single permission */
private static class Single extends PermissionRequirement {
public final String name;
public Single(@NonNull PsiAnnotation annotation, @NonNull String name) {
super(annotation);
this.name = name;
}
@Override
public boolean isRevocable(@NonNull PermissionHolder revocable) {
return revocable.isRevocable(name) || isRevocableSystemPermission(name);
}
@Nullable
@Override
public IElementType getOperator() {
return null;
}
@NonNull
@Override
public Iterable getChildren() {
return Collections.emptyList();
}
@Override
public boolean isSingle() {
return true;
}
@Override
public String toString() {
return name;
}
@Override
public boolean isSatisfied(@NonNull PermissionHolder available) {
return available.hasPermission(name) || !appliesTo(available);
}
@Override
public String describeMissingPermissions(@NonNull PermissionHolder available) {
return isSatisfied(available) ? "" : name;
}
@Override
protected void addMissingPermissions(@NonNull PermissionHolder available,
@NonNull Set missing) {
if (!isSatisfied(available)) {
missing.add(name);
}
}
@Override
protected void addRevocablePermissions(@NonNull Set result,
@NonNull PermissionHolder revocable) {
if (isRevocable(revocable)) {
result.add(name);
}
}
}
protected static void appendOperator(StringBuilder sb, IElementType operator) {
sb.append(' ');
if (operator == JavaTokenType.ANDAND) {
sb.append("and");
} else if (operator == JavaTokenType.OROR) {
sb.append("or");
} else {
assert operator == JavaTokenType.XOR : operator;
sb.append("xor");
}
sb.append(' ');
}
/**
* Require a series of permissions, all with the same operator.
*/
private static class Many extends PermissionRequirement {
public final IElementType operator;
public final List permissions;
public Many(
@NonNull PsiAnnotation annotation,
IElementType operator,
String[] names) {
super(annotation);
assert operator == JavaTokenType.OROR
|| operator == JavaTokenType.ANDAND : operator;
assert names.length >= 2;
this.operator = operator;
this.permissions = Lists.newArrayListWithExpectedSize(names.length);
for (String name : names) {
permissions.add(new Single(annotation, name));
}
}
@Override
public boolean isSingle() {
return false;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(permissions.get(0));
for (int i = 1; i < permissions.size(); i++) {
appendOperator(sb, operator);
sb.append(permissions.get(i));
}
return sb.toString();
}
@Override
public boolean isSatisfied(@NonNull PermissionHolder available) {
if (operator == JavaTokenType.ANDAND) {
for (PermissionRequirement requirement : permissions) {
if (!requirement.isSatisfied(available) && requirement.appliesTo(available)) {
return false;
}
}
return true;
} else {
assert operator == JavaTokenType.OROR : operator;
for (PermissionRequirement requirement : permissions) {
if (requirement.isSatisfied(available) || !requirement.appliesTo(available)) {
return true;
}
}
return false;
}
}
@Override
public String describeMissingPermissions(@NonNull PermissionHolder available) {
StringBuilder sb = new StringBuilder();
boolean first = true;
for (PermissionRequirement requirement : permissions) {
if (!requirement.isSatisfied(available)) {
if (first) {
first = false;
} else {
appendOperator(sb, operator);
}
sb.append(requirement.describeMissingPermissions(available));
}
}
return sb.toString();
}
@Override
protected void addMissingPermissions(@NonNull PermissionHolder available,
@NonNull Set missing) {
for (PermissionRequirement requirement : permissions) {
if (!requirement.isSatisfied(available)) {
requirement.addMissingPermissions(available, missing);
}
}
}
@Override
protected void addRevocablePermissions(@NonNull Set result,
@NonNull PermissionHolder revocable) {
for (PermissionRequirement requirement : permissions) {
requirement.addRevocablePermissions(result, revocable);
}
}
@Override
public boolean isRevocable(@NonNull PermissionHolder revocable) {
// TODO: Pass in the available set of permissions here, and if
// the operator is JavaTokenType.OROR, only return revocable=true
// if an unsatisfied permission is also revocable. In other words,
// if multiple permissions are allowed, and some of them are satisfied and
// not revocable the overall permission requirement is not revocable.
for (PermissionRequirement requirement : permissions) {
if (requirement.isRevocable(revocable)) {
return true;
}
}
return false;
}
@Nullable
@Override
public IElementType getOperator() {
return operator;
}
@NonNull
@Override
public Iterable getChildren() {
return permissions;
}
}
/**
* Returns true if the given permission name is a revocable permission for
* targetSdkVersion ≥ 23
*
* @param name permission name
* @return true if this is a revocable permission
*/
public static boolean isRevocableSystemPermission(@NonNull String name) {
return Arrays.binarySearch(REVOCABLE_PERMISSION_NAMES, name) >= 0;
}
@VisibleForTesting
static final String[] REVOCABLE_PERMISSION_NAMES = new String[] {
"android.permission.ACCESS_COARSE_LOCATION",
"android.permission.ACCESS_FINE_LOCATION",
"android.permission.BODY_SENSORS",
"android.permission.CALL_PHONE",
"android.permission.CAMERA",
"android.permission.GET_ACCOUNTS",
"android.permission.PROCESS_OUTGOING_CALLS",
"android.permission.READ_CALENDAR",
"android.permission.READ_CALL_LOG",
"android.permission.READ_CELL_BROADCASTS",
"android.permission.READ_CONTACTS",
"android.permission.READ_EXTERNAL_STORAGE",
"android.permission.READ_PHONE_STATE",
"android.permission.READ_PROFILE",
"android.permission.READ_SMS",
"android.permission.READ_SOCIAL_STREAM",
"android.permission.RECEIVE_MMS",
"android.permission.RECEIVE_SMS",
"android.permission.RECEIVE_WAP_PUSH",
"android.permission.RECORD_AUDIO",
"android.permission.SEND_SMS",
"android.permission.USE_FINGERPRINT",
"android.permission.USE_SIP",
"android.permission.WRITE_CALENDAR",
"android.permission.WRITE_CALL_LOG",
"android.permission.WRITE_CONTACTS",
"android.permission.WRITE_EXTERNAL_STORAGE",
"android.permission.WRITE_PROFILE",
"android.permission.WRITE_SETTINGS",
"android.permission.WRITE_SOCIAL_STREAM",
"com.android.voicemail.permission.ADD_VOICEMAIL"
};
}