• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.google.android.lint.aidl
18 
19 import com.android.tools.lint.detector.api.JavaContext
20 import com.android.tools.lint.detector.api.LintFix
21 import com.android.tools.lint.detector.api.Location
22 import com.android.tools.lint.detector.api.UastLintUtils.Companion.getAnnotationBooleanValue
23 import com.android.tools.lint.detector.api.UastLintUtils.Companion.getAnnotationStringValues
24 import com.android.tools.lint.detector.api.findSelector
25 import com.android.tools.lint.detector.api.getUMethod
26 import com.google.android.lint.findCallExpression
27 import com.google.android.lint.getPermissionMethodAnnotation
28 import com.google.android.lint.hasPermissionNameAnnotation
29 import com.google.android.lint.isPermissionMethodCall
30 import com.intellij.psi.PsiClassType
31 import com.intellij.psi.PsiType
32 import org.jetbrains.kotlin.psi.psiUtil.parameterIndex
33 import org.jetbrains.uast.UBinaryExpression
34 import org.jetbrains.uast.UBlockExpression
35 import org.jetbrains.uast.UCallExpression
36 import org.jetbrains.uast.UExpression
37 import org.jetbrains.uast.UExpressionList
38 import org.jetbrains.uast.UIfExpression
39 import org.jetbrains.uast.UMethod
40 import org.jetbrains.uast.UThrowExpression
41 import org.jetbrains.uast.UastBinaryOperator
42 import org.jetbrains.uast.evaluateString
43 import org.jetbrains.uast.skipParenthesizedExprDown
44 import org.jetbrains.uast.visitor.AbstractUastVisitor
45 
46 /**
47  * Helper class that facilitates the creation of lint auto fixes
48  */
49 data class EnforcePermissionFix(
50     val manualCheckLocations: List<Location>,
51     val permissionNames: List<String>,
52     val errorLevel: Boolean,
53     val anyOf: Boolean,
54 ) {
55     fun toLintFix(context: JavaContext, node: UMethod): LintFix {
56         val methodLocation = context.getLocation(node)
57         val replaceOrRemoveFixes = manualCheckLocations.mapIndexed { index, manualCheckLocation ->
58             if (index == 0) {
59                 // Replace the first manual check with a call to the helper method
60                 getHelperMethodFix(node, manualCheckLocation, false)
61             } else {
62                 // Remove all subsequent manual checks
63                 LintFix.create()
64                     .replace()
65                     .reformat(true)
66                     .range(manualCheckLocation)
67                     .with("")
68                     .autoFix()
69                     .build()
70             }
71         }
72 
73         // Annotate the method with @EnforcePermission(...)
74         val annotateFix = LintFix.create()
75             .annotate(annotation)
76             .range(methodLocation)
77             .autoFix()
78             .build()
79 
80         return LintFix.create().composite(annotateFix, *replaceOrRemoveFixes.toTypedArray())
81     }
82 
83     private val annotation: String
84         get() {
85             val quotedPermissions = permissionNames.joinToString(", ") { """"$it"""" }
86 
87             val attributeName =
88                 if (permissionNames.size > 1) {
89                     if (anyOf) "anyOf" else "allOf"
90                 } else null
91 
92             val annotationParameter =
93                 if (attributeName != null) "$attributeName={$quotedPermissions}"
94                 else quotedPermissions
95 
96             return "@$ANNOTATION_ENFORCE_PERMISSION($annotationParameter)"
97         }
98 
99     companion object {
100         /**
101          * Walks the expressions in a block, looking for simple permission checks.
102          *
103          * As soon as something other than a permission check is encountered, stop looking,
104          * as some other business logic is happening that prevents an automated fix.
105          */
106         fun fromBlockExpression(
107             context: JavaContext,
108             blockExpression: UBlockExpression
109         ): EnforcePermissionFix? {
110             try {
111                 val singleFixes = mutableListOf<EnforcePermissionFix>()
112                 for (expression in blockExpression.expressions) {
113                     val fix = fromExpression(context, expression) ?: break
114                     singleFixes.add(fix)
115                 }
116                 return compose(singleFixes)
117             } catch (e: AnyOfAllOfException) {
118                 return null
119             }
120         }
121 
122         /**
123          * Conditionally constructs EnforcePermissionFix from any UExpression
124          *
125          * @return EnforcePermissionFix if the expression boils down to a permission check,
126          * else null
127          */
128         fun fromExpression(
129             context: JavaContext,
130             expression: UExpression
131         ): EnforcePermissionFix? {
132             val trimmedExpression = expression.skipParenthesizedExprDown()
133             if (trimmedExpression is UIfExpression) {
134                 return fromIfExpression(context, trimmedExpression)
135             }
136             findCallExpression(trimmedExpression)?.let {
137                 return fromCallExpression(context, it)
138             }
139             return null
140         }
141 
142         /**
143          * Conditionally constructs EnforcePermissionFix from a UCallExpression
144          *
145          * @return EnforcePermissionFix if the called method is annotated with @PermissionMethod, else null
146          */
147         fun fromCallExpression(
148             context: JavaContext,
149             callExpression: UCallExpression
150         ): EnforcePermissionFix? {
151             val method = callExpression.resolve()?.getUMethod() ?: return null
152             val annotation = getPermissionMethodAnnotation(method) ?: return null
153             val returnsVoid = method.returnType == PsiType.VOID
154             val orSelf = getAnnotationBooleanValue(annotation, "orSelf") ?: false
155             val anyOf = getAnnotationBooleanValue(annotation, "anyOf") ?: false
156             return EnforcePermissionFix(
157                     listOf(getPermissionCheckLocation(context, callExpression)),
158                     getPermissionCheckValues(callExpression),
159                     errorLevel = isErrorLevel(throws = returnsVoid, orSelf = orSelf),
160                     anyOf,
161             )
162         }
163 
164         /**
165          * Conditionally constructs EnforcePermissionFix from a UCallExpression
166          *
167          * @return EnforcePermissionFix IF AND ONLY IF:
168          * * The condition of the if statement compares the return value of a
169          *   PermissionMethod to one of the PackageManager.PermissionResult values
170          * * The expression inside the if statement does nothing but throw SecurityException
171          */
172         fun fromIfExpression(
173             context: JavaContext,
174             ifExpression: UIfExpression
175         ): EnforcePermissionFix? {
176             val condition = ifExpression.condition.skipParenthesizedExprDown()
177             if (condition !is UBinaryExpression) return null
178 
179             val maybeLeftCall = findCallExpression(condition.leftOperand)
180             val maybeRightCall = findCallExpression(condition.rightOperand)
181 
182             val (callExpression, comparison) =
183                     if (maybeLeftCall is UCallExpression) {
184                         Pair(maybeLeftCall, condition.rightOperand)
185                     } else if (maybeRightCall is UCallExpression) {
186                         Pair(maybeRightCall, condition.leftOperand)
187                     } else return null
188 
189             val permissionMethodAnnotation = getPermissionMethodAnnotation(
190                     callExpression.resolve()?.getUMethod()) ?: return null
191 
192             val equalityCheck =
193                     when (comparison.findSelector().asSourceString()
194                             .filterNot(Char::isWhitespace)) {
195                         "PERMISSION_GRANTED" -> UastBinaryOperator.IDENTITY_NOT_EQUALS
196                         "PERMISSION_DENIED" -> UastBinaryOperator.IDENTITY_EQUALS
197                         else -> return null
198                     }
199 
200             if (condition.operator != equalityCheck) return null
201 
202             val throwExpression: UThrowExpression? =
203                     ifExpression.thenExpression as? UThrowExpression
204                             ?: (ifExpression.thenExpression as? UBlockExpression)
205                                     ?.expressions?.firstOrNull()
206                                     as? UThrowExpression
207 
208 
209             val thrownClass = (throwExpression?.thrownExpression?.getExpressionType()
210                     as? PsiClassType)?.resolve() ?: return null
211             if (!context.evaluator.inheritsFrom(
212                             thrownClass, "java.lang.SecurityException")){
213                 return null
214             }
215 
216             val orSelf = getAnnotationBooleanValue(permissionMethodAnnotation, "orSelf") ?: false
217             val anyOf = getAnnotationBooleanValue(permissionMethodAnnotation, "anyOf") ?: false
218 
219             return EnforcePermissionFix(
220                     listOf(context.getLocation(ifExpression)),
221                     getPermissionCheckValues(callExpression),
222                     errorLevel = isErrorLevel(throws = true, orSelf = orSelf),
223                     anyOf = anyOf
224             )
225         }
226 
227 
228         fun compose(individuals: List<EnforcePermissionFix>): EnforcePermissionFix? {
229             if (individuals.isEmpty()) return null
230             val anyOfs = individuals.filter(EnforcePermissionFix::anyOf)
231             // anyOf/allOf should be consistent.  If we encounter some @PermissionMethods that are anyOf
232             // and others that aren't, we don't know what to do.
233             if (anyOfs.isNotEmpty() && anyOfs.size < individuals.size) {
234                 throw AnyOfAllOfException()
235             }
236             return EnforcePermissionFix(
237                     individuals.flatMap(EnforcePermissionFix::manualCheckLocations),
238                     individuals.flatMap(EnforcePermissionFix::permissionNames),
239                     errorLevel = individuals.all(EnforcePermissionFix::errorLevel),
240                     anyOf = anyOfs.isNotEmpty()
241             )
242         }
243 
244         /**
245          * Given a permission check, get its proper location
246          * so that a lint fix can remove the entire expression
247          */
248         private fun getPermissionCheckLocation(
249             context: JavaContext,
250             callExpression: UCallExpression
251         ):
252                 Location {
253             val javaPsi = callExpression.javaPsi!!
254             return Location.create(
255                 context.file,
256                 javaPsi.containingFile?.text,
257                 javaPsi.textRange.startOffset,
258                 // unfortunately the element doesn't include the ending semicolon
259                 javaPsi.textRange.endOffset + 1
260             )
261         }
262 
263         /**
264          * Given a @PermissionMethod, find arguments annotated with @PermissionName
265          * and pull out the permission value(s) being used.  Also evaluates nested calls
266          * to @PermissionMethod(s) in the given method's body.
267          */
268         @Throws(AnyOfAllOfException::class)
269         private fun getPermissionCheckValues(
270             callExpression: UCallExpression
271         ): List<String> {
272             if (!isPermissionMethodCall(callExpression)) return emptyList()
273 
274             val result = mutableSetOf<String>() // protect against duplicate permission values
275             val visitedCalls = mutableSetOf<UCallExpression>() // don't visit the same call twice
276             val bfsQueue = ArrayDeque(listOf(callExpression))
277 
278             var anyOfAllOfState: AnyOfAllOfState = AnyOfAllOfState.INITIAL
279 
280             // Bread First Search - evaluating nested @PermissionMethod(s) in the available
281             // source code for @PermissionName(s).
282             while (bfsQueue.isNotEmpty()) {
283                 val currentCallExpression = bfsQueue.removeFirst()
284                 visitedCalls.add(currentCallExpression)
285                 val currentPermissions = findPermissions(currentCallExpression)
286                 result.addAll(currentPermissions)
287 
288                 val currentAnnotation = getPermissionMethodAnnotation(
289                         currentCallExpression.resolve()?.getUMethod())
290                 val currentAnyOf = getAnnotationBooleanValue(currentAnnotation, "anyOf") ?: false
291 
292                 // anyOf/allOf should be consistent.  If we encounter a nesting of @PermissionMethods
293                 // where we start in an anyOf state and switch to allOf, or vice versa,
294                 // we don't know what to do.
295                 if (anyOfAllOfState == AnyOfAllOfState.INITIAL) {
296                     if (currentAnyOf) anyOfAllOfState = AnyOfAllOfState.ANY_OF
297                     else if (result.isNotEmpty()) anyOfAllOfState = AnyOfAllOfState.ALL_OF
298                 }
299 
300                 if (anyOfAllOfState == AnyOfAllOfState.ALL_OF && currentAnyOf) {
301                     throw AnyOfAllOfException()
302                 }
303 
304                 if (anyOfAllOfState == AnyOfAllOfState.ANY_OF &&
305                         !currentAnyOf && currentPermissions.size > 1) {
306                     throw AnyOfAllOfException()
307                 }
308 
309                 currentCallExpression.resolve()?.getUMethod()
310                         ?.accept(PermissionCheckValuesVisitor(visitedCalls, bfsQueue))
311             }
312 
313             return result.toList()
314         }
315 
316         private enum class AnyOfAllOfState {
317             INITIAL,
318             ANY_OF,
319             ALL_OF
320         }
321 
322         /**
323          * Adds visited permission method calls to the provided
324          * queue in support of the BFS traversal happening while
325          * this is used
326          */
327         private class PermissionCheckValuesVisitor(
328                 val visitedCalls: Set<UCallExpression>,
329                 val bfsQueue: ArrayDeque<UCallExpression>
330         ) : AbstractUastVisitor() {
331             override fun visitCallExpression(node: UCallExpression): Boolean {
332                 if (isPermissionMethodCall(node) && node !in visitedCalls) {
333                     bfsQueue.add(node)
334                 }
335                 return false
336             }
337         }
338 
339         private fun findPermissions(
340             callExpression: UCallExpression,
341         ): List<String> {
342             val annotation = getPermissionMethodAnnotation(callExpression.resolve()?.getUMethod())
343 
344             val hardCodedPermissions = (getAnnotationStringValues(annotation, "value")
345                     ?: emptyArray())
346                     .toList()
347 
348             val indices = callExpression.resolve()?.getUMethod()
349                     ?.uastParameters
350                     ?.filter(::hasPermissionNameAnnotation)
351                     ?.mapNotNull { it.sourcePsi?.parameterIndex() }
352                     ?: emptyList()
353 
354             val argPermissions = indices
355                     .flatMap { i ->
356                         when (val argument = callExpression.getArgumentForParameter(i)) {
357                             null -> listOf(null)
358                             is UExpressionList -> // varargs e.g. someMethod(String...)
359                                 argument.expressions.map(UExpression::evaluateString)
360                             else -> listOf(argument.evaluateString())
361                         }
362                     }
363                     .filterNotNull()
364 
365             return hardCodedPermissions + argPermissions
366         }
367 
368         /**
369          * If we detect that the PermissionMethod enforces that permission is granted,
370          * AND is of the "orSelf" variety, we are very confident that this is a behavior
371          * preserving migration to @EnforcePermission.  Thus, the incident should be ERROR
372          * level.
373          */
374         private fun isErrorLevel(throws: Boolean, orSelf: Boolean): Boolean = throws && orSelf
375     }
376 }
377 /**
378  * anyOf/allOf @PermissionMethods must be consistent to apply @EnforcePermission -
379  * meaning if we encounter some @PermissionMethods that are anyOf, and others are allOf,
380  * we don't know which to apply.
381  */
382 class AnyOfAllOfException : Exception() {
383     override val message: String = "anyOf/allOf permission methods cannot be mixed"
384 }
385