All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.android.tools.lint.checks.PermissionRequirement Maven / Gradle / Ivy

There is a newer version: 25.3.0
Show newest version
/*
 * 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.client.api.JavaParser;
import com.android.tools.lint.client.api.JavaParser.ResolvedAnnotation;
import com.android.tools.lint.client.api.JavaParser.ResolvedField;
import com.android.tools.lint.detector.api.Context;
import com.android.tools.lint.detector.api.JavaContext;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;

import lombok.ast.BinaryExpression;
import lombok.ast.BinaryOperator;
import lombok.ast.Expression;
import lombok.ast.ForwardingAstVisitor;
import lombok.ast.Node;
import lombok.ast.Select;
import lombok.ast.VariableDefinitionEntry;

/**
 * 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 ResolvedAnnotation 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 BinaryOperator getOperator() {
            return null;
        }

        @NonNull
        @Override
        public Iterable getChildren() {
            return Collections.emptyList();
        }
    };

    private PermissionRequirement(@NonNull ResolvedAnnotation annotation) {
        this.annotation = annotation;
    }

    @NonNull
    public static PermissionRequirement create(
            @Nullable Context context,
            @NonNull ResolvedAnnotation annotation) {
        String value = (String)annotation.getValue(ATTR_VALUE);
        if (value != null && !value.isEmpty()) {
            for (int i = 0, n = value.length(); i < n; i++) {
                char c = value.charAt(i);
                // See if it's a complex expression and if so build it up
                if (c == '&' || c == '|' || c == '^') {
                    return Complex.parse(annotation, context, value);
                }
            }

            return new Single(annotation, value);
        }

        Object v = annotation.getValue(ATTR_ANY_OF);
        String[] anyOf = getAnnotationStrings(v);
        if (anyOf != null) {
            if (anyOf.length > 1) {
                return new Many(annotation, BinaryOperator.LOGICAL_OR, anyOf);
            } else if (anyOf.length == 1) {
                return new Single(annotation, anyOf[0]);
            }
        }

        v = annotation.getValue(ATTR_ALL_OF);
        String[] allOf = getAnnotationStrings(v);
        if (allOf != null) {
            if (allOf.length > 1) {
                return new Many(annotation, BinaryOperator.LOGICAL_AND, allOf);
            } else if (allOf.length == 1) {
                return new Single(annotation, allOf[0]);
            }
        }

        return NONE;
    }

    @Nullable
    private static String[] getAnnotationStrings(@Nullable Object v) {
        if (v != null) {
            if (v instanceof String[]) {
                return (String[])v;
            } else if (v instanceof String) {
                return new String[] { (String)v };
            } else if (v instanceof Object[]) {
                List strings = Lists.newArrayList();
                for (Object o : (Object[])v) {
                    if (o instanceof ResolvedField) {
                        Object vs = ((ResolvedField)o).getValue();
                        if (vs instanceof String) {
                            strings.add((String)vs);
                        }
                    } else if (o instanceof String) {
                        strings.add((String)o);
                    }
                }
                return strings.toArray(new String[strings.size()]);
            }
        }

        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
            Object o = annotation.getValue("apis");
            if (o instanceof String) {
                String range = (String)o;
                // 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() { Object o = annotation.getValue(ATTR_CONDITIONAL); if (o instanceof Boolean) { return (Boolean)o; } else if (o instanceof ResolvedField) { o = ((ResolvedField)o).getValue(); if (o instanceof Boolean) { return (Boolean)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 BinaryOperator 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 ResolvedAnnotation 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 BinaryOperator 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, BinaryOperator operator) { sb.append(' '); if (operator == BinaryOperator.LOGICAL_AND) { sb.append("and"); } else if (operator == BinaryOperator.LOGICAL_OR) { sb.append("or"); } else { assert operator == BinaryOperator.BITWISE_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 BinaryOperator operator; public final List permissions; public Many( @NonNull ResolvedAnnotation annotation, BinaryOperator operator, String[] names) { super(annotation); assert operator == BinaryOperator.LOGICAL_OR || operator == BinaryOperator.LOGICAL_AND : 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 == BinaryOperator.LOGICAL_AND) { for (PermissionRequirement requirement : permissions) { if (!requirement.isSatisfied(available) && requirement.appliesTo(available)) { return false; } } return true; } else { assert operator == BinaryOperator.LOGICAL_OR : 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 BinaryOperator.LOGICAL_OR, 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 BinaryOperator getOperator() { return operator; } @NonNull @Override public Iterable getChildren() { return permissions; } } /** * Require multiple permissions. This is a group of permissions with some * associated boolean logic, such as "B or (C and (D or E))". */ private static class Complex extends PermissionRequirement { public final BinaryOperator operator; public final PermissionRequirement left; public final PermissionRequirement right; public Complex( @NonNull ResolvedAnnotation annotation, BinaryOperator operator, PermissionRequirement left, PermissionRequirement right) { super(annotation); this.operator = operator; this.left = left; this.right = right; } @Override public boolean isSingle() { return false; } @Override public String toString() { StringBuilder sb = new StringBuilder(); boolean needsParentheses = left instanceof Complex && ((Complex) left).operator != BinaryOperator.LOGICAL_AND; if (needsParentheses) { sb.append('('); } sb.append(left.toString()); if (needsParentheses) { sb.append(')'); } appendOperator(sb, operator); needsParentheses = right instanceof Complex && ((Complex) right).operator != BinaryOperator.LOGICAL_AND; if (needsParentheses) { sb.append('('); } sb.append(right.toString()); if (needsParentheses) { sb.append(')'); } return sb.toString(); } @Override public boolean isSatisfied(@NonNull PermissionHolder available) { boolean satisfiedLeft = left.isSatisfied(available) || !left.appliesTo(available); boolean satisfiedRight = right.isSatisfied(available) || !right.appliesTo(available); if (operator == BinaryOperator.LOGICAL_AND) { return satisfiedLeft && satisfiedRight; } else if (operator == BinaryOperator.LOGICAL_OR) { return satisfiedLeft || satisfiedRight; } else { assert operator == BinaryOperator.BITWISE_XOR : operator; return satisfiedLeft ^ satisfiedRight; } } @Override public String describeMissingPermissions(@NonNull PermissionHolder available) { boolean satisfiedLeft = left.isSatisfied(available); boolean satisfiedRight = right.isSatisfied(available); if (operator == BinaryOperator.LOGICAL_AND || operator == BinaryOperator.LOGICAL_OR) { if (satisfiedLeft) { if (satisfiedRight) { return ""; } return right.describeMissingPermissions(available); } else if (satisfiedRight) { return left.describeMissingPermissions(available); } else { StringBuilder sb = new StringBuilder(); sb.append(left.describeMissingPermissions(available)); appendOperator(sb, operator); sb.append(right.describeMissingPermissions(available)); return sb.toString(); } } else { assert operator == BinaryOperator.BITWISE_XOR : operator; return toString(); } } @Override protected void addMissingPermissions(@NonNull PermissionHolder available, @NonNull Set missing) { boolean satisfiedLeft = left.isSatisfied(available); boolean satisfiedRight = right.isSatisfied(available); if (operator == BinaryOperator.LOGICAL_AND || operator == BinaryOperator.LOGICAL_OR) { if (satisfiedLeft) { if (satisfiedRight) { return; } right.addMissingPermissions(available, missing); } else if (satisfiedRight) { left.addMissingPermissions(available, missing); } else { left.addMissingPermissions(available, missing); right.addMissingPermissions(available, missing); } } else { assert operator == BinaryOperator.BITWISE_XOR : operator; left.addMissingPermissions(available, missing); right.addMissingPermissions(available, missing); } } @Override protected void addRevocablePermissions(@NonNull Set result, @NonNull PermissionHolder revocable) { left.addRevocablePermissions(result, revocable); right.addRevocablePermissions(result, revocable); } @Override public boolean isRevocable(@NonNull PermissionHolder revocable) { // TODO: If operator == BinaryOperator.LOGICAL_OR only return // revocable the there isn't a non-revocable term which is also satisfied. return left.isRevocable(revocable) || right.isRevocable(revocable); } @NonNull public static PermissionRequirement parse(@NonNull ResolvedAnnotation annotation, @Nullable Context context, @NonNull final String value) { // Parse an expression of the form (A op1 B op2 C) op3 (D op4 E) etc. // We'll just use the Java parser to handle this to ensure that operator // precedence etc is correct. if (context == null) { return NONE; } JavaParser javaParser = context.getClient().getJavaParser(null); if (javaParser == null) { return NONE; } try { JavaContext javaContext = new JavaContext(context.getDriver(), context.getProject(), context.getMainProject(), context.file, javaParser) { @Nullable @Override public String getContents() { return "" + "class Test { void test() {\n" + "boolean result=" + value + ";\n}\n}"; } }; Node node = javaParser.parseJava(javaContext); if (node != null) { final AtomicReference reference = new AtomicReference(); node.accept(new ForwardingAstVisitor() { @Override public boolean visitVariableDefinitionEntry(VariableDefinitionEntry node) { reference.set(node.astInitializer()); return true; } }); Expression expression = reference.get(); if (expression != null) { return parse(annotation, expression); } } return NONE; } finally { javaParser.dispose(); } } private static PermissionRequirement parse( @NonNull ResolvedAnnotation annotation, @NonNull Expression expression) { if (expression instanceof Select) { return new Single(annotation, expression.toString()); } else if (expression instanceof BinaryExpression) { BinaryExpression binaryExpression = (BinaryExpression) expression; BinaryOperator operator = binaryExpression.astOperator(); if (operator == BinaryOperator.LOGICAL_AND || operator == BinaryOperator.LOGICAL_OR || operator == BinaryOperator.BITWISE_XOR) { PermissionRequirement left = parse(annotation, binaryExpression.astLeft()); PermissionRequirement right = parse(annotation, binaryExpression.astRight()); return new Complex(annotation, operator, left, right); } } return NONE; } @Nullable @Override public BinaryOperator getOperator() { return operator; } @NonNull @Override public Iterable getChildren() { return Arrays.asList(left, right); } } /** * 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.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_SETTINGS", "android.permission.WRITE_PROFILE", "android.permission.WRITE_SOCIAL_STREAM", "com.android.voicemail.permission.ADD_VOICEMAIL", }; }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy