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()
81             .name(annotateFix.getDisplayName())
82             .composite(annotateFix, *replaceOrRemoveFixes.toTypedArray())
83     }
84 
85     private val annotation: String
86         get() {
87             val quotedPermissions = permissionNames.joinToString(", ") { """"$it"""" }
88 
89             val attributeName =
90                 if (permissionNames.size > 1) {
91                     if (anyOf) "anyOf" else "allOf"
92                 } else null
93 
94             val annotationParameter =
95                 if (attributeName != null) "$attributeName={$quotedPermissions}"
96                 else quotedPermissions
97 
98             return "@$ANNOTATION_ENFORCE_PERMISSION($annotationParameter)"
99         }
100 
101     companion object {
102         /**
103          * Walks the expressions in a block, looking for simple permission checks.
104          *
105          * As soon as something other than a permission check is encountered, stop looking,
106          * as some other business logic is happening that prevents an automated fix.
107          */
108         fun fromBlockExpression(
109             context: JavaContext,
110             blockExpression: UBlockExpression
111         ): EnforcePermissionFix? {
112             try {
113                 val singleFixes = mutableListOf<EnforcePermissionFix>()
114                 for (expression in blockExpression.expressions) {
115                     val fix = fromExpression(context, expression) ?: break
116                     singleFixes.add(fix)
117                 }
118                 return compose(singleFixes)
119             } catch (e: AnyOfAllOfException) {
120                 return null
121             }
122         }
123 
124         /**
125          * Conditionally constructs EnforcePermissionFix from any UExpression
126          *
127          * @return EnforcePermissionFix if the expression boils down to a permission check,
128          * else null
129          */
130         fun fromExpression(
131             context: JavaContext,
132             expression: UExpression
133         ): EnforcePermissionFix? {
134             val trimmedExpression = expression.skipParenthesizedExprDown()
135             if (trimmedExpression is UIfExpression) {
136                 return fromIfExpression(context, trimmedExpression)
137             }
138             findCallExpression(trimmedExpression)?.let {
139                 return fromCallExpression(context, it)
140             }
141             return null
142         }
143 
144         /**
145          * Conditionally constructs EnforcePermissionFix from a UCallExpression
146          *
147          * @return EnforcePermissionFix if the called method is annotated with @PermissionMethod, else null
148          */
149         fun fromCallExpression(
150             context: JavaContext,
151             callExpression: UCallExpression
152         ): EnforcePermissionFix? {
153             val method = callExpression.resolve()?.getUMethod() ?: return null
154             val annotation = getPermissionMethodAnnotation(method) ?: return null
155             val returnsVoid = method.returnType == PsiType.VOID
156             val orSelf = getAnnotationBooleanValue(annotation, "orSelf") ?: false
157             val anyOf = getAnnotationBooleanValue(annotation, "anyOf") ?: false
158             return EnforcePermissionFix(
159                     listOf(getPermissionCheckLocation(context, callExpression)),
160                     getPermissionCheckValues(callExpression),
161                     errorLevel = isErrorLevel(throws = returnsVoid, orSelf = orSelf),
162                     anyOf,
163             )
164         }
165 
166         /**
167          * Conditionally constructs EnforcePermissionFix from a UCallExpression
168          *
169          * @return EnforcePermissionFix IF AND ONLY IF:
170          * * The condition of the if statement compares the return value of a
171          *   PermissionMethod to one of the PackageManager.PermissionResult values
172          * * The expression inside the if statement does nothing but throw SecurityException
173          */
174         fun fromIfExpression(
175             context: JavaContext,
176             ifExpression: UIfExpression
177         ): EnforcePermissionFix? {
178             val condition = ifExpression.condition.skipParenthesizedExprDown()
179             if (condition !is UBinaryExpression) return null
180 
181             val maybeLeftCall = findCallExpression(condition.leftOperand)
182             val maybeRightCall = findCallExpression(condition.rightOperand)
183 
184             val (callExpression, comparison) =
185                     if (maybeLeftCall is UCallExpression) {
186                         Pair(maybeLeftCall, condition.rightOperand)
187                     } else if (maybeRightCall is UCallExpression) {
188                         Pair(maybeRightCall, condition.leftOperand)
189                     } else return null
190 
191             val permissionMethodAnnotation = getPermissionMethodAnnotation(
192                     callExpression.resolve()?.getUMethod()) ?: return null
193 
194             val equalityCheck =
195                     when (comparison.findSelector().asSourceString()
196                             .filterNot(Char::isWhitespace)) {
197                         "PERMISSION_GRANTED" -> UastBinaryOperator.IDENTITY_NOT_EQUALS
198                         "PERMISSION_DENIED" -> UastBinaryOperator.IDENTITY_EQUALS
199                         else -> return null
200                     }
201 
202             if (condition.operator != equalityCheck) return null
203 
204             val throwExpression: UThrowExpression? =
205                     ifExpression.thenExpression as? UThrowExpression
206                             ?: (ifExpression.thenExpression as? UBlockExpression)
207                                     ?.expressions?.firstOrNull()
208                                     as? UThrowExpression
209 
210 
211             val thrownClass = (throwExpression?.thrownExpression?.getExpressionType()
212                     as? PsiClassType)?.resolve() ?: return null
213             if (!context.evaluator.inheritsFrom(
214                             thrownClass, "java.lang.SecurityException")){
215                 return null
216             }
217 
218             val orSelf = getAnnotationBooleanValue(permissionMethodAnnotation, "orSelf") ?: false
219             val anyOf = getAnnotationBooleanValue(permissionMethodAnnotation, "anyOf") ?: false
220 
221             return EnforcePermissionFix(
222                     listOf(context.getLocation(ifExpression)),
223                     getPermissionCheckValues(callExpression),
224                     errorLevel = isErrorLevel(throws = true, orSelf = orSelf),
225                     anyOf = anyOf
226             )
227         }
228 
229 
230         fun compose(individuals: List<EnforcePermissionFix>): EnforcePermissionFix? {
231             if (individuals.isEmpty()) return null
232             val anyOfs = individuals.filter(EnforcePermissionFix::anyOf)
233             // anyOf/allOf should be consistent.  If we encounter some @PermissionMethods that are anyOf
234             // and others that aren't, we don't know what to do.
235             if (anyOfs.isNotEmpty() && anyOfs.size < individuals.size) {
236                 throw AnyOfAllOfException()
237             }
238             return EnforcePermissionFix(
239                     individuals.flatMap(EnforcePermissionFix::manualCheckLocations),
240                     individuals.flatMap(EnforcePermissionFix::permissionNames),
241                     errorLevel = individuals.all(EnforcePermissionFix::errorLevel),
242                     anyOf = anyOfs.isNotEmpty()
243             )
244         }
245 
246         /**
247          * Given a permission check, get its proper location
248          * so that a lint fix can remove the entire expression
249          */
250         private fun getPermissionCheckLocation(
251             context: JavaContext,
252             callExpression: UCallExpression
253         ):
254                 Location {
255             val javaPsi = callExpression.javaPsi!!
256             return Location.create(
257                 context.file,
258                 javaPsi.containingFile?.text,
259                 javaPsi.textRange.startOffset,
260                 // unfortunately the element doesn't include the ending semicolon
261                 javaPsi.textRange.endOffset + 1
262             )
263         }
264 
265         /**
266          * Given a @PermissionMethod, find arguments annotated with @PermissionName
267          * and pull out the permission value(s) being used.  Also evaluates nested calls
268          * to @PermissionMethod(s) in the given method's body.
269          */
270         @Throws(AnyOfAllOfException::class)
271         private fun getPermissionCheckValues(
272             callExpression: UCallExpression
273         ): List<String> {
274             if (!isPermissionMethodCall(callExpression)) return emptyList()
275 
276             val result = mutableSetOf<String>() // protect against duplicate permission values
277             val visitedCalls = mutableSetOf<UCallExpression>() // don't visit the same call twice
278             val bfsQueue = ArrayDeque(listOf(callExpression))
279 
280             var anyOfAllOfState: AnyOfAllOfState = AnyOfAllOfState.INITIAL
281 
282             // Bread First Search - evaluating nested @PermissionMethod(s) in the available
283             // source code for @PermissionName(s).
284             while (bfsQueue.isNotEmpty()) {
285                 val currentCallExpression = bfsQueue.removeFirst()
286                 visitedCalls.add(currentCallExpression)
287                 val currentPermissions = findPermissions(currentCallExpression)
288                 result.addAll(currentPermissions)
289 
290                 val currentAnnotation = getPermissionMethodAnnotation(
291                         currentCallExpression.resolve()?.getUMethod())
292                 val currentAnyOf = getAnnotationBooleanValue(currentAnnotation, "anyOf") ?: false
293 
294                 // anyOf/allOf should be consistent.  If we encounter a nesting of @PermissionMethods
295                 // where we start in an anyOf state and switch to allOf, or vice versa,
296                 // we don't know what to do.
297                 if (anyOfAllOfState == AnyOfAllOfState.INITIAL) {
298                     if (currentAnyOf) anyOfAllOfState = AnyOfAllOfState.ANY_OF
299                     else if (result.isNotEmpty()) anyOfAllOfState = AnyOfAllOfState.ALL_OF
300                 }
301 
302                 if (anyOfAllOfState == AnyOfAllOfState.ALL_OF && currentAnyOf) {
303                     throw AnyOfAllOfException()
304                 }
305 
306                 if (anyOfAllOfState == AnyOfAllOfState.ANY_OF &&
307                         !currentAnyOf && currentPermissions.size > 1) {
308                     throw AnyOfAllOfException()
309                 }
310 
311                 currentCallExpression.resolve()?.getUMethod()
312                         ?.accept(PermissionCheckValuesVisitor(visitedCalls, bfsQueue))
313             }
314 
315             return result.toList()
316         }
317 
318         private enum class AnyOfAllOfState {
319             INITIAL,
320             ANY_OF,
321             ALL_OF
322         }
323 
324         /**
325          * Adds visited permission method calls to the provided
326          * queue in support of the BFS traversal happening while
327          * this is used
328          */
329         private class PermissionCheckValuesVisitor(
330                 val visitedCalls: Set<UCallExpression>,
331                 val bfsQueue: ArrayDeque<UCallExpression>
332         ) : AbstractUastVisitor() {
333             override fun visitCallExpression(node: UCallExpression): Boolean {
334                 if (isPermissionMethodCall(node) && node !in visitedCalls) {
335                     bfsQueue.add(node)
336                 }
337                 return false
338             }
339         }
340 
341         private fun findPermissions(
342             callExpression: UCallExpression,
343         ): List<String> {
344             val annotation = getPermissionMethodAnnotation(callExpression.resolve()?.getUMethod())
345 
346             val hardCodedPermissions = (getAnnotationStringValues(annotation, "value")
347                     ?: emptyArray())
348                     .toList()
349 
350             val indices = callExpression.resolve()?.getUMethod()
351                     ?.uastParameters
352                     ?.filter(::hasPermissionNameAnnotation)
353                     ?.mapNotNull { it.sourcePsi?.parameterIndex() }
354                     ?: emptyList()
355 
356             val argPermissions = indices
357                     .flatMap { i ->
358                         when (val argument = callExpression.getArgumentForParameter(i)) {
359                             null -> listOf(null)
360                             is UExpressionList -> // varargs e.g. someMethod(String...)
361                                 argument.expressions.map(UExpression::evaluateString)
362                             else -> listOf(argument.evaluateString())
363                         }
364                     }
365                     .filterNotNull()
366 
367             return hardCodedPermissions + argPermissions
368         }
369 
370         /**
371          * If we detect that the PermissionMethod enforces that permission is granted,
372          * AND is of the "orSelf" variety, we are very confident that this is a behavior
373          * preserving migration to @EnforcePermission.  Thus, the incident should be ERROR
374          * level.
375          */
376         private fun isErrorLevel(throws: Boolean, orSelf: Boolean): Boolean = throws && orSelf
377     }
378 }
379 /**
380  * anyOf/allOf @PermissionMethods must be consistent to apply @EnforcePermission -
381  * meaning if we encounter some @PermissionMethods that are anyOf, and others are allOf,
382  * we don't know which to apply.
383  */
384 class AnyOfAllOfException : Exception() {
385     override val message: String = "anyOf/allOf permission methods cannot be mixed"
386 }
387