blob: 6494e78c36105105f53837b1455e6938fd6d6a18 [file] [log] [blame]
/*
* 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);
}
}