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