/* * 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.CLASS_INTENT; import static com.android.tools.lint.checks.SupportAnnotationDetector.PERMISSION_ANNOTATION; import static com.android.tools.lint.checks.SupportAnnotationDetector.PERMISSION_ANNOTATION_READ; import static com.android.tools.lint.checks.SupportAnnotationDetector.PERMISSION_ANNOTATION_WRITE; import static com.android.tools.lint.detector.api.JavaContext.getParentOfType; import com.android.annotations.NonNull; import com.android.annotations.Nullable; import com.android.tools.lint.client.api.JavaParser.ResolvedAnnotation; import com.android.tools.lint.client.api.JavaParser.ResolvedField; import com.android.tools.lint.client.api.JavaParser.ResolvedNode; import com.android.tools.lint.detector.api.JavaContext; import java.util.ListIterator; import lombok.ast.BinaryExpression; import lombok.ast.BinaryOperator; import lombok.ast.Cast; import lombok.ast.ConstructorInvocation; import lombok.ast.Expression; import lombok.ast.ExpressionStatement; import lombok.ast.InlineIfExpression; import lombok.ast.Node; import lombok.ast.NullLiteral; import lombok.ast.Select; import lombok.ast.Statement; import lombok.ast.VariableDeclaration; import lombok.ast.VariableDefinition; import lombok.ast.VariableDefinitionEntry; import lombok.ast.VariableReference; /** * Utility for locating permissions required by an intent or content resolver */ public class PermissionFinder { /** * Operation that has a permission requirement -- such as a method call, * a content resolver read or write operation, an intent, etc. */ public enum Operation { CALL, ACTION, READ, WRITE; /** Prefix to use when describing a name with a permission requirement */ public String prefix() { switch (this) { case ACTION: return "by intent"; case READ: return "to read"; case WRITE: return "to write"; case CALL: default: return "by"; } } } /** A permission requirement given a name and operation */ public static class Result { @NonNull public final PermissionRequirement requirement; @NonNull public final String name; @NonNull public final Operation operation; public Result( @NonNull Operation operation, @NonNull PermissionRequirement requirement, @NonNull String name) { this.operation = operation; this.requirement = requirement; this.name = name; } } /** * Searches for a permission requirement for the given parameter in the given call * * @param operation the operation to look up * @param context the context to use for lookup * @param parameter the parameter which contains the value which implies the permission * @return the result with the permission requirement, or null if nothing is found */ @Nullable public static Result findRequiredPermissions( @NonNull Operation operation, @NonNull JavaContext context, @NonNull Node parameter) { // To find the permission required by an intent, we proceed in 3 steps: // (1) Locate the parameter in the start call that corresponds to // the Intent // // (2) Find the place where the intent is initialized, and figure // out the action name being passed to it. // // (3) Find the place where the action is defined, and look for permission // annotations on that action declaration! return new PermissionFinder(context, operation).search(parameter); } private PermissionFinder(@NonNull JavaContext context, @NonNull Operation operation) { mContext = context; mOperation = operation; } @NonNull private final JavaContext mContext; @NonNull private final Operation mOperation; @Nullable public Result search(@NonNull Node node) { if (node instanceof NullLiteral) { return null; } else if (node instanceof InlineIfExpression) { InlineIfExpression expression = (InlineIfExpression) node; if (expression.astIfTrue() != null) { Result result = search(expression.astIfTrue()); if (result != null) { return result; } } if (expression.astIfFalse() != null) { Result result = search(expression.astIfFalse()); if (result != null) { return result; } } } else if (node instanceof Cast) { Cast cast = (Cast) node; return search(cast.astOperand()); } else if (node instanceof ConstructorInvocation && mOperation == Operation.ACTION) { // Identifies "new Intent(argument)" calls and, if found, continues // resolving the argument instead looking for the action definition ConstructorInvocation call = (ConstructorInvocation) node; String type = call.astTypeReference().getTypeName(); if (type.equals("Intent") || type.equals(CLASS_INTENT)) { Expression action = call.astArguments().first(); if (action != null) { return search(action); } } return null; } else if ((node instanceof VariableReference || node instanceof Select)) { ResolvedNode resolved = mContext.resolve(node); if (resolved instanceof ResolvedField) { ResolvedField field = (ResolvedField) resolved; if (mOperation == Operation.ACTION) { ResolvedAnnotation annotation = field.getAnnotation(PERMISSION_ANNOTATION); if (annotation != null) { return getPermissionRequirement(field, annotation); } } else if (mOperation == Operation.READ || mOperation == Operation.WRITE) { String fqn = mOperation == Operation.READ ? PERMISSION_ANNOTATION_READ : PERMISSION_ANNOTATION_WRITE; ResolvedAnnotation annotation = field.getAnnotation(fqn); if (annotation != null) { Object o = annotation.getValue(); if (o instanceof ResolvedAnnotation) { annotation = (ResolvedAnnotation) o; if (annotation.matches(PERMISSION_ANNOTATION)) { return getPermissionRequirement(field, annotation); } } else { // The complex annotations used for read/write cannot be // expressed in the external annotations format, so they're inlined. // (See Extractor.AnnotationData#write). // // Instead we've inlined the fields of the annotation on the // outer one: return getPermissionRequirement(field, annotation); } } } else { assert false : mOperation; } } else if (node instanceof VariableReference) { Statement statement = getParentOfType(node, Statement.class, false); if (statement != null) { ListIterator<Node> iterator = statement.getParent().getChildren().listIterator(); while (iterator.hasNext()) { if (iterator.next() == statement) { if (iterator.hasPrevious()) { // should always be true iterator.previous(); } break; } } String targetName = ((VariableReference)node).astIdentifier().astValue(); while (iterator.hasPrevious()) { Node previous = iterator.previous(); if (previous instanceof VariableDeclaration) { VariableDeclaration declaration = (VariableDeclaration) previous; VariableDefinition definition = declaration.astDefinition(); for (VariableDefinitionEntry entry : definition .astVariables()) { if (entry.astInitializer() != null && entry.astName().astValue().equals(targetName)) { return search(entry.astInitializer()); } } } else if (previous instanceof ExpressionStatement) { ExpressionStatement expressionStatement = (ExpressionStatement) previous; Expression expression = expressionStatement.astExpression(); if (expression instanceof BinaryExpression && ((BinaryExpression) expression).astOperator() == BinaryOperator.ASSIGN) { BinaryExpression binaryExpression = (BinaryExpression) expression; if (targetName.equals(binaryExpression.astLeft().toString())) { return search(binaryExpression.astRight()); } } } } } } } return null; } @NonNull private Result getPermissionRequirement( @NonNull ResolvedField field, @NonNull ResolvedAnnotation annotation) { PermissionRequirement requirement = PermissionRequirement.create(mContext, annotation); String name = field.getContainingClass().getSimpleName() + "." + field.getName(); return new Result(mOperation, requirement, name); } }