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