Merge "Update lint checks to their state on internal master"
This commit is contained in:
@@ -1,15 +1,44 @@
|
|||||||
# Android Framework Lint Checker
|
# Android Lint Checks for AOSP
|
||||||
|
|
||||||
Custom lint checks written here are going to be executed for modules that opt in to those (e.g. any
|
Custom Android Lint checks are written here to be executed against java modules
|
||||||
|
in AOSP. These checks are broken down into two subdirectories:
|
||||||
|
|
||||||
|
1. [Global Checks](#android-global-lint-checker)
|
||||||
|
2. [Framework Checks](#android-framework-lint-checker)
|
||||||
|
|
||||||
|
# [Android Global Lint Checker](/global)
|
||||||
|
Checks written here are executed for the entire tree. The `AndroidGlobalLintChecker`
|
||||||
|
build target produces a jar file that is included in the overall build output
|
||||||
|
(`AndroidGlobalLintChecker.jar`). This file is then downloaded as a prebuilt under the
|
||||||
|
`prebuilts/cmdline-tools` subproject, and included by soong with all invocations of lint.
|
||||||
|
|
||||||
|
## How to add new global lint checks
|
||||||
|
1. Write your detector with its issues and put it into
|
||||||
|
`global/checks/src/main/java/com/google/android/lint`.
|
||||||
|
2. Add your detector's issues into `AndroidGlobalIssueRegistry`'s `issues`
|
||||||
|
field.
|
||||||
|
3. Write unit tests for your detector in one file and put it into
|
||||||
|
`global/checks/test/java/com/google/android/lint`.
|
||||||
|
4. Have your change reviewed and merged. Once your change is merged,
|
||||||
|
obtain a build number from a successful build that includes your change.
|
||||||
|
5. Run `prebuilts/cmdline-tools/update-android-global-lint-checker.sh
|
||||||
|
<build_number>`. The script will create a commit that you can upload for
|
||||||
|
approval to the `prebuilts/cmdline-tools` subproject.
|
||||||
|
6. Done! Your lint check should be applied in lint report builds across the
|
||||||
|
entire tree!
|
||||||
|
|
||||||
|
# [Android Framework Lint Checker](/framework)
|
||||||
|
|
||||||
|
Checks written here are going to be executed for modules that opt in to those (e.g. any
|
||||||
`services.XXX` module) and results will be automatically reported on CLs on gerrit.
|
`services.XXX` module) and results will be automatically reported on CLs on gerrit.
|
||||||
|
|
||||||
## How to add new lint checks
|
## How to add new framework lint checks
|
||||||
|
|
||||||
1. Write your detector with its issues and put it into
|
1. Write your detector with its issues and put it into
|
||||||
`checks/src/main/java/com/google/android/lint`.
|
`framework/checks/src/main/java/com/google/android/lint`.
|
||||||
2. Add your detector's issues into `AndroidFrameworkIssueRegistry`'s `issues` field.
|
2. Add your detector's issues into `AndroidFrameworkIssueRegistry`'s `issues` field.
|
||||||
3. Write unit tests for your detector in one file and put it into
|
3. Write unit tests for your detector in one file and put it into
|
||||||
`checks/test/java/com/google/android/lint`.
|
`framework/checks/test/java/com/google/android/lint`.
|
||||||
4. Done! Your lint checks should be applied in lint report builds for modules that include
|
4. Done! Your lint checks should be applied in lint report builds for modules that include
|
||||||
`AndroidFrameworkLintChecker`.
|
`AndroidFrameworkLintChecker`.
|
||||||
|
|
||||||
@@ -44,7 +73,11 @@ m out/soong/.intermediates/frameworks/base/services/autofill/services.autofill/a
|
|||||||
environment variable with the id of the lint. For example:
|
environment variable with the id of the lint. For example:
|
||||||
`ANDROID_LINT_CHECK=UnusedTokenOfOriginalCallingIdentity m out/[...]/lint-report.html`
|
`ANDROID_LINT_CHECK=UnusedTokenOfOriginalCallingIdentity m out/[...]/lint-report.html`
|
||||||
|
|
||||||
## Create or update a baseline
|
# How to apply automatic fixes suggested by lint
|
||||||
|
|
||||||
|
See [lint_fix](fix/README.md)
|
||||||
|
|
||||||
|
# Create or update a baseline
|
||||||
|
|
||||||
Baseline files can be used to silence known errors (and warnings) that are deemed to be safe. When
|
Baseline files can be used to silence known errors (and warnings) that are deemed to be safe. When
|
||||||
there is a lint-baseline.xml file in the root folder of the java library, soong will
|
there is a lint-baseline.xml file in the root folder of the java library, soong will
|
||||||
@@ -75,9 +108,10 @@ locally change the soong code in
|
|||||||
[lint.go](http://cs/aosp-master/build/soong/java/lint.go;l=451;rcl=2e778d5bc4a8d1d77b4f4a3029a4a254ad57db75)
|
[lint.go](http://cs/aosp-master/build/soong/java/lint.go;l=451;rcl=2e778d5bc4a8d1d77b4f4a3029a4a254ad57db75)
|
||||||
adding `cmd.Flag("--nowarn")` and running lint again.
|
adding `cmd.Flag("--nowarn")` and running lint again.
|
||||||
|
|
||||||
## Documentation
|
# Documentation
|
||||||
|
|
||||||
- [Android Lint Docs](https://googlesamples.github.io/android-custom-lint-rules/)
|
- [Android Lint Docs](https://googlesamples.github.io/android-custom-lint-rules/)
|
||||||
|
- [Lint Check Unit Testing](https://googlesamples.github.io/android-custom-lint-rules/api-guide/unit-testing.md.html)
|
||||||
- [Android Lint source files](https://source.corp.google.com/studio-main/tools/base/lint/libs/lint-api/src/main/java/com/android/tools/lint/)
|
- [Android Lint source files](https://source.corp.google.com/studio-main/tools/base/lint/libs/lint-api/src/main/java/com/android/tools/lint/)
|
||||||
- [PSI source files](https://github.com/JetBrains/intellij-community/tree/master/java/java-psi-api/src/com/intellij/psi)
|
- [PSI source files](https://github.com/JetBrains/intellij-community/tree/master/java/java-psi-api/src/com/intellij/psi)
|
||||||
- [UAST source files](https://upsource.jetbrains.com/idea-ce/structure/idea-ce-7b9b8cc138bbd90aec26433f82cd2c6838694003/uast/uast-common/src/org/jetbrains/uast)
|
- [UAST source files](https://upsource.jetbrains.com/idea-ce/structure/idea-ce-7b9b8cc138bbd90aec26433f82cd2c6838694003/uast/uast-common/src/org/jetbrains/uast)
|
||||||
|
|||||||
@@ -1,177 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2022 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.google.android.lint
|
|
||||||
|
|
||||||
import com.android.tools.lint.detector.api.AnnotationInfo
|
|
||||||
import com.android.tools.lint.detector.api.AnnotationOrigin
|
|
||||||
import com.android.tools.lint.detector.api.AnnotationUsageInfo
|
|
||||||
import com.android.tools.lint.detector.api.AnnotationUsageType
|
|
||||||
import com.android.tools.lint.detector.api.ConstantEvaluator
|
|
||||||
import com.android.tools.lint.detector.api.Category
|
|
||||||
import com.android.tools.lint.detector.api.Detector
|
|
||||||
import com.android.tools.lint.detector.api.Implementation
|
|
||||||
import com.android.tools.lint.detector.api.Issue
|
|
||||||
import com.android.tools.lint.detector.api.JavaContext
|
|
||||||
import com.android.tools.lint.detector.api.Scope
|
|
||||||
import com.android.tools.lint.detector.api.Severity
|
|
||||||
import com.android.tools.lint.detector.api.SourceCodeScanner
|
|
||||||
import com.intellij.psi.PsiAnnotation
|
|
||||||
import com.intellij.psi.PsiClass
|
|
||||||
import com.intellij.psi.PsiMethod
|
|
||||||
import org.jetbrains.uast.UElement
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Lint Detector that ensures that any method overriding a method annotated
|
|
||||||
* with @EnforcePermission is also annotated with the exact same annotation.
|
|
||||||
* The intent is to surface the effective permission checks to the service
|
|
||||||
* implementations.
|
|
||||||
*/
|
|
||||||
class EnforcePermissionDetector : Detector(), SourceCodeScanner {
|
|
||||||
|
|
||||||
val ENFORCE_PERMISSION = "android.annotation.EnforcePermission"
|
|
||||||
|
|
||||||
override fun applicableAnnotations(): List<String> {
|
|
||||||
return listOf(ENFORCE_PERMISSION)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun areAnnotationsEquivalent(
|
|
||||||
context: JavaContext,
|
|
||||||
anno1: PsiAnnotation,
|
|
||||||
anno2: PsiAnnotation
|
|
||||||
): Boolean {
|
|
||||||
if (anno1.qualifiedName != anno2.qualifiedName) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
val attr1 = anno1.parameterList.attributes
|
|
||||||
val attr2 = anno2.parameterList.attributes
|
|
||||||
if (attr1.size != attr2.size) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
for (i in attr1.indices) {
|
|
||||||
if (attr1[i].name != attr2[i].name) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
val value1 = attr1[i].value
|
|
||||||
val value2 = attr2[i].value
|
|
||||||
if (value1 == null && value2 == null) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if (value1 == null || value2 == null) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
val v1 = ConstantEvaluator.evaluate(context, value1)
|
|
||||||
val v2 = ConstantEvaluator.evaluate(context, value2)
|
|
||||||
if (v1 != v2) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun visitAnnotationUsage(
|
|
||||||
context: JavaContext,
|
|
||||||
element: UElement,
|
|
||||||
annotationInfo: AnnotationInfo,
|
|
||||||
usageInfo: AnnotationUsageInfo
|
|
||||||
) {
|
|
||||||
if (usageInfo.type == AnnotationUsageType.EXTENDS) {
|
|
||||||
val newClass = element.sourcePsi?.parent?.parent as PsiClass
|
|
||||||
val extendedClass: PsiClass = usageInfo.referenced as PsiClass
|
|
||||||
val newAnnotation = newClass.getAnnotation(ENFORCE_PERMISSION)
|
|
||||||
val extendedAnnotation = extendedClass.getAnnotation(ENFORCE_PERMISSION)!!
|
|
||||||
|
|
||||||
val location = context.getLocation(element)
|
|
||||||
val newClassName = newClass.qualifiedName
|
|
||||||
val extendedClassName = extendedClass.qualifiedName
|
|
||||||
if (newAnnotation == null) {
|
|
||||||
val msg = "The class $newClassName extends the class $extendedClassName which " +
|
|
||||||
"is annotated with @EnforcePermission. The same annotation must be used " +
|
|
||||||
"on $newClassName."
|
|
||||||
context.report(ISSUE_MISSING_ENFORCE_PERMISSION, element, location, msg)
|
|
||||||
} else if (!areAnnotationsEquivalent(context, newAnnotation, extendedAnnotation)) {
|
|
||||||
val msg = "The class $newClassName is annotated with ${newAnnotation.text} " +
|
|
||||||
"which differs from the parent class $extendedClassName: " +
|
|
||||||
"${extendedAnnotation.text}. The same annotation must be used for " +
|
|
||||||
"both classes."
|
|
||||||
context.report(ISSUE_MISMATCHING_ENFORCE_PERMISSION, element, location, msg)
|
|
||||||
}
|
|
||||||
} else if (usageInfo.type == AnnotationUsageType.METHOD_OVERRIDE &&
|
|
||||||
annotationInfo.origin == AnnotationOrigin.METHOD) {
|
|
||||||
val overridingMethod = element.sourcePsi as PsiMethod
|
|
||||||
val overriddenMethod = usageInfo.referenced as PsiMethod
|
|
||||||
val overridingAnnotation = overridingMethod.getAnnotation(ENFORCE_PERMISSION)
|
|
||||||
val overriddenAnnotation = overriddenMethod.getAnnotation(ENFORCE_PERMISSION)!!
|
|
||||||
|
|
||||||
val location = context.getLocation(element)
|
|
||||||
val overridingClass = overridingMethod.parent as PsiClass
|
|
||||||
val overriddenClass = overriddenMethod.parent as PsiClass
|
|
||||||
val overridingName = "${overridingClass.name}.${overridingMethod.name}"
|
|
||||||
val overriddenName = "${overriddenClass.name}.${overriddenMethod.name}"
|
|
||||||
if (overridingAnnotation == null) {
|
|
||||||
val msg = "The method $overridingName overrides the method $overriddenName which " +
|
|
||||||
"is annotated with @EnforcePermission. The same annotation must be used " +
|
|
||||||
"on $overridingName"
|
|
||||||
context.report(ISSUE_MISSING_ENFORCE_PERMISSION, element, location, msg)
|
|
||||||
} else if (!areAnnotationsEquivalent(
|
|
||||||
context, overridingAnnotation, overriddenAnnotation)) {
|
|
||||||
val msg = "The method $overridingName is annotated with " +
|
|
||||||
"${overridingAnnotation.text} which differs from the overridden " +
|
|
||||||
"method $overriddenName: ${overriddenAnnotation.text}. The same " +
|
|
||||||
"annotation must be used for both methods."
|
|
||||||
context.report(ISSUE_MISMATCHING_ENFORCE_PERMISSION, element, location, msg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
val EXPLANATION = """
|
|
||||||
The @EnforcePermission annotation is used to indicate that the underlying binder code
|
|
||||||
has already verified the caller's permissions before calling the appropriate method. The
|
|
||||||
verification code is usually generated by the AIDL compiler, which also takes care of
|
|
||||||
annotating the generated Java code.
|
|
||||||
|
|
||||||
In order to surface that information to platform developers, the same annotation must be
|
|
||||||
used on the implementation class or methods.
|
|
||||||
"""
|
|
||||||
|
|
||||||
val ISSUE_MISSING_ENFORCE_PERMISSION: Issue = Issue.create(
|
|
||||||
id = "MissingEnforcePermissionAnnotation",
|
|
||||||
briefDescription = "Missing @EnforcePermission annotation on Binder method",
|
|
||||||
explanation = EXPLANATION,
|
|
||||||
category = Category.SECURITY,
|
|
||||||
priority = 6,
|
|
||||||
severity = Severity.ERROR,
|
|
||||||
implementation = Implementation(
|
|
||||||
EnforcePermissionDetector::class.java,
|
|
||||||
Scope.JAVA_FILE_SCOPE
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
val ISSUE_MISMATCHING_ENFORCE_PERMISSION: Issue = Issue.create(
|
|
||||||
id = "MismatchingEnforcePermissionAnnotation",
|
|
||||||
briefDescription = "Incorrect @EnforcePermission annotation on Binder method",
|
|
||||||
explanation = EXPLANATION,
|
|
||||||
category = Category.SECURITY,
|
|
||||||
priority = 6,
|
|
||||||
severity = Severity.ERROR,
|
|
||||||
implementation = Implementation(
|
|
||||||
EnforcePermissionDetector::class.java,
|
|
||||||
Scope.JAVA_FILE_SCOPE
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,202 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2022 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.google.android.lint
|
|
||||||
|
|
||||||
import com.android.tools.lint.checks.infrastructure.LintDetectorTest
|
|
||||||
import com.android.tools.lint.checks.infrastructure.TestFile
|
|
||||||
import com.android.tools.lint.checks.infrastructure.TestLintTask
|
|
||||||
import com.android.tools.lint.detector.api.Detector
|
|
||||||
import com.android.tools.lint.detector.api.Issue
|
|
||||||
|
|
||||||
@Suppress("UnstableApiUsage")
|
|
||||||
class EnforcePermissionDetectorTest : LintDetectorTest() {
|
|
||||||
override fun getDetector(): Detector = EnforcePermissionDetector()
|
|
||||||
|
|
||||||
override fun getIssues(): List<Issue> = listOf(
|
|
||||||
EnforcePermissionDetector.ISSUE_MISSING_ENFORCE_PERMISSION,
|
|
||||||
EnforcePermissionDetector.ISSUE_MISMATCHING_ENFORCE_PERMISSION
|
|
||||||
)
|
|
||||||
|
|
||||||
override fun lint(): TestLintTask = super.lint().allowMissingSdk(true)
|
|
||||||
|
|
||||||
fun testDoesNotDetectIssuesCorrectAnnotationOnClass() {
|
|
||||||
lint().files(java(
|
|
||||||
"""
|
|
||||||
package test.pkg;
|
|
||||||
@android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE)
|
|
||||||
public class TestClass1 extends IFoo.Stub {
|
|
||||||
public void testMethod() {}
|
|
||||||
}
|
|
||||||
""").indented(),
|
|
||||||
*stubs
|
|
||||||
)
|
|
||||||
.run()
|
|
||||||
.expectClean()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun testDoesNotDetectIssuesCorrectAnnotationOnMethod() {
|
|
||||||
lint().files(java(
|
|
||||||
"""
|
|
||||||
package test.pkg;
|
|
||||||
import android.annotation.EnforcePermission;
|
|
||||||
public class TestClass2 extends IFooMethod.Stub {
|
|
||||||
@Override
|
|
||||||
@EnforcePermission(android.Manifest.permission.READ_PHONE_STATE)
|
|
||||||
public void testMethod() {}
|
|
||||||
}
|
|
||||||
""").indented(),
|
|
||||||
*stubs
|
|
||||||
)
|
|
||||||
.run()
|
|
||||||
.expectClean()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun testDetectIssuesMismatchingAnnotationOnClass() {
|
|
||||||
lint().files(java(
|
|
||||||
"""
|
|
||||||
package test.pkg;
|
|
||||||
@android.annotation.EnforcePermission(android.Manifest.permission.INTERNET)
|
|
||||||
public class TestClass3 extends IFoo.Stub {
|
|
||||||
public void testMethod() {}
|
|
||||||
}
|
|
||||||
""").indented(),
|
|
||||||
*stubs
|
|
||||||
)
|
|
||||||
.run()
|
|
||||||
.expect("""src/test/pkg/TestClass3.java:3: Error: The class test.pkg.TestClass3 is \
|
|
||||||
annotated with @android.annotation.EnforcePermission(android.Manifest.permission.INTERNET) \
|
|
||||||
which differs from the parent class IFoo.Stub: \
|
|
||||||
@android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE). The \
|
|
||||||
same annotation must be used for both classes. [MismatchingEnforcePermissionAnnotation]
|
|
||||||
public class TestClass3 extends IFoo.Stub {
|
|
||||||
~~~~~~~~~
|
|
||||||
1 errors, 0 warnings""".addLineContinuation())
|
|
||||||
}
|
|
||||||
|
|
||||||
fun testDetectIssuesMismatchingAnnotationOnMethod() {
|
|
||||||
lint().files(java(
|
|
||||||
"""
|
|
||||||
package test.pkg;
|
|
||||||
public class TestClass4 extends IFooMethod.Stub {
|
|
||||||
@android.annotation.EnforcePermission(android.Manifest.permission.INTERNET)
|
|
||||||
public void testMethod() {}
|
|
||||||
}
|
|
||||||
""").indented(),
|
|
||||||
*stubs
|
|
||||||
)
|
|
||||||
.run()
|
|
||||||
.expect("""src/test/pkg/TestClass4.java:4: Error: The method TestClass4.testMethod is \
|
|
||||||
annotated with @android.annotation.EnforcePermission(android.Manifest.permission.INTERNET) \
|
|
||||||
which differs from the overridden method Stub.testMethod: \
|
|
||||||
@android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE). The same \
|
|
||||||
annotation must be used for both methods. [MismatchingEnforcePermissionAnnotation]
|
|
||||||
public void testMethod() {}
|
|
||||||
~~~~~~~~~~
|
|
||||||
1 errors, 0 warnings""".addLineContinuation())
|
|
||||||
}
|
|
||||||
|
|
||||||
fun testDetectIssuesMissingAnnotationOnClass() {
|
|
||||||
lint().files(java(
|
|
||||||
"""
|
|
||||||
package test.pkg;
|
|
||||||
public class TestClass5 extends IFoo.Stub {
|
|
||||||
public void testMethod() {}
|
|
||||||
}
|
|
||||||
""").indented(),
|
|
||||||
*stubs
|
|
||||||
)
|
|
||||||
.run()
|
|
||||||
.expect("""src/test/pkg/TestClass5.java:2: Error: The class test.pkg.TestClass5 extends \
|
|
||||||
the class IFoo.Stub which is annotated with @EnforcePermission. The same annotation must be \
|
|
||||||
used on test.pkg.TestClass5. [MissingEnforcePermissionAnnotation]
|
|
||||||
public class TestClass5 extends IFoo.Stub {
|
|
||||||
~~~~~~~~~
|
|
||||||
1 errors, 0 warnings""".addLineContinuation())
|
|
||||||
}
|
|
||||||
|
|
||||||
fun testDetectIssuesMissingAnnotationOnMethod() {
|
|
||||||
lint().files(java(
|
|
||||||
"""
|
|
||||||
package test.pkg;
|
|
||||||
public class TestClass6 extends IFooMethod.Stub {
|
|
||||||
public void testMethod() {}
|
|
||||||
}
|
|
||||||
""").indented(),
|
|
||||||
*stubs
|
|
||||||
)
|
|
||||||
.run()
|
|
||||||
.expect("""src/test/pkg/TestClass6.java:3: Error: The method TestClass6.testMethod \
|
|
||||||
overrides the method Stub.testMethod which is annotated with @EnforcePermission. The same \
|
|
||||||
annotation must be used on TestClass6.testMethod [MissingEnforcePermissionAnnotation]
|
|
||||||
public void testMethod() {}
|
|
||||||
~~~~~~~~~~
|
|
||||||
1 errors, 0 warnings""".addLineContinuation())
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Stubs */
|
|
||||||
|
|
||||||
private val interfaceIFooStub: TestFile = java(
|
|
||||||
"""
|
|
||||||
@android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE)
|
|
||||||
public interface IFoo {
|
|
||||||
@android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE)
|
|
||||||
public static abstract class Stub implements IFoo {
|
|
||||||
@Override
|
|
||||||
public void testMethod() {}
|
|
||||||
}
|
|
||||||
public void testMethod();
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
).indented()
|
|
||||||
|
|
||||||
private val interfaceIFooMethodStub: TestFile = java(
|
|
||||||
"""
|
|
||||||
public interface IFooMethod {
|
|
||||||
public static abstract class Stub implements IFooMethod {
|
|
||||||
@Override
|
|
||||||
@android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE)
|
|
||||||
public void testMethod() {}
|
|
||||||
}
|
|
||||||
@android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE)
|
|
||||||
public void testMethod();
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
).indented()
|
|
||||||
|
|
||||||
private val manifestPermissionStub: TestFile = java(
|
|
||||||
"""
|
|
||||||
package android.Manifest;
|
|
||||||
class permission {
|
|
||||||
public static final String READ_PHONE_STATE = "android.permission.READ_PHONE_STATE";
|
|
||||||
public static final String INTERNET = "android.permission.INTERNET";
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
).indented()
|
|
||||||
|
|
||||||
private val enforcePermissionAnnotationStub: TestFile = java(
|
|
||||||
"""
|
|
||||||
package android.annotation;
|
|
||||||
public @interface EnforcePermission {}
|
|
||||||
"""
|
|
||||||
).indented()
|
|
||||||
|
|
||||||
private val stubs = arrayOf(interfaceIFooStub, interfaceIFooMethodStub,
|
|
||||||
manifestPermissionStub, enforcePermissionAnnotationStub)
|
|
||||||
|
|
||||||
// Substitutes "backslash + new line" with an empty string to imitate line continuation
|
|
||||||
private fun String.addLineContinuation(): String = this.trimIndent().replace("\\\n", "")
|
|
||||||
}
|
|
||||||
29
tools/lint/common/Android.bp
Normal file
29
tools/lint/common/Android.bp
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
// Copyright (C) 2022 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 {
|
||||||
|
// See: http://go/android-license-faq
|
||||||
|
// A large-scale-change added 'default_applicable_licenses' to import
|
||||||
|
// all of the 'license_kinds' from "frameworks_base_license"
|
||||||
|
// to get the below license kinds:
|
||||||
|
// SPDX-license-identifier-Apache-2.0
|
||||||
|
default_applicable_licenses: ["frameworks_base_license"],
|
||||||
|
}
|
||||||
|
|
||||||
|
java_library_host {
|
||||||
|
name: "AndroidCommonLint",
|
||||||
|
srcs: ["src/main/java/**/*.kt"],
|
||||||
|
libs: ["lint_api"],
|
||||||
|
kotlincflags: ["-Xjvm-default=all"],
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2022 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.google.android.lint
|
||||||
|
|
||||||
|
import com.google.android.lint.model.Method
|
||||||
|
|
||||||
|
const val CLASS_STUB = "Stub"
|
||||||
|
const val CLASS_CONTEXT = "android.content.Context"
|
||||||
|
const val CLASS_ACTIVITY_MANAGER_SERVICE = "com.android.server.am.ActivityManagerService"
|
||||||
|
const val CLASS_ACTIVITY_MANAGER_INTERNAL = "android.app.ActivityManagerInternal"
|
||||||
|
|
||||||
|
// Enforce permission APIs
|
||||||
|
val ENFORCE_PERMISSION_METHODS = listOf(
|
||||||
|
Method(CLASS_CONTEXT, "checkPermission"),
|
||||||
|
Method(CLASS_CONTEXT, "checkCallingPermission"),
|
||||||
|
Method(CLASS_CONTEXT, "checkCallingOrSelfPermission"),
|
||||||
|
Method(CLASS_CONTEXT, "enforcePermission"),
|
||||||
|
Method(CLASS_CONTEXT, "enforceCallingPermission"),
|
||||||
|
Method(CLASS_CONTEXT, "enforceCallingOrSelfPermission"),
|
||||||
|
Method(CLASS_ACTIVITY_MANAGER_SERVICE, "checkPermission"),
|
||||||
|
Method(CLASS_ACTIVITY_MANAGER_INTERNAL, "enforceCallingPermission")
|
||||||
|
)
|
||||||
|
|
||||||
|
const val ANNOTATION_PERMISSION_METHOD = "android.annotation.PermissionMethod"
|
||||||
|
const val ANNOTATION_PERMISSION_NAME = "android.annotation.PermissionName"
|
||||||
|
const val ANNOTATION_PERMISSION_RESULT = "android.content.pm.PackageManager.PermissionResult"
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2022 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.google.android.lint
|
||||||
|
|
||||||
|
import com.android.tools.lint.detector.api.getUMethod
|
||||||
|
import org.jetbrains.uast.UAnnotation
|
||||||
|
import org.jetbrains.uast.UCallExpression
|
||||||
|
import org.jetbrains.uast.UElement
|
||||||
|
import org.jetbrains.uast.UMethod
|
||||||
|
import org.jetbrains.uast.UParameter
|
||||||
|
import org.jetbrains.uast.UQualifiedReferenceExpression
|
||||||
|
|
||||||
|
fun isPermissionMethodCall(callExpression: UCallExpression): Boolean {
|
||||||
|
val method = callExpression.resolve()?.getUMethod() ?: return false
|
||||||
|
return hasPermissionMethodAnnotation(method)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun hasPermissionMethodAnnotation(method: UMethod): Boolean =
|
||||||
|
getPermissionMethodAnnotation(method) != null
|
||||||
|
|
||||||
|
fun getPermissionMethodAnnotation(method: UMethod?): UAnnotation? = method?.uAnnotations
|
||||||
|
?.firstOrNull { it.qualifiedName == ANNOTATION_PERMISSION_METHOD }
|
||||||
|
|
||||||
|
fun hasPermissionNameAnnotation(parameter: UParameter) = parameter.annotations.any {
|
||||||
|
it.hasQualifiedName(ANNOTATION_PERMISSION_NAME)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to return a CallExpression from a QualifiedReferenceExpression (or returns it directly if passed directly)
|
||||||
|
* @param callOrReferenceCall expected to be UCallExpression or UQualifiedReferenceExpression
|
||||||
|
* @return UCallExpression, if available
|
||||||
|
*/
|
||||||
|
fun findCallExpression(callOrReferenceCall: UElement?): UCallExpression? =
|
||||||
|
when (callOrReferenceCall) {
|
||||||
|
is UCallExpression -> callOrReferenceCall
|
||||||
|
is UQualifiedReferenceExpression -> callOrReferenceCall.selector as? UCallExpression
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2022 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.google.android.lint.model
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data class to represent a Method
|
||||||
|
*/
|
||||||
|
data class Method(val clazz: String, val name: String) {
|
||||||
|
override fun toString(): String {
|
||||||
|
return "$clazz#$name"
|
||||||
|
}
|
||||||
|
}
|
||||||
33
tools/lint/fix/Android.bp
Normal file
33
tools/lint/fix/Android.bp
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
// Copyright (C) 2022 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 {
|
||||||
|
// See: http://go/android-license-faq
|
||||||
|
// A large-scale-change added 'default_applicable_licenses' to import
|
||||||
|
// all of the 'license_kinds' from "frameworks_base_license"
|
||||||
|
// to get the below license kinds:
|
||||||
|
// SPDX-license-identifier-Apache-2.0
|
||||||
|
default_applicable_licenses: ["frameworks_base_license"],
|
||||||
|
}
|
||||||
|
|
||||||
|
python_binary_host {
|
||||||
|
name: "lint_fix",
|
||||||
|
main: "soong_lint_fix.py",
|
||||||
|
srcs: ["soong_lint_fix.py"],
|
||||||
|
}
|
||||||
|
|
||||||
|
python_library_host {
|
||||||
|
name: "soong_lint_fix",
|
||||||
|
srcs: ["soong_lint_fix.py"],
|
||||||
|
}
|
||||||
30
tools/lint/fix/README.md
Normal file
30
tools/lint/fix/README.md
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# Refactoring the platform with lint
|
||||||
|
Inspiration: go/refactor-the-platform-with-lint\
|
||||||
|
**Special Thanks: brufino@, azharaa@, for the prior work that made this all possible**
|
||||||
|
|
||||||
|
## What is this?
|
||||||
|
|
||||||
|
It's a python script that runs the framework linter,
|
||||||
|
and then (optionally) copies modified files back into the source tree.\
|
||||||
|
Why python, you ask? Because python is cool ¯\_(ツ)_/¯.
|
||||||
|
|
||||||
|
Incidentally, this exposes a much simpler way to run individual lint checks
|
||||||
|
against individual modules, so it's useful beyond applying fixes.
|
||||||
|
|
||||||
|
## Why?
|
||||||
|
|
||||||
|
Lint is not allowed to modify source files directly via lint's `--apply-suggestions` flag.
|
||||||
|
As a compromise, soong zips up the (potentially) modified sources and leaves them in an intermediate
|
||||||
|
directory. This script runs the lint, unpacks those files, and copies them back into the tree.
|
||||||
|
|
||||||
|
## How do I run it?
|
||||||
|
**WARNING: You probably want to commit/stash any changes to your working tree before doing this...**
|
||||||
|
|
||||||
|
```
|
||||||
|
source build/envsetup.sh
|
||||||
|
lunch cf_x86_64_phone-userdebug # or any lunch target
|
||||||
|
m lint_fix
|
||||||
|
lint_fix -h
|
||||||
|
```
|
||||||
|
|
||||||
|
The script's help output explains things that are omitted here.
|
||||||
173
tools/lint/fix/soong_lint_fix.py
Normal file
173
tools/lint/fix/soong_lint_fix.py
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
# Copyright (C) 2022 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.
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import zipfile
|
||||||
|
|
||||||
|
ANDROID_BUILD_TOP = os.environ.get("ANDROID_BUILD_TOP")
|
||||||
|
ANDROID_PRODUCT_OUT = os.environ.get("ANDROID_PRODUCT_OUT")
|
||||||
|
PRODUCT_OUT = ANDROID_PRODUCT_OUT.removeprefix(f"{ANDROID_BUILD_TOP}/")
|
||||||
|
|
||||||
|
SOONG_UI = "build/soong/soong_ui.bash"
|
||||||
|
PATH_PREFIX = "out/soong/.intermediates"
|
||||||
|
PATH_SUFFIX = "android_common/lint"
|
||||||
|
FIX_ZIP = "suggested-fixes.zip"
|
||||||
|
|
||||||
|
class SoongLintFix:
|
||||||
|
"""
|
||||||
|
This class creates a command line tool that will
|
||||||
|
apply lint fixes to the platform via the necessary
|
||||||
|
combination of soong and shell commands.
|
||||||
|
|
||||||
|
It breaks up these operations into a few "private" methods
|
||||||
|
that are intentionally exposed so experimental code can tweak behavior.
|
||||||
|
|
||||||
|
The entry point, `run`, will apply lint fixes using the
|
||||||
|
intermediate `suggested-fixes` directory that soong creates during its
|
||||||
|
invocation of lint.
|
||||||
|
|
||||||
|
Basic usage:
|
||||||
|
```
|
||||||
|
from soong_lint_fix import SoongLintFix
|
||||||
|
|
||||||
|
SoongLintFix().run()
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
def __init__(self):
|
||||||
|
self._parser = _setup_parser()
|
||||||
|
self._args = None
|
||||||
|
self._kwargs = None
|
||||||
|
self._path = None
|
||||||
|
self._target = None
|
||||||
|
|
||||||
|
|
||||||
|
def run(self, additional_setup=None, custom_fix=None):
|
||||||
|
"""
|
||||||
|
Run the script
|
||||||
|
"""
|
||||||
|
self._setup()
|
||||||
|
self._find_module()
|
||||||
|
self._lint()
|
||||||
|
|
||||||
|
if not self._args.no_fix:
|
||||||
|
self._fix()
|
||||||
|
|
||||||
|
if self._args.print:
|
||||||
|
self._print()
|
||||||
|
|
||||||
|
def _setup(self):
|
||||||
|
self._args = self._parser.parse_args()
|
||||||
|
env = os.environ.copy()
|
||||||
|
if self._args.check:
|
||||||
|
env["ANDROID_LINT_CHECK"] = self._args.check
|
||||||
|
if self._args.lint_module:
|
||||||
|
env["ANDROID_LINT_CHECK_EXTRA_MODULES"] = self._args.lint_module
|
||||||
|
|
||||||
|
self._kwargs = {
|
||||||
|
"env": env,
|
||||||
|
"executable": "/bin/bash",
|
||||||
|
"shell": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
os.chdir(ANDROID_BUILD_TOP)
|
||||||
|
|
||||||
|
|
||||||
|
def _find_module(self):
|
||||||
|
print("Refreshing soong modules...")
|
||||||
|
try:
|
||||||
|
os.mkdir(ANDROID_PRODUCT_OUT)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
subprocess.call(f"{SOONG_UI} --make-mode {PRODUCT_OUT}/module-info.json", **self._kwargs)
|
||||||
|
print("done.")
|
||||||
|
|
||||||
|
with open(f"{ANDROID_PRODUCT_OUT}/module-info.json") as f:
|
||||||
|
module_info = json.load(f)
|
||||||
|
|
||||||
|
if self._args.module not in module_info:
|
||||||
|
sys.exit(f"Module {self._args.module} not found!")
|
||||||
|
|
||||||
|
module_path = module_info[self._args.module]["path"][0]
|
||||||
|
print(f"Found module {module_path}/{self._args.module}.")
|
||||||
|
|
||||||
|
self._path = f"{PATH_PREFIX}/{module_path}/{self._args.module}/{PATH_SUFFIX}"
|
||||||
|
self._target = f"{self._path}/lint-report.txt"
|
||||||
|
|
||||||
|
|
||||||
|
def _lint(self):
|
||||||
|
print("Cleaning up any old lint results...")
|
||||||
|
try:
|
||||||
|
os.remove(f"{self._target}")
|
||||||
|
os.remove(f"{self._path}/{FIX_ZIP}")
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
print("done.")
|
||||||
|
|
||||||
|
print(f"Generating {self._target}")
|
||||||
|
subprocess.call(f"{SOONG_UI} --make-mode {self._target}", **self._kwargs)
|
||||||
|
print("done.")
|
||||||
|
|
||||||
|
|
||||||
|
def _fix(self):
|
||||||
|
print("Copying suggested fixes to the tree...")
|
||||||
|
with zipfile.ZipFile(f"{self._path}/{FIX_ZIP}") as zip:
|
||||||
|
for name in zip.namelist():
|
||||||
|
if name.startswith("out") or not name.endswith(".java"):
|
||||||
|
continue
|
||||||
|
with zip.open(name) as src, open(f"{ANDROID_BUILD_TOP}/{name}", "wb") as dst:
|
||||||
|
shutil.copyfileobj(src, dst)
|
||||||
|
print("done.")
|
||||||
|
|
||||||
|
|
||||||
|
def _print(self):
|
||||||
|
print("### lint-report.txt ###", end="\n\n")
|
||||||
|
with open(self._target, "r") as f:
|
||||||
|
print(f.read())
|
||||||
|
|
||||||
|
|
||||||
|
def _setup_parser():
|
||||||
|
parser = argparse.ArgumentParser(description="""
|
||||||
|
This is a python script that applies lint fixes to the platform:
|
||||||
|
1. Set up the environment, etc.
|
||||||
|
2. Run lint on the specified target.
|
||||||
|
3. Copy the modified files, from soong's intermediate directory, back into the tree.
|
||||||
|
|
||||||
|
**Gotcha**: You must have run `source build/envsetup.sh` and `lunch` first.
|
||||||
|
""", formatter_class=argparse.RawTextHelpFormatter)
|
||||||
|
|
||||||
|
parser.add_argument('module',
|
||||||
|
help='The soong build module to run '
|
||||||
|
'(e.g. framework-minus-apex or services.core.unboosted)')
|
||||||
|
|
||||||
|
parser.add_argument('--check',
|
||||||
|
help='Which lint to run. Passed to the ANDROID_LINT_CHECK environment variable.')
|
||||||
|
|
||||||
|
parser.add_argument('--lint-module',
|
||||||
|
help='Specific lint module to run. Passed to the ANDROID_LINT_CHECK_EXTRA_MODULES environment variable.')
|
||||||
|
|
||||||
|
parser.add_argument('--no-fix', action='store_true',
|
||||||
|
help='Just build and run the lint, do NOT apply the fixes.')
|
||||||
|
|
||||||
|
parser.add_argument('--print', action='store_true',
|
||||||
|
help='Print the contents of the generated lint-report.txt at the end.')
|
||||||
|
|
||||||
|
return parser
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
SoongLintFix().run()
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright (C) 2021 The Android Open Source Project
|
// Copyright (C) 2022 The Android Open Source Project
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
@@ -29,17 +29,14 @@ java_library_host {
|
|||||||
"auto_service_annotations",
|
"auto_service_annotations",
|
||||||
"lint_api",
|
"lint_api",
|
||||||
],
|
],
|
||||||
|
static_libs: [
|
||||||
|
"AndroidCommonLint",
|
||||||
|
],
|
||||||
kotlincflags: ["-Xjvm-default=all"],
|
kotlincflags: ["-Xjvm-default=all"],
|
||||||
}
|
}
|
||||||
|
|
||||||
java_test_host {
|
java_test_host {
|
||||||
name: "AndroidFrameworkLintCheckerTest",
|
name: "AndroidFrameworkLintCheckerTest",
|
||||||
// TODO(b/239881504): Since this test was written, Android
|
|
||||||
// Lint was updated, and now includes classes that were
|
|
||||||
// compiled for java 15. The soong build doesn't support
|
|
||||||
// java 15 yet, so we can't compile against "lint". Disable
|
|
||||||
// the test until java 15 is supported.
|
|
||||||
enabled: false,
|
|
||||||
srcs: ["checks/src/test/java/**/*.kt"],
|
srcs: ["checks/src/test/java/**/*.kt"],
|
||||||
static_libs: [
|
static_libs: [
|
||||||
"AndroidFrameworkLintChecker",
|
"AndroidFrameworkLintChecker",
|
||||||
@@ -49,5 +46,19 @@ java_test_host {
|
|||||||
],
|
],
|
||||||
test_options: {
|
test_options: {
|
||||||
unit_test: true,
|
unit_test: true,
|
||||||
|
tradefed_options: [
|
||||||
|
{
|
||||||
|
// lint bundles in some classes that were built with older versions
|
||||||
|
// of libraries, and no longer load. Since tradefed tries to load
|
||||||
|
// all classes in the jar to look for tests, it crashes loading them.
|
||||||
|
// Exclude these classes from tradefed's search.
|
||||||
|
name: "exclude-paths",
|
||||||
|
value: "org/apache",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "exclude-paths",
|
||||||
|
value: "META-INF",
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2021 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.google.android.lint
|
||||||
|
|
||||||
|
import com.android.tools.lint.client.api.IssueRegistry
|
||||||
|
import com.android.tools.lint.client.api.Vendor
|
||||||
|
import com.android.tools.lint.detector.api.CURRENT_API
|
||||||
|
import com.google.android.lint.parcel.SaferParcelChecker
|
||||||
|
import com.google.auto.service.AutoService
|
||||||
|
|
||||||
|
@AutoService(IssueRegistry::class)
|
||||||
|
@Suppress("UnstableApiUsage")
|
||||||
|
class AndroidFrameworkIssueRegistry : IssueRegistry() {
|
||||||
|
override val issues = listOf(
|
||||||
|
CallingIdentityTokenDetector.ISSUE_UNUSED_TOKEN,
|
||||||
|
CallingIdentityTokenDetector.ISSUE_NON_FINAL_TOKEN,
|
||||||
|
CallingIdentityTokenDetector.ISSUE_NESTED_CLEAR_IDENTITY_CALLS,
|
||||||
|
CallingIdentityTokenDetector.ISSUE_RESTORE_IDENTITY_CALL_NOT_IN_FINALLY_BLOCK,
|
||||||
|
CallingIdentityTokenDetector.ISSUE_USE_OF_CALLER_AWARE_METHODS_WITH_CLEARED_IDENTITY,
|
||||||
|
CallingIdentityTokenDetector.ISSUE_CLEAR_IDENTITY_CALL_NOT_FOLLOWED_BY_TRY_FINALLY,
|
||||||
|
CallingIdentityTokenDetector.ISSUE_RESULT_OF_CLEAR_IDENTITY_CALL_NOT_STORED_IN_VARIABLE,
|
||||||
|
CallingSettingsNonUserGetterMethodsDetector.ISSUE_NON_USER_GETTER_CALLED,
|
||||||
|
SaferParcelChecker.ISSUE_UNSAFE_API_USAGE,
|
||||||
|
PackageVisibilityDetector.ISSUE_PACKAGE_NAME_NO_PACKAGE_VISIBILITY_FILTERS,
|
||||||
|
PermissionMethodDetector.ISSUE_PERMISSION_METHOD_USAGE,
|
||||||
|
PermissionMethodDetector.ISSUE_CAN_BE_PERMISSION_METHOD,
|
||||||
|
)
|
||||||
|
|
||||||
|
override val api: Int
|
||||||
|
get() = CURRENT_API
|
||||||
|
|
||||||
|
override val minApi: Int
|
||||||
|
get() = 8
|
||||||
|
|
||||||
|
override val vendor: Vendor = Vendor(
|
||||||
|
vendorName = "Android",
|
||||||
|
feedbackUrl = "http://b/issues/new?component=315013",
|
||||||
|
contact = "brufino@google.com"
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -33,6 +33,7 @@ import org.jetbrains.uast.UBlockExpression
|
|||||||
import org.jetbrains.uast.UCallExpression
|
import org.jetbrains.uast.UCallExpression
|
||||||
import org.jetbrains.uast.UDeclarationsExpression
|
import org.jetbrains.uast.UDeclarationsExpression
|
||||||
import org.jetbrains.uast.UElement
|
import org.jetbrains.uast.UElement
|
||||||
|
import org.jetbrains.uast.UIfExpression
|
||||||
import org.jetbrains.uast.ULocalVariable
|
import org.jetbrains.uast.ULocalVariable
|
||||||
import org.jetbrains.uast.USimpleNameReferenceExpression
|
import org.jetbrains.uast.USimpleNameReferenceExpression
|
||||||
import org.jetbrains.uast.UTryExpression
|
import org.jetbrains.uast.UTryExpression
|
||||||
@@ -144,16 +145,40 @@ class CallingIdentityTokenDetector : Detector(), SourceCodeScanner {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* For every method():
|
|
||||||
* - Checks use of caller-aware methods issue
|
|
||||||
* For every call of Binder.restoreCallingIdentity(token):
|
|
||||||
* - Checks for restoreCallingIdentity() not in the finally block issue
|
|
||||||
* - Removes token from tokensMap if token is within the scope of the method
|
|
||||||
*/
|
|
||||||
override fun visitCallExpression(node: UCallExpression) {
|
override fun visitCallExpression(node: UCallExpression) {
|
||||||
|
when {
|
||||||
|
isMethodCall(node, Method.BINDER_CLEAR_CALLING_IDENTITY) -> {
|
||||||
|
checkClearCallingIdentityCall(node)
|
||||||
|
}
|
||||||
|
isMethodCall(node, Method.BINDER_RESTORE_CALLING_IDENTITY) -> {
|
||||||
|
checkRestoreCallingIdentityCall(node)
|
||||||
|
}
|
||||||
|
isCallerAwareMethod(node) -> checkCallerAwareMethod(node)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkClearCallingIdentityCall(node: UCallExpression) {
|
||||||
|
var firstNonQualifiedParent = getFirstNonQualifiedParent(node)
|
||||||
|
// if the call expression is inside a ternary, and the ternary is assigned
|
||||||
|
// to a variable, then we are still technically assigning
|
||||||
|
// any result of clearCallingIdentity to a variable
|
||||||
|
if (firstNonQualifiedParent is UIfExpression && firstNonQualifiedParent.isTernary) {
|
||||||
|
firstNonQualifiedParent = firstNonQualifiedParent.uastParent
|
||||||
|
}
|
||||||
|
if (firstNonQualifiedParent !is ULocalVariable) {
|
||||||
|
context.report(
|
||||||
|
ISSUE_RESULT_OF_CLEAR_IDENTITY_CALL_NOT_STORED_IN_VARIABLE,
|
||||||
|
context.getLocation(node),
|
||||||
|
getIncidentMessageResultOfClearIdentityCallNotStoredInVariable(
|
||||||
|
node.getQualifiedParentOrThis().asRenderString()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkCallerAwareMethod(node: UCallExpression) {
|
||||||
val token = findFirstTokenInScope(node)
|
val token = findFirstTokenInScope(node)
|
||||||
if (isCallerAwareMethod(node) && token != null) {
|
if (token != null) {
|
||||||
context.report(
|
context.report(
|
||||||
ISSUE_USE_OF_CALLER_AWARE_METHODS_WITH_CLEARED_IDENTITY,
|
ISSUE_USE_OF_CALLER_AWARE_METHODS_WITH_CLEARED_IDENTITY,
|
||||||
context.getLocation(node),
|
context.getLocation(node),
|
||||||
@@ -162,11 +187,15 @@ class CallingIdentityTokenDetector : Detector(), SourceCodeScanner {
|
|||||||
node.asRenderString()
|
node.asRenderString()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
if (!isMethodCall(node, Method.BINDER_RESTORE_CALLING_IDENTITY)) return
|
}
|
||||||
val first = node.valueArguments[0].skipParenthesizedExprDown()
|
|
||||||
val arg = first as? USimpleNameReferenceExpression ?: return
|
/**
|
||||||
|
* - Checks for restoreCallingIdentity() not in the finally block issue
|
||||||
|
* - Removes token from tokensMap if token is within the scope of the method
|
||||||
|
*/
|
||||||
|
private fun checkRestoreCallingIdentityCall(node: UCallExpression) {
|
||||||
|
val arg = node.valueArguments[0] as? USimpleNameReferenceExpression ?: return
|
||||||
val variableName = arg.identifier
|
val variableName = arg.identifier
|
||||||
val originalScope = tokensMap[variableName]?.scope ?: return
|
val originalScope = tokensMap[variableName]?.scope ?: return
|
||||||
val psi = arg.sourcePsi ?: return
|
val psi = arg.sourcePsi ?: return
|
||||||
@@ -174,15 +203,13 @@ class CallingIdentityTokenDetector : Detector(), SourceCodeScanner {
|
|||||||
// token declaration. If not within the scope, no action is needed because the token is
|
// token declaration. If not within the scope, no action is needed because the token is
|
||||||
// irrelevant i.e. not in the same scope or was not declared with clearCallingIdentity()
|
// irrelevant i.e. not in the same scope or was not declared with clearCallingIdentity()
|
||||||
if (!PsiSearchScopeUtil.isInScope(originalScope, psi)) return
|
if (!PsiSearchScopeUtil.isInScope(originalScope, psi)) return
|
||||||
// - We do not report "restore identity call not in finally" issue when there is no
|
// We do not report "restore identity call not in finally" issue when there is no
|
||||||
// finally block because that case is already handled by "clear identity call not
|
// finally block because that case is already handled by "clear identity call not
|
||||||
// followed by try-finally" issue
|
// followed by try-finally" issue
|
||||||
// - UCallExpression can be a child of UQualifiedReferenceExpression, i.e.
|
|
||||||
// receiver.selector, so to get the call's immediate parent we need to get the topmost
|
|
||||||
// parent qualified reference expression and access its parent
|
|
||||||
if (tokensMap[variableName]?.finallyBlock != null &&
|
if (tokensMap[variableName]?.finallyBlock != null &&
|
||||||
skipParenthesizedExprUp(node.getQualifiedParentOrThis().uastParent) !=
|
getFirstNonQualifiedParent(node) !=
|
||||||
tokensMap[variableName]?.finallyBlock) {
|
tokensMap[variableName]?.finallyBlock
|
||||||
|
) {
|
||||||
context.report(
|
context.report(
|
||||||
ISSUE_RESTORE_IDENTITY_CALL_NOT_IN_FINALLY_BLOCK,
|
ISSUE_RESTORE_IDENTITY_CALL_NOT_IN_FINALLY_BLOCK,
|
||||||
context.getLocation(node),
|
context.getLocation(node),
|
||||||
@@ -192,6 +219,13 @@ class CallingIdentityTokenDetector : Detector(), SourceCodeScanner {
|
|||||||
tokensMap.remove(variableName)
|
tokensMap.remove(variableName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getFirstNonQualifiedParent(expression: UCallExpression): UElement? {
|
||||||
|
// UCallExpression can be a child of UQualifiedReferenceExpression, i.e.
|
||||||
|
// receiver.selector, so to get the call's immediate parent we need to get the topmost
|
||||||
|
// parent qualified reference expression and access its parent
|
||||||
|
return skipParenthesizedExprUp(expression.getQualifiedParentOrThis().uastParent)
|
||||||
|
}
|
||||||
|
|
||||||
private fun isCallerAwareMethod(expression: UCallExpression): Boolean =
|
private fun isCallerAwareMethod(expression: UCallExpression): Boolean =
|
||||||
callerAwareMethods.any { method -> isMethodCall(expression, method) }
|
callerAwareMethods.any { method -> isMethodCall(expression, method) }
|
||||||
|
|
||||||
@@ -573,5 +607,42 @@ class CallingIdentityTokenDetector : Detector(), SourceCodeScanner {
|
|||||||
"identity instead of the caller's. Either explicitly query your own identity or " +
|
"identity instead of the caller's. Either explicitly query your own identity or " +
|
||||||
"move it after restoring the identity with " +
|
"move it after restoring the identity with " +
|
||||||
"`Binder.restoreCallingIdentity($variableName)`."
|
"`Binder.restoreCallingIdentity($variableName)`."
|
||||||
|
|
||||||
|
/** Issue: Result of Binder.clearCallingIdentity() is not stored in a variable */
|
||||||
|
@JvmField
|
||||||
|
val ISSUE_RESULT_OF_CLEAR_IDENTITY_CALL_NOT_STORED_IN_VARIABLE: Issue = Issue.create(
|
||||||
|
id = "ResultOfClearIdentityCallNotStoredInVariable",
|
||||||
|
briefDescription = "Result of Binder.clearCallingIdentity() is not stored in a " +
|
||||||
|
"variable",
|
||||||
|
explanation = """
|
||||||
|
You cleared the original calling identity with \
|
||||||
|
`Binder.clearCallingIdentity()`, but did not store the result of the method \
|
||||||
|
call in a variable. You need to store the result in a variable and restore it later.
|
||||||
|
|
||||||
|
Use the following pattern for running operations with your own identity:
|
||||||
|
|
||||||
|
```
|
||||||
|
final long token = Binder.clearCallingIdentity();
|
||||||
|
try {
|
||||||
|
// Code using your own identity
|
||||||
|
} finally {
|
||||||
|
Binder.restoreCallingIdentity(token);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
""",
|
||||||
|
category = Category.SECURITY,
|
||||||
|
priority = 6,
|
||||||
|
severity = Severity.WARNING,
|
||||||
|
implementation = Implementation(
|
||||||
|
CallingIdentityTokenDetector::class.java,
|
||||||
|
Scope.JAVA_FILE_SCOPE
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun getIncidentMessageResultOfClearIdentityCallNotStoredInVariable(
|
||||||
|
methodName: String
|
||||||
|
): String = "You cleared the original identity with `$methodName` but did not store the " +
|
||||||
|
"result in a variable. You need to store the result in a variable and restore it " +
|
||||||
|
"later."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,515 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2022 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.google.android.lint
|
||||||
|
|
||||||
|
import com.android.tools.lint.client.api.UastParser
|
||||||
|
import com.android.tools.lint.detector.api.Category
|
||||||
|
import com.android.tools.lint.detector.api.Context
|
||||||
|
import com.android.tools.lint.detector.api.Detector
|
||||||
|
import com.android.tools.lint.detector.api.Implementation
|
||||||
|
import com.android.tools.lint.detector.api.Issue
|
||||||
|
import com.android.tools.lint.detector.api.Scope
|
||||||
|
import com.android.tools.lint.detector.api.Severity
|
||||||
|
import com.android.tools.lint.detector.api.SourceCodeScanner
|
||||||
|
import com.android.tools.lint.detector.api.interprocedural.CallGraph
|
||||||
|
import com.android.tools.lint.detector.api.interprocedural.CallGraphResult
|
||||||
|
import com.android.tools.lint.detector.api.interprocedural.searchForPaths
|
||||||
|
import com.intellij.psi.PsiAnonymousClass
|
||||||
|
import com.intellij.psi.PsiMethod
|
||||||
|
import java.util.LinkedList
|
||||||
|
import org.jetbrains.uast.UCallExpression
|
||||||
|
import org.jetbrains.uast.UElement
|
||||||
|
import org.jetbrains.uast.UMethod
|
||||||
|
import org.jetbrains.uast.UParameter
|
||||||
|
import org.jetbrains.uast.USimpleNameReferenceExpression
|
||||||
|
import org.jetbrains.uast.visitor.AbstractUastVisitor
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A lint checker to detect potential package visibility issues for system's APIs. APIs working
|
||||||
|
* in the system_server and taking the package name as a parameter may have chance to reveal
|
||||||
|
* package existence status on the device, and break the
|
||||||
|
* <a href="https://developer.android.com/about/versions/11/privacy/package-visibility">
|
||||||
|
* Package Visibility</a> that we introduced in Android 11.
|
||||||
|
* <p>
|
||||||
|
* Take an example of the API `boolean setFoo(String packageName)`, a malicious app may have chance
|
||||||
|
* to detect package existence state on the device from the result of the API, if there is no
|
||||||
|
* package visibility filtering rule or uid identify checks applying to the parameter of the
|
||||||
|
* package name.
|
||||||
|
*/
|
||||||
|
class PackageVisibilityDetector : Detector(), SourceCodeScanner {
|
||||||
|
|
||||||
|
// Enables call graph analysis
|
||||||
|
override fun isCallGraphRequired(): Boolean = true
|
||||||
|
|
||||||
|
override fun analyzeCallGraph(
|
||||||
|
context: Context,
|
||||||
|
callGraph: CallGraphResult
|
||||||
|
) {
|
||||||
|
val systemServerApiNodes = callGraph.callGraph.nodes.filter(::isSystemServerApi)
|
||||||
|
val sinkMethodNodes = callGraph.callGraph.nodes.filter {
|
||||||
|
// TODO(b/228285232): Remove enforce permission sink methods
|
||||||
|
isNodeInList(it, ENFORCE_PERMISSION_METHODS) || isNodeInList(it, APPOPS_METHODS)
|
||||||
|
}
|
||||||
|
val parser = context.client.getUastParser(context.project)
|
||||||
|
analyzeApisContainPackageNameParameters(
|
||||||
|
context, parser, systemServerApiNodes, sinkMethodNodes)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Looking for API contains package name parameters, report the lint issue if the API does not
|
||||||
|
* invoke any sink methods.
|
||||||
|
*/
|
||||||
|
private fun analyzeApisContainPackageNameParameters(
|
||||||
|
context: Context,
|
||||||
|
parser: UastParser,
|
||||||
|
systemServerApiNodes: List<CallGraph.Node>,
|
||||||
|
sinkMethodNodes: List<CallGraph.Node>
|
||||||
|
) {
|
||||||
|
for (apiNode in systemServerApiNodes) {
|
||||||
|
val apiMethod = apiNode.getUMethod() ?: continue
|
||||||
|
val pkgNameParamIndexes = apiMethod.uastParameters.mapIndexedNotNull { index, param ->
|
||||||
|
if (Parameter(param) in PACKAGE_NAME_PATTERNS && apiNode.isArgumentInUse(index)) {
|
||||||
|
index
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}.takeIf(List<Int>::isNotEmpty) ?: continue
|
||||||
|
|
||||||
|
for (pkgNameParamIndex in pkgNameParamIndexes) {
|
||||||
|
// Trace the call path of the method's argument, pass the lint checks if a sink
|
||||||
|
// method is found
|
||||||
|
if (traceArgumentCallPath(
|
||||||
|
apiNode, pkgNameParamIndex, PACKAGE_NAME_SINK_METHOD_LIST)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Pass the check if one of the sink methods is invoked
|
||||||
|
if (hasValidPath(
|
||||||
|
searchForPaths(
|
||||||
|
sources = listOf(apiNode),
|
||||||
|
isSink = { it in sinkMethodNodes },
|
||||||
|
getNeighbors = { node -> node.edges.map { it.node!! } }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
) continue
|
||||||
|
|
||||||
|
// Report issue
|
||||||
|
val reportElement = apiMethod.uastParameters[pkgNameParamIndex] as UElement
|
||||||
|
val location = parser.createLocation(reportElement)
|
||||||
|
context.report(
|
||||||
|
ISSUE_PACKAGE_NAME_NO_PACKAGE_VISIBILITY_FILTERS,
|
||||||
|
location,
|
||||||
|
getMsgPackageNameNoPackageVisibilityFilters(apiMethod, pkgNameParamIndex)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns {@code true} if the method associated with the given node is a system server's
|
||||||
|
* public API that extends from Stub class.
|
||||||
|
*/
|
||||||
|
private fun isSystemServerApi(
|
||||||
|
node: CallGraph.Node
|
||||||
|
): Boolean {
|
||||||
|
val method = node.getUMethod() ?: return false
|
||||||
|
if (!method.hasModifierProperty("public") ||
|
||||||
|
method.uastBody == null ||
|
||||||
|
method.containingClass is PsiAnonymousClass) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
val className = method.containingClass?.qualifiedName ?: return false
|
||||||
|
if (!className.startsWith(SYSTEM_PACKAGE_PREFIX)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return (method.containingClass ?: return false).supers
|
||||||
|
.filter { it.name == CLASS_STUB }
|
||||||
|
.filter { it.qualifiedName !in BYPASS_STUBS }
|
||||||
|
.any { it.findMethodBySignature(method, /* checkBases */ true) != null }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns {@code true} if the list contains the node of the call graph.
|
||||||
|
*/
|
||||||
|
private fun isNodeInList(
|
||||||
|
node: CallGraph.Node,
|
||||||
|
filters: List<Method>
|
||||||
|
): Boolean {
|
||||||
|
val method = node.getUMethod() ?: return false
|
||||||
|
return Method(method) in filters
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trace the call paths of the argument of the method in the start entry. Return {@code true}
|
||||||
|
* if one of methods in the sink call list is invoked.
|
||||||
|
* Take an example of the call path:
|
||||||
|
* foo(packageName) -> a(packageName) -> b(packageName) -> filterAppAccess()
|
||||||
|
* It returns {@code true} if the filterAppAccess() is in the sink call list.
|
||||||
|
*/
|
||||||
|
private fun traceArgumentCallPath(
|
||||||
|
apiNode: CallGraph.Node,
|
||||||
|
pkgNameParamIndex: Int,
|
||||||
|
sinkList: List<Method>
|
||||||
|
): Boolean {
|
||||||
|
val startEntry = TraceEntry(apiNode, pkgNameParamIndex)
|
||||||
|
val traceQueue = LinkedList<TraceEntry>().apply { add(startEntry) }
|
||||||
|
val allVisits = mutableSetOf<TraceEntry>().apply { add(startEntry) }
|
||||||
|
while (!traceQueue.isEmpty()) {
|
||||||
|
val entry = traceQueue.poll()
|
||||||
|
val entryNode = entry.node
|
||||||
|
val entryMethod = entryNode.getUMethod() ?: continue
|
||||||
|
val entryArgumentName = entryMethod.uastParameters[entry.argumentIndex].name
|
||||||
|
for (outEdge in entryNode.edges) {
|
||||||
|
val outNode = outEdge.node ?: continue
|
||||||
|
val outMethod = outNode.getUMethod() ?: continue
|
||||||
|
val outArgumentIndex =
|
||||||
|
outEdge.call?.findArgumentIndex(
|
||||||
|
entryArgumentName, outMethod.uastParameters.size)
|
||||||
|
val sinkMethod = findInSinkList(outMethod, sinkList)
|
||||||
|
if (sinkMethod == null) {
|
||||||
|
if (outArgumentIndex == null) {
|
||||||
|
// Path is not relevant to the sink method and argument
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Path is relevant to the argument, add a new trace entry if never visit before
|
||||||
|
val newEntry = TraceEntry(outNode, outArgumentIndex)
|
||||||
|
if (newEntry !in allVisits) {
|
||||||
|
traceQueue.add(newEntry)
|
||||||
|
allVisits.add(newEntry)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (sinkMethod.matchArgument && outArgumentIndex == null) {
|
||||||
|
// The sink call is required to match the argument, but not found
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (sinkMethod.checkCaller &&
|
||||||
|
entryMethod.isInClearCallingIdentityScope(outEdge.call!!)) {
|
||||||
|
// The sink call is in the scope of Binder.clearCallingIdentify
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// A sink method is matched
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the UMethod associated with the given node of call graph.
|
||||||
|
*/
|
||||||
|
private fun CallGraph.Node.getUMethod(): UMethod? = this.target.element as? UMethod
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the system module name (e.g. com.android.server.pm) of the method of the
|
||||||
|
* call graph node.
|
||||||
|
*/
|
||||||
|
private fun CallGraph.Node.getModuleName(): String? {
|
||||||
|
val method = getUMethod() ?: return null
|
||||||
|
val className = method.containingClass?.qualifiedName ?: return null
|
||||||
|
if (!className.startsWith(SYSTEM_PACKAGE_PREFIX)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
val dotPos = className.indexOf(".", SYSTEM_PACKAGE_PREFIX.length)
|
||||||
|
if (dotPos == -1) {
|
||||||
|
return SYSTEM_PACKAGE_PREFIX
|
||||||
|
}
|
||||||
|
return className.substring(0, dotPos)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return {@code true} if the argument in the method's body is in-use.
|
||||||
|
*/
|
||||||
|
private fun CallGraph.Node.isArgumentInUse(argIndex: Int): Boolean {
|
||||||
|
val method = getUMethod() ?: return false
|
||||||
|
val argumentName = method.uastParameters[argIndex].name
|
||||||
|
var foundArg = false
|
||||||
|
val methodVisitor = object : AbstractUastVisitor() {
|
||||||
|
override fun visitSimpleNameReferenceExpression(
|
||||||
|
node: USimpleNameReferenceExpression
|
||||||
|
): Boolean {
|
||||||
|
if (node.identifier == argumentName) {
|
||||||
|
foundArg = true
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
method.uastBody?.accept(methodVisitor)
|
||||||
|
return foundArg
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given an argument name, returns the index of argument in the call expression.
|
||||||
|
*/
|
||||||
|
private fun UCallExpression.findArgumentIndex(
|
||||||
|
argumentName: String,
|
||||||
|
parameterSize: Int
|
||||||
|
): Int? {
|
||||||
|
if (valueArgumentCount == 0 || parameterSize == 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
var match = false
|
||||||
|
val argVisitor = object : AbstractUastVisitor() {
|
||||||
|
override fun visitSimpleNameReferenceExpression(
|
||||||
|
node: USimpleNameReferenceExpression
|
||||||
|
): Boolean {
|
||||||
|
if (node.identifier == argumentName) {
|
||||||
|
match = true
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
override fun visitCallExpression(node: UCallExpression): Boolean {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
valueArguments.take(parameterSize).forEachIndexed { index, argument ->
|
||||||
|
argument.accept(argVisitor)
|
||||||
|
if (match) {
|
||||||
|
return index
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a UMethod, returns a method from the sink method list.
|
||||||
|
*/
|
||||||
|
private fun findInSinkList(
|
||||||
|
uMethod: UMethod,
|
||||||
|
sinkCallList: List<Method>
|
||||||
|
): Method? {
|
||||||
|
return sinkCallList.find {
|
||||||
|
it == Method(uMethod) ||
|
||||||
|
it == Method(uMethod.containingClass?.qualifiedName ?: "", "*")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns {@code true} if the call expression is in the scope of the
|
||||||
|
* Binder.clearCallingIdentify.
|
||||||
|
*/
|
||||||
|
private fun UMethod.isInClearCallingIdentityScope(call: UCallExpression): Boolean {
|
||||||
|
var isInScope = false
|
||||||
|
val methodVisitor = object : AbstractUastVisitor() {
|
||||||
|
private var clearCallingIdentity = 0
|
||||||
|
override fun visitCallExpression(node: UCallExpression): Boolean {
|
||||||
|
if (call == node && clearCallingIdentity != 0) {
|
||||||
|
isInScope = true
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
val visitMethod = Method(node.resolve() ?: return false)
|
||||||
|
if (visitMethod == METHOD_CLEAR_CALLING_IDENTITY) {
|
||||||
|
clearCallingIdentity++
|
||||||
|
} else if (visitMethod == METHOD_RESTORE_CALLING_IDENTITY) {
|
||||||
|
clearCallingIdentity--
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
accept(methodVisitor)
|
||||||
|
return isInScope
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks the module name of the start node and the last node that invokes the sink method
|
||||||
|
* (e.g. checkPermission) in a path, returns {@code true} if one of the paths has the same
|
||||||
|
* module name for both nodes.
|
||||||
|
*/
|
||||||
|
private fun hasValidPath(paths: Collection<List<CallGraph.Node>>): Boolean {
|
||||||
|
for (pathNodes in paths) {
|
||||||
|
if (pathNodes.size < VALID_CALL_PATH_NODES_SIZE) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
val startModule = pathNodes[0].getModuleName() ?: continue
|
||||||
|
val lastCallModule = pathNodes[pathNodes.size - 2].getModuleName() ?: continue
|
||||||
|
if (startModule == lastCallModule) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A data class to represent the method.
|
||||||
|
*/
|
||||||
|
private data class Method(
|
||||||
|
val clazz: String,
|
||||||
|
val name: String
|
||||||
|
) {
|
||||||
|
// Used by traceArgumentCallPath to indicate that the method is required to match the
|
||||||
|
// argument name
|
||||||
|
var matchArgument = true
|
||||||
|
|
||||||
|
// Used by traceArgumentCallPath to indicate that the method is required to check whether
|
||||||
|
// the Binder.clearCallingIdentity is invoked.
|
||||||
|
var checkCaller = false
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
clazz: String,
|
||||||
|
name: String,
|
||||||
|
matchArgument: Boolean = true,
|
||||||
|
checkCaller: Boolean = false
|
||||||
|
) : this(clazz, name) {
|
||||||
|
this.matchArgument = matchArgument
|
||||||
|
this.checkCaller = checkCaller
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
method: PsiMethod
|
||||||
|
) : this(method.containingClass?.qualifiedName ?: "", method.name)
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
method: com.google.android.lint.model.Method
|
||||||
|
) : this(method.clazz, method.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A data class to represent the parameter of the method. The parameter name is converted to
|
||||||
|
* lower case letters for comparison.
|
||||||
|
*/
|
||||||
|
private data class Parameter private constructor(
|
||||||
|
val typeName: String,
|
||||||
|
val parameterName: String
|
||||||
|
) {
|
||||||
|
constructor(uParameter: UParameter) : this(
|
||||||
|
uParameter.type.canonicalText,
|
||||||
|
uParameter.name.lowercase()
|
||||||
|
)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun create(typeName: String, parameterName: String) =
|
||||||
|
Parameter(typeName, parameterName.lowercase())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A data class wraps a method node of the call graph and an index that indicates an
|
||||||
|
* argument of the method to record call trace information.
|
||||||
|
*/
|
||||||
|
private data class TraceEntry(
|
||||||
|
val node: CallGraph.Node,
|
||||||
|
val argumentIndex: Int
|
||||||
|
)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val SYSTEM_PACKAGE_PREFIX = "com.android.server."
|
||||||
|
// A valid call path list needs to contain a start node and a sink node
|
||||||
|
private const val VALID_CALL_PATH_NODES_SIZE = 2
|
||||||
|
|
||||||
|
private const val CLASS_STRING = "java.lang.String"
|
||||||
|
private const val CLASS_PACKAGE_MANAGER = "android.content.pm.PackageManager"
|
||||||
|
private const val CLASS_IPACKAGE_MANAGER = "android.content.pm.IPackageManager"
|
||||||
|
private const val CLASS_APPOPS_MANAGER = "android.app.AppOpsManager"
|
||||||
|
private const val CLASS_BINDER = "android.os.Binder"
|
||||||
|
private const val CLASS_PACKAGE_MANAGER_INTERNAL =
|
||||||
|
"android.content.pm.PackageManagerInternal"
|
||||||
|
|
||||||
|
// Patterns of package name parameter
|
||||||
|
private val PACKAGE_NAME_PATTERNS = setOf(
|
||||||
|
Parameter.create(CLASS_STRING, "packageName"),
|
||||||
|
Parameter.create(CLASS_STRING, "callingPackage"),
|
||||||
|
Parameter.create(CLASS_STRING, "callingPackageName"),
|
||||||
|
Parameter.create(CLASS_STRING, "pkgName"),
|
||||||
|
Parameter.create(CLASS_STRING, "callingPkg"),
|
||||||
|
Parameter.create(CLASS_STRING, "pkg")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Package manager APIs
|
||||||
|
private val PACKAGE_NAME_SINK_METHOD_LIST = listOf(
|
||||||
|
Method(CLASS_PACKAGE_MANAGER_INTERNAL, "filterAppAccess", matchArgument = false),
|
||||||
|
Method(CLASS_PACKAGE_MANAGER_INTERNAL, "canQueryPackage"),
|
||||||
|
Method(CLASS_PACKAGE_MANAGER_INTERNAL, "isSameApp"),
|
||||||
|
Method(CLASS_PACKAGE_MANAGER, "*", checkCaller = true),
|
||||||
|
Method(CLASS_IPACKAGE_MANAGER, "*", checkCaller = true),
|
||||||
|
Method(CLASS_PACKAGE_MANAGER, "getPackagesForUid", matchArgument = false),
|
||||||
|
Method(CLASS_IPACKAGE_MANAGER, "getPackagesForUid", matchArgument = false)
|
||||||
|
)
|
||||||
|
|
||||||
|
// AppOps APIs which include uid and package visibility filters checks
|
||||||
|
private val APPOPS_METHODS = listOf(
|
||||||
|
Method(CLASS_APPOPS_MANAGER, "noteOp"),
|
||||||
|
Method(CLASS_APPOPS_MANAGER, "noteOpNoThrow"),
|
||||||
|
Method(CLASS_APPOPS_MANAGER, "noteOperation"),
|
||||||
|
Method(CLASS_APPOPS_MANAGER, "noteProxyOp"),
|
||||||
|
Method(CLASS_APPOPS_MANAGER, "noteProxyOpNoThrow"),
|
||||||
|
Method(CLASS_APPOPS_MANAGER, "startOp"),
|
||||||
|
Method(CLASS_APPOPS_MANAGER, "startOpNoThrow"),
|
||||||
|
Method(CLASS_APPOPS_MANAGER, "FinishOp"),
|
||||||
|
Method(CLASS_APPOPS_MANAGER, "finishProxyOp"),
|
||||||
|
Method(CLASS_APPOPS_MANAGER, "checkPackage")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Enforce permission APIs
|
||||||
|
private val ENFORCE_PERMISSION_METHODS =
|
||||||
|
com.google.android.lint.ENFORCE_PERMISSION_METHODS
|
||||||
|
.map(PackageVisibilityDetector::Method)
|
||||||
|
|
||||||
|
private val BYPASS_STUBS = listOf(
|
||||||
|
"android.content.pm.IPackageDataObserver.Stub",
|
||||||
|
"android.content.pm.IPackageDeleteObserver.Stub",
|
||||||
|
"android.content.pm.IPackageDeleteObserver2.Stub",
|
||||||
|
"android.content.pm.IPackageInstallObserver2.Stub",
|
||||||
|
"com.android.internal.app.IAppOpsCallback.Stub",
|
||||||
|
|
||||||
|
// TODO(b/228285637): Do not bypass PackageManagerService API
|
||||||
|
"android.content.pm.IPackageManager.Stub",
|
||||||
|
"android.content.pm.IPackageManagerNative.Stub"
|
||||||
|
)
|
||||||
|
|
||||||
|
private val METHOD_CLEAR_CALLING_IDENTITY =
|
||||||
|
Method(CLASS_BINDER, "clearCallingIdentity")
|
||||||
|
private val METHOD_RESTORE_CALLING_IDENTITY =
|
||||||
|
Method(CLASS_BINDER, "restoreCallingIdentity")
|
||||||
|
|
||||||
|
private fun getMsgPackageNameNoPackageVisibilityFilters(
|
||||||
|
method: UMethod,
|
||||||
|
argumentIndex: Int
|
||||||
|
): String = "Api: ${method.name} contains a package name parameter: " +
|
||||||
|
"${method.uastParameters[argumentIndex].name} does not apply " +
|
||||||
|
"package visibility filtering rules."
|
||||||
|
|
||||||
|
private val EXPLANATION = """
|
||||||
|
APIs working in the system_server and taking the package name as a parameter may have
|
||||||
|
chance to reveal package existence status on the device, and break the package
|
||||||
|
visibility that we introduced in Android 11.
|
||||||
|
(https://developer.android.com/about/versions/11/privacy/package-visibility)
|
||||||
|
|
||||||
|
Take an example of the API `boolean setFoo(String packageName)`, a malicious app may
|
||||||
|
have chance to get package existence state on the device from the result of the API,
|
||||||
|
if there is no package visibility filtering rule or uid identify checks applying to
|
||||||
|
the parameter of the package name.
|
||||||
|
|
||||||
|
To resolve it, you could apply package visibility filtering rules to the package name
|
||||||
|
via PackageManagerInternal.filterAppAccess API, before starting to use the package name.
|
||||||
|
If the parameter is a calling package name, use the PackageManager API such as
|
||||||
|
PackageManager.getPackagesForUid to verify the calling identify.
|
||||||
|
"""
|
||||||
|
|
||||||
|
val ISSUE_PACKAGE_NAME_NO_PACKAGE_VISIBILITY_FILTERS = Issue.create(
|
||||||
|
id = "ApiMightLeakAppVisibility",
|
||||||
|
briefDescription = "Api takes package name parameter doesn't apply " +
|
||||||
|
"package visibility filters",
|
||||||
|
explanation = EXPLANATION,
|
||||||
|
category = Category.SECURITY,
|
||||||
|
priority = 1,
|
||||||
|
severity = Severity.WARNING,
|
||||||
|
implementation = Implementation(
|
||||||
|
PackageVisibilityDetector::class.java,
|
||||||
|
Scope.JAVA_FILE_SCOPE
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,199 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2022 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.google.android.lint
|
||||||
|
|
||||||
|
import com.android.tools.lint.client.api.UElementHandler
|
||||||
|
import com.android.tools.lint.detector.api.Category
|
||||||
|
import com.android.tools.lint.detector.api.Detector
|
||||||
|
import com.android.tools.lint.detector.api.Implementation
|
||||||
|
import com.android.tools.lint.detector.api.Issue
|
||||||
|
import com.android.tools.lint.detector.api.JavaContext
|
||||||
|
import com.android.tools.lint.detector.api.Scope
|
||||||
|
import com.android.tools.lint.detector.api.Severity
|
||||||
|
import com.android.tools.lint.detector.api.SourceCodeScanner
|
||||||
|
import com.android.tools.lint.detector.api.getUMethod
|
||||||
|
import com.intellij.psi.PsiType
|
||||||
|
import org.jetbrains.uast.UAnnotation
|
||||||
|
import org.jetbrains.uast.UBlockExpression
|
||||||
|
import org.jetbrains.uast.UCallExpression
|
||||||
|
import org.jetbrains.uast.UElement
|
||||||
|
import org.jetbrains.uast.UExpression
|
||||||
|
import org.jetbrains.uast.UIfExpression
|
||||||
|
import org.jetbrains.uast.UMethod
|
||||||
|
import org.jetbrains.uast.UQualifiedReferenceExpression
|
||||||
|
import org.jetbrains.uast.UReturnExpression
|
||||||
|
import org.jetbrains.uast.getContainingUMethod
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops incorrect usage of {@link PermissionMethod}
|
||||||
|
* TODO: add tests once re-enabled (b/240445172, b/247542171)
|
||||||
|
*/
|
||||||
|
class PermissionMethodDetector : Detector(), SourceCodeScanner {
|
||||||
|
|
||||||
|
override fun getApplicableUastTypes(): List<Class<out UElement>> =
|
||||||
|
listOf(UAnnotation::class.java, UMethod::class.java)
|
||||||
|
|
||||||
|
override fun createUastHandler(context: JavaContext): UElementHandler =
|
||||||
|
PermissionMethodHandler(context)
|
||||||
|
|
||||||
|
private inner class PermissionMethodHandler(val context: JavaContext) : UElementHandler() {
|
||||||
|
override fun visitMethod(node: UMethod) {
|
||||||
|
if (hasPermissionMethodAnnotation(node)) return
|
||||||
|
if (onlyCallsPermissionMethod(node)) {
|
||||||
|
val location = context.getLocation(node.javaPsi.modifierList)
|
||||||
|
val fix = fix()
|
||||||
|
.annotate(ANNOTATION_PERMISSION_METHOD)
|
||||||
|
.range(location)
|
||||||
|
.autoFix()
|
||||||
|
.build()
|
||||||
|
|
||||||
|
context.report(
|
||||||
|
ISSUE_CAN_BE_PERMISSION_METHOD,
|
||||||
|
location,
|
||||||
|
"Annotate method with @PermissionMethod",
|
||||||
|
fix
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun visitAnnotation(node: UAnnotation) {
|
||||||
|
if (node.qualifiedName != ANNOTATION_PERMISSION_METHOD) return
|
||||||
|
val method = node.getContainingUMethod() ?: return
|
||||||
|
|
||||||
|
if (!isPermissionMethodReturnType(method)) {
|
||||||
|
context.report(
|
||||||
|
ISSUE_PERMISSION_METHOD_USAGE,
|
||||||
|
context.getLocation(node),
|
||||||
|
"""
|
||||||
|
Methods annotated with `@PermissionMethod` should return `void`, \
|
||||||
|
`boolean`, or `@PackageManager.PermissionResult int`."
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (method.returnType == PsiType.INT &&
|
||||||
|
method.annotations.none { it.hasQualifiedName(ANNOTATION_PERMISSION_RESULT) }
|
||||||
|
) {
|
||||||
|
context.report(
|
||||||
|
ISSUE_PERMISSION_METHOD_USAGE,
|
||||||
|
context.getLocation(node),
|
||||||
|
"""
|
||||||
|
Methods annotated with `@PermissionMethod` that return `int` should \
|
||||||
|
also be annotated with `@PackageManager.PermissionResult.`"
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private val EXPLANATION_PERMISSION_METHOD_USAGE = """
|
||||||
|
`@PermissionMethod` should annotate methods that ONLY perform permission lookups. \
|
||||||
|
Said methods should return `boolean`, `@PackageManager.PermissionResult int`, or return \
|
||||||
|
`void` and potentially throw `SecurityException`.
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
val ISSUE_PERMISSION_METHOD_USAGE = Issue.create(
|
||||||
|
id = "PermissionMethodUsage",
|
||||||
|
briefDescription = "@PermissionMethod used incorrectly",
|
||||||
|
explanation = EXPLANATION_PERMISSION_METHOD_USAGE,
|
||||||
|
category = Category.CORRECTNESS,
|
||||||
|
priority = 5,
|
||||||
|
severity = Severity.ERROR,
|
||||||
|
implementation = Implementation(
|
||||||
|
PermissionMethodDetector::class.java,
|
||||||
|
Scope.JAVA_FILE_SCOPE
|
||||||
|
),
|
||||||
|
enabledByDefault = true
|
||||||
|
)
|
||||||
|
|
||||||
|
private val EXPLANATION_CAN_BE_PERMISSION_METHOD = """
|
||||||
|
Methods that only call other methods annotated with @PermissionMethod (and do NOTHING else) can themselves \
|
||||||
|
be annotated with @PermissionMethod. For example:
|
||||||
|
```
|
||||||
|
void wrapperHelper() {
|
||||||
|
// Context.enforceCallingPermission is annotated with @PermissionMethod
|
||||||
|
context.enforceCallingPermission(SOME_PERMISSION)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
val ISSUE_CAN_BE_PERMISSION_METHOD = Issue.create(
|
||||||
|
id = "CanBePermissionMethod",
|
||||||
|
briefDescription = "Method can be annotated with @PermissionMethod",
|
||||||
|
explanation = EXPLANATION_CAN_BE_PERMISSION_METHOD,
|
||||||
|
category = Category.SECURITY,
|
||||||
|
priority = 5,
|
||||||
|
severity = Severity.WARNING,
|
||||||
|
implementation = Implementation(
|
||||||
|
PermissionMethodDetector::class.java,
|
||||||
|
Scope.JAVA_FILE_SCOPE
|
||||||
|
),
|
||||||
|
enabledByDefault = false
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun isPermissionMethodReturnType(method: UMethod): Boolean =
|
||||||
|
listOf(PsiType.VOID, PsiType.INT, PsiType.BOOLEAN).contains(method.returnType)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Identifies methods that...
|
||||||
|
* DO call other methods annotated with @PermissionMethod
|
||||||
|
* DO NOT do anything else
|
||||||
|
*/
|
||||||
|
private fun onlyCallsPermissionMethod(method: UMethod): Boolean {
|
||||||
|
val body = method.uastBody as? UBlockExpression ?: return false
|
||||||
|
if (body.expressions.isEmpty()) return false
|
||||||
|
for (expression in body.expressions) {
|
||||||
|
when (expression) {
|
||||||
|
is UQualifiedReferenceExpression -> {
|
||||||
|
if (!isPermissionMethodCall(expression.selector)) return false
|
||||||
|
}
|
||||||
|
is UReturnExpression -> {
|
||||||
|
if (!isPermissionMethodCall(expression.returnExpression)) return false
|
||||||
|
}
|
||||||
|
is UCallExpression -> {
|
||||||
|
if (!isPermissionMethodCall(expression)) return false
|
||||||
|
}
|
||||||
|
is UIfExpression -> {
|
||||||
|
if (expression.thenExpression !is UReturnExpression) return false
|
||||||
|
if (!isPermissionMethodCall(expression.condition)) return false
|
||||||
|
}
|
||||||
|
else -> return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isPermissionMethodCall(expression: UExpression?): Boolean {
|
||||||
|
return when (expression) {
|
||||||
|
is UQualifiedReferenceExpression ->
|
||||||
|
return isPermissionMethodCall(expression.selector)
|
||||||
|
is UCallExpression -> {
|
||||||
|
val calledMethod = expression.resolve()?.getUMethod() ?: return false
|
||||||
|
return hasPermissionMethodAnnotation(calledMethod)
|
||||||
|
}
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun hasPermissionMethodAnnotation(method: UMethod): Boolean = method.annotations
|
||||||
|
.any { it.hasQualifiedName(ANNOTATION_PERMISSION_METHOD) }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,229 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2022 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.google.android.lint.parcel
|
||||||
|
|
||||||
|
import com.android.tools.lint.detector.api.JavaContext
|
||||||
|
import com.android.tools.lint.detector.api.LintFix
|
||||||
|
import com.android.tools.lint.detector.api.Location
|
||||||
|
import com.intellij.psi.PsiArrayType
|
||||||
|
import com.intellij.psi.PsiCallExpression
|
||||||
|
import com.intellij.psi.PsiClassType
|
||||||
|
import com.intellij.psi.PsiIntersectionType
|
||||||
|
import com.intellij.psi.PsiMethod
|
||||||
|
import com.intellij.psi.PsiType
|
||||||
|
import com.intellij.psi.PsiTypeParameter
|
||||||
|
import com.intellij.psi.PsiWildcardType
|
||||||
|
import org.jetbrains.uast.UCallExpression
|
||||||
|
import org.jetbrains.uast.UElement
|
||||||
|
import org.jetbrains.uast.UExpression
|
||||||
|
import org.jetbrains.uast.UVariable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subclass this class and override {@link #getBoundingClass} to report an unsafe Parcel API issue
|
||||||
|
* with a fix that migrates towards the new safer API by appending an argument in the form of
|
||||||
|
* {@code com.package.ItemType.class} coming from the result of the overridden method.
|
||||||
|
*/
|
||||||
|
abstract class CallMigrator(
|
||||||
|
val method: Method,
|
||||||
|
private val rejects: Set<String> = emptySet(),
|
||||||
|
) {
|
||||||
|
open fun report(context: JavaContext, call: UCallExpression, method: PsiMethod) {
|
||||||
|
val location = context.getLocation(call)
|
||||||
|
val itemType = filter(getBoundingClass(context, call, method))
|
||||||
|
val fix = (itemType as? PsiClassType)?.let { type ->
|
||||||
|
getParcelFix(location, this.method.name, getArgumentSuffix(type))
|
||||||
|
}
|
||||||
|
val message = "Unsafe `${this.method.className}.${this.method.name}()` API usage"
|
||||||
|
context.report(SaferParcelChecker.ISSUE_UNSAFE_API_USAGE, call, location, message, fix)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected open fun getArgumentSuffix(type: PsiClassType) =
|
||||||
|
", ${type.rawType().canonicalText}.class"
|
||||||
|
|
||||||
|
protected open fun getBoundingClass(
|
||||||
|
context: JavaContext,
|
||||||
|
call: UCallExpression,
|
||||||
|
method: PsiMethod,
|
||||||
|
): PsiType? = null
|
||||||
|
|
||||||
|
protected fun getItemType(type: PsiType, container: String): PsiClassType? {
|
||||||
|
val supers = getParentTypes(type).mapNotNull { it as? PsiClassType }
|
||||||
|
val containerType = supers.firstOrNull { it.rawType().canonicalText == container }
|
||||||
|
?: return null
|
||||||
|
val itemType = containerType.parameters.getOrNull(0) ?: return null
|
||||||
|
// TODO: Expand to other types, see PsiTypeVisitor
|
||||||
|
return when (itemType) {
|
||||||
|
is PsiClassType -> itemType
|
||||||
|
is PsiWildcardType -> itemType.bound as PsiClassType
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tries to obtain the type expected by the "receiving" end given a certain {@link UElement}.
|
||||||
|
*
|
||||||
|
* This could be an assignment, an argument passed to a method call, to a constructor call, a
|
||||||
|
* type cast, etc. If no receiving end is found, the type of the UExpression itself is returned.
|
||||||
|
*/
|
||||||
|
protected fun getReceivingType(expression: UElement): PsiType? {
|
||||||
|
val parent = expression.uastParent
|
||||||
|
var type = when (parent) {
|
||||||
|
is UCallExpression -> {
|
||||||
|
val i = parent.valueArguments.indexOf(expression)
|
||||||
|
val psiCall = parent.sourcePsi as? PsiCallExpression ?: return null
|
||||||
|
val typeSubstitutor = psiCall.resolveMethodGenerics().substitutor
|
||||||
|
val method = psiCall.resolveMethod()!!
|
||||||
|
method.getSignature(typeSubstitutor).parameterTypes[i]
|
||||||
|
}
|
||||||
|
is UVariable -> parent.type
|
||||||
|
is UExpression -> parent.getExpressionType()
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
if (type == null && expression is UExpression) {
|
||||||
|
type = expression.getExpressionType()
|
||||||
|
}
|
||||||
|
return type
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun filter(type: PsiType?): PsiType? {
|
||||||
|
// It's important that PsiIntersectionType case is above the one that check the type in
|
||||||
|
// rejects, because for intersect types, the canonicalText is one of the terms.
|
||||||
|
if (type is PsiIntersectionType) {
|
||||||
|
return type.conjuncts.mapNotNull(this::filter).firstOrNull()
|
||||||
|
}
|
||||||
|
if (type == null || type.canonicalText in rejects) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
if (type is PsiClassType && type.resolve() is PsiTypeParameter) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return type
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getParentTypes(type: PsiType): Set<PsiType> =
|
||||||
|
type.superTypes.flatMap(::getParentTypes).toSet() + type
|
||||||
|
|
||||||
|
protected fun getParcelFix(location: Location, method: String, arguments: String) =
|
||||||
|
LintFix
|
||||||
|
.create()
|
||||||
|
.name("Migrate to safer Parcel.$method() API")
|
||||||
|
.replace()
|
||||||
|
.range(location)
|
||||||
|
.pattern("$method\\s*\\(((?:.|\\n)*)\\)")
|
||||||
|
.with("\\k<1>$arguments")
|
||||||
|
.autoFix()
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class derives the type to be appended by inferring the generic type of the {@code container}
|
||||||
|
* type (eg. "java.util.List") of the {@code argument}-th argument.
|
||||||
|
*/
|
||||||
|
class ContainerArgumentMigrator(
|
||||||
|
method: Method,
|
||||||
|
private val argument: Int,
|
||||||
|
private val container: String,
|
||||||
|
rejects: Set<String> = emptySet(),
|
||||||
|
) : CallMigrator(method, rejects) {
|
||||||
|
override fun getBoundingClass(
|
||||||
|
context: JavaContext, call: UCallExpression, method: PsiMethod
|
||||||
|
): PsiType? {
|
||||||
|
val firstParamType = call.valueArguments[argument].getExpressionType() ?: return null
|
||||||
|
return getItemType(firstParamType, container)!!
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We need to insert a casting construct in the class parameter. For example:
|
||||||
|
* (Class<Foo<Bar>>) (Class<?>) Foo.class.
|
||||||
|
* This is needed for when the arguments of the conflict (eg. when there is List<Foo<Bar>> and
|
||||||
|
* class type is Class<Foo?).
|
||||||
|
*/
|
||||||
|
override fun getArgumentSuffix(type: PsiClassType): String {
|
||||||
|
if (type.parameters.isNotEmpty()) {
|
||||||
|
val rawType = type.rawType()
|
||||||
|
return ", (Class<${type.canonicalText}>) (Class<?>) ${rawType.canonicalText}.class"
|
||||||
|
}
|
||||||
|
return super.getArgumentSuffix(type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class derives the type to be appended by inferring the generic type of the {@code container}
|
||||||
|
* type (eg. "java.util.List") of the return type of the method.
|
||||||
|
*/
|
||||||
|
class ContainerReturnMigrator(
|
||||||
|
method: Method,
|
||||||
|
private val container: String,
|
||||||
|
rejects: Set<String> = emptySet(),
|
||||||
|
) : CallMigrator(method, rejects) {
|
||||||
|
override fun getBoundingClass(
|
||||||
|
context: JavaContext, call: UCallExpression, method: PsiMethod
|
||||||
|
): PsiType? {
|
||||||
|
val type = getReceivingType(call.uastParent!!) ?: return null
|
||||||
|
return getItemType(type, container)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class derives the type to be appended by inferring the expected type for the method result.
|
||||||
|
*/
|
||||||
|
class ReturnMigrator(
|
||||||
|
method: Method,
|
||||||
|
rejects: Set<String> = emptySet(),
|
||||||
|
) : CallMigrator(method, rejects) {
|
||||||
|
override fun getBoundingClass(
|
||||||
|
context: JavaContext, call: UCallExpression, method: PsiMethod
|
||||||
|
): PsiType? {
|
||||||
|
return getReceivingType(call.uastParent!!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class appends the class loader and the class object by deriving the type from the method
|
||||||
|
* result.
|
||||||
|
*/
|
||||||
|
class ReturnMigratorWithClassLoader(
|
||||||
|
method: Method,
|
||||||
|
rejects: Set<String> = emptySet(),
|
||||||
|
) : CallMigrator(method, rejects) {
|
||||||
|
override fun getBoundingClass(
|
||||||
|
context: JavaContext, call: UCallExpression, method: PsiMethod
|
||||||
|
): PsiType? {
|
||||||
|
return getReceivingType(call.uastParent!!)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getArgumentSuffix(type: PsiClassType): String =
|
||||||
|
"${type.rawType().canonicalText}.class.getClassLoader(), " +
|
||||||
|
"${type.rawType().canonicalText}.class"
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class derives the type to be appended by inferring the expected array type
|
||||||
|
* for the method result.
|
||||||
|
*/
|
||||||
|
class ArrayReturnMigrator(
|
||||||
|
method: Method,
|
||||||
|
rejects: Set<String> = emptySet(),
|
||||||
|
) : CallMigrator(method, rejects) {
|
||||||
|
override fun getBoundingClass(
|
||||||
|
context: JavaContext, call: UCallExpression, method: PsiMethod
|
||||||
|
): PsiType? {
|
||||||
|
val type = getReceivingType(call.uastParent!!)
|
||||||
|
return (type as? PsiArrayType)?.componentType
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2022 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.google.android.lint.parcel
|
||||||
|
|
||||||
|
data class Method(
|
||||||
|
val params: List<String>,
|
||||||
|
val clazz: String,
|
||||||
|
val name: String,
|
||||||
|
val parameters: List<String>
|
||||||
|
) {
|
||||||
|
constructor(
|
||||||
|
clazz: String,
|
||||||
|
name: String,
|
||||||
|
parameters: List<String>
|
||||||
|
) : this(
|
||||||
|
listOf(), clazz, name, parameters
|
||||||
|
)
|
||||||
|
|
||||||
|
val signature: String
|
||||||
|
get() {
|
||||||
|
val prefix = if (params.isEmpty()) "" else "${params.joinToString(", ", "<", ">")} "
|
||||||
|
return "$prefix$clazz.$name(${parameters.joinToString()})"
|
||||||
|
}
|
||||||
|
|
||||||
|
val className: String by lazy {
|
||||||
|
clazz.split(".").last()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2022 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.google.android.lint.parcel
|
||||||
|
|
||||||
|
import com.android.tools.lint.detector.api.*
|
||||||
|
import com.intellij.psi.PsiMethod
|
||||||
|
import com.intellij.psi.PsiSubstitutor
|
||||||
|
import com.intellij.psi.PsiType
|
||||||
|
import com.intellij.psi.PsiTypeParameter
|
||||||
|
import org.jetbrains.uast.UCallExpression
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
@Suppress("UnstableApiUsage")
|
||||||
|
class SaferParcelChecker : Detector(), SourceCodeScanner {
|
||||||
|
override fun getApplicableMethodNames(): List<String> =
|
||||||
|
MIGRATORS
|
||||||
|
.map(CallMigrator::method)
|
||||||
|
.map(Method::name)
|
||||||
|
|
||||||
|
override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
|
||||||
|
if (!isAtLeastT(context)) return
|
||||||
|
val signature = getSignature(method)
|
||||||
|
val migrator = MIGRATORS.firstOrNull { it.method.signature == signature } ?: return
|
||||||
|
migrator.report(context, node, method)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getSignature(method: PsiMethod): String {
|
||||||
|
val name = UastLintUtils.getQualifiedName(method)
|
||||||
|
val signature = method.getSignature(PsiSubstitutor.EMPTY)
|
||||||
|
val parameters =
|
||||||
|
signature.parameterTypes.joinToString(transform = PsiType::getCanonicalText)
|
||||||
|
val types = signature.typeParameters.map(PsiTypeParameter::getName)
|
||||||
|
val prefix = if (types.isEmpty()) "" else types.joinToString(", ", "<", ">") + " "
|
||||||
|
return "$prefix$name($parameters)"
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isAtLeastT(context: Context): Boolean {
|
||||||
|
val project = if (context.isGlobalAnalysis()) context.mainProject else context.project
|
||||||
|
return project.isAndroidProject && project.minSdkVersion.featureLevel >= 33
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@JvmField
|
||||||
|
val ISSUE_UNSAFE_API_USAGE: Issue = Issue.create(
|
||||||
|
id = "UnsafeParcelApi",
|
||||||
|
briefDescription = "Use of unsafe deserialization API",
|
||||||
|
explanation = """
|
||||||
|
You are using a deprecated deserialization API that doesn't accept the expected class as\
|
||||||
|
a parameter. This means that unexpected classes could be instantiated and\
|
||||||
|
unexpected code executed.
|
||||||
|
|
||||||
|
Please migrate to the safer alternative that takes an extra Class<T> parameter.
|
||||||
|
""",
|
||||||
|
category = Category.SECURITY,
|
||||||
|
priority = 8,
|
||||||
|
severity = Severity.WARNING,
|
||||||
|
|
||||||
|
implementation = Implementation(
|
||||||
|
SaferParcelChecker::class.java,
|
||||||
|
Scope.JAVA_FILE_SCOPE
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Parcel
|
||||||
|
private val PARCEL_METHOD_READ_SERIALIZABLE = Method("android.os.Parcel", "readSerializable", listOf())
|
||||||
|
private val PARCEL_METHOD_READ_ARRAY_LIST = Method("android.os.Parcel", "readArrayList", listOf("java.lang.ClassLoader"))
|
||||||
|
private val PARCEL_METHOD_READ_LIST = Method("android.os.Parcel", "readList", listOf("java.util.List", "java.lang.ClassLoader"))
|
||||||
|
private val PARCEL_METHOD_READ_PARCELABLE = Method(listOf("T"), "android.os.Parcel", "readParcelable", listOf("java.lang.ClassLoader"))
|
||||||
|
private val PARCEL_METHOD_READ_PARCELABLE_LIST = Method(listOf("T"), "android.os.Parcel", "readParcelableList", listOf("java.util.List<T>", "java.lang.ClassLoader"))
|
||||||
|
private val PARCEL_METHOD_READ_SPARSE_ARRAY = Method(listOf("T"), "android.os.Parcel", "readSparseArray", listOf("java.lang.ClassLoader"))
|
||||||
|
private val PARCEL_METHOD_READ_ARRAY = Method("android.os.Parcel", "readArray", listOf("java.lang.ClassLoader"))
|
||||||
|
private val PARCEL_METHOD_READ_PARCELABLE_ARRAY = Method("android.os.Parcel", "readParcelableArray", listOf("java.lang.ClassLoader"))
|
||||||
|
|
||||||
|
// Bundle
|
||||||
|
private val BUNDLE_METHOD_GET_SERIALIZABLE = Method("android.os.Bundle", "getSerializable", listOf("java.lang.String"))
|
||||||
|
private val BUNDLE_METHOD_GET_PARCELABLE = Method(listOf("T"), "android.os.Bundle", "getParcelable", listOf("java.lang.String"))
|
||||||
|
private val BUNDLE_METHOD_GET_PARCELABLE_ARRAY_LIST = Method(listOf("T"), "android.os.Bundle", "getParcelableArrayList", listOf("java.lang.String"))
|
||||||
|
private val BUNDLE_METHOD_GET_PARCELABLE_ARRAY = Method("android.os.Bundle", "getParcelableArray", listOf("java.lang.String"))
|
||||||
|
private val BUNDLE_METHOD_GET_SPARSE_PARCELABLE_ARRAY = Method(listOf("T"), "android.os.Bundle", "getSparseParcelableArray", listOf("java.lang.String"))
|
||||||
|
|
||||||
|
// Intent
|
||||||
|
private val INTENT_METHOD_GET_SERIALIZABLE_EXTRA = Method("android.content.Intent", "getSerializableExtra", listOf("java.lang.String"))
|
||||||
|
private val INTENT_METHOD_GET_PARCELABLE_EXTRA = Method(listOf("T"), "android.content.Intent", "getParcelableExtra", listOf("java.lang.String"))
|
||||||
|
private val INTENT_METHOD_GET_PARCELABLE_ARRAY_EXTRA = Method("android.content.Intent", "getParcelableArrayExtra", listOf("java.lang.String"))
|
||||||
|
private val INTENT_METHOD_GET_PARCELABLE_ARRAY_LIST_EXTRA = Method(listOf("T"), "android.content.Intent", "getParcelableArrayListExtra", listOf("java.lang.String"))
|
||||||
|
|
||||||
|
// TODO: Write migrators for methods below
|
||||||
|
private val PARCEL_METHOD_READ_PARCELABLE_CREATOR = Method("android.os.Parcel", "readParcelableCreator", listOf("java.lang.ClassLoader"))
|
||||||
|
|
||||||
|
private val MIGRATORS = listOf(
|
||||||
|
ReturnMigrator(PARCEL_METHOD_READ_PARCELABLE, setOf("android.os.Parcelable")),
|
||||||
|
ContainerArgumentMigrator(PARCEL_METHOD_READ_LIST, 0, "java.util.List"),
|
||||||
|
ContainerReturnMigrator(PARCEL_METHOD_READ_ARRAY_LIST, "java.util.Collection"),
|
||||||
|
ContainerReturnMigrator(PARCEL_METHOD_READ_SPARSE_ARRAY, "android.util.SparseArray"),
|
||||||
|
ContainerArgumentMigrator(PARCEL_METHOD_READ_PARCELABLE_LIST, 0, "java.util.List"),
|
||||||
|
ReturnMigratorWithClassLoader(PARCEL_METHOD_READ_SERIALIZABLE),
|
||||||
|
ArrayReturnMigrator(PARCEL_METHOD_READ_ARRAY, setOf("java.lang.Object")),
|
||||||
|
ArrayReturnMigrator(PARCEL_METHOD_READ_PARCELABLE_ARRAY, setOf("android.os.Parcelable")),
|
||||||
|
|
||||||
|
ReturnMigrator(BUNDLE_METHOD_GET_PARCELABLE, setOf("android.os.Parcelable")),
|
||||||
|
ContainerReturnMigrator(BUNDLE_METHOD_GET_PARCELABLE_ARRAY_LIST, "java.util.Collection", setOf("android.os.Parcelable")),
|
||||||
|
ArrayReturnMigrator(BUNDLE_METHOD_GET_PARCELABLE_ARRAY, setOf("android.os.Parcelable")),
|
||||||
|
ContainerReturnMigrator(BUNDLE_METHOD_GET_SPARSE_PARCELABLE_ARRAY, "android.util.SparseArray", setOf("android.os.Parcelable")),
|
||||||
|
ReturnMigrator(BUNDLE_METHOD_GET_SERIALIZABLE, setOf("java.io.Serializable")),
|
||||||
|
|
||||||
|
ReturnMigrator(INTENT_METHOD_GET_PARCELABLE_EXTRA, setOf("android.os.Parcelable")),
|
||||||
|
ContainerReturnMigrator(INTENT_METHOD_GET_PARCELABLE_ARRAY_LIST_EXTRA, "java.util.Collection", setOf("android.os.Parcelable")),
|
||||||
|
ArrayReturnMigrator(INTENT_METHOD_GET_PARCELABLE_ARRAY_EXTRA, setOf("android.os.Parcelable")),
|
||||||
|
ReturnMigrator(INTENT_METHOD_GET_SERIALIZABLE_EXTRA, setOf("java.io.Serializable")),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -32,7 +32,8 @@ class CallingIdentityTokenDetectorTest : LintDetectorTest() {
|
|||||||
CallingIdentityTokenDetector.ISSUE_NESTED_CLEAR_IDENTITY_CALLS,
|
CallingIdentityTokenDetector.ISSUE_NESTED_CLEAR_IDENTITY_CALLS,
|
||||||
CallingIdentityTokenDetector.ISSUE_RESTORE_IDENTITY_CALL_NOT_IN_FINALLY_BLOCK,
|
CallingIdentityTokenDetector.ISSUE_RESTORE_IDENTITY_CALL_NOT_IN_FINALLY_BLOCK,
|
||||||
CallingIdentityTokenDetector.ISSUE_USE_OF_CALLER_AWARE_METHODS_WITH_CLEARED_IDENTITY,
|
CallingIdentityTokenDetector.ISSUE_USE_OF_CALLER_AWARE_METHODS_WITH_CLEARED_IDENTITY,
|
||||||
CallingIdentityTokenDetector.ISSUE_CLEAR_IDENTITY_CALL_NOT_FOLLOWED_BY_TRY_FINALLY
|
CallingIdentityTokenDetector.ISSUE_CLEAR_IDENTITY_CALL_NOT_FOLLOWED_BY_TRY_FINALLY,
|
||||||
|
CallingIdentityTokenDetector.ISSUE_RESULT_OF_CLEAR_IDENTITY_CALL_NOT_STORED_IN_VARIABLE
|
||||||
)
|
)
|
||||||
|
|
||||||
override fun lint(): TestLintTask = super.lint().allowMissingSdk(true)
|
override fun lint(): TestLintTask = super.lint().allowMissingSdk(true)
|
||||||
@@ -62,6 +63,13 @@ class CallingIdentityTokenDetectorTest : LintDetectorTest() {
|
|||||||
} finally {
|
} finally {
|
||||||
restoreCallingIdentity(token3);
|
restoreCallingIdentity(token3);
|
||||||
}
|
}
|
||||||
|
final Long token4 = true ? Binder.clearCallingIdentity() : null;
|
||||||
|
try {
|
||||||
|
} finally {
|
||||||
|
if (token4 != null) {
|
||||||
|
restoreCallingIdentity(token4);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
@@ -739,6 +747,51 @@ class CallingIdentityTokenDetectorTest : LintDetectorTest() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Result of Binder.clearCallingIdentity() is not stored in a variable issue tests */
|
||||||
|
|
||||||
|
fun testDetectsResultOfClearIdentityCallNotStoredInVariable() {
|
||||||
|
lint().files(
|
||||||
|
java(
|
||||||
|
"""
|
||||||
|
package test.pkg;
|
||||||
|
import android.os.Binder;
|
||||||
|
public class TestClass1 extends Binder {
|
||||||
|
private void testMethod() {
|
||||||
|
Binder.clearCallingIdentity();
|
||||||
|
android.os.Binder.clearCallingIdentity();
|
||||||
|
clearCallingIdentity();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
).indented(),
|
||||||
|
*stubs
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
.expect(
|
||||||
|
"""
|
||||||
|
src/test/pkg/TestClass1.java:5: Warning: You cleared the original identity \
|
||||||
|
with Binder.clearCallingIdentity() but did not store the result in a \
|
||||||
|
variable. You need to store the result in a variable and restore it later. \
|
||||||
|
[ResultOfClearIdentityCallNotStoredInVariable]
|
||||||
|
Binder.clearCallingIdentity();
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
src/test/pkg/TestClass1.java:6: Warning: You cleared the original identity \
|
||||||
|
with android.os.Binder.clearCallingIdentity() but did not store the result \
|
||||||
|
in a variable. You need to store the result in a variable and restore it \
|
||||||
|
later. [ResultOfClearIdentityCallNotStoredInVariable]
|
||||||
|
android.os.Binder.clearCallingIdentity();
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
src/test/pkg/TestClass1.java:7: Warning: You cleared the original identity \
|
||||||
|
with clearCallingIdentity() but did not store the result in a variable. \
|
||||||
|
You need to store the result in a variable and restore it later. \
|
||||||
|
[ResultOfClearIdentityCallNotStoredInVariable]
|
||||||
|
clearCallingIdentity();
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
0 errors, 3 warnings
|
||||||
|
""".addLineContinuation()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/** Stubs for classes used for testing */
|
/** Stubs for classes used for testing */
|
||||||
|
|
||||||
private val binderStub: TestFile = java(
|
private val binderStub: TestFile = java(
|
||||||
@@ -0,0 +1,271 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2022 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.google.android.lint
|
||||||
|
|
||||||
|
import com.android.tools.lint.checks.infrastructure.LintDetectorTest
|
||||||
|
import com.android.tools.lint.checks.infrastructure.TestFile
|
||||||
|
import com.android.tools.lint.checks.infrastructure.TestLintTask
|
||||||
|
import com.android.tools.lint.detector.api.Detector
|
||||||
|
import com.android.tools.lint.detector.api.Issue
|
||||||
|
|
||||||
|
@Suppress("UnstableApiUsage")
|
||||||
|
class PackageVisibilityDetectorTest : LintDetectorTest() {
|
||||||
|
override fun getDetector(): Detector = PackageVisibilityDetector()
|
||||||
|
|
||||||
|
override fun getIssues(): MutableList<Issue> = mutableListOf(
|
||||||
|
PackageVisibilityDetector.ISSUE_PACKAGE_NAME_NO_PACKAGE_VISIBILITY_FILTERS
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun lint(): TestLintTask = super.lint().allowMissingSdk(true)
|
||||||
|
|
||||||
|
fun testDetectIssuesParameterDoesNotApplyPackageVisibilityFilters() {
|
||||||
|
lint().files(java(
|
||||||
|
"""
|
||||||
|
package com.android.server.lint.test;
|
||||||
|
import android.internal.test.IFoo;
|
||||||
|
|
||||||
|
public class TestClass extends IFoo.Stub {
|
||||||
|
@Override
|
||||||
|
public boolean hasPackage(String packageName) {
|
||||||
|
return packageName != null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""").indented(), *stubs
|
||||||
|
).run().expect(
|
||||||
|
"""
|
||||||
|
src/com/android/server/lint/test/TestClass.java:6: Warning: \
|
||||||
|
Api: hasPackage contains a package name parameter: packageName does not apply \
|
||||||
|
package visibility filtering rules. \
|
||||||
|
[ApiMightLeakAppVisibility]
|
||||||
|
public boolean hasPackage(String packageName) {
|
||||||
|
~~~~~~~~~~~~~~~~~~
|
||||||
|
0 errors, 1 warnings
|
||||||
|
""".addLineContinuation()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testDoesNotDetectIssuesApiInvokesAppOps() {
|
||||||
|
lint().files(java(
|
||||||
|
"""
|
||||||
|
package com.android.server.lint.test;
|
||||||
|
import android.app.AppOpsManager;
|
||||||
|
import android.os.Binder;
|
||||||
|
import android.internal.test.IFoo;
|
||||||
|
|
||||||
|
public class TestClass extends IFoo.Stub {
|
||||||
|
private AppOpsManager mAppOpsManager;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasPackage(String packageName) {
|
||||||
|
checkPackage(packageName);
|
||||||
|
return packageName != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkPackage(String packageName) {
|
||||||
|
mAppOpsManager.checkPackage(Binder.getCallingUid(), packageName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
).indented(), *stubs).run().expectClean()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testDoesNotDetectIssuesApiInvokesEnforcePermission() {
|
||||||
|
lint().files(java(
|
||||||
|
"""
|
||||||
|
package com.android.server.lint.test;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.internal.test.IFoo;
|
||||||
|
|
||||||
|
public class TestClass extends IFoo.Stub {
|
||||||
|
private Context mContext;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasPackage(String packageName) {
|
||||||
|
enforcePermission();
|
||||||
|
return packageName != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void enforcePermission() {
|
||||||
|
mContext.checkCallingPermission(
|
||||||
|
android.Manifest.permission.ACCESS_INPUT_FLINGER);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
).indented(), *stubs).run().expectClean()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testDoesNotDetectIssuesApiInvokesPackageManager() {
|
||||||
|
lint().files(java(
|
||||||
|
"""
|
||||||
|
package com.android.server.lint.test;
|
||||||
|
import android.content.pm.PackageInfo;
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
|
import android.internal.test.IFoo;
|
||||||
|
|
||||||
|
public class TestClass extends IFoo.Stub {
|
||||||
|
private PackageManager mPackageManager;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasPackage(String packageName) {
|
||||||
|
return getPackageInfo(packageName) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private PackageInfo getPackageInfo(String packageName) {
|
||||||
|
try {
|
||||||
|
return mPackageManager.getPackageInfo(packageName, 0);
|
||||||
|
} catch (PackageManager.NameNotFoundException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
).indented(), *stubs).run().expectClean()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testDetectIssuesApiInvokesPackageManagerAndClearCallingIdentify() {
|
||||||
|
lint().files(java(
|
||||||
|
"""
|
||||||
|
package com.android.server.lint.test;
|
||||||
|
import android.content.pm.PackageInfo;
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
|
import android.internal.test.IFoo;import android.os.Binder;
|
||||||
|
|
||||||
|
public class TestClass extends IFoo.Stub {
|
||||||
|
private PackageManager mPackageManager;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasPackage(String packageName) {
|
||||||
|
return getPackageInfo(packageName) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private PackageInfo getPackageInfo(String packageName) {
|
||||||
|
long token = Binder.clearCallingIdentity();
|
||||||
|
try {
|
||||||
|
try {
|
||||||
|
return mPackageManager.getPackageInfo(packageName, 0);
|
||||||
|
} catch (PackageManager.NameNotFoundException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} finally{
|
||||||
|
Binder.restoreCallingIdentity(token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""").indented(), *stubs
|
||||||
|
).run().expect(
|
||||||
|
"""
|
||||||
|
src/com/android/server/lint/test/TestClass.java:10: Warning: \
|
||||||
|
Api: hasPackage contains a package name parameter: packageName does not apply \
|
||||||
|
package visibility filtering rules. \
|
||||||
|
[ApiMightLeakAppVisibility]
|
||||||
|
public boolean hasPackage(String packageName) {
|
||||||
|
~~~~~~~~~~~~~~~~~~
|
||||||
|
0 errors, 1 warnings
|
||||||
|
""".addLineContinuation()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testDoesNotDetectIssuesApiNotSystemPackagePrefix() {
|
||||||
|
lint().files(java(
|
||||||
|
"""
|
||||||
|
package com.test.not.system.prefix;
|
||||||
|
import android.internal.test.IFoo;
|
||||||
|
|
||||||
|
public class TestClass extends IFoo.Stub {
|
||||||
|
@Override
|
||||||
|
public boolean hasPackage(String packageName) {
|
||||||
|
return packageName != null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
).indented(), *stubs).run().expectClean()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val contextStub: TestFile = java(
|
||||||
|
"""
|
||||||
|
package android.content;
|
||||||
|
|
||||||
|
public abstract class Context {
|
||||||
|
public abstract int checkCallingPermission(String permission);
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
).indented()
|
||||||
|
|
||||||
|
private val appOpsManagerStub: TestFile = java(
|
||||||
|
"""
|
||||||
|
package android.app;
|
||||||
|
|
||||||
|
public class AppOpsManager {
|
||||||
|
public void checkPackage(int uid, String packageName) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
).indented()
|
||||||
|
|
||||||
|
private val packageManagerStub: TestFile = java(
|
||||||
|
"""
|
||||||
|
package android.content.pm;
|
||||||
|
import android.content.pm.PackageInfo;
|
||||||
|
|
||||||
|
public abstract class PackageManager {
|
||||||
|
public static class NameNotFoundException extends AndroidException {
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract PackageInfo getPackageInfo(String packageName, int flags)
|
||||||
|
throws NameNotFoundException;
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
).indented()
|
||||||
|
|
||||||
|
private val packageInfoStub: TestFile = java(
|
||||||
|
"""
|
||||||
|
package android.content.pm;
|
||||||
|
public class PackageInfo {}
|
||||||
|
"""
|
||||||
|
).indented()
|
||||||
|
|
||||||
|
private val binderStub: TestFile = java(
|
||||||
|
"""
|
||||||
|
package android.os;
|
||||||
|
|
||||||
|
public class Binder {
|
||||||
|
public static final native long clearCallingIdentity();
|
||||||
|
public static final native void restoreCallingIdentity(long token);
|
||||||
|
public static final native int getCallingUid();
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
).indented()
|
||||||
|
|
||||||
|
private val interfaceIFooStub: TestFile = java(
|
||||||
|
"""
|
||||||
|
package android.internal.test;
|
||||||
|
import android.os.Binder;
|
||||||
|
|
||||||
|
public interface IFoo {
|
||||||
|
boolean hasPackage(String packageName);
|
||||||
|
public abstract static class Stub extends Binder implements IFoo {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
).indented()
|
||||||
|
|
||||||
|
private val stubs = arrayOf(contextStub, appOpsManagerStub, packageManagerStub,
|
||||||
|
packageInfoStub, binderStub, interfaceIFooStub)
|
||||||
|
|
||||||
|
// Substitutes "backslash + new line" with an empty string to imitate line continuation
|
||||||
|
private fun String.addLineContinuation(): String = this.trimIndent().replace("\\\n", "")
|
||||||
|
}
|
||||||
@@ -0,0 +1,823 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2022 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.google.android.lint.parcel
|
||||||
|
|
||||||
|
import com.android.tools.lint.checks.infrastructure.LintDetectorTest
|
||||||
|
import com.android.tools.lint.checks.infrastructure.TestLintTask
|
||||||
|
import com.android.tools.lint.checks.infrastructure.TestMode
|
||||||
|
import com.android.tools.lint.detector.api.Detector
|
||||||
|
import com.android.tools.lint.detector.api.Issue
|
||||||
|
|
||||||
|
@Suppress("UnstableApiUsage")
|
||||||
|
class SaferParcelCheckerTest : LintDetectorTest() {
|
||||||
|
override fun getDetector(): Detector = SaferParcelChecker()
|
||||||
|
|
||||||
|
override fun getIssues(): List<Issue> = listOf(
|
||||||
|
SaferParcelChecker.ISSUE_UNSAFE_API_USAGE
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun lint(): TestLintTask =
|
||||||
|
super.lint()
|
||||||
|
.allowMissingSdk(true)
|
||||||
|
// We don't do partial analysis in the platform
|
||||||
|
.skipTestModes(TestMode.PARTIAL)
|
||||||
|
|
||||||
|
/** Parcel Tests */
|
||||||
|
|
||||||
|
fun testParcelDetectUnsafeReadSerializable() {
|
||||||
|
lint()
|
||||||
|
.files(
|
||||||
|
java(
|
||||||
|
"""
|
||||||
|
package test.pkg;
|
||||||
|
import android.os.Parcel;
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
public class TestClass {
|
||||||
|
private TestClass(Parcel p) {
|
||||||
|
Serializable ans = p.readSerializable();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
).indented(),
|
||||||
|
*includes
|
||||||
|
)
|
||||||
|
.expectIdenticalTestModeOutput(false)
|
||||||
|
.run()
|
||||||
|
.expect(
|
||||||
|
"""
|
||||||
|
src/test/pkg/TestClass.java:7: Warning: Unsafe Parcel.readSerializable() \
|
||||||
|
API usage [UnsafeParcelApi]
|
||||||
|
Serializable ans = p.readSerializable();
|
||||||
|
~~~~~~~~~~~~~~~~~~~~
|
||||||
|
0 errors, 1 warnings
|
||||||
|
""".addLineContinuation()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testParcelDoesNotDetectSafeReadSerializable() {
|
||||||
|
lint()
|
||||||
|
.files(
|
||||||
|
java(
|
||||||
|
"""
|
||||||
|
package test.pkg;
|
||||||
|
import android.os.Parcel;
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
public class TestClass {
|
||||||
|
private TestClass(Parcel p) {
|
||||||
|
String ans = p.readSerializable(null, String.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
).indented(),
|
||||||
|
*includes
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
.expect("No warnings.")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testParcelDetectUnsafeReadArrayList() {
|
||||||
|
lint()
|
||||||
|
.files(
|
||||||
|
java(
|
||||||
|
"""
|
||||||
|
package test.pkg;
|
||||||
|
import android.os.Parcel;
|
||||||
|
|
||||||
|
public class TestClass {
|
||||||
|
private TestClass(Parcel p) {
|
||||||
|
ArrayList ans = p.readArrayList(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
).indented(),
|
||||||
|
*includes
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
.expect(
|
||||||
|
"""
|
||||||
|
src/test/pkg/TestClass.java:6: Warning: Unsafe Parcel.readArrayList() API \
|
||||||
|
usage [UnsafeParcelApi]
|
||||||
|
ArrayList ans = p.readArrayList(null);
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
0 errors, 1 warnings
|
||||||
|
""".addLineContinuation()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testParcelDoesNotDetectSafeReadArrayList() {
|
||||||
|
lint()
|
||||||
|
.files(
|
||||||
|
java(
|
||||||
|
"""
|
||||||
|
package test.pkg;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.Parcel;
|
||||||
|
|
||||||
|
public class TestClass {
|
||||||
|
private TestClass(Parcel p) {
|
||||||
|
ArrayList<Intent> ans = p.readArrayList(null, Intent.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
).indented(),
|
||||||
|
*includes
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
.expect("No warnings.")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testParcelDetectUnsafeReadList() {
|
||||||
|
lint()
|
||||||
|
.files(
|
||||||
|
java(
|
||||||
|
"""
|
||||||
|
package test.pkg;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.Parcel;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class TestClass {
|
||||||
|
private TestClass(Parcel p) {
|
||||||
|
List<Intent> list = new ArrayList<Intent>();
|
||||||
|
p.readList(list, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
).indented(),
|
||||||
|
*includes
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
.expect(
|
||||||
|
"""
|
||||||
|
src/test/pkg/TestClass.java:9: Warning: Unsafe Parcel.readList() API usage \
|
||||||
|
[UnsafeParcelApi]
|
||||||
|
p.readList(list, null);
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
0 errors, 1 warnings
|
||||||
|
""".addLineContinuation()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testDParceloesNotDetectSafeReadList() {
|
||||||
|
lint()
|
||||||
|
.files(
|
||||||
|
java(
|
||||||
|
"""
|
||||||
|
package test.pkg;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.Parcel;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class TestClass {
|
||||||
|
private TestClass(Parcel p) {
|
||||||
|
List<Intent> list = new ArrayList<Intent>();
|
||||||
|
p.readList(list, null, Intent.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
).indented(),
|
||||||
|
*includes
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
.expect("No warnings.")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testParcelDetectUnsafeReadParcelable() {
|
||||||
|
lint()
|
||||||
|
.files(
|
||||||
|
java(
|
||||||
|
"""
|
||||||
|
package test.pkg;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.Parcel;
|
||||||
|
|
||||||
|
public class TestClass {
|
||||||
|
private TestClass(Parcel p) {
|
||||||
|
Intent ans = p.readParcelable(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
).indented(),
|
||||||
|
*includes
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
.expect(
|
||||||
|
"""
|
||||||
|
src/test/pkg/TestClass.java:7: Warning: Unsafe Parcel.readParcelable() API \
|
||||||
|
usage [UnsafeParcelApi]
|
||||||
|
Intent ans = p.readParcelable(null);
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
0 errors, 1 warnings
|
||||||
|
""".addLineContinuation()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testParcelDoesNotDetectSafeReadParcelable() {
|
||||||
|
lint()
|
||||||
|
.files(
|
||||||
|
java(
|
||||||
|
"""
|
||||||
|
package test.pkg;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.Parcel;
|
||||||
|
|
||||||
|
public class TestClass {
|
||||||
|
private TestClass(Parcel p) {
|
||||||
|
Intent ans = p.readParcelable(null, Intent.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
).indented(),
|
||||||
|
*includes
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
.expect("No warnings.")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testParcelDetectUnsafeReadParcelableList() {
|
||||||
|
lint()
|
||||||
|
.files(
|
||||||
|
java(
|
||||||
|
"""
|
||||||
|
package test.pkg;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.Parcel;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class TestClass {
|
||||||
|
private TestClass(Parcel p) {
|
||||||
|
List<Intent> list = new ArrayList<Intent>();
|
||||||
|
List<Intent> ans = p.readParcelableList(list, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
).indented(),
|
||||||
|
*includes
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
.expect(
|
||||||
|
"""
|
||||||
|
src/test/pkg/TestClass.java:9: Warning: Unsafe Parcel.readParcelableList() \
|
||||||
|
API usage [UnsafeParcelApi]
|
||||||
|
List<Intent> ans = p.readParcelableList(list, null);
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
0 errors, 1 warnings
|
||||||
|
""".addLineContinuation()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testParcelDoesNotDetectSafeReadParcelableList() {
|
||||||
|
lint()
|
||||||
|
.files(
|
||||||
|
java(
|
||||||
|
"""
|
||||||
|
package test.pkg;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.Parcel;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class TestClass {
|
||||||
|
private TestClass(Parcel p) {
|
||||||
|
List<Intent> list = new ArrayList<Intent>();
|
||||||
|
List<Intent> ans =
|
||||||
|
p.readParcelableList(list, null, Intent.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
).indented(),
|
||||||
|
*includes
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
.expect("No warnings.")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testParcelDetectUnsafeReadSparseArray() {
|
||||||
|
lint()
|
||||||
|
.files(
|
||||||
|
java(
|
||||||
|
"""
|
||||||
|
package test.pkg;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.Parcel;
|
||||||
|
import android.util.SparseArray;
|
||||||
|
|
||||||
|
public class TestClass {
|
||||||
|
private TestClass(Parcel p) {
|
||||||
|
SparseArray<Intent> ans = p.readSparseArray(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
).indented(),
|
||||||
|
*includes
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
.expect(
|
||||||
|
"""
|
||||||
|
src/test/pkg/TestClass.java:8: Warning: Unsafe Parcel.readSparseArray() API\
|
||||||
|
usage [UnsafeParcelApi]
|
||||||
|
SparseArray<Intent> ans = p.readSparseArray(null);
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
0 errors, 1 warnings
|
||||||
|
""".addLineContinuation()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testParcelDoesNotDetectSafeReadSparseArray() {
|
||||||
|
lint()
|
||||||
|
.files(
|
||||||
|
java(
|
||||||
|
"""
|
||||||
|
package test.pkg;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.Parcel;
|
||||||
|
import android.util.SparseArray;
|
||||||
|
|
||||||
|
public class TestClass {
|
||||||
|
private TestClass(Parcel p) {
|
||||||
|
SparseArray<Intent> ans =
|
||||||
|
p.readSparseArray(null, Intent.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
).indented(),
|
||||||
|
*includes
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
.expect("No warnings.")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testParcelDetectUnsafeReadSArray() {
|
||||||
|
lint()
|
||||||
|
.files(
|
||||||
|
java(
|
||||||
|
"""
|
||||||
|
package test.pkg;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.Parcel;
|
||||||
|
|
||||||
|
public class TestClass {
|
||||||
|
private TestClass(Parcel p) {
|
||||||
|
Intent[] ans = p.readArray(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
).indented(),
|
||||||
|
*includes
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
.expect(
|
||||||
|
"""
|
||||||
|
src/test/pkg/TestClass.java:7: Warning: Unsafe Parcel.readArray() API\
|
||||||
|
usage [UnsafeParcelApi]
|
||||||
|
Intent[] ans = p.readArray(null);
|
||||||
|
~~~~~~~~~~~~~~~~~
|
||||||
|
0 errors, 1 warnings
|
||||||
|
""".addLineContinuation()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testParcelDoesNotDetectSafeReadArray() {
|
||||||
|
lint()
|
||||||
|
.files(
|
||||||
|
java(
|
||||||
|
"""
|
||||||
|
package test.pkg;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.Parcel;
|
||||||
|
|
||||||
|
public class TestClass {
|
||||||
|
private TestClass(Parcel p) {
|
||||||
|
Intent[] ans = p.readArray(null, Intent.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
).indented(),
|
||||||
|
*includes
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
.expect("No warnings.")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testParcelDetectUnsafeReadParcelableSArray() {
|
||||||
|
lint()
|
||||||
|
.files(
|
||||||
|
java(
|
||||||
|
"""
|
||||||
|
package test.pkg;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.Parcel;
|
||||||
|
|
||||||
|
public class TestClass {
|
||||||
|
private TestClass(Parcel p) {
|
||||||
|
Intent[] ans = p.readParcelableArray(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
).indented(),
|
||||||
|
*includes
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
.expect(
|
||||||
|
"""
|
||||||
|
src/test/pkg/TestClass.java:7: Warning: Unsafe Parcel.readParcelableArray() API\
|
||||||
|
usage [UnsafeParcelApi]
|
||||||
|
Intent[] ans = p.readParcelableArray(null);
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
0 errors, 1 warnings
|
||||||
|
""".addLineContinuation()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testParcelDoesNotDetectSafeReadParcelableArray() {
|
||||||
|
lint()
|
||||||
|
.files(
|
||||||
|
java(
|
||||||
|
"""
|
||||||
|
package test.pkg;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.Parcel;
|
||||||
|
|
||||||
|
public class TestClass {
|
||||||
|
private TestClass(Parcel p) {
|
||||||
|
Intent[] ans = p.readParcelableArray(null, Intent.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
).indented(),
|
||||||
|
*includes
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
.expect("No warnings.")
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Bundle Tests */
|
||||||
|
|
||||||
|
fun testBundleDetectUnsafeGetParcelable() {
|
||||||
|
lint()
|
||||||
|
.files(
|
||||||
|
java(
|
||||||
|
"""
|
||||||
|
package test.pkg;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.Bundle;
|
||||||
|
|
||||||
|
public class TestClass {
|
||||||
|
private TestClass(Bundle b) {
|
||||||
|
Intent ans = b.getParcelable("key");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
).indented(),
|
||||||
|
*includes
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
.expect(
|
||||||
|
"""
|
||||||
|
src/test/pkg/TestClass.java:7: Warning: Unsafe Bundle.getParcelable() API usage [UnsafeParcelApi]
|
||||||
|
Intent ans = b.getParcelable("key");
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
0 errors, 1 warnings
|
||||||
|
""".addLineContinuation()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testBundleDoesNotDetectSafeGetParcelable() {
|
||||||
|
lint()
|
||||||
|
.files(
|
||||||
|
java(
|
||||||
|
"""
|
||||||
|
package test.pkg;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.Bundle;
|
||||||
|
|
||||||
|
public class TestClass {
|
||||||
|
private TestClass(Bundle b) {
|
||||||
|
Intent ans = b.getParcelable("key", Intent.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
).indented(),
|
||||||
|
*includes
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
.expect("No warnings.")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testBundleDetectUnsafeGetParcelableArrayList() {
|
||||||
|
lint()
|
||||||
|
.files(
|
||||||
|
java(
|
||||||
|
"""
|
||||||
|
package test.pkg;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.Bundle;
|
||||||
|
|
||||||
|
public class TestClass {
|
||||||
|
private TestClass(Bundle b) {
|
||||||
|
ArrayList<Intent> ans = b.getParcelableArrayList("key");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
).indented(),
|
||||||
|
*includes
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
.expect(
|
||||||
|
"""
|
||||||
|
src/test/pkg/TestClass.java:7: Warning: Unsafe Bundle.getParcelableArrayList() API usage [UnsafeParcelApi]
|
||||||
|
ArrayList<Intent> ans = b.getParcelableArrayList("key");
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
0 errors, 1 warnings
|
||||||
|
""".addLineContinuation()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testBundleDoesNotDetectSafeGetParcelableArrayList() {
|
||||||
|
lint()
|
||||||
|
.files(
|
||||||
|
java(
|
||||||
|
"""
|
||||||
|
package test.pkg;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.Bundle;
|
||||||
|
|
||||||
|
public class TestClass {
|
||||||
|
private TestClass(Bundle b) {
|
||||||
|
ArrayList<Intent> ans = b.getParcelableArrayList("key", Intent.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
).indented(),
|
||||||
|
*includes
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
.expect("No warnings.")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testBundleDetectUnsafeGetParcelableArray() {
|
||||||
|
lint()
|
||||||
|
.files(
|
||||||
|
java(
|
||||||
|
"""
|
||||||
|
package test.pkg;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.Bundle;
|
||||||
|
|
||||||
|
public class TestClass {
|
||||||
|
private TestClass(Bundle b) {
|
||||||
|
Intent[] ans = b.getParcelableArray("key");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
).indented(),
|
||||||
|
*includes
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
.expect(
|
||||||
|
"""
|
||||||
|
src/test/pkg/TestClass.java:7: Warning: Unsafe Bundle.getParcelableArray() API usage [UnsafeParcelApi]
|
||||||
|
Intent[] ans = b.getParcelableArray("key");
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
0 errors, 1 warnings
|
||||||
|
""".addLineContinuation()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testBundleDoesNotDetectSafeGetParcelableArray() {
|
||||||
|
lint()
|
||||||
|
.files(
|
||||||
|
java(
|
||||||
|
"""
|
||||||
|
package test.pkg;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.Bundle;
|
||||||
|
|
||||||
|
public class TestClass {
|
||||||
|
private TestClass(Bundle b) {
|
||||||
|
Intent[] ans = b.getParcelableArray("key", Intent.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
).indented(),
|
||||||
|
*includes
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
.expect("No warnings.")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testBundleDetectUnsafeGetSparseParcelableArray() {
|
||||||
|
lint()
|
||||||
|
.files(
|
||||||
|
java(
|
||||||
|
"""
|
||||||
|
package test.pkg;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.Bundle;
|
||||||
|
|
||||||
|
public class TestClass {
|
||||||
|
private TestClass(Bundle b) {
|
||||||
|
SparseArray<Intent> ans = b.getSparseParcelableArray("key");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
).indented(),
|
||||||
|
*includes
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
.expect(
|
||||||
|
"""
|
||||||
|
src/test/pkg/TestClass.java:7: Warning: Unsafe Bundle.getSparseParcelableArray() API usage [UnsafeParcelApi]
|
||||||
|
SparseArray<Intent> ans = b.getSparseParcelableArray("key");
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
0 errors, 1 warnings
|
||||||
|
""".addLineContinuation()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testBundleDoesNotDetectSafeGetSparseParcelableArray() {
|
||||||
|
lint()
|
||||||
|
.files(
|
||||||
|
java(
|
||||||
|
"""
|
||||||
|
package test.pkg;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.Bundle;
|
||||||
|
|
||||||
|
public class TestClass {
|
||||||
|
private TestClass(Bundle b) {
|
||||||
|
SparseArray<Intent> ans = b.getSparseParcelableArray("key", Intent.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
).indented(),
|
||||||
|
*includes
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
.expect("No warnings.")
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Intent Tests */
|
||||||
|
|
||||||
|
fun testIntentDetectUnsafeGetParcelableExtra() {
|
||||||
|
lint()
|
||||||
|
.files(
|
||||||
|
java(
|
||||||
|
"""
|
||||||
|
package test.pkg;
|
||||||
|
import android.content.Intent;
|
||||||
|
|
||||||
|
public class TestClass {
|
||||||
|
private TestClass(Intent i) {
|
||||||
|
Intent ans = i.getParcelableExtra("name");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
).indented(),
|
||||||
|
*includes
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
.expect(
|
||||||
|
"""
|
||||||
|
src/test/pkg/TestClass.java:6: Warning: Unsafe Intent.getParcelableExtra() API usage [UnsafeParcelApi]
|
||||||
|
Intent ans = i.getParcelableExtra("name");
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
0 errors, 1 warnings
|
||||||
|
""".addLineContinuation()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testIntentDoesNotDetectSafeGetParcelableExtra() {
|
||||||
|
lint()
|
||||||
|
.files(
|
||||||
|
java(
|
||||||
|
"""
|
||||||
|
package test.pkg;
|
||||||
|
import android.content.Intent;
|
||||||
|
|
||||||
|
public class TestClass {
|
||||||
|
private TestClass(Intent i) {
|
||||||
|
Intent ans = i.getParcelableExtra("name", Intent.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
).indented(),
|
||||||
|
*includes
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
.expect("No warnings.")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** Stubs for classes used for testing */
|
||||||
|
|
||||||
|
|
||||||
|
private val includes =
|
||||||
|
arrayOf(
|
||||||
|
manifest().minSdk("33"),
|
||||||
|
java(
|
||||||
|
"""
|
||||||
|
package android.os;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.HashMap;
|
||||||
|
|
||||||
|
public final class Parcel {
|
||||||
|
// Deprecated
|
||||||
|
public Object[] readArray(ClassLoader loader) { return null; }
|
||||||
|
public ArrayList readArrayList(ClassLoader loader) { return null; }
|
||||||
|
public HashMap readHashMap(ClassLoader loader) { return null; }
|
||||||
|
public void readList(List outVal, ClassLoader loader) {}
|
||||||
|
public void readMap(Map outVal, ClassLoader loader) {}
|
||||||
|
public <T extends Parcelable> T readParcelable(ClassLoader loader) { return null; }
|
||||||
|
public Parcelable[] readParcelableArray(ClassLoader loader) { return null; }
|
||||||
|
public Parcelable.Creator<?> readParcelableCreator(ClassLoader loader) { return null; }
|
||||||
|
public <T extends Parcelable> List<T> readParcelableList(List<T> list, ClassLoader cl) { return null; }
|
||||||
|
public Serializable readSerializable() { return null; }
|
||||||
|
public <T> SparseArray<T> readSparseArray(ClassLoader loader) { return null; }
|
||||||
|
|
||||||
|
// Replacements
|
||||||
|
public <T> T[] readArray(ClassLoader loader, Class<T> clazz) { return null; }
|
||||||
|
public <T> ArrayList<T> readArrayList(ClassLoader loader, Class<? extends T> clazz) { return null; }
|
||||||
|
public <K, V> HashMap<K,V> readHashMap(ClassLoader loader, Class<? extends K> clazzKey, Class<? extends V> clazzValue) { return null; }
|
||||||
|
public <T> void readList(List<? super T> outVal, ClassLoader loader, Class<T> clazz) {}
|
||||||
|
public <K, V> void readMap(Map<? super K, ? super V> outVal, ClassLoader loader, Class<K> clazzKey, Class<V> clazzValue) {}
|
||||||
|
public <T> T readParcelable(ClassLoader loader, Class<T> clazz) { return null; }
|
||||||
|
public <T> T[] readParcelableArray(ClassLoader loader, Class<T> clazz) { return null; }
|
||||||
|
public <T> Parcelable.Creator<T> readParcelableCreator(ClassLoader loader, Class<T> clazz) { return null; }
|
||||||
|
public <T> List<T> readParcelableList(List<T> list, ClassLoader cl, Class<T> clazz) { return null; }
|
||||||
|
public <T> T readSerializable(ClassLoader loader, Class<T> clazz) { return null; }
|
||||||
|
public <T> SparseArray<T> readSparseArray(ClassLoader loader, Class<? extends T> clazz) { return null; }
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
).indented(),
|
||||||
|
java(
|
||||||
|
"""
|
||||||
|
package android.os;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.HashMap;
|
||||||
|
|
||||||
|
public final class Bundle {
|
||||||
|
// Deprecated
|
||||||
|
public <T extends Parcelable> T getParcelable(String key) { return null; }
|
||||||
|
public <T extends Parcelable> ArrayList<T> getParcelableArrayList(String key) { return null; }
|
||||||
|
public Parcelable[] getParcelableArray(String key) { return null; }
|
||||||
|
public <T extends Parcelable> SparseArray<T> getSparseParcelableArray(String key) { return null; }
|
||||||
|
|
||||||
|
// Replacements
|
||||||
|
public <T> T getParcelable(String key, Class<T> clazz) { return null; }
|
||||||
|
public <T> ArrayList<T> getParcelableArrayList(String key, Class<? extends T> clazz) { return null; }
|
||||||
|
public <T> T[] getParcelableArray(String key, Class<T> clazz) { return null; }
|
||||||
|
public <T> SparseArray<T> getSparseParcelableArray(String key, Class<? extends T> clazz) { return null; }
|
||||||
|
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
).indented(),
|
||||||
|
java(
|
||||||
|
"""
|
||||||
|
package android.os;
|
||||||
|
public interface Parcelable {}
|
||||||
|
"""
|
||||||
|
).indented(),
|
||||||
|
java(
|
||||||
|
"""
|
||||||
|
package android.content;
|
||||||
|
public class Intent implements Parcelable, Cloneable {
|
||||||
|
// Deprecated
|
||||||
|
public <T extends Parcelable> T getParcelableExtra(String name) { return null; }
|
||||||
|
|
||||||
|
// Replacements
|
||||||
|
public <T> T getParcelableExtra(String name, Class<T> clazz) { return null; }
|
||||||
|
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
).indented(),
|
||||||
|
java(
|
||||||
|
"""
|
||||||
|
package android.util;
|
||||||
|
public class SparseArray<E> implements Cloneable {}
|
||||||
|
"""
|
||||||
|
).indented(),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Substitutes "backslash + new line" with an empty string to imitate line continuation
|
||||||
|
private fun String.addLineContinuation(): String = this.trimIndent().replace("\\\n", "")
|
||||||
|
}
|
||||||
65
tools/lint/global/Android.bp
Normal file
65
tools/lint/global/Android.bp
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
// Copyright (C) 2022 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 {
|
||||||
|
// See: http://go/android-license-faq
|
||||||
|
// A large-scale-change added 'default_applicable_licenses' to import
|
||||||
|
// all of the 'license_kinds' from "frameworks_base_license"
|
||||||
|
// to get the below license kinds:
|
||||||
|
// SPDX-license-identifier-Apache-2.0
|
||||||
|
default_applicable_licenses: ["frameworks_base_license"],
|
||||||
|
}
|
||||||
|
|
||||||
|
java_library_host {
|
||||||
|
name: "AndroidGlobalLintChecker",
|
||||||
|
srcs: ["checks/src/main/java/**/*.kt"],
|
||||||
|
plugins: ["auto_service_plugin"],
|
||||||
|
libs: [
|
||||||
|
"auto_service_annotations",
|
||||||
|
"lint_api",
|
||||||
|
],
|
||||||
|
static_libs: ["AndroidCommonLint"],
|
||||||
|
kotlincflags: ["-Xjvm-default=all"],
|
||||||
|
dist: {
|
||||||
|
targets: ["droid"],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
java_test_host {
|
||||||
|
name: "AndroidGlobalLintCheckerTest",
|
||||||
|
srcs: ["checks/src/test/java/**/*.kt"],
|
||||||
|
static_libs: [
|
||||||
|
"AndroidGlobalLintChecker",
|
||||||
|
"junit",
|
||||||
|
"lint",
|
||||||
|
"lint_tests",
|
||||||
|
],
|
||||||
|
test_options: {
|
||||||
|
unit_test: true,
|
||||||
|
tradefed_options: [
|
||||||
|
{
|
||||||
|
// lint bundles in some classes that were built with older versions
|
||||||
|
// of libraries, and no longer load. Since tradefed tries to load
|
||||||
|
// all classes in the jar to look for tests, it crashes loading them.
|
||||||
|
// Exclude these classes from tradefed's search.
|
||||||
|
name: "exclude-paths",
|
||||||
|
value: "org/apache",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "exclude-paths",
|
||||||
|
value: "META-INF",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2021 The Android Open Source Project
|
* Copyright (C) 2022 The Android Open Source Project
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@@ -19,21 +19,19 @@ package com.google.android.lint
|
|||||||
import com.android.tools.lint.client.api.IssueRegistry
|
import com.android.tools.lint.client.api.IssueRegistry
|
||||||
import com.android.tools.lint.client.api.Vendor
|
import com.android.tools.lint.client.api.Vendor
|
||||||
import com.android.tools.lint.detector.api.CURRENT_API
|
import com.android.tools.lint.detector.api.CURRENT_API
|
||||||
|
import com.google.android.lint.aidl.EnforcePermissionDetector
|
||||||
|
import com.google.android.lint.aidl.EnforcePermissionHelperDetector
|
||||||
|
import com.google.android.lint.aidl.SimpleManualPermissionEnforcementDetector
|
||||||
import com.google.auto.service.AutoService
|
import com.google.auto.service.AutoService
|
||||||
|
|
||||||
@AutoService(IssueRegistry::class)
|
@AutoService(IssueRegistry::class)
|
||||||
@Suppress("UnstableApiUsage")
|
@Suppress("UnstableApiUsage")
|
||||||
class AndroidFrameworkIssueRegistry : IssueRegistry() {
|
class AndroidGlobalIssueRegistry : IssueRegistry() {
|
||||||
override val issues = listOf(
|
override val issues = listOf(
|
||||||
CallingIdentityTokenDetector.ISSUE_UNUSED_TOKEN,
|
|
||||||
CallingIdentityTokenDetector.ISSUE_NON_FINAL_TOKEN,
|
|
||||||
CallingIdentityTokenDetector.ISSUE_NESTED_CLEAR_IDENTITY_CALLS,
|
|
||||||
CallingIdentityTokenDetector.ISSUE_RESTORE_IDENTITY_CALL_NOT_IN_FINALLY_BLOCK,
|
|
||||||
CallingIdentityTokenDetector.ISSUE_USE_OF_CALLER_AWARE_METHODS_WITH_CLEARED_IDENTITY,
|
|
||||||
CallingIdentityTokenDetector.ISSUE_CLEAR_IDENTITY_CALL_NOT_FOLLOWED_BY_TRY_FINALLY,
|
|
||||||
CallingSettingsNonUserGetterMethodsDetector.ISSUE_NON_USER_GETTER_CALLED,
|
|
||||||
EnforcePermissionDetector.ISSUE_MISSING_ENFORCE_PERMISSION,
|
EnforcePermissionDetector.ISSUE_MISSING_ENFORCE_PERMISSION,
|
||||||
EnforcePermissionDetector.ISSUE_MISMATCHING_ENFORCE_PERMISSION
|
EnforcePermissionDetector.ISSUE_MISMATCHING_ENFORCE_PERMISSION,
|
||||||
|
EnforcePermissionHelperDetector.ISSUE_ENFORCE_PERMISSION_HELPER,
|
||||||
|
SimpleManualPermissionEnforcementDetector.ISSUE_SIMPLE_MANUAL_PERMISSION_ENFORCEMENT,
|
||||||
)
|
)
|
||||||
|
|
||||||
override val api: Int
|
override val api: Int
|
||||||
@@ -45,6 +43,6 @@ class AndroidFrameworkIssueRegistry : IssueRegistry() {
|
|||||||
override val vendor: Vendor = Vendor(
|
override val vendor: Vendor = Vendor(
|
||||||
vendorName = "Android",
|
vendorName = "Android",
|
||||||
feedbackUrl = "http://b/issues/new?component=315013",
|
feedbackUrl = "http://b/issues/new?component=315013",
|
||||||
contact = "brufino@google.com"
|
contact = "repsonsible-apis@google.com"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2022 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.google.android.lint.aidl
|
||||||
|
|
||||||
|
import com.android.tools.lint.client.api.UElementHandler
|
||||||
|
import com.android.tools.lint.detector.api.Detector
|
||||||
|
import com.android.tools.lint.detector.api.JavaContext
|
||||||
|
import com.android.tools.lint.detector.api.SourceCodeScanner
|
||||||
|
import org.jetbrains.uast.UBlockExpression
|
||||||
|
import org.jetbrains.uast.UElement
|
||||||
|
import org.jetbrains.uast.UMethod
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract class for detectors that look for methods implementing
|
||||||
|
* generated AIDL interface stubs
|
||||||
|
*/
|
||||||
|
abstract class AidlImplementationDetector : Detector(), SourceCodeScanner {
|
||||||
|
override fun getApplicableUastTypes(): List<Class<out UElement?>> =
|
||||||
|
listOf(UMethod::class.java)
|
||||||
|
|
||||||
|
override fun createUastHandler(context: JavaContext): UElementHandler = AidlStubHandler(context)
|
||||||
|
|
||||||
|
private inner class AidlStubHandler(val context: JavaContext) : UElementHandler() {
|
||||||
|
override fun visitMethod(node: UMethod) {
|
||||||
|
val interfaceName = getContainingAidlInterface(context, node)
|
||||||
|
.takeUnless(EXCLUDED_CPP_INTERFACES::contains) ?: return
|
||||||
|
val body = (node.uastBody as? UBlockExpression) ?: return
|
||||||
|
visitAidlMethod(context, node, interfaceName, body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract fun visitAidlMethod(
|
||||||
|
context: JavaContext,
|
||||||
|
node: UMethod,
|
||||||
|
interfaceName: String,
|
||||||
|
body: UBlockExpression,
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2022 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.google.android.lint.aidl
|
||||||
|
|
||||||
|
const val ANNOTATION_ENFORCE_PERMISSION = "android.annotation.EnforcePermission"
|
||||||
|
const val ANNOTATION_REQUIRES_NO_PERMISSION = "android.annotation.RequiresNoPermission"
|
||||||
|
const val ANNOTATION_PERMISSION_MANUALLY_ENFORCED = "android.annotation.PermissionManuallyEnforced"
|
||||||
|
|
||||||
|
val AIDL_PERMISSION_ANNOTATIONS = listOf(
|
||||||
|
ANNOTATION_ENFORCE_PERMISSION,
|
||||||
|
ANNOTATION_REQUIRES_NO_PERMISSION,
|
||||||
|
ANNOTATION_PERMISSION_MANUALLY_ENFORCED
|
||||||
|
)
|
||||||
|
|
||||||
|
const val BINDER_CLASS = "android.os.Binder"
|
||||||
|
const val IINTERFACE_INTERFACE = "android.os.IInterface"
|
||||||
|
|
||||||
|
const val AIDL_PERMISSION_HELPER_SUFFIX = "_enforcePermission"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If a non java (e.g. c++) backend is enabled, the @EnforcePermission
|
||||||
|
* annotation cannot be used. At time of writing, the mechanism
|
||||||
|
* is not implemented for non java backends.
|
||||||
|
* TODO: b/242564874 (have lint know which interfaces have the c++ backend enabled)
|
||||||
|
* rather than hard coding this list?
|
||||||
|
*/
|
||||||
|
val EXCLUDED_CPP_INTERFACES = listOf(
|
||||||
|
"AdbTransportType",
|
||||||
|
"FingerprintAndPairDevice",
|
||||||
|
"IAdbCallback",
|
||||||
|
"IAdbManager",
|
||||||
|
"PairDevice",
|
||||||
|
"IStatsBootstrapAtomService",
|
||||||
|
"StatsBootstrapAtom",
|
||||||
|
"StatsBootstrapAtomValue",
|
||||||
|
"FixedSizeArrayExample",
|
||||||
|
"PlaybackTrackMetadata",
|
||||||
|
"RecordTrackMetadata",
|
||||||
|
"SinkMetadata",
|
||||||
|
"SourceMetadata",
|
||||||
|
"IUpdateEngineStable",
|
||||||
|
"IUpdateEngineStableCallback",
|
||||||
|
"AudioCapabilities",
|
||||||
|
"ConfidenceLevel",
|
||||||
|
"ModelParameter",
|
||||||
|
"ModelParameterRange",
|
||||||
|
"Phrase",
|
||||||
|
"PhraseRecognitionEvent",
|
||||||
|
"PhraseRecognitionExtra",
|
||||||
|
"PhraseSoundModel",
|
||||||
|
"Properties",
|
||||||
|
"RecognitionConfig",
|
||||||
|
"RecognitionEvent",
|
||||||
|
"RecognitionMode",
|
||||||
|
"RecognitionStatus",
|
||||||
|
"SoundModel",
|
||||||
|
"SoundModelType",
|
||||||
|
"Status",
|
||||||
|
"IThermalService",
|
||||||
|
"IPowerManager",
|
||||||
|
"ITunerResourceManager"
|
||||||
|
)
|
||||||
@@ -0,0 +1,226 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2022 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.google.android.lint.aidl
|
||||||
|
|
||||||
|
import com.android.tools.lint.client.api.UElementHandler
|
||||||
|
import com.android.tools.lint.detector.api.AnnotationInfo
|
||||||
|
import com.android.tools.lint.detector.api.AnnotationOrigin
|
||||||
|
import com.android.tools.lint.detector.api.AnnotationUsageInfo
|
||||||
|
import com.android.tools.lint.detector.api.AnnotationUsageType
|
||||||
|
import com.android.tools.lint.detector.api.Category
|
||||||
|
import com.android.tools.lint.detector.api.ConstantEvaluator
|
||||||
|
import com.android.tools.lint.detector.api.Detector
|
||||||
|
import com.android.tools.lint.detector.api.Implementation
|
||||||
|
import com.android.tools.lint.detector.api.Issue
|
||||||
|
import com.android.tools.lint.detector.api.JavaContext
|
||||||
|
import com.android.tools.lint.detector.api.Scope
|
||||||
|
import com.android.tools.lint.detector.api.Severity
|
||||||
|
import com.android.tools.lint.detector.api.SourceCodeScanner
|
||||||
|
import com.intellij.psi.PsiAnnotation
|
||||||
|
import com.intellij.psi.PsiArrayInitializerMemberValue
|
||||||
|
import com.intellij.psi.PsiClass
|
||||||
|
import com.intellij.psi.PsiElement
|
||||||
|
import com.intellij.psi.PsiMethod
|
||||||
|
import org.jetbrains.uast.UAnnotation
|
||||||
|
import org.jetbrains.uast.UElement
|
||||||
|
import org.jetbrains.uast.UMethod
|
||||||
|
import org.jetbrains.uast.toUElement
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lint Detector that ensures that any method overriding a method annotated
|
||||||
|
* with @EnforcePermission is also annotated with the exact same annotation.
|
||||||
|
* The intent is to surface the effective permission checks to the service
|
||||||
|
* implementations.
|
||||||
|
*
|
||||||
|
* This is done with 2 mechanisms:
|
||||||
|
* 1. Visit any annotation usage, to ensure that any derived class will have
|
||||||
|
* the correct annotation on each methods. This is for the top to bottom
|
||||||
|
* propagation.
|
||||||
|
* 2. Visit any annotation, to ensure that if a method is annotated, it has
|
||||||
|
* its ancestor also annotated. This is to avoid having an annotation on a
|
||||||
|
* Java method without the corresponding annotation on the AIDL interface.
|
||||||
|
*/
|
||||||
|
class EnforcePermissionDetector : Detector(), SourceCodeScanner {
|
||||||
|
|
||||||
|
override fun applicableAnnotations(): List<String> {
|
||||||
|
return listOf(ANNOTATION_ENFORCE_PERMISSION)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getApplicableUastTypes(): List<Class<out UElement>> {
|
||||||
|
return listOf(UAnnotation::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun annotationValueGetChildren(elem: PsiElement): Array<PsiElement> {
|
||||||
|
if (elem is PsiArrayInitializerMemberValue)
|
||||||
|
return elem.getInitializers().map { it as PsiElement }.toTypedArray()
|
||||||
|
return elem.getChildren()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun areAnnotationsEquivalent(
|
||||||
|
context: JavaContext,
|
||||||
|
anno1: PsiAnnotation,
|
||||||
|
anno2: PsiAnnotation
|
||||||
|
): Boolean {
|
||||||
|
if (anno1.qualifiedName != anno2.qualifiedName) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
val attr1 = anno1.parameterList.attributes
|
||||||
|
val attr2 = anno2.parameterList.attributes
|
||||||
|
if (attr1.size != attr2.size) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for (i in attr1.indices) {
|
||||||
|
if (attr1[i].name != attr2[i].name) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
val value1 = attr1[i].value ?: return false
|
||||||
|
val value2 = attr2[i].value ?: return false
|
||||||
|
// Try to compare values directly with each other.
|
||||||
|
val v1 = ConstantEvaluator.evaluate(context, value1)
|
||||||
|
val v2 = ConstantEvaluator.evaluate(context, value2)
|
||||||
|
if (v1 != null && v2 != null) {
|
||||||
|
if (v1 != v2) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val children1 = annotationValueGetChildren(value1)
|
||||||
|
val children2 = annotationValueGetChildren(value2)
|
||||||
|
if (children1.size != children2.size) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for (j in children1.indices) {
|
||||||
|
val c1 = ConstantEvaluator.evaluate(context, children1[j])
|
||||||
|
val c2 = ConstantEvaluator.evaluate(context, children2[j])
|
||||||
|
if (c1 != c2) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun compareMethods(
|
||||||
|
context: JavaContext,
|
||||||
|
element: UElement,
|
||||||
|
overridingMethod: PsiMethod,
|
||||||
|
overriddenMethod: PsiMethod,
|
||||||
|
checkEquivalence: Boolean = true
|
||||||
|
) {
|
||||||
|
// If method is not from a Stub subclass, this method shouldn't use @EP at all.
|
||||||
|
// This is handled by EnforcePermissionHelperDetector.
|
||||||
|
if (!isContainedInSubclassOfStub(context, overridingMethod.toUElement() as? UMethod)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val overridingAnnotation = overridingMethod.getAnnotation(ANNOTATION_ENFORCE_PERMISSION)
|
||||||
|
val overriddenAnnotation = overriddenMethod.getAnnotation(ANNOTATION_ENFORCE_PERMISSION)
|
||||||
|
val location = context.getLocation(element)
|
||||||
|
val overridingClass = overridingMethod.parent as PsiClass
|
||||||
|
val overriddenClass = overriddenMethod.parent as PsiClass
|
||||||
|
val overridingName = "${overridingClass.name}.${overridingMethod.name}"
|
||||||
|
val overriddenName = "${overriddenClass.name}.${overriddenMethod.name}"
|
||||||
|
if (overridingAnnotation == null) {
|
||||||
|
val msg = "The method $overridingName overrides the method $overriddenName which " +
|
||||||
|
"is annotated with @EnforcePermission. The same annotation must be used " +
|
||||||
|
"on $overridingName"
|
||||||
|
context.report(ISSUE_MISSING_ENFORCE_PERMISSION, element, location, msg)
|
||||||
|
} else if (overriddenAnnotation == null) {
|
||||||
|
val msg = "The method $overridingName overrides the method $overriddenName which " +
|
||||||
|
"is not annotated with @EnforcePermission. The same annotation must be " +
|
||||||
|
"used on $overriddenName. Did you forget to annotate the AIDL definition?"
|
||||||
|
context.report(ISSUE_MISSING_ENFORCE_PERMISSION, element, location, msg)
|
||||||
|
} else if (checkEquivalence && !areAnnotationsEquivalent(
|
||||||
|
context, overridingAnnotation, overriddenAnnotation)) {
|
||||||
|
val msg = "The method $overridingName is annotated with " +
|
||||||
|
"${overridingAnnotation.text} which differs from the overridden " +
|
||||||
|
"method $overriddenName: ${overriddenAnnotation.text}. The same " +
|
||||||
|
"annotation must be used for both methods."
|
||||||
|
context.report(ISSUE_MISMATCHING_ENFORCE_PERMISSION, element, location, msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun visitAnnotationUsage(
|
||||||
|
context: JavaContext,
|
||||||
|
element: UElement,
|
||||||
|
annotationInfo: AnnotationInfo,
|
||||||
|
usageInfo: AnnotationUsageInfo
|
||||||
|
) {
|
||||||
|
if (usageInfo.type == AnnotationUsageType.METHOD_OVERRIDE &&
|
||||||
|
annotationInfo.origin == AnnotationOrigin.METHOD) {
|
||||||
|
val overridingMethod = element.sourcePsi as PsiMethod
|
||||||
|
val overriddenMethod = usageInfo.referenced as PsiMethod
|
||||||
|
compareMethods(context, element, overridingMethod, overriddenMethod)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createUastHandler(context: JavaContext): UElementHandler {
|
||||||
|
return object : UElementHandler() {
|
||||||
|
override fun visitAnnotation(node: UAnnotation) {
|
||||||
|
if (node.qualifiedName != ANNOTATION_ENFORCE_PERMISSION) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val method = node.uastParent as? UMethod ?: return
|
||||||
|
val overridingMethod = method as PsiMethod
|
||||||
|
val parents = overridingMethod.findSuperMethods()
|
||||||
|
for (overriddenMethod in parents) {
|
||||||
|
// The equivalence check can be skipped, if both methods are
|
||||||
|
// annotated, it will be verified by visitAnnotationUsage.
|
||||||
|
compareMethods(context, method, overridingMethod,
|
||||||
|
overriddenMethod, checkEquivalence = false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val EXPLANATION = """
|
||||||
|
The @EnforcePermission annotation is used to indicate that the underlying binder code
|
||||||
|
has already verified the caller's permissions before calling the appropriate method. The
|
||||||
|
verification code is usually generated by the AIDL compiler, which also takes care of
|
||||||
|
annotating the generated Java code.
|
||||||
|
|
||||||
|
In order to surface that information to platform developers, the same annotation must be
|
||||||
|
used on the implementation class or methods.
|
||||||
|
"""
|
||||||
|
|
||||||
|
val ISSUE_MISSING_ENFORCE_PERMISSION: Issue = Issue.create(
|
||||||
|
id = "MissingEnforcePermissionAnnotation",
|
||||||
|
briefDescription = "Missing @EnforcePermission annotation on Binder method",
|
||||||
|
explanation = EXPLANATION,
|
||||||
|
category = Category.SECURITY,
|
||||||
|
priority = 6,
|
||||||
|
severity = Severity.ERROR,
|
||||||
|
implementation = Implementation(
|
||||||
|
EnforcePermissionDetector::class.java,
|
||||||
|
Scope.JAVA_FILE_SCOPE
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val ISSUE_MISMATCHING_ENFORCE_PERMISSION: Issue = Issue.create(
|
||||||
|
id = "MismatchingEnforcePermissionAnnotation",
|
||||||
|
briefDescription = "Incorrect @EnforcePermission annotation on Binder method",
|
||||||
|
explanation = EXPLANATION,
|
||||||
|
category = Category.SECURITY,
|
||||||
|
priority = 6,
|
||||||
|
severity = Severity.ERROR,
|
||||||
|
implementation = Implementation(
|
||||||
|
EnforcePermissionDetector::class.java,
|
||||||
|
Scope.JAVA_FILE_SCOPE
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,384 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2022 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.google.android.lint.aidl
|
||||||
|
|
||||||
|
import com.android.tools.lint.detector.api.JavaContext
|
||||||
|
import com.android.tools.lint.detector.api.LintFix
|
||||||
|
import com.android.tools.lint.detector.api.Location
|
||||||
|
import com.android.tools.lint.detector.api.UastLintUtils.Companion.getAnnotationBooleanValue
|
||||||
|
import com.android.tools.lint.detector.api.UastLintUtils.Companion.getAnnotationStringValues
|
||||||
|
import com.android.tools.lint.detector.api.findSelector
|
||||||
|
import com.android.tools.lint.detector.api.getUMethod
|
||||||
|
import com.google.android.lint.findCallExpression
|
||||||
|
import com.google.android.lint.getPermissionMethodAnnotation
|
||||||
|
import com.google.android.lint.hasPermissionNameAnnotation
|
||||||
|
import com.google.android.lint.isPermissionMethodCall
|
||||||
|
import com.intellij.psi.PsiClassType
|
||||||
|
import com.intellij.psi.PsiType
|
||||||
|
import org.jetbrains.kotlin.psi.psiUtil.parameterIndex
|
||||||
|
import org.jetbrains.uast.UBinaryExpression
|
||||||
|
import org.jetbrains.uast.UBlockExpression
|
||||||
|
import org.jetbrains.uast.UCallExpression
|
||||||
|
import org.jetbrains.uast.UExpression
|
||||||
|
import org.jetbrains.uast.UExpressionList
|
||||||
|
import org.jetbrains.uast.UIfExpression
|
||||||
|
import org.jetbrains.uast.UMethod
|
||||||
|
import org.jetbrains.uast.UThrowExpression
|
||||||
|
import org.jetbrains.uast.UastBinaryOperator
|
||||||
|
import org.jetbrains.uast.evaluateString
|
||||||
|
import org.jetbrains.uast.skipParenthesizedExprDown
|
||||||
|
import org.jetbrains.uast.visitor.AbstractUastVisitor
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper class that facilitates the creation of lint auto fixes
|
||||||
|
*/
|
||||||
|
data class EnforcePermissionFix(
|
||||||
|
val manualCheckLocations: List<Location>,
|
||||||
|
val permissionNames: List<String>,
|
||||||
|
val errorLevel: Boolean,
|
||||||
|
val anyOf: Boolean,
|
||||||
|
) {
|
||||||
|
fun toLintFix(context: JavaContext, node: UMethod): LintFix {
|
||||||
|
val methodLocation = context.getLocation(node)
|
||||||
|
val replaceOrRemoveFixes = manualCheckLocations.mapIndexed { index, manualCheckLocation ->
|
||||||
|
if (index == 0) {
|
||||||
|
// Replace the first manual check with a call to the helper method
|
||||||
|
getHelperMethodFix(node, manualCheckLocation, false)
|
||||||
|
} else {
|
||||||
|
// Remove all subsequent manual checks
|
||||||
|
LintFix.create()
|
||||||
|
.replace()
|
||||||
|
.reformat(true)
|
||||||
|
.range(manualCheckLocation)
|
||||||
|
.with("")
|
||||||
|
.autoFix()
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Annotate the method with @EnforcePermission(...)
|
||||||
|
val annotateFix = LintFix.create()
|
||||||
|
.annotate(annotation)
|
||||||
|
.range(methodLocation)
|
||||||
|
.autoFix()
|
||||||
|
.build()
|
||||||
|
|
||||||
|
return LintFix.create().composite(annotateFix, *replaceOrRemoveFixes.toTypedArray())
|
||||||
|
}
|
||||||
|
|
||||||
|
private val annotation: String
|
||||||
|
get() {
|
||||||
|
val quotedPermissions = permissionNames.joinToString(", ") { """"$it"""" }
|
||||||
|
|
||||||
|
val attributeName =
|
||||||
|
if (permissionNames.size > 1) {
|
||||||
|
if (anyOf) "anyOf" else "allOf"
|
||||||
|
} else null
|
||||||
|
|
||||||
|
val annotationParameter =
|
||||||
|
if (attributeName != null) "$attributeName={$quotedPermissions}"
|
||||||
|
else quotedPermissions
|
||||||
|
|
||||||
|
return "@$ANNOTATION_ENFORCE_PERMISSION($annotationParameter)"
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* Walks the expressions in a block, looking for simple permission checks.
|
||||||
|
*
|
||||||
|
* As soon as something other than a permission check is encountered, stop looking,
|
||||||
|
* as some other business logic is happening that prevents an automated fix.
|
||||||
|
*/
|
||||||
|
fun fromBlockExpression(
|
||||||
|
context: JavaContext,
|
||||||
|
blockExpression: UBlockExpression
|
||||||
|
): EnforcePermissionFix? {
|
||||||
|
try {
|
||||||
|
val singleFixes = mutableListOf<EnforcePermissionFix>()
|
||||||
|
for (expression in blockExpression.expressions) {
|
||||||
|
val fix = fromExpression(context, expression) ?: break
|
||||||
|
singleFixes.add(fix)
|
||||||
|
}
|
||||||
|
return compose(singleFixes)
|
||||||
|
} catch (e: AnyOfAllOfException) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Conditionally constructs EnforcePermissionFix from any UExpression
|
||||||
|
*
|
||||||
|
* @return EnforcePermissionFix if the expression boils down to a permission check,
|
||||||
|
* else null
|
||||||
|
*/
|
||||||
|
fun fromExpression(
|
||||||
|
context: JavaContext,
|
||||||
|
expression: UExpression
|
||||||
|
): EnforcePermissionFix? {
|
||||||
|
val trimmedExpression = expression.skipParenthesizedExprDown()
|
||||||
|
if (trimmedExpression is UIfExpression) {
|
||||||
|
return fromIfExpression(context, trimmedExpression)
|
||||||
|
}
|
||||||
|
findCallExpression(trimmedExpression)?.let {
|
||||||
|
return fromCallExpression(context, it)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Conditionally constructs EnforcePermissionFix from a UCallExpression
|
||||||
|
*
|
||||||
|
* @return EnforcePermissionFix if the called method is annotated with @PermissionMethod, else null
|
||||||
|
*/
|
||||||
|
fun fromCallExpression(
|
||||||
|
context: JavaContext,
|
||||||
|
callExpression: UCallExpression
|
||||||
|
): EnforcePermissionFix? {
|
||||||
|
val method = callExpression.resolve()?.getUMethod() ?: return null
|
||||||
|
val annotation = getPermissionMethodAnnotation(method) ?: return null
|
||||||
|
val returnsVoid = method.returnType == PsiType.VOID
|
||||||
|
val orSelf = getAnnotationBooleanValue(annotation, "orSelf") ?: false
|
||||||
|
val anyOf = getAnnotationBooleanValue(annotation, "anyOf") ?: false
|
||||||
|
return EnforcePermissionFix(
|
||||||
|
listOf(getPermissionCheckLocation(context, callExpression)),
|
||||||
|
getPermissionCheckValues(callExpression),
|
||||||
|
errorLevel = isErrorLevel(throws = returnsVoid, orSelf = orSelf),
|
||||||
|
anyOf,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Conditionally constructs EnforcePermissionFix from a UCallExpression
|
||||||
|
*
|
||||||
|
* @return EnforcePermissionFix IF AND ONLY IF:
|
||||||
|
* * The condition of the if statement compares the return value of a
|
||||||
|
* PermissionMethod to one of the PackageManager.PermissionResult values
|
||||||
|
* * The expression inside the if statement does nothing but throw SecurityException
|
||||||
|
*/
|
||||||
|
fun fromIfExpression(
|
||||||
|
context: JavaContext,
|
||||||
|
ifExpression: UIfExpression
|
||||||
|
): EnforcePermissionFix? {
|
||||||
|
val condition = ifExpression.condition.skipParenthesizedExprDown()
|
||||||
|
if (condition !is UBinaryExpression) return null
|
||||||
|
|
||||||
|
val maybeLeftCall = findCallExpression(condition.leftOperand)
|
||||||
|
val maybeRightCall = findCallExpression(condition.rightOperand)
|
||||||
|
|
||||||
|
val (callExpression, comparison) =
|
||||||
|
if (maybeLeftCall is UCallExpression) {
|
||||||
|
Pair(maybeLeftCall, condition.rightOperand)
|
||||||
|
} else if (maybeRightCall is UCallExpression) {
|
||||||
|
Pair(maybeRightCall, condition.leftOperand)
|
||||||
|
} else return null
|
||||||
|
|
||||||
|
val permissionMethodAnnotation = getPermissionMethodAnnotation(
|
||||||
|
callExpression.resolve()?.getUMethod()) ?: return null
|
||||||
|
|
||||||
|
val equalityCheck =
|
||||||
|
when (comparison.findSelector().asSourceString()
|
||||||
|
.filterNot(Char::isWhitespace)) {
|
||||||
|
"PERMISSION_GRANTED" -> UastBinaryOperator.IDENTITY_NOT_EQUALS
|
||||||
|
"PERMISSION_DENIED" -> UastBinaryOperator.IDENTITY_EQUALS
|
||||||
|
else -> return null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (condition.operator != equalityCheck) return null
|
||||||
|
|
||||||
|
val throwExpression: UThrowExpression? =
|
||||||
|
ifExpression.thenExpression as? UThrowExpression
|
||||||
|
?: (ifExpression.thenExpression as? UBlockExpression)
|
||||||
|
?.expressions?.firstOrNull()
|
||||||
|
as? UThrowExpression
|
||||||
|
|
||||||
|
|
||||||
|
val thrownClass = (throwExpression?.thrownExpression?.getExpressionType()
|
||||||
|
as? PsiClassType)?.resolve() ?: return null
|
||||||
|
if (!context.evaluator.inheritsFrom(
|
||||||
|
thrownClass, "java.lang.SecurityException")){
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
val orSelf = getAnnotationBooleanValue(permissionMethodAnnotation, "orSelf") ?: false
|
||||||
|
val anyOf = getAnnotationBooleanValue(permissionMethodAnnotation, "anyOf") ?: false
|
||||||
|
|
||||||
|
return EnforcePermissionFix(
|
||||||
|
listOf(context.getLocation(ifExpression)),
|
||||||
|
getPermissionCheckValues(callExpression),
|
||||||
|
errorLevel = isErrorLevel(throws = true, orSelf = orSelf),
|
||||||
|
anyOf = anyOf
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun compose(individuals: List<EnforcePermissionFix>): EnforcePermissionFix? {
|
||||||
|
if (individuals.isEmpty()) return null
|
||||||
|
val anyOfs = individuals.filter(EnforcePermissionFix::anyOf)
|
||||||
|
// anyOf/allOf should be consistent. If we encounter some @PermissionMethods that are anyOf
|
||||||
|
// and others that aren't, we don't know what to do.
|
||||||
|
if (anyOfs.isNotEmpty() && anyOfs.size < individuals.size) {
|
||||||
|
throw AnyOfAllOfException()
|
||||||
|
}
|
||||||
|
return EnforcePermissionFix(
|
||||||
|
individuals.flatMap(EnforcePermissionFix::manualCheckLocations),
|
||||||
|
individuals.flatMap(EnforcePermissionFix::permissionNames),
|
||||||
|
errorLevel = individuals.all(EnforcePermissionFix::errorLevel),
|
||||||
|
anyOf = anyOfs.isNotEmpty()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a permission check, get its proper location
|
||||||
|
* so that a lint fix can remove the entire expression
|
||||||
|
*/
|
||||||
|
private fun getPermissionCheckLocation(
|
||||||
|
context: JavaContext,
|
||||||
|
callExpression: UCallExpression
|
||||||
|
):
|
||||||
|
Location {
|
||||||
|
val javaPsi = callExpression.javaPsi!!
|
||||||
|
return Location.create(
|
||||||
|
context.file,
|
||||||
|
javaPsi.containingFile?.text,
|
||||||
|
javaPsi.textRange.startOffset,
|
||||||
|
// unfortunately the element doesn't include the ending semicolon
|
||||||
|
javaPsi.textRange.endOffset + 1
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a @PermissionMethod, find arguments annotated with @PermissionName
|
||||||
|
* and pull out the permission value(s) being used. Also evaluates nested calls
|
||||||
|
* to @PermissionMethod(s) in the given method's body.
|
||||||
|
*/
|
||||||
|
@Throws(AnyOfAllOfException::class)
|
||||||
|
private fun getPermissionCheckValues(
|
||||||
|
callExpression: UCallExpression
|
||||||
|
): List<String> {
|
||||||
|
if (!isPermissionMethodCall(callExpression)) return emptyList()
|
||||||
|
|
||||||
|
val result = mutableSetOf<String>() // protect against duplicate permission values
|
||||||
|
val visitedCalls = mutableSetOf<UCallExpression>() // don't visit the same call twice
|
||||||
|
val bfsQueue = ArrayDeque(listOf(callExpression))
|
||||||
|
|
||||||
|
var anyOfAllOfState: AnyOfAllOfState = AnyOfAllOfState.INITIAL
|
||||||
|
|
||||||
|
// Bread First Search - evaluating nested @PermissionMethod(s) in the available
|
||||||
|
// source code for @PermissionName(s).
|
||||||
|
while (bfsQueue.isNotEmpty()) {
|
||||||
|
val currentCallExpression = bfsQueue.removeFirst()
|
||||||
|
visitedCalls.add(currentCallExpression)
|
||||||
|
val currentPermissions = findPermissions(currentCallExpression)
|
||||||
|
result.addAll(currentPermissions)
|
||||||
|
|
||||||
|
val currentAnnotation = getPermissionMethodAnnotation(
|
||||||
|
currentCallExpression.resolve()?.getUMethod())
|
||||||
|
val currentAnyOf = getAnnotationBooleanValue(currentAnnotation, "anyOf") ?: false
|
||||||
|
|
||||||
|
// anyOf/allOf should be consistent. If we encounter a nesting of @PermissionMethods
|
||||||
|
// where we start in an anyOf state and switch to allOf, or vice versa,
|
||||||
|
// we don't know what to do.
|
||||||
|
if (anyOfAllOfState == AnyOfAllOfState.INITIAL) {
|
||||||
|
if (currentAnyOf) anyOfAllOfState = AnyOfAllOfState.ANY_OF
|
||||||
|
else if (result.isNotEmpty()) anyOfAllOfState = AnyOfAllOfState.ALL_OF
|
||||||
|
}
|
||||||
|
|
||||||
|
if (anyOfAllOfState == AnyOfAllOfState.ALL_OF && currentAnyOf) {
|
||||||
|
throw AnyOfAllOfException()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (anyOfAllOfState == AnyOfAllOfState.ANY_OF &&
|
||||||
|
!currentAnyOf && currentPermissions.size > 1) {
|
||||||
|
throw AnyOfAllOfException()
|
||||||
|
}
|
||||||
|
|
||||||
|
currentCallExpression.resolve()?.getUMethod()
|
||||||
|
?.accept(PermissionCheckValuesVisitor(visitedCalls, bfsQueue))
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.toList()
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum class AnyOfAllOfState {
|
||||||
|
INITIAL,
|
||||||
|
ANY_OF,
|
||||||
|
ALL_OF
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds visited permission method calls to the provided
|
||||||
|
* queue in support of the BFS traversal happening while
|
||||||
|
* this is used
|
||||||
|
*/
|
||||||
|
private class PermissionCheckValuesVisitor(
|
||||||
|
val visitedCalls: Set<UCallExpression>,
|
||||||
|
val bfsQueue: ArrayDeque<UCallExpression>
|
||||||
|
) : AbstractUastVisitor() {
|
||||||
|
override fun visitCallExpression(node: UCallExpression): Boolean {
|
||||||
|
if (isPermissionMethodCall(node) && node !in visitedCalls) {
|
||||||
|
bfsQueue.add(node)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun findPermissions(
|
||||||
|
callExpression: UCallExpression,
|
||||||
|
): List<String> {
|
||||||
|
val annotation = getPermissionMethodAnnotation(callExpression.resolve()?.getUMethod())
|
||||||
|
|
||||||
|
val hardCodedPermissions = (getAnnotationStringValues(annotation, "value")
|
||||||
|
?: emptyArray())
|
||||||
|
.toList()
|
||||||
|
|
||||||
|
val indices = callExpression.resolve()?.getUMethod()
|
||||||
|
?.uastParameters
|
||||||
|
?.filter(::hasPermissionNameAnnotation)
|
||||||
|
?.mapNotNull { it.sourcePsi?.parameterIndex() }
|
||||||
|
?: emptyList()
|
||||||
|
|
||||||
|
val argPermissions = indices
|
||||||
|
.flatMap { i ->
|
||||||
|
when (val argument = callExpression.getArgumentForParameter(i)) {
|
||||||
|
null -> listOf(null)
|
||||||
|
is UExpressionList -> // varargs e.g. someMethod(String...)
|
||||||
|
argument.expressions.map(UExpression::evaluateString)
|
||||||
|
else -> listOf(argument.evaluateString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.filterNotNull()
|
||||||
|
|
||||||
|
return hardCodedPermissions + argPermissions
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If we detect that the PermissionMethod enforces that permission is granted,
|
||||||
|
* AND is of the "orSelf" variety, we are very confident that this is a behavior
|
||||||
|
* preserving migration to @EnforcePermission. Thus, the incident should be ERROR
|
||||||
|
* level.
|
||||||
|
*/
|
||||||
|
private fun isErrorLevel(throws: Boolean, orSelf: Boolean): Boolean = throws && orSelf
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* anyOf/allOf @PermissionMethods must be consistent to apply @EnforcePermission -
|
||||||
|
* meaning if we encounter some @PermissionMethods that are anyOf, and others are allOf,
|
||||||
|
* we don't know which to apply.
|
||||||
|
*/
|
||||||
|
class AnyOfAllOfException : Exception() {
|
||||||
|
override val message: String = "anyOf/allOf permission methods cannot be mixed"
|
||||||
|
}
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2022 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.google.android.lint.aidl
|
||||||
|
|
||||||
|
import com.android.tools.lint.client.api.UElementHandler
|
||||||
|
import com.android.tools.lint.detector.api.Category
|
||||||
|
import com.android.tools.lint.detector.api.Detector
|
||||||
|
import com.android.tools.lint.detector.api.Implementation
|
||||||
|
import com.android.tools.lint.detector.api.Issue
|
||||||
|
import com.android.tools.lint.detector.api.JavaContext
|
||||||
|
import com.android.tools.lint.detector.api.Scope
|
||||||
|
import com.android.tools.lint.detector.api.Severity
|
||||||
|
import com.android.tools.lint.detector.api.SourceCodeScanner
|
||||||
|
import com.google.android.lint.findCallExpression
|
||||||
|
import com.intellij.psi.PsiElement
|
||||||
|
import org.jetbrains.uast.UBlockExpression
|
||||||
|
import org.jetbrains.uast.UDeclarationsExpression
|
||||||
|
import org.jetbrains.uast.UElement
|
||||||
|
import org.jetbrains.uast.UExpression
|
||||||
|
import org.jetbrains.uast.UMethod
|
||||||
|
import org.jetbrains.uast.skipParenthesizedExprDown
|
||||||
|
|
||||||
|
class EnforcePermissionHelperDetector : Detector(), SourceCodeScanner {
|
||||||
|
override fun getApplicableUastTypes(): List<Class<out UElement?>> =
|
||||||
|
listOf(UMethod::class.java)
|
||||||
|
|
||||||
|
override fun createUastHandler(context: JavaContext): UElementHandler = AidlStubHandler(context)
|
||||||
|
|
||||||
|
private inner class AidlStubHandler(val context: JavaContext) : UElementHandler() {
|
||||||
|
override fun visitMethod(node: UMethod) {
|
||||||
|
if (context.evaluator.isAbstract(node)) return
|
||||||
|
if (!node.hasAnnotation(ANNOTATION_ENFORCE_PERMISSION)) return
|
||||||
|
|
||||||
|
if (!isContainedInSubclassOfStub(context, node)) {
|
||||||
|
context.report(
|
||||||
|
ISSUE_MISUSING_ENFORCE_PERMISSION,
|
||||||
|
node,
|
||||||
|
context.getLocation(node),
|
||||||
|
"The class of ${node.name} does not inherit from an AIDL generated Stub class"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val targetExpression = getHelperMethodCallSourceString(node)
|
||||||
|
val message =
|
||||||
|
"Method must start with $targetExpression or super.${node.name}(), if applicable"
|
||||||
|
|
||||||
|
val firstExpression = (node.uastBody as? UBlockExpression)
|
||||||
|
?.expressions?.firstOrNull()
|
||||||
|
|
||||||
|
if (firstExpression == null) {
|
||||||
|
context.report(
|
||||||
|
ISSUE_ENFORCE_PERMISSION_HELPER,
|
||||||
|
context.getLocation(node),
|
||||||
|
message,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val firstExpressionSource = firstExpression.skipParenthesizedExprDown()
|
||||||
|
.asSourceString()
|
||||||
|
.filterNot(Char::isWhitespace)
|
||||||
|
|
||||||
|
if (firstExpressionSource != targetExpression &&
|
||||||
|
firstExpressionSource != "super.$targetExpression") {
|
||||||
|
// calling super.<methodName>() is also legal
|
||||||
|
val directSuper = context.evaluator.getSuperMethod(node)
|
||||||
|
val firstCall = findCallExpression(firstExpression)?.resolve()
|
||||||
|
if (directSuper != null && firstCall == directSuper) return
|
||||||
|
|
||||||
|
val locationTarget = getLocationTarget(firstExpression)
|
||||||
|
val expressionLocation = context.getLocation(locationTarget)
|
||||||
|
|
||||||
|
context.report(
|
||||||
|
ISSUE_ENFORCE_PERMISSION_HELPER,
|
||||||
|
context.getLocation(node),
|
||||||
|
message,
|
||||||
|
getHelperMethodFix(node, expressionLocation),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val HELPER_SUFFIX = "_enforcePermission"
|
||||||
|
|
||||||
|
private const val EXPLANATION = """
|
||||||
|
The @EnforcePermission annotation can only be used on methods whose class extends from
|
||||||
|
the Stub class generated by the AIDL compiler. When @EnforcePermission is applied, the
|
||||||
|
AIDL compiler generates a Stub method to do the permission check called yourMethodName$HELPER_SUFFIX.
|
||||||
|
|
||||||
|
yourMethodName$HELPER_SUFFIX must be executed before any other operation. To do that, you can
|
||||||
|
either call it directly, or call it indirectly via super.yourMethodName().
|
||||||
|
"""
|
||||||
|
|
||||||
|
val ISSUE_ENFORCE_PERMISSION_HELPER: Issue = Issue.create(
|
||||||
|
id = "MissingEnforcePermissionHelper",
|
||||||
|
briefDescription = """Missing permission-enforcing method call in AIDL method
|
||||||
|
|annotated with @EnforcePermission""".trimMargin(),
|
||||||
|
explanation = EXPLANATION,
|
||||||
|
category = Category.SECURITY,
|
||||||
|
priority = 6,
|
||||||
|
severity = Severity.ERROR,
|
||||||
|
implementation = Implementation(
|
||||||
|
EnforcePermissionHelperDetector::class.java,
|
||||||
|
Scope.JAVA_FILE_SCOPE
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val ISSUE_MISUSING_ENFORCE_PERMISSION: Issue = Issue.create(
|
||||||
|
id = "MisusingEnforcePermissionAnnotation",
|
||||||
|
briefDescription = "@EnforcePermission cannot be used here",
|
||||||
|
explanation = EXPLANATION,
|
||||||
|
category = Category.SECURITY,
|
||||||
|
priority = 6,
|
||||||
|
severity = Severity.ERROR,
|
||||||
|
implementation = Implementation(
|
||||||
|
EnforcePermissionDetector::class.java,
|
||||||
|
Scope.JAVA_FILE_SCOPE
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* handles an edge case with UDeclarationsExpression, where sourcePsi is null,
|
||||||
|
* resulting in an incorrect Location if used directly
|
||||||
|
*/
|
||||||
|
private fun getLocationTarget(firstExpression: UExpression): PsiElement? {
|
||||||
|
if (firstExpression.sourcePsi != null) return firstExpression.sourcePsi
|
||||||
|
if (firstExpression is UDeclarationsExpression) {
|
||||||
|
return firstExpression.declarations.firstOrNull()?.sourcePsi
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2022 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.google.android.lint.aidl
|
||||||
|
|
||||||
|
import com.android.tools.lint.detector.api.JavaContext
|
||||||
|
import com.android.tools.lint.detector.api.LintFix
|
||||||
|
import com.android.tools.lint.detector.api.Location
|
||||||
|
import com.intellij.psi.PsiClass
|
||||||
|
import com.intellij.psi.PsiReferenceList
|
||||||
|
import org.jetbrains.uast.UMethod
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a UMethod, determine if this method is
|
||||||
|
* the entrypoint to an interface generated by AIDL,
|
||||||
|
* returning the interface name if so, otherwise returning null
|
||||||
|
*/
|
||||||
|
fun getContainingAidlInterface(context: JavaContext, node: UMethod): String? {
|
||||||
|
if (!isContainedInSubclassOfStub(context, node)) return null
|
||||||
|
for (superMethod in node.findSuperMethods()) {
|
||||||
|
for (extendsInterface in superMethod.containingClass?.extendsList?.referenceElements
|
||||||
|
?: continue) {
|
||||||
|
if (extendsInterface.qualifiedName == IINTERFACE_INTERFACE) {
|
||||||
|
return superMethod.containingClass?.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isContainedInSubclassOfStub(context: JavaContext, node: UMethod?): Boolean {
|
||||||
|
var superClass = node?.containingClass?.superClass
|
||||||
|
while (superClass != null) {
|
||||||
|
if (isStub(context, superClass)) return true
|
||||||
|
superClass = superClass.superClass
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isStub(context: JavaContext, psiClass: PsiClass?): Boolean {
|
||||||
|
if (psiClass == null) return false
|
||||||
|
if (psiClass.name != "Stub") return false
|
||||||
|
if (!context.evaluator.isStatic(psiClass)) return false
|
||||||
|
if (!context.evaluator.isAbstract(psiClass)) return false
|
||||||
|
|
||||||
|
if (!hasSingleAncestor(psiClass.extendsList, BINDER_CLASS)) return false
|
||||||
|
|
||||||
|
val parent = psiClass.parent as? PsiClass ?: return false
|
||||||
|
if (!hasSingleAncestor(parent.extendsList, IINTERFACE_INTERFACE)) return false
|
||||||
|
|
||||||
|
val parentName = parent.qualifiedName ?: return false
|
||||||
|
if (!hasSingleAncestor(psiClass.implementsList, parentName)) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun hasSingleAncestor(references: PsiReferenceList?, qualifiedName: String) =
|
||||||
|
references != null &&
|
||||||
|
references.referenceElements.size == 1 &&
|
||||||
|
references.referenceElements[0].qualifiedName == qualifiedName
|
||||||
|
|
||||||
|
fun getHelperMethodCallSourceString(node: UMethod) = "${node.name}$AIDL_PERMISSION_HELPER_SUFFIX()"
|
||||||
|
|
||||||
|
fun getHelperMethodFix(
|
||||||
|
node: UMethod,
|
||||||
|
manualCheckLocation: Location,
|
||||||
|
prepend: Boolean = true
|
||||||
|
): LintFix {
|
||||||
|
val helperMethodSource = getHelperMethodCallSourceString(node)
|
||||||
|
val indent = " ".repeat(manualCheckLocation.start?.column ?: 0)
|
||||||
|
val newText = "$helperMethodSource;${if (prepend) "\n\n$indent" else ""}"
|
||||||
|
|
||||||
|
val fix = LintFix.create()
|
||||||
|
.replace()
|
||||||
|
.range(manualCheckLocation)
|
||||||
|
.with(newText)
|
||||||
|
.reformat(true)
|
||||||
|
.autoFix()
|
||||||
|
|
||||||
|
if (prepend) fix.beginning()
|
||||||
|
|
||||||
|
return fix.build()
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2022 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.google.android.lint.aidl
|
||||||
|
|
||||||
|
import com.android.tools.lint.detector.api.Category
|
||||||
|
import com.android.tools.lint.detector.api.Implementation
|
||||||
|
import com.android.tools.lint.detector.api.Incident
|
||||||
|
import com.android.tools.lint.detector.api.Issue
|
||||||
|
import com.android.tools.lint.detector.api.JavaContext
|
||||||
|
import com.android.tools.lint.detector.api.Scope
|
||||||
|
import com.android.tools.lint.detector.api.Severity
|
||||||
|
import org.jetbrains.uast.UBlockExpression
|
||||||
|
import org.jetbrains.uast.UMethod
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Looks for methods implementing generated AIDL interface stubs
|
||||||
|
* that can have simple permission checks migrated to
|
||||||
|
* @EnforcePermission annotations
|
||||||
|
*/
|
||||||
|
@Suppress("UnstableApiUsage")
|
||||||
|
class SimpleManualPermissionEnforcementDetector : AidlImplementationDetector() {
|
||||||
|
override fun visitAidlMethod(
|
||||||
|
context: JavaContext,
|
||||||
|
node: UMethod,
|
||||||
|
interfaceName: String,
|
||||||
|
body: UBlockExpression
|
||||||
|
) {
|
||||||
|
val enforcePermissionFix = EnforcePermissionFix.fromBlockExpression(context, body) ?: return
|
||||||
|
val lintFix = enforcePermissionFix.toLintFix(context, node)
|
||||||
|
val message =
|
||||||
|
"$interfaceName permission check ${
|
||||||
|
if (enforcePermissionFix.errorLevel) "should" else "can"
|
||||||
|
} be converted to @EnforcePermission annotation"
|
||||||
|
|
||||||
|
val incident = Incident(
|
||||||
|
ISSUE_SIMPLE_MANUAL_PERMISSION_ENFORCEMENT,
|
||||||
|
enforcePermissionFix.manualCheckLocations.last(),
|
||||||
|
message,
|
||||||
|
lintFix
|
||||||
|
)
|
||||||
|
|
||||||
|
// TODO(b/265014041): turn on errors once all code that would cause one is fixed
|
||||||
|
// if (enforcePermissionFix.errorLevel) {
|
||||||
|
// incident.overrideSeverity(Severity.ERROR)
|
||||||
|
// }
|
||||||
|
|
||||||
|
context.report(incident)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private val EXPLANATION = """
|
||||||
|
Whenever possible, method implementations of AIDL interfaces should use the @EnforcePermission
|
||||||
|
annotation to declare the permissions to be enforced. The verification code is then
|
||||||
|
generated by the AIDL compiler, which also takes care of annotating the generated java
|
||||||
|
code.
|
||||||
|
|
||||||
|
This reduces the risk of bugs around these permission checks (that often become vulnerabilities).
|
||||||
|
It also enables easier auditing and review.
|
||||||
|
|
||||||
|
Please migrate to an @EnforcePermission annotation. (See: go/aidl-enforce-howto)
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
val ISSUE_SIMPLE_MANUAL_PERMISSION_ENFORCEMENT = Issue.create(
|
||||||
|
id = "SimpleManualPermissionEnforcement",
|
||||||
|
briefDescription = "Manual permission check can be @EnforcePermission annotation",
|
||||||
|
explanation = EXPLANATION,
|
||||||
|
category = Category.SECURITY,
|
||||||
|
priority = 5,
|
||||||
|
severity = Severity.WARNING,
|
||||||
|
implementation = Implementation(
|
||||||
|
SimpleManualPermissionEnforcementDetector::class.java,
|
||||||
|
Scope.JAVA_FILE_SCOPE
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,557 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2022 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.google.android.lint.aidl
|
||||||
|
|
||||||
|
import com.android.tools.lint.checks.infrastructure.LintDetectorTest
|
||||||
|
import com.android.tools.lint.checks.infrastructure.TestFile
|
||||||
|
import com.android.tools.lint.checks.infrastructure.TestLintTask
|
||||||
|
import com.android.tools.lint.detector.api.Detector
|
||||||
|
import com.android.tools.lint.detector.api.Issue
|
||||||
|
|
||||||
|
@Suppress("UnstableApiUsage")
|
||||||
|
class EnforcePermissionDetectorCodegenTest : LintDetectorTest() {
|
||||||
|
override fun getDetector(): Detector = EnforcePermissionDetector()
|
||||||
|
|
||||||
|
override fun getIssues(): List<Issue> = listOf(
|
||||||
|
EnforcePermissionDetector.ISSUE_MISSING_ENFORCE_PERMISSION,
|
||||||
|
EnforcePermissionDetector.ISSUE_MISMATCHING_ENFORCE_PERMISSION
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun lint(): TestLintTask = super.lint().allowMissingSdk(true)
|
||||||
|
|
||||||
|
fun test_generated_IProtected() {
|
||||||
|
lint().files(
|
||||||
|
java(
|
||||||
|
"""
|
||||||
|
/*
|
||||||
|
* This file is auto-generated. DO NOT MODIFY.
|
||||||
|
*/
|
||||||
|
package android.aidl.tests.permission;
|
||||||
|
public interface IProtected extends android.os.IInterface
|
||||||
|
{
|
||||||
|
/** Default implementation for IProtected. */
|
||||||
|
public static class Default implements android.aidl.tests.permission.IProtected
|
||||||
|
{
|
||||||
|
@Override public void PermissionProtected() throws android.os.RemoteException
|
||||||
|
{
|
||||||
|
}
|
||||||
|
@Override public void MultiplePermissionsAll() throws android.os.RemoteException
|
||||||
|
{
|
||||||
|
}
|
||||||
|
@Override public void MultiplePermissionsAny() throws android.os.RemoteException
|
||||||
|
{
|
||||||
|
}
|
||||||
|
@Override public void NonManifestPermission() throws android.os.RemoteException
|
||||||
|
{
|
||||||
|
}
|
||||||
|
// Used by the integration tests to dynamically set permissions that are considered granted.
|
||||||
|
@Override public void SetGranted(java.util.List<java.lang.String> permissions) throws android.os.RemoteException
|
||||||
|
{
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public android.os.IBinder asBinder() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/** Local-side IPC implementation stub class. */
|
||||||
|
public static abstract class Stub extends android.os.Binder implements android.aidl.tests.permission.IProtected
|
||||||
|
{
|
||||||
|
private final android.os.PermissionEnforcer mEnforcer;
|
||||||
|
/** Construct the stub using the Enforcer provided. */
|
||||||
|
public Stub(android.os.PermissionEnforcer enforcer)
|
||||||
|
{
|
||||||
|
this.attachInterface(this, DESCRIPTOR);
|
||||||
|
if (enforcer == null) {
|
||||||
|
throw new IllegalArgumentException("enforcer cannot be null");
|
||||||
|
}
|
||||||
|
mEnforcer = enforcer;
|
||||||
|
}
|
||||||
|
@Deprecated
|
||||||
|
/** Default constructor. */
|
||||||
|
public Stub() {
|
||||||
|
this(android.os.PermissionEnforcer.fromContext(
|
||||||
|
android.app.ActivityThread.currentActivityThread().getSystemContext()));
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Cast an IBinder object into an android.aidl.tests.permission.IProtected interface,
|
||||||
|
* generating a proxy if needed.
|
||||||
|
*/
|
||||||
|
public static android.aidl.tests.permission.IProtected asInterface(android.os.IBinder obj)
|
||||||
|
{
|
||||||
|
if ((obj==null)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
|
||||||
|
if (((iin!=null)&&(iin instanceof android.aidl.tests.permission.IProtected))) {
|
||||||
|
return ((android.aidl.tests.permission.IProtected)iin);
|
||||||
|
}
|
||||||
|
return new android.aidl.tests.permission.IProtected.Stub.Proxy(obj);
|
||||||
|
}
|
||||||
|
@Override public android.os.IBinder asBinder()
|
||||||
|
{
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
/** @hide */
|
||||||
|
public static java.lang.String getDefaultTransactionName(int transactionCode)
|
||||||
|
{
|
||||||
|
switch (transactionCode)
|
||||||
|
{
|
||||||
|
case TRANSACTION_PermissionProtected:
|
||||||
|
{
|
||||||
|
return "PermissionProtected";
|
||||||
|
}
|
||||||
|
case TRANSACTION_MultiplePermissionsAll:
|
||||||
|
{
|
||||||
|
return "MultiplePermissionsAll";
|
||||||
|
}
|
||||||
|
case TRANSACTION_MultiplePermissionsAny:
|
||||||
|
{
|
||||||
|
return "MultiplePermissionsAny";
|
||||||
|
}
|
||||||
|
case TRANSACTION_NonManifestPermission:
|
||||||
|
{
|
||||||
|
return "NonManifestPermission";
|
||||||
|
}
|
||||||
|
case TRANSACTION_SetGranted:
|
||||||
|
{
|
||||||
|
return "SetGranted";
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/** @hide */
|
||||||
|
public java.lang.String getTransactionName(int transactionCode)
|
||||||
|
{
|
||||||
|
return this.getDefaultTransactionName(transactionCode);
|
||||||
|
}
|
||||||
|
@Override public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException
|
||||||
|
{
|
||||||
|
java.lang.String descriptor = DESCRIPTOR;
|
||||||
|
if (code >= android.os.IBinder.FIRST_CALL_TRANSACTION && code <= android.os.IBinder.LAST_CALL_TRANSACTION) {
|
||||||
|
data.enforceInterface(descriptor);
|
||||||
|
}
|
||||||
|
switch (code)
|
||||||
|
{
|
||||||
|
case INTERFACE_TRANSACTION:
|
||||||
|
{
|
||||||
|
reply.writeString(descriptor);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch (code)
|
||||||
|
{
|
||||||
|
case TRANSACTION_PermissionProtected:
|
||||||
|
{
|
||||||
|
this.PermissionProtected();
|
||||||
|
reply.writeNoException();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case TRANSACTION_MultiplePermissionsAll:
|
||||||
|
{
|
||||||
|
this.MultiplePermissionsAll();
|
||||||
|
reply.writeNoException();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case TRANSACTION_MultiplePermissionsAny:
|
||||||
|
{
|
||||||
|
this.MultiplePermissionsAny();
|
||||||
|
reply.writeNoException();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case TRANSACTION_NonManifestPermission:
|
||||||
|
{
|
||||||
|
this.NonManifestPermission();
|
||||||
|
reply.writeNoException();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case TRANSACTION_SetGranted:
|
||||||
|
{
|
||||||
|
java.util.List<java.lang.String> _arg0;
|
||||||
|
_arg0 = data.createStringArrayList();
|
||||||
|
data.enforceNoDataAvail();
|
||||||
|
this.SetGranted(_arg0);
|
||||||
|
reply.writeNoException();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
{
|
||||||
|
return super.onTransact(code, data, reply, flags);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
private static class Proxy implements android.aidl.tests.permission.IProtected
|
||||||
|
{
|
||||||
|
private android.os.IBinder mRemote;
|
||||||
|
Proxy(android.os.IBinder remote)
|
||||||
|
{
|
||||||
|
mRemote = remote;
|
||||||
|
}
|
||||||
|
@Override public android.os.IBinder asBinder()
|
||||||
|
{
|
||||||
|
return mRemote;
|
||||||
|
}
|
||||||
|
public java.lang.String getInterfaceDescriptor()
|
||||||
|
{
|
||||||
|
return DESCRIPTOR;
|
||||||
|
}
|
||||||
|
@Override public void PermissionProtected() throws android.os.RemoteException
|
||||||
|
{
|
||||||
|
android.os.Parcel _data = android.os.Parcel.obtain(asBinder());
|
||||||
|
android.os.Parcel _reply = android.os.Parcel.obtain();
|
||||||
|
try {
|
||||||
|
_data.writeInterfaceToken(DESCRIPTOR);
|
||||||
|
boolean _status = mRemote.transact(Stub.TRANSACTION_PermissionProtected, _data, _reply, 0);
|
||||||
|
_reply.readException();
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
_reply.recycle();
|
||||||
|
_data.recycle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@Override public void MultiplePermissionsAll() throws android.os.RemoteException
|
||||||
|
{
|
||||||
|
android.os.Parcel _data = android.os.Parcel.obtain(asBinder());
|
||||||
|
android.os.Parcel _reply = android.os.Parcel.obtain();
|
||||||
|
try {
|
||||||
|
_data.writeInterfaceToken(DESCRIPTOR);
|
||||||
|
boolean _status = mRemote.transact(Stub.TRANSACTION_MultiplePermissionsAll, _data, _reply, 0);
|
||||||
|
_reply.readException();
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
_reply.recycle();
|
||||||
|
_data.recycle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@Override public void MultiplePermissionsAny() throws android.os.RemoteException
|
||||||
|
{
|
||||||
|
android.os.Parcel _data = android.os.Parcel.obtain(asBinder());
|
||||||
|
android.os.Parcel _reply = android.os.Parcel.obtain();
|
||||||
|
try {
|
||||||
|
_data.writeInterfaceToken(DESCRIPTOR);
|
||||||
|
boolean _status = mRemote.transact(Stub.TRANSACTION_MultiplePermissionsAny, _data, _reply, 0);
|
||||||
|
_reply.readException();
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
_reply.recycle();
|
||||||
|
_data.recycle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@Override public void NonManifestPermission() throws android.os.RemoteException
|
||||||
|
{
|
||||||
|
android.os.Parcel _data = android.os.Parcel.obtain(asBinder());
|
||||||
|
android.os.Parcel _reply = android.os.Parcel.obtain();
|
||||||
|
try {
|
||||||
|
_data.writeInterfaceToken(DESCRIPTOR);
|
||||||
|
boolean _status = mRemote.transact(Stub.TRANSACTION_NonManifestPermission, _data, _reply, 0);
|
||||||
|
_reply.readException();
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
_reply.recycle();
|
||||||
|
_data.recycle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Used by the integration tests to dynamically set permissions that are considered granted.
|
||||||
|
@Override public void SetGranted(java.util.List<java.lang.String> permissions) throws android.os.RemoteException
|
||||||
|
{
|
||||||
|
android.os.Parcel _data = android.os.Parcel.obtain(asBinder());
|
||||||
|
android.os.Parcel _reply = android.os.Parcel.obtain();
|
||||||
|
try {
|
||||||
|
_data.writeInterfaceToken(DESCRIPTOR);
|
||||||
|
_data.writeStringList(permissions);
|
||||||
|
boolean _status = mRemote.transact(Stub.TRANSACTION_SetGranted, _data, _reply, 0);
|
||||||
|
_reply.readException();
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
_reply.recycle();
|
||||||
|
_data.recycle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static final int TRANSACTION_PermissionProtected = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
|
||||||
|
/** Helper method to enforce permissions for PermissionProtected */
|
||||||
|
protected void PermissionProtected_enforcePermission() throws SecurityException {
|
||||||
|
android.content.AttributionSource source = new android.content.AttributionSource(getCallingUid(), null, null);
|
||||||
|
mEnforcer.enforcePermission(android.Manifest.permission.READ_PHONE_STATE, source);
|
||||||
|
}
|
||||||
|
static final int TRANSACTION_MultiplePermissionsAll = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);
|
||||||
|
/** Helper method to enforce permissions for MultiplePermissionsAll */
|
||||||
|
protected void MultiplePermissionsAll_enforcePermission() throws SecurityException {
|
||||||
|
android.content.AttributionSource source = new android.content.AttributionSource(getCallingUid(), null, null);
|
||||||
|
mEnforcer.enforcePermissionAllOf(new String[]{android.Manifest.permission.INTERNET, android.Manifest.permission.VIBRATE}, source);
|
||||||
|
}
|
||||||
|
static final int TRANSACTION_MultiplePermissionsAny = (android.os.IBinder.FIRST_CALL_TRANSACTION + 2);
|
||||||
|
/** Helper method to enforce permissions for MultiplePermissionsAny */
|
||||||
|
protected void MultiplePermissionsAny_enforcePermission() throws SecurityException {
|
||||||
|
android.content.AttributionSource source = new android.content.AttributionSource(getCallingUid(), null, null);
|
||||||
|
mEnforcer.enforcePermissionAnyOf(new String[]{android.Manifest.permission.INTERNET, android.Manifest.permission.VIBRATE}, source);
|
||||||
|
}
|
||||||
|
static final int TRANSACTION_NonManifestPermission = (android.os.IBinder.FIRST_CALL_TRANSACTION + 3);
|
||||||
|
/** Helper method to enforce permissions for NonManifestPermission */
|
||||||
|
protected void NonManifestPermission_enforcePermission() throws SecurityException {
|
||||||
|
android.content.AttributionSource source = new android.content.AttributionSource(getCallingUid(), null, null);
|
||||||
|
mEnforcer.enforcePermission(android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, source);
|
||||||
|
}
|
||||||
|
static final int TRANSACTION_SetGranted = (android.os.IBinder.FIRST_CALL_TRANSACTION + 4);
|
||||||
|
/** @hide */
|
||||||
|
public int getMaxTransactionId()
|
||||||
|
{
|
||||||
|
return 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE)
|
||||||
|
public void PermissionProtected() throws android.os.RemoteException;
|
||||||
|
@android.annotation.EnforcePermission(allOf = {android.Manifest.permission.INTERNET, android.Manifest.permission.VIBRATE})
|
||||||
|
public void MultiplePermissionsAll() throws android.os.RemoteException;
|
||||||
|
@android.annotation.EnforcePermission(anyOf = {android.Manifest.permission.INTERNET, android.Manifest.permission.VIBRATE})
|
||||||
|
public void MultiplePermissionsAny() throws android.os.RemoteException;
|
||||||
|
@android.annotation.EnforcePermission(android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK)
|
||||||
|
public void NonManifestPermission() throws android.os.RemoteException;
|
||||||
|
// Used by the integration tests to dynamically set permissions that are considered granted.
|
||||||
|
@android.annotation.RequiresNoPermission
|
||||||
|
public void SetGranted(java.util.List<java.lang.String> permissions) throws android.os.RemoteException;
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
).indented(),
|
||||||
|
*stubs
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
.expectClean()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun test_generated_IProtectedInterface() {
|
||||||
|
lint().files(
|
||||||
|
java(
|
||||||
|
"""
|
||||||
|
/*
|
||||||
|
* This file is auto-generated. DO NOT MODIFY.
|
||||||
|
*/
|
||||||
|
package android.aidl.tests.permission;
|
||||||
|
public interface IProtectedInterface extends android.os.IInterface
|
||||||
|
{
|
||||||
|
/** Default implementation for IProtectedInterface. */
|
||||||
|
public static class Default implements android.aidl.tests.permission.IProtectedInterface
|
||||||
|
{
|
||||||
|
@Override public void Method1() throws android.os.RemoteException
|
||||||
|
{
|
||||||
|
}
|
||||||
|
@Override public void Method2() throws android.os.RemoteException
|
||||||
|
{
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public android.os.IBinder asBinder() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/** Local-side IPC implementation stub class. */
|
||||||
|
public static abstract class Stub extends android.os.Binder implements android.aidl.tests.permission.IProtectedInterface
|
||||||
|
{
|
||||||
|
private final android.os.PermissionEnforcer mEnforcer;
|
||||||
|
/** Construct the stub using the Enforcer provided. */
|
||||||
|
public Stub(android.os.PermissionEnforcer enforcer)
|
||||||
|
{
|
||||||
|
this.attachInterface(this, DESCRIPTOR);
|
||||||
|
if (enforcer == null) {
|
||||||
|
throw new IllegalArgumentException("enforcer cannot be null");
|
||||||
|
}
|
||||||
|
mEnforcer = enforcer;
|
||||||
|
}
|
||||||
|
@Deprecated
|
||||||
|
/** Default constructor. */
|
||||||
|
public Stub() {
|
||||||
|
this(android.os.PermissionEnforcer.fromContext(
|
||||||
|
android.app.ActivityThread.currentActivityThread().getSystemContext()));
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Cast an IBinder object into an android.aidl.tests.permission.IProtectedInterface interface,
|
||||||
|
* generating a proxy if needed.
|
||||||
|
*/
|
||||||
|
public static android.aidl.tests.permission.IProtectedInterface asInterface(android.os.IBinder obj)
|
||||||
|
{
|
||||||
|
if ((obj==null)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
|
||||||
|
if (((iin!=null)&&(iin instanceof android.aidl.tests.permission.IProtectedInterface))) {
|
||||||
|
return ((android.aidl.tests.permission.IProtectedInterface)iin);
|
||||||
|
}
|
||||||
|
return new android.aidl.tests.permission.IProtectedInterface.Stub.Proxy(obj);
|
||||||
|
}
|
||||||
|
@Override public android.os.IBinder asBinder()
|
||||||
|
{
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
/** @hide */
|
||||||
|
public static java.lang.String getDefaultTransactionName(int transactionCode)
|
||||||
|
{
|
||||||
|
switch (transactionCode)
|
||||||
|
{
|
||||||
|
case TRANSACTION_Method1:
|
||||||
|
{
|
||||||
|
return "Method1";
|
||||||
|
}
|
||||||
|
case TRANSACTION_Method2:
|
||||||
|
{
|
||||||
|
return "Method2";
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/** @hide */
|
||||||
|
public java.lang.String getTransactionName(int transactionCode)
|
||||||
|
{
|
||||||
|
return this.getDefaultTransactionName(transactionCode);
|
||||||
|
}
|
||||||
|
@Override public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException
|
||||||
|
{
|
||||||
|
java.lang.String descriptor = DESCRIPTOR;
|
||||||
|
if (code >= android.os.IBinder.FIRST_CALL_TRANSACTION && code <= android.os.IBinder.LAST_CALL_TRANSACTION) {
|
||||||
|
data.enforceInterface(descriptor);
|
||||||
|
}
|
||||||
|
switch (code)
|
||||||
|
{
|
||||||
|
case INTERFACE_TRANSACTION:
|
||||||
|
{
|
||||||
|
reply.writeString(descriptor);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch (code)
|
||||||
|
{
|
||||||
|
case TRANSACTION_Method1:
|
||||||
|
{
|
||||||
|
this.Method1();
|
||||||
|
reply.writeNoException();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case TRANSACTION_Method2:
|
||||||
|
{
|
||||||
|
this.Method2();
|
||||||
|
reply.writeNoException();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
{
|
||||||
|
return super.onTransact(code, data, reply, flags);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
private static class Proxy implements android.aidl.tests.permission.IProtectedInterface
|
||||||
|
{
|
||||||
|
private android.os.IBinder mRemote;
|
||||||
|
Proxy(android.os.IBinder remote)
|
||||||
|
{
|
||||||
|
mRemote = remote;
|
||||||
|
}
|
||||||
|
@Override public android.os.IBinder asBinder()
|
||||||
|
{
|
||||||
|
return mRemote;
|
||||||
|
}
|
||||||
|
public java.lang.String getInterfaceDescriptor()
|
||||||
|
{
|
||||||
|
return DESCRIPTOR;
|
||||||
|
}
|
||||||
|
@Override public void Method1() throws android.os.RemoteException
|
||||||
|
{
|
||||||
|
android.os.Parcel _data = android.os.Parcel.obtain(asBinder());
|
||||||
|
android.os.Parcel _reply = android.os.Parcel.obtain();
|
||||||
|
try {
|
||||||
|
_data.writeInterfaceToken(DESCRIPTOR);
|
||||||
|
boolean _status = mRemote.transact(Stub.TRANSACTION_Method1, _data, _reply, 0);
|
||||||
|
_reply.readException();
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
_reply.recycle();
|
||||||
|
_data.recycle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@Override public void Method2() throws android.os.RemoteException
|
||||||
|
{
|
||||||
|
android.os.Parcel _data = android.os.Parcel.obtain(asBinder());
|
||||||
|
android.os.Parcel _reply = android.os.Parcel.obtain();
|
||||||
|
try {
|
||||||
|
_data.writeInterfaceToken(DESCRIPTOR);
|
||||||
|
boolean _status = mRemote.transact(Stub.TRANSACTION_Method2, _data, _reply, 0);
|
||||||
|
_reply.readException();
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
_reply.recycle();
|
||||||
|
_data.recycle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static final int TRANSACTION_Method1 = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
|
||||||
|
/** Helper method to enforce permissions for Method1 */
|
||||||
|
protected void Method1_enforcePermission() throws SecurityException {
|
||||||
|
android.content.AttributionSource source = new android.content.AttributionSource(getCallingUid(), null, null);
|
||||||
|
mEnforcer.enforcePermission(android.Manifest.permission.ACCESS_FINE_LOCATION, source);
|
||||||
|
}
|
||||||
|
static final int TRANSACTION_Method2 = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);
|
||||||
|
/** Helper method to enforce permissions for Method2 */
|
||||||
|
protected void Method2_enforcePermission() throws SecurityException {
|
||||||
|
android.content.AttributionSource source = new android.content.AttributionSource(getCallingUid(), null, null);
|
||||||
|
mEnforcer.enforcePermission(android.Manifest.permission.ACCESS_FINE_LOCATION, source);
|
||||||
|
}
|
||||||
|
/** @hide */
|
||||||
|
public int getMaxTransactionId()
|
||||||
|
{
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_FINE_LOCATION)
|
||||||
|
public void Method1() throws android.os.RemoteException;
|
||||||
|
@android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_FINE_LOCATION)
|
||||||
|
public void Method2() throws android.os.RemoteException;
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
).indented(),
|
||||||
|
*stubs
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
.expectClean()
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stubs */
|
||||||
|
|
||||||
|
private val manifestPermissionStub: TestFile = java(
|
||||||
|
"""
|
||||||
|
package android.Manifest;
|
||||||
|
class permission {
|
||||||
|
public static final String READ_PHONE_STATE = "android.permission.READ_PHONE_STATE";
|
||||||
|
public static final String INTERNET = "android.permission.INTERNET";
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
).indented()
|
||||||
|
|
||||||
|
private val enforcePermissionAnnotationStub: TestFile = java(
|
||||||
|
"""
|
||||||
|
package android.annotation;
|
||||||
|
public @interface EnforcePermission {}
|
||||||
|
"""
|
||||||
|
).indented()
|
||||||
|
|
||||||
|
private val stubs = arrayOf(manifestPermissionStub, enforcePermissionAnnotationStub)
|
||||||
|
}
|
||||||
@@ -0,0 +1,425 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2022 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.google.android.lint.aidl
|
||||||
|
|
||||||
|
import com.android.tools.lint.checks.infrastructure.LintDetectorTest
|
||||||
|
import com.android.tools.lint.checks.infrastructure.TestFile
|
||||||
|
import com.android.tools.lint.checks.infrastructure.TestLintTask
|
||||||
|
import com.android.tools.lint.detector.api.Detector
|
||||||
|
import com.android.tools.lint.detector.api.Issue
|
||||||
|
|
||||||
|
@Suppress("UnstableApiUsage")
|
||||||
|
class EnforcePermissionDetectorTest : LintDetectorTest() {
|
||||||
|
override fun getDetector(): Detector = EnforcePermissionDetector()
|
||||||
|
|
||||||
|
override fun getIssues(): List<Issue> = listOf(
|
||||||
|
EnforcePermissionDetector.ISSUE_MISSING_ENFORCE_PERMISSION,
|
||||||
|
EnforcePermissionDetector.ISSUE_MISMATCHING_ENFORCE_PERMISSION
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun lint(): TestLintTask = super.lint().allowMissingSdk(true)
|
||||||
|
|
||||||
|
fun testDoesNotDetectIssuesCorrectAnnotationOnMethod() {
|
||||||
|
lint().files(java(
|
||||||
|
"""
|
||||||
|
package test.pkg;
|
||||||
|
import android.annotation.EnforcePermission;
|
||||||
|
public class TestClass2 extends IFooMethod.Stub {
|
||||||
|
@Override
|
||||||
|
@EnforcePermission(android.Manifest.permission.READ_PHONE_STATE)
|
||||||
|
public void testMethod() {}
|
||||||
|
}
|
||||||
|
""").indented(),
|
||||||
|
*stubs
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
.expectClean()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testDoesNotDetectIssuesCorrectAnnotationAllOnMethod() {
|
||||||
|
lint().files(java(
|
||||||
|
"""
|
||||||
|
package test.pkg;
|
||||||
|
import android.annotation.EnforcePermission;
|
||||||
|
public class TestClass11 extends IFooMethod.Stub {
|
||||||
|
@Override
|
||||||
|
@EnforcePermission(allOf={android.Manifest.permission.INTERNET, android.Manifest.permission.READ_PHONE_STATE})
|
||||||
|
public void testMethodAll() {}
|
||||||
|
}
|
||||||
|
""").indented(),
|
||||||
|
*stubs
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
.expectClean()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testDoesNotDetectIssuesCorrectAnnotationAllLiteralOnMethod() {
|
||||||
|
lint().files(java(
|
||||||
|
"""
|
||||||
|
package test.pkg;
|
||||||
|
import android.annotation.EnforcePermission;
|
||||||
|
public class TestClass111 extends IFooMethod.Stub {
|
||||||
|
@Override
|
||||||
|
@EnforcePermission(allOf={"android.permission.INTERNET", android.Manifest.permission.READ_PHONE_STATE})
|
||||||
|
public void testMethodAllLiteral() {}
|
||||||
|
}
|
||||||
|
""").indented(),
|
||||||
|
*stubs
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
.expectClean()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testDoesNotDetectIssuesCorrectAnnotationAnyOnMethod() {
|
||||||
|
lint().files(java(
|
||||||
|
"""
|
||||||
|
package test.pkg;
|
||||||
|
import android.annotation.EnforcePermission;
|
||||||
|
public class TestClass12 extends IFooMethod.Stub {
|
||||||
|
@Override
|
||||||
|
@EnforcePermission(anyOf={android.Manifest.permission.INTERNET, android.Manifest.permission.READ_PHONE_STATE})
|
||||||
|
public void testMethodAny() {}
|
||||||
|
}
|
||||||
|
""").indented(),
|
||||||
|
*stubs
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
.expectClean()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testDoesNotDetectIssuesCorrectAnnotationAnyLiteralOnMethod() {
|
||||||
|
lint().files(java(
|
||||||
|
"""
|
||||||
|
package test.pkg;
|
||||||
|
import android.annotation.EnforcePermission;
|
||||||
|
public class TestClass121 extends IFooMethod.Stub {
|
||||||
|
@Override
|
||||||
|
@EnforcePermission(anyOf={"android.permission.INTERNET", android.Manifest.permission.READ_PHONE_STATE})
|
||||||
|
public void testMethodAnyLiteral() {}
|
||||||
|
}
|
||||||
|
""").indented(),
|
||||||
|
*stubs
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
.expectClean()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testDetectIssuesMismatchingAnnotationOnMethod() {
|
||||||
|
lint().files(java(
|
||||||
|
"""
|
||||||
|
package test.pkg;
|
||||||
|
public class TestClass4 extends IFooMethod.Stub {
|
||||||
|
@android.annotation.EnforcePermission(android.Manifest.permission.INTERNET)
|
||||||
|
public void testMethod() {}
|
||||||
|
}
|
||||||
|
""").indented(),
|
||||||
|
*stubs
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
.expect("""
|
||||||
|
src/test/pkg/TestClass4.java:4: Error: The method TestClass4.testMethod is annotated with @android.annotation.EnforcePermission(android.Manifest.permission.INTERNET) \
|
||||||
|
which differs from the overridden method Stub.testMethod: @android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE). \
|
||||||
|
The same annotation must be used for both methods. [MismatchingEnforcePermissionAnnotation]
|
||||||
|
public void testMethod() {}
|
||||||
|
~~~~~~~~~~
|
||||||
|
1 errors, 0 warnings
|
||||||
|
""".addLineContinuation())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testDetectIssuesEmptyAnnotationOnMethod() {
|
||||||
|
lint().files(java(
|
||||||
|
"""
|
||||||
|
package test.pkg;
|
||||||
|
public class TestClass41 extends IFooMethod.Stub {
|
||||||
|
@android.annotation.EnforcePermission
|
||||||
|
public void testMethod() {}
|
||||||
|
}
|
||||||
|
""").indented(),
|
||||||
|
*stubs
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
.expect("""
|
||||||
|
src/test/pkg/TestClass41.java:4: Error: The method TestClass41.testMethod is annotated with @android.annotation.EnforcePermission \
|
||||||
|
which differs from the overridden method Stub.testMethod: @android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE). \
|
||||||
|
The same annotation must be used for both methods. [MismatchingEnforcePermissionAnnotation]
|
||||||
|
public void testMethod() {}
|
||||||
|
~~~~~~~~~~
|
||||||
|
1 errors, 0 warnings
|
||||||
|
""".addLineContinuation())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testDetectIssuesMismatchingAnyAnnotationOnMethod() {
|
||||||
|
lint().files(java(
|
||||||
|
"""
|
||||||
|
package test.pkg;
|
||||||
|
public class TestClass9 extends IFooMethod.Stub {
|
||||||
|
@android.annotation.EnforcePermission(anyOf={android.Manifest.permission.INTERNET, android.Manifest.permission.NFC})
|
||||||
|
public void testMethodAny() {}
|
||||||
|
}
|
||||||
|
""").indented(),
|
||||||
|
*stubs
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
.expect("""
|
||||||
|
src/test/pkg/TestClass9.java:4: Error: The method TestClass9.testMethodAny is annotated with \
|
||||||
|
@android.annotation.EnforcePermission(anyOf={android.Manifest.permission.INTERNET, android.Manifest.permission.NFC}) \
|
||||||
|
which differs from the overridden method Stub.testMethodAny: \
|
||||||
|
@android.annotation.EnforcePermission(anyOf={android.Manifest.permission.INTERNET, android.Manifest.permission.READ_PHONE_STATE}). \
|
||||||
|
The same annotation must be used for both methods. [MismatchingEnforcePermissionAnnotation]
|
||||||
|
public void testMethodAny() {}
|
||||||
|
~~~~~~~~~~~~~
|
||||||
|
1 errors, 0 warnings
|
||||||
|
""".addLineContinuation())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testDetectIssuesMismatchingAnyLiteralAnnotationOnMethod() {
|
||||||
|
lint().files(java(
|
||||||
|
"""
|
||||||
|
package test.pkg;
|
||||||
|
public class TestClass91 extends IFooMethod.Stub {
|
||||||
|
@android.annotation.EnforcePermission(anyOf={"android.permission.INTERNET", "android.permissionoopsthisisatypo.READ_PHONE_STATE"})
|
||||||
|
public void testMethodAnyLiteral() {}
|
||||||
|
}
|
||||||
|
""").indented(),
|
||||||
|
*stubs
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
.expect("""
|
||||||
|
src/test/pkg/TestClass91.java:4: Error: The method TestClass91.testMethodAnyLiteral is annotated with \
|
||||||
|
@android.annotation.EnforcePermission(anyOf={"android.permission.INTERNET", "android.permissionoopsthisisatypo.READ_PHONE_STATE"}) \
|
||||||
|
which differs from the overridden method Stub.testMethodAnyLiteral: \
|
||||||
|
@android.annotation.EnforcePermission(anyOf={android.Manifest.permission.INTERNET, "android.permission.READ_PHONE_STATE"}). \
|
||||||
|
The same annotation must be used for both methods. [MismatchingEnforcePermissionAnnotation]
|
||||||
|
public void testMethodAnyLiteral() {}
|
||||||
|
~~~~~~~~~~~~~~~~~~~~
|
||||||
|
1 errors, 0 warnings
|
||||||
|
""".addLineContinuation())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testDetectIssuesMismatchingAllAnnotationOnMethod() {
|
||||||
|
lint().files(java(
|
||||||
|
"""
|
||||||
|
package test.pkg;
|
||||||
|
public class TestClass10 extends IFooMethod.Stub {
|
||||||
|
@android.annotation.EnforcePermission(allOf={android.Manifest.permission.INTERNET, android.Manifest.permission.NFC})
|
||||||
|
public void testMethodAll() {}
|
||||||
|
}
|
||||||
|
""").indented(),
|
||||||
|
*stubs
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
.expect("""
|
||||||
|
src/test/pkg/TestClass10.java:4: Error: The method TestClass10.testMethodAll is annotated with \
|
||||||
|
@android.annotation.EnforcePermission(allOf={android.Manifest.permission.INTERNET, android.Manifest.permission.NFC}) \
|
||||||
|
which differs from the overridden method Stub.testMethodAll: \
|
||||||
|
@android.annotation.EnforcePermission(allOf={android.Manifest.permission.INTERNET, android.Manifest.permission.READ_PHONE_STATE}). \
|
||||||
|
The same annotation must be used for both methods. [MismatchingEnforcePermissionAnnotation]
|
||||||
|
public void testMethodAll() {}
|
||||||
|
~~~~~~~~~~~~~
|
||||||
|
1 errors, 0 warnings
|
||||||
|
""".addLineContinuation())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testDetectIssuesMismatchingAllLiteralAnnotationOnMethod() {
|
||||||
|
lint().files(java(
|
||||||
|
"""
|
||||||
|
package test.pkg;
|
||||||
|
public class TestClass101 extends IFooMethod.Stub {
|
||||||
|
@android.annotation.EnforcePermission(allOf={"android.permission.INTERNET", "android.permissionoopsthisisatypo.READ_PHONE_STATE"})
|
||||||
|
public void testMethodAllLiteral() {}
|
||||||
|
}
|
||||||
|
""").indented(),
|
||||||
|
*stubs
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
.expect("""
|
||||||
|
src/test/pkg/TestClass101.java:4: Error: The method TestClass101.testMethodAllLiteral is annotated with \
|
||||||
|
@android.annotation.EnforcePermission(allOf={"android.permission.INTERNET", "android.permissionoopsthisisatypo.READ_PHONE_STATE"}) \
|
||||||
|
which differs from the overridden method Stub.testMethodAllLiteral: \
|
||||||
|
@android.annotation.EnforcePermission(allOf={android.Manifest.permission.INTERNET, "android.permission.READ_PHONE_STATE"}). \
|
||||||
|
The same annotation must be used for both methods. [MismatchingEnforcePermissionAnnotation]
|
||||||
|
public void testMethodAllLiteral() {}
|
||||||
|
~~~~~~~~~~~~~~~~~~~~
|
||||||
|
1 errors, 0 warnings
|
||||||
|
""".addLineContinuation())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testDetectIssuesMissingAnnotationOnMethod() {
|
||||||
|
lint().files(java(
|
||||||
|
"""
|
||||||
|
package test.pkg;
|
||||||
|
public class TestClass6 extends IFooMethod.Stub {
|
||||||
|
public void testMethod() {}
|
||||||
|
}
|
||||||
|
""").indented(),
|
||||||
|
*stubs
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
.expect("""
|
||||||
|
src/test/pkg/TestClass6.java:3: Error: The method TestClass6.testMethod overrides the method Stub.testMethod which is annotated with @EnforcePermission. \
|
||||||
|
The same annotation must be used on TestClass6.testMethod [MissingEnforcePermissionAnnotation]
|
||||||
|
public void testMethod() {}
|
||||||
|
~~~~~~~~~~
|
||||||
|
1 errors, 0 warnings
|
||||||
|
""".addLineContinuation())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testDetectIssuesExtraAnnotationMethod() {
|
||||||
|
lint().files(java(
|
||||||
|
"""
|
||||||
|
package test.pkg;
|
||||||
|
public class TestClass7 extends IBar.Stub {
|
||||||
|
@android.annotation.EnforcePermission(android.Manifest.permission.INTERNET)
|
||||||
|
public void testMethod() {}
|
||||||
|
}
|
||||||
|
""").indented(),
|
||||||
|
*stubs
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
.expect("""
|
||||||
|
src/test/pkg/TestClass7.java:4: Error: The method TestClass7.testMethod overrides the method Stub.testMethod which is not annotated with @EnforcePermission. \
|
||||||
|
The same annotation must be used on Stub.testMethod. Did you forget to annotate the AIDL definition? [MissingEnforcePermissionAnnotation]
|
||||||
|
public void testMethod() {}
|
||||||
|
~~~~~~~~~~
|
||||||
|
1 errors, 0 warnings
|
||||||
|
""".addLineContinuation())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testDetectIssuesMissingAnnotationOnMethodWhenClassIsCalledDefault() {
|
||||||
|
lint().files(java(
|
||||||
|
"""
|
||||||
|
package test.pkg;
|
||||||
|
public class Default extends IFooMethod.Stub {
|
||||||
|
public void testMethod() {}
|
||||||
|
}
|
||||||
|
""").indented(),
|
||||||
|
*stubs
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
.expect(
|
||||||
|
"""
|
||||||
|
src/test/pkg/Default.java:3: Error: The method Default.testMethod \
|
||||||
|
overrides the method Stub.testMethod which is annotated with @EnforcePermission. The same annotation must be used on Default.testMethod [MissingEnforcePermissionAnnotation]
|
||||||
|
public void testMethod() {}
|
||||||
|
~~~~~~~~~~
|
||||||
|
1 errors, 0 warnings
|
||||||
|
""".addLineContinuation()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testDoesDetectIssuesShortStringsNotAllowed() {
|
||||||
|
lint().files(java(
|
||||||
|
"""
|
||||||
|
package test.pkg;
|
||||||
|
import android.annotation.EnforcePermission;
|
||||||
|
public class TestClass121 extends IFooMethod.Stub {
|
||||||
|
@Override
|
||||||
|
@EnforcePermission(anyOf={"INTERNET", "READ_PHONE_STATE"})
|
||||||
|
public void testMethodAnyLiteral() {}
|
||||||
|
}
|
||||||
|
""").indented(),
|
||||||
|
*stubs
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
.expect(
|
||||||
|
"""
|
||||||
|
src/test/pkg/TestClass121.java:6: Error: The method \
|
||||||
|
TestClass121.testMethodAnyLiteral is annotated with @EnforcePermission(anyOf={"INTERNET", "READ_PHONE_STATE"}) \
|
||||||
|
which differs from the overridden method Stub.testMethodAnyLiteral: \
|
||||||
|
@android.annotation.EnforcePermission(anyOf={android.Manifest.permission.INTERNET, "android.permission.READ_PHONE_STATE"}). \
|
||||||
|
The same annotation must be used for both methods. [MismatchingEnforcePermissionAnnotation]
|
||||||
|
public void testMethodAnyLiteral() {}
|
||||||
|
~~~~~~~~~~~~~~~~~~~~
|
||||||
|
1 errors, 0 warnings
|
||||||
|
""".addLineContinuation()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stubs */
|
||||||
|
|
||||||
|
// A service with permission annotation on the method.
|
||||||
|
private val interfaceIFooMethodStub: TestFile = java(
|
||||||
|
"""
|
||||||
|
public interface IFooMethod extends android.os.IInterface {
|
||||||
|
public static abstract class Stub extends android.os.Binder implements IFooMethod {
|
||||||
|
@Override
|
||||||
|
@android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE)
|
||||||
|
public void testMethod() {}
|
||||||
|
@Override
|
||||||
|
@android.annotation.EnforcePermission(anyOf={android.Manifest.permission.INTERNET, android.Manifest.permission.READ_PHONE_STATE})
|
||||||
|
public void testMethodAny() {}
|
||||||
|
@Override
|
||||||
|
@android.annotation.EnforcePermission(anyOf={android.Manifest.permission.INTERNET, "android.permission.READ_PHONE_STATE"})
|
||||||
|
public void testMethodAnyLiteral() {}
|
||||||
|
@Override
|
||||||
|
@android.annotation.EnforcePermission(allOf={android.Manifest.permission.INTERNET, android.Manifest.permission.READ_PHONE_STATE})
|
||||||
|
public void testMethodAll() {}
|
||||||
|
@Override
|
||||||
|
@android.annotation.EnforcePermission(allOf={android.Manifest.permission.INTERNET, "android.permission.READ_PHONE_STATE"})
|
||||||
|
public void testMethodAllLiteral() {}
|
||||||
|
}
|
||||||
|
@android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE)
|
||||||
|
public void testMethod();
|
||||||
|
@android.annotation.EnforcePermission(anyOf={android.Manifest.permission.INTERNET, android.Manifest.permission.READ_PHONE_STATE})
|
||||||
|
public void testMethodAny() {}
|
||||||
|
@android.annotation.EnforcePermission(anyOf={android.Manifest.permission.INTERNET, "android.permission.READ_PHONE_STATE"})
|
||||||
|
public void testMethodAnyLiteral() {}
|
||||||
|
@android.annotation.EnforcePermission(allOf={android.Manifest.permission.INTERNET, android.Manifest.permission.READ_PHONE_STATE})
|
||||||
|
public void testMethodAll() {}
|
||||||
|
@android.annotation.EnforcePermission(allOf={android.Manifest.permission.INTERNET, "android.permission.READ_PHONE_STATE"})
|
||||||
|
public void testMethodAllLiteral() {}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
).indented()
|
||||||
|
|
||||||
|
// A service without any permission annotation.
|
||||||
|
private val interfaceIBarStub: TestFile = java(
|
||||||
|
"""
|
||||||
|
public interface IBar extends android.os.IInterface {
|
||||||
|
public static abstract class Stub extends android.os.Binder implements IBar {
|
||||||
|
@Override
|
||||||
|
public void testMethod() {}
|
||||||
|
}
|
||||||
|
public void testMethod();
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
).indented()
|
||||||
|
|
||||||
|
private val manifestPermissionStub: TestFile = java(
|
||||||
|
"""
|
||||||
|
package android.Manifest;
|
||||||
|
class permission {
|
||||||
|
public static final String READ_PHONE_STATE = "android.permission.READ_PHONE_STATE";
|
||||||
|
public static final String NFC = "android.permission.NFC";
|
||||||
|
public static final String INTERNET = "android.permission.INTERNET";
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
).indented()
|
||||||
|
|
||||||
|
private val enforcePermissionAnnotationStub: TestFile = java(
|
||||||
|
"""
|
||||||
|
package android.annotation;
|
||||||
|
public @interface EnforcePermission {}
|
||||||
|
"""
|
||||||
|
).indented()
|
||||||
|
|
||||||
|
private val stubs = arrayOf(interfaceIFooMethodStub, interfaceIBarStub,
|
||||||
|
manifestPermissionStub, enforcePermissionAnnotationStub)
|
||||||
|
|
||||||
|
// Substitutes "backslash + new line" with an empty string to imitate line continuation
|
||||||
|
private fun String.addLineContinuation(): String = this.trimIndent().replace("\\\n", "")
|
||||||
|
}
|
||||||
@@ -0,0 +1,557 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2022 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.google.android.lint.aidl
|
||||||
|
|
||||||
|
import com.android.tools.lint.checks.infrastructure.LintDetectorTest
|
||||||
|
import com.android.tools.lint.checks.infrastructure.TestFile
|
||||||
|
import com.android.tools.lint.checks.infrastructure.TestLintTask
|
||||||
|
import com.android.tools.lint.checks.infrastructure.TestMode
|
||||||
|
import com.android.tools.lint.detector.api.Detector
|
||||||
|
import com.android.tools.lint.detector.api.Issue
|
||||||
|
|
||||||
|
@Suppress("UnstableApiUsage")
|
||||||
|
class EnforcePermissionHelperDetectorCodegenTest : LintDetectorTest() {
|
||||||
|
override fun getDetector(): Detector = EnforcePermissionHelperDetector()
|
||||||
|
|
||||||
|
override fun getIssues(): List<Issue> = listOf(
|
||||||
|
EnforcePermissionHelperDetector.ISSUE_ENFORCE_PERMISSION_HELPER
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun lint(): TestLintTask = super.lint().allowMissingSdk(true)
|
||||||
|
|
||||||
|
fun test_generated_IProtected() {
|
||||||
|
lint().testModes(TestMode.DEFAULT).files(
|
||||||
|
java(
|
||||||
|
"""
|
||||||
|
/*
|
||||||
|
* This file is auto-generated. DO NOT MODIFY.
|
||||||
|
*/
|
||||||
|
package android.aidl.tests.permission;
|
||||||
|
public interface IProtected extends android.os.IInterface
|
||||||
|
{
|
||||||
|
/** Default implementation for IProtected. */
|
||||||
|
public static class Default implements android.aidl.tests.permission.IProtected
|
||||||
|
{
|
||||||
|
@Override public void PermissionProtected() throws android.os.RemoteException
|
||||||
|
{
|
||||||
|
}
|
||||||
|
@Override public void MultiplePermissionsAll() throws android.os.RemoteException
|
||||||
|
{
|
||||||
|
}
|
||||||
|
@Override public void MultiplePermissionsAny() throws android.os.RemoteException
|
||||||
|
{
|
||||||
|
}
|
||||||
|
@Override public void NonManifestPermission() throws android.os.RemoteException
|
||||||
|
{
|
||||||
|
}
|
||||||
|
// Used by the integration tests to dynamically set permissions that are considered granted.
|
||||||
|
@Override public void SetGranted(java.util.List<java.lang.String> permissions) throws android.os.RemoteException
|
||||||
|
{
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public android.os.IBinder asBinder() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/** Local-side IPC implementation stub class. */
|
||||||
|
public static abstract class Stub extends android.os.Binder implements android.aidl.tests.permission.IProtected
|
||||||
|
{
|
||||||
|
private final android.os.PermissionEnforcer mEnforcer;
|
||||||
|
/** Construct the stub using the Enforcer provided. */
|
||||||
|
public Stub(android.os.PermissionEnforcer enforcer)
|
||||||
|
{
|
||||||
|
this.attachInterface(this, DESCRIPTOR);
|
||||||
|
if (enforcer == null) {
|
||||||
|
throw new IllegalArgumentException("enforcer cannot be null");
|
||||||
|
}
|
||||||
|
mEnforcer = enforcer;
|
||||||
|
}
|
||||||
|
@Deprecated
|
||||||
|
/** Default constructor. */
|
||||||
|
public Stub() {
|
||||||
|
this(android.os.PermissionEnforcer.fromContext(
|
||||||
|
android.app.ActivityThread.currentActivityThread().getSystemContext()));
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Cast an IBinder object into an android.aidl.tests.permission.IProtected interface,
|
||||||
|
* generating a proxy if needed.
|
||||||
|
*/
|
||||||
|
public static android.aidl.tests.permission.IProtected asInterface(android.os.IBinder obj)
|
||||||
|
{
|
||||||
|
if ((obj==null)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
|
||||||
|
if (((iin!=null)&&(iin instanceof android.aidl.tests.permission.IProtected))) {
|
||||||
|
return ((android.aidl.tests.permission.IProtected)iin);
|
||||||
|
}
|
||||||
|
return new android.aidl.tests.permission.IProtected.Stub.Proxy(obj);
|
||||||
|
}
|
||||||
|
@Override public android.os.IBinder asBinder()
|
||||||
|
{
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
/** @hide */
|
||||||
|
public static java.lang.String getDefaultTransactionName(int transactionCode)
|
||||||
|
{
|
||||||
|
switch (transactionCode)
|
||||||
|
{
|
||||||
|
case TRANSACTION_PermissionProtected:
|
||||||
|
{
|
||||||
|
return "PermissionProtected";
|
||||||
|
}
|
||||||
|
case TRANSACTION_MultiplePermissionsAll:
|
||||||
|
{
|
||||||
|
return "MultiplePermissionsAll";
|
||||||
|
}
|
||||||
|
case TRANSACTION_MultiplePermissionsAny:
|
||||||
|
{
|
||||||
|
return "MultiplePermissionsAny";
|
||||||
|
}
|
||||||
|
case TRANSACTION_NonManifestPermission:
|
||||||
|
{
|
||||||
|
return "NonManifestPermission";
|
||||||
|
}
|
||||||
|
case TRANSACTION_SetGranted:
|
||||||
|
{
|
||||||
|
return "SetGranted";
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/** @hide */
|
||||||
|
public java.lang.String getTransactionName(int transactionCode)
|
||||||
|
{
|
||||||
|
return this.getDefaultTransactionName(transactionCode);
|
||||||
|
}
|
||||||
|
@Override public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException
|
||||||
|
{
|
||||||
|
java.lang.String descriptor = DESCRIPTOR;
|
||||||
|
if (code >= android.os.IBinder.FIRST_CALL_TRANSACTION && code <= android.os.IBinder.LAST_CALL_TRANSACTION) {
|
||||||
|
data.enforceInterface(descriptor);
|
||||||
|
}
|
||||||
|
switch (code)
|
||||||
|
{
|
||||||
|
case INTERFACE_TRANSACTION:
|
||||||
|
{
|
||||||
|
reply.writeString(descriptor);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch (code)
|
||||||
|
{
|
||||||
|
case TRANSACTION_PermissionProtected:
|
||||||
|
{
|
||||||
|
this.PermissionProtected();
|
||||||
|
reply.writeNoException();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case TRANSACTION_MultiplePermissionsAll:
|
||||||
|
{
|
||||||
|
this.MultiplePermissionsAll();
|
||||||
|
reply.writeNoException();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case TRANSACTION_MultiplePermissionsAny:
|
||||||
|
{
|
||||||
|
this.MultiplePermissionsAny();
|
||||||
|
reply.writeNoException();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case TRANSACTION_NonManifestPermission:
|
||||||
|
{
|
||||||
|
this.NonManifestPermission();
|
||||||
|
reply.writeNoException();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case TRANSACTION_SetGranted:
|
||||||
|
{
|
||||||
|
java.util.List<java.lang.String> _arg0;
|
||||||
|
_arg0 = data.createStringArrayList();
|
||||||
|
data.enforceNoDataAvail();
|
||||||
|
this.SetGranted(_arg0);
|
||||||
|
reply.writeNoException();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
{
|
||||||
|
return super.onTransact(code, data, reply, flags);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
private static class Proxy implements android.aidl.tests.permission.IProtected
|
||||||
|
{
|
||||||
|
private android.os.IBinder mRemote;
|
||||||
|
Proxy(android.os.IBinder remote)
|
||||||
|
{
|
||||||
|
mRemote = remote;
|
||||||
|
}
|
||||||
|
@Override public android.os.IBinder asBinder()
|
||||||
|
{
|
||||||
|
return mRemote;
|
||||||
|
}
|
||||||
|
public java.lang.String getInterfaceDescriptor()
|
||||||
|
{
|
||||||
|
return DESCRIPTOR;
|
||||||
|
}
|
||||||
|
@Override public void PermissionProtected() throws android.os.RemoteException
|
||||||
|
{
|
||||||
|
android.os.Parcel _data = android.os.Parcel.obtain(asBinder());
|
||||||
|
android.os.Parcel _reply = android.os.Parcel.obtain();
|
||||||
|
try {
|
||||||
|
_data.writeInterfaceToken(DESCRIPTOR);
|
||||||
|
boolean _status = mRemote.transact(Stub.TRANSACTION_PermissionProtected, _data, _reply, 0);
|
||||||
|
_reply.readException();
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
_reply.recycle();
|
||||||
|
_data.recycle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@Override public void MultiplePermissionsAll() throws android.os.RemoteException
|
||||||
|
{
|
||||||
|
android.os.Parcel _data = android.os.Parcel.obtain(asBinder());
|
||||||
|
android.os.Parcel _reply = android.os.Parcel.obtain();
|
||||||
|
try {
|
||||||
|
_data.writeInterfaceToken(DESCRIPTOR);
|
||||||
|
boolean _status = mRemote.transact(Stub.TRANSACTION_MultiplePermissionsAll, _data, _reply, 0);
|
||||||
|
_reply.readException();
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
_reply.recycle();
|
||||||
|
_data.recycle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@Override public void MultiplePermissionsAny() throws android.os.RemoteException
|
||||||
|
{
|
||||||
|
android.os.Parcel _data = android.os.Parcel.obtain(asBinder());
|
||||||
|
android.os.Parcel _reply = android.os.Parcel.obtain();
|
||||||
|
try {
|
||||||
|
_data.writeInterfaceToken(DESCRIPTOR);
|
||||||
|
boolean _status = mRemote.transact(Stub.TRANSACTION_MultiplePermissionsAny, _data, _reply, 0);
|
||||||
|
_reply.readException();
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
_reply.recycle();
|
||||||
|
_data.recycle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@Override public void NonManifestPermission() throws android.os.RemoteException
|
||||||
|
{
|
||||||
|
android.os.Parcel _data = android.os.Parcel.obtain(asBinder());
|
||||||
|
android.os.Parcel _reply = android.os.Parcel.obtain();
|
||||||
|
try {
|
||||||
|
_data.writeInterfaceToken(DESCRIPTOR);
|
||||||
|
boolean _status = mRemote.transact(Stub.TRANSACTION_NonManifestPermission, _data, _reply, 0);
|
||||||
|
_reply.readException();
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
_reply.recycle();
|
||||||
|
_data.recycle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Used by the integration tests to dynamically set permissions that are considered granted.
|
||||||
|
@Override public void SetGranted(java.util.List<java.lang.String> permissions) throws android.os.RemoteException
|
||||||
|
{
|
||||||
|
android.os.Parcel _data = android.os.Parcel.obtain(asBinder());
|
||||||
|
android.os.Parcel _reply = android.os.Parcel.obtain();
|
||||||
|
try {
|
||||||
|
_data.writeInterfaceToken(DESCRIPTOR);
|
||||||
|
_data.writeStringList(permissions);
|
||||||
|
boolean _status = mRemote.transact(Stub.TRANSACTION_SetGranted, _data, _reply, 0);
|
||||||
|
_reply.readException();
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
_reply.recycle();
|
||||||
|
_data.recycle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static final int TRANSACTION_PermissionProtected = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
|
||||||
|
/** Helper method to enforce permissions for PermissionProtected */
|
||||||
|
protected void PermissionProtected_enforcePermission() throws SecurityException {
|
||||||
|
android.content.AttributionSource source = new android.content.AttributionSource(getCallingUid(), null, null);
|
||||||
|
mEnforcer.enforcePermission(android.Manifest.permission.READ_PHONE_STATE, source);
|
||||||
|
}
|
||||||
|
static final int TRANSACTION_MultiplePermissionsAll = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);
|
||||||
|
/** Helper method to enforce permissions for MultiplePermissionsAll */
|
||||||
|
protected void MultiplePermissionsAll_enforcePermission() throws SecurityException {
|
||||||
|
android.content.AttributionSource source = new android.content.AttributionSource(getCallingUid(), null, null);
|
||||||
|
mEnforcer.enforcePermissionAllOf(new String[]{android.Manifest.permission.INTERNET, android.Manifest.permission.VIBRATE}, source);
|
||||||
|
}
|
||||||
|
static final int TRANSACTION_MultiplePermissionsAny = (android.os.IBinder.FIRST_CALL_TRANSACTION + 2);
|
||||||
|
/** Helper method to enforce permissions for MultiplePermissionsAny */
|
||||||
|
protected void MultiplePermissionsAny_enforcePermission() throws SecurityException {
|
||||||
|
android.content.AttributionSource source = new android.content.AttributionSource(getCallingUid(), null, null);
|
||||||
|
mEnforcer.enforcePermissionAnyOf(new String[]{android.Manifest.permission.INTERNET, android.Manifest.permission.VIBRATE}, source);
|
||||||
|
}
|
||||||
|
static final int TRANSACTION_NonManifestPermission = (android.os.IBinder.FIRST_CALL_TRANSACTION + 3);
|
||||||
|
/** Helper method to enforce permissions for NonManifestPermission */
|
||||||
|
protected void NonManifestPermission_enforcePermission() throws SecurityException {
|
||||||
|
android.content.AttributionSource source = new android.content.AttributionSource(getCallingUid(), null, null);
|
||||||
|
mEnforcer.enforcePermission(android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, source);
|
||||||
|
}
|
||||||
|
static final int TRANSACTION_SetGranted = (android.os.IBinder.FIRST_CALL_TRANSACTION + 4);
|
||||||
|
/** @hide */
|
||||||
|
public int getMaxTransactionId()
|
||||||
|
{
|
||||||
|
return 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE)
|
||||||
|
public void PermissionProtected() throws android.os.RemoteException;
|
||||||
|
@android.annotation.EnforcePermission(allOf = {android.Manifest.permission.INTERNET, android.Manifest.permission.VIBRATE})
|
||||||
|
public void MultiplePermissionsAll() throws android.os.RemoteException;
|
||||||
|
@android.annotation.EnforcePermission(anyOf = {android.Manifest.permission.INTERNET, android.Manifest.permission.VIBRATE})
|
||||||
|
public void MultiplePermissionsAny() throws android.os.RemoteException;
|
||||||
|
@android.annotation.EnforcePermission(android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK)
|
||||||
|
public void NonManifestPermission() throws android.os.RemoteException;
|
||||||
|
// Used by the integration tests to dynamically set permissions that are considered granted.
|
||||||
|
@android.annotation.RequiresNoPermission
|
||||||
|
public void SetGranted(java.util.List<java.lang.String> permissions) throws android.os.RemoteException;
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
).indented(),
|
||||||
|
*stubs
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
.expectClean()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun test_generated_IProtectedInterface() {
|
||||||
|
lint().files(
|
||||||
|
java(
|
||||||
|
"""
|
||||||
|
/*
|
||||||
|
* This file is auto-generated. DO NOT MODIFY.
|
||||||
|
*/
|
||||||
|
package android.aidl.tests.permission;
|
||||||
|
public interface IProtectedInterface extends android.os.IInterface
|
||||||
|
{
|
||||||
|
/** Default implementation for IProtectedInterface. */
|
||||||
|
public static class Default implements android.aidl.tests.permission.IProtectedInterface
|
||||||
|
{
|
||||||
|
@Override public void Method1() throws android.os.RemoteException
|
||||||
|
{
|
||||||
|
}
|
||||||
|
@Override public void Method2() throws android.os.RemoteException
|
||||||
|
{
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public android.os.IBinder asBinder() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/** Local-side IPC implementation stub class. */
|
||||||
|
public static abstract class Stub extends android.os.Binder implements android.aidl.tests.permission.IProtectedInterface
|
||||||
|
{
|
||||||
|
private final android.os.PermissionEnforcer mEnforcer;
|
||||||
|
/** Construct the stub using the Enforcer provided. */
|
||||||
|
public Stub(android.os.PermissionEnforcer enforcer)
|
||||||
|
{
|
||||||
|
this.attachInterface(this, DESCRIPTOR);
|
||||||
|
if (enforcer == null) {
|
||||||
|
throw new IllegalArgumentException("enforcer cannot be null");
|
||||||
|
}
|
||||||
|
mEnforcer = enforcer;
|
||||||
|
}
|
||||||
|
@Deprecated
|
||||||
|
/** Default constructor. */
|
||||||
|
public Stub() {
|
||||||
|
this(android.os.PermissionEnforcer.fromContext(
|
||||||
|
android.app.ActivityThread.currentActivityThread().getSystemContext()));
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Cast an IBinder object into an android.aidl.tests.permission.IProtectedInterface interface,
|
||||||
|
* generating a proxy if needed.
|
||||||
|
*/
|
||||||
|
public static android.aidl.tests.permission.IProtectedInterface asInterface(android.os.IBinder obj)
|
||||||
|
{
|
||||||
|
if ((obj==null)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
|
||||||
|
if (((iin!=null)&&(iin instanceof android.aidl.tests.permission.IProtectedInterface))) {
|
||||||
|
return ((android.aidl.tests.permission.IProtectedInterface)iin);
|
||||||
|
}
|
||||||
|
return new android.aidl.tests.permission.IProtectedInterface.Stub.Proxy(obj);
|
||||||
|
}
|
||||||
|
@Override public android.os.IBinder asBinder()
|
||||||
|
{
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
/** @hide */
|
||||||
|
public static java.lang.String getDefaultTransactionName(int transactionCode)
|
||||||
|
{
|
||||||
|
switch (transactionCode)
|
||||||
|
{
|
||||||
|
case TRANSACTION_Method1:
|
||||||
|
{
|
||||||
|
return "Method1";
|
||||||
|
}
|
||||||
|
case TRANSACTION_Method2:
|
||||||
|
{
|
||||||
|
return "Method2";
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/** @hide */
|
||||||
|
public java.lang.String getTransactionName(int transactionCode)
|
||||||
|
{
|
||||||
|
return this.getDefaultTransactionName(transactionCode);
|
||||||
|
}
|
||||||
|
@Override public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException
|
||||||
|
{
|
||||||
|
java.lang.String descriptor = DESCRIPTOR;
|
||||||
|
if (code >= android.os.IBinder.FIRST_CALL_TRANSACTION && code <= android.os.IBinder.LAST_CALL_TRANSACTION) {
|
||||||
|
data.enforceInterface(descriptor);
|
||||||
|
}
|
||||||
|
switch (code)
|
||||||
|
{
|
||||||
|
case INTERFACE_TRANSACTION:
|
||||||
|
{
|
||||||
|
reply.writeString(descriptor);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch (code)
|
||||||
|
{
|
||||||
|
case TRANSACTION_Method1:
|
||||||
|
{
|
||||||
|
this.Method1();
|
||||||
|
reply.writeNoException();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case TRANSACTION_Method2:
|
||||||
|
{
|
||||||
|
this.Method2();
|
||||||
|
reply.writeNoException();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
{
|
||||||
|
return super.onTransact(code, data, reply, flags);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
private static class Proxy implements android.aidl.tests.permission.IProtectedInterface
|
||||||
|
{
|
||||||
|
private android.os.IBinder mRemote;
|
||||||
|
Proxy(android.os.IBinder remote)
|
||||||
|
{
|
||||||
|
mRemote = remote;
|
||||||
|
}
|
||||||
|
@Override public android.os.IBinder asBinder()
|
||||||
|
{
|
||||||
|
return mRemote;
|
||||||
|
}
|
||||||
|
public java.lang.String getInterfaceDescriptor()
|
||||||
|
{
|
||||||
|
return DESCRIPTOR;
|
||||||
|
}
|
||||||
|
@Override public void Method1() throws android.os.RemoteException
|
||||||
|
{
|
||||||
|
android.os.Parcel _data = android.os.Parcel.obtain(asBinder());
|
||||||
|
android.os.Parcel _reply = android.os.Parcel.obtain();
|
||||||
|
try {
|
||||||
|
_data.writeInterfaceToken(DESCRIPTOR);
|
||||||
|
boolean _status = mRemote.transact(Stub.TRANSACTION_Method1, _data, _reply, 0);
|
||||||
|
_reply.readException();
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
_reply.recycle();
|
||||||
|
_data.recycle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@Override public void Method2() throws android.os.RemoteException
|
||||||
|
{
|
||||||
|
android.os.Parcel _data = android.os.Parcel.obtain(asBinder());
|
||||||
|
android.os.Parcel _reply = android.os.Parcel.obtain();
|
||||||
|
try {
|
||||||
|
_data.writeInterfaceToken(DESCRIPTOR);
|
||||||
|
boolean _status = mRemote.transact(Stub.TRANSACTION_Method2, _data, _reply, 0);
|
||||||
|
_reply.readException();
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
_reply.recycle();
|
||||||
|
_data.recycle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static final int TRANSACTION_Method1 = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
|
||||||
|
/** Helper method to enforce permissions for Method1 */
|
||||||
|
protected void Method1_enforcePermission() throws SecurityException {
|
||||||
|
android.content.AttributionSource source = new android.content.AttributionSource(getCallingUid(), null, null);
|
||||||
|
mEnforcer.enforcePermission(android.Manifest.permission.ACCESS_FINE_LOCATION, source);
|
||||||
|
}
|
||||||
|
static final int TRANSACTION_Method2 = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);
|
||||||
|
/** Helper method to enforce permissions for Method2 */
|
||||||
|
protected void Method2_enforcePermission() throws SecurityException {
|
||||||
|
android.content.AttributionSource source = new android.content.AttributionSource(getCallingUid(), null, null);
|
||||||
|
mEnforcer.enforcePermission(android.Manifest.permission.ACCESS_FINE_LOCATION, source);
|
||||||
|
}
|
||||||
|
/** @hide */
|
||||||
|
public int getMaxTransactionId()
|
||||||
|
{
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_FINE_LOCATION)
|
||||||
|
public void Method1() throws android.os.RemoteException;
|
||||||
|
@android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_FINE_LOCATION)
|
||||||
|
public void Method2() throws android.os.RemoteException;
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
).indented(),
|
||||||
|
*stubs
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
.expectClean()
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stubs */
|
||||||
|
|
||||||
|
private val manifestPermissionStub: TestFile = java(
|
||||||
|
"""
|
||||||
|
package android.Manifest;
|
||||||
|
class permission {
|
||||||
|
public static final String READ_PHONE_STATE = "android.permission.READ_PHONE_STATE";
|
||||||
|
public static final String INTERNET = "android.permission.INTERNET";
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
).indented()
|
||||||
|
|
||||||
|
private val enforcePermissionAnnotationStub: TestFile = java(
|
||||||
|
"""
|
||||||
|
package android.annotation;
|
||||||
|
public @interface EnforcePermission {}
|
||||||
|
"""
|
||||||
|
).indented()
|
||||||
|
|
||||||
|
private val stubs = arrayOf(manifestPermissionStub, enforcePermissionAnnotationStub)
|
||||||
|
}
|
||||||
@@ -0,0 +1,443 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2022 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.google.android.lint.aidl
|
||||||
|
|
||||||
|
import com.android.tools.lint.checks.infrastructure.LintDetectorTest
|
||||||
|
import com.android.tools.lint.checks.infrastructure.TestLintTask
|
||||||
|
|
||||||
|
class EnforcePermissionHelperDetectorTest : LintDetectorTest() {
|
||||||
|
override fun getDetector() = EnforcePermissionHelperDetector()
|
||||||
|
override fun getIssues() = listOf(
|
||||||
|
EnforcePermissionHelperDetector.ISSUE_ENFORCE_PERMISSION_HELPER,
|
||||||
|
EnforcePermissionHelperDetector.ISSUE_MISUSING_ENFORCE_PERMISSION
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun lint(): TestLintTask = super.lint().allowMissingSdk()
|
||||||
|
|
||||||
|
fun testFirstExpressionIsFunctionCall() {
|
||||||
|
lint().files(
|
||||||
|
java(
|
||||||
|
"""
|
||||||
|
import android.content.Context;
|
||||||
|
import android.test.ITest;
|
||||||
|
public class Foo extends ITest.Stub {
|
||||||
|
private Context mContext;
|
||||||
|
@Override
|
||||||
|
@android.annotation.EnforcePermission("android.Manifest.permission.READ_CONTACTS")
|
||||||
|
public void test() throws android.os.RemoteException {
|
||||||
|
Binder.getCallingUid();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
).indented(),
|
||||||
|
*stubs
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
.expect(
|
||||||
|
"""
|
||||||
|
src/Foo.java:5: Error: Method must start with test_enforcePermission() or super.test(), if applicable [MissingEnforcePermissionHelper]
|
||||||
|
@Override
|
||||||
|
^
|
||||||
|
1 errors, 0 warnings
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
.expectFixDiffs(
|
||||||
|
"""
|
||||||
|
Autofix for src/Foo.java line 5: Replace with test_enforcePermission();...:
|
||||||
|
@@ -8 +8
|
||||||
|
+ test_enforcePermission();
|
||||||
|
+
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testFirstExpressionIsVariableDeclaration() {
|
||||||
|
lint().files(
|
||||||
|
java(
|
||||||
|
"""
|
||||||
|
import android.content.Context;
|
||||||
|
import android.test.ITest;
|
||||||
|
public class Foo extends ITest.Stub {
|
||||||
|
private Context mContext;
|
||||||
|
@Override
|
||||||
|
@android.annotation.EnforcePermission("android.Manifest.permission.READ_CONTACTS")
|
||||||
|
public void test() throws android.os.RemoteException {
|
||||||
|
String foo = "bar";
|
||||||
|
Binder.getCallingUid();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
).indented(),
|
||||||
|
*stubs
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
.expect(
|
||||||
|
"""
|
||||||
|
src/Foo.java:5: Error: Method must start with test_enforcePermission() or super.test(), if applicable [MissingEnforcePermissionHelper]
|
||||||
|
@Override
|
||||||
|
^
|
||||||
|
1 errors, 0 warnings
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
.expectFixDiffs(
|
||||||
|
"""
|
||||||
|
Autofix for src/Foo.java line 5: Replace with test_enforcePermission();...:
|
||||||
|
@@ -8 +8
|
||||||
|
+ test_enforcePermission();
|
||||||
|
+
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testMethodIsEmpty() {
|
||||||
|
lint().files(
|
||||||
|
java(
|
||||||
|
"""
|
||||||
|
import android.content.Context;
|
||||||
|
import android.test.ITest;
|
||||||
|
public class Foo extends ITest.Stub {
|
||||||
|
private Context mContext;
|
||||||
|
@Override
|
||||||
|
@android.annotation.EnforcePermission("android.Manifest.permission.READ_CONTACTS")
|
||||||
|
public void test() throws android.os.RemoteException {}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
).indented(),
|
||||||
|
*stubs
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
.expect(
|
||||||
|
"""
|
||||||
|
src/Foo.java:5: Error: Method must start with test_enforcePermission() or super.test(), if applicable [MissingEnforcePermissionHelper]
|
||||||
|
@Override
|
||||||
|
^
|
||||||
|
1 errors, 0 warnings
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testOkay() {
|
||||||
|
lint().files(
|
||||||
|
java(
|
||||||
|
"""
|
||||||
|
import android.content.Context;
|
||||||
|
import android.test.ITest;
|
||||||
|
public class Foo extends ITest.Stub {
|
||||||
|
private Context mContext;
|
||||||
|
@Override
|
||||||
|
@android.annotation.EnforcePermission("android.Manifest.permission.READ_CONTACTS")
|
||||||
|
public void test() throws android.os.RemoteException {
|
||||||
|
super.test_enforcePermission();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
).indented(),
|
||||||
|
*stubs
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
.expectClean()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testHelperWithoutSuperPrefix_Okay() {
|
||||||
|
lint().files(
|
||||||
|
java(
|
||||||
|
"""
|
||||||
|
import android.content.Context;
|
||||||
|
import android.test.ITest;
|
||||||
|
public class Foo extends ITest.Stub {
|
||||||
|
private Context mContext;
|
||||||
|
@Override
|
||||||
|
@android.annotation.EnforcePermission("android.Manifest.permission.READ_CONTACTS")
|
||||||
|
public void test() throws android.os.RemoteException {
|
||||||
|
test_enforcePermission();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
).indented(),
|
||||||
|
*stubs
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
.expectClean()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testInterfaceDefaultMethod_notStubAncestor_error() {
|
||||||
|
lint().files(
|
||||||
|
java(
|
||||||
|
"""
|
||||||
|
public interface IProtected extends android.os.IInterface {
|
||||||
|
@android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE)
|
||||||
|
default void PermissionProtected() throws android.os.RemoteException {
|
||||||
|
String foo = "bar";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
).indented(),
|
||||||
|
*stubs
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
.expect(
|
||||||
|
"""
|
||||||
|
src/IProtected.java:2: Error: The class of PermissionProtected does not inherit from an AIDL generated Stub class [MisusingEnforcePermissionAnnotation]
|
||||||
|
@android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE)
|
||||||
|
^
|
||||||
|
1 errors, 0 warnings
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testInheritance_callSuper_okay() {
|
||||||
|
lint().files(
|
||||||
|
java(
|
||||||
|
"""
|
||||||
|
package test;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.test.ITest;
|
||||||
|
public class Foo extends ITest.Stub {
|
||||||
|
private Context mContext;
|
||||||
|
@Override
|
||||||
|
@android.annotation.EnforcePermission("android.Manifest.permission.READ_CONTACTS")
|
||||||
|
public void test() throws android.os.RemoteException {
|
||||||
|
super.test_enforcePermission();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
).indented(),
|
||||||
|
java(
|
||||||
|
"""
|
||||||
|
package test;
|
||||||
|
import test.Foo;
|
||||||
|
public class Bar extends Foo {
|
||||||
|
@Override
|
||||||
|
@android.annotation.EnforcePermission("android.Manifest.permission.READ_CONTACTS")
|
||||||
|
public void test() throws android.os.RemoteException {
|
||||||
|
super.test();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
).indented(),
|
||||||
|
java(
|
||||||
|
"""
|
||||||
|
package test;
|
||||||
|
import test.Bar;
|
||||||
|
public class Baz extends Bar {
|
||||||
|
@Override
|
||||||
|
@android.annotation.EnforcePermission("android.Manifest.permission.READ_CONTACTS")
|
||||||
|
public void test() throws android.os.RemoteException {
|
||||||
|
super.test();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
).indented(),
|
||||||
|
*stubs
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
.expectClean()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testInheritance_callHelper_okay() {
|
||||||
|
lint().files(
|
||||||
|
java(
|
||||||
|
"""
|
||||||
|
package test;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.test.ITest;
|
||||||
|
public class Foo extends ITest.Stub {
|
||||||
|
private Context mContext;
|
||||||
|
@Override
|
||||||
|
@android.annotation.EnforcePermission("android.Manifest.permission.READ_CONTACTS")
|
||||||
|
public void test() throws android.os.RemoteException {
|
||||||
|
super.test_enforcePermission();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
).indented(),
|
||||||
|
java(
|
||||||
|
"""
|
||||||
|
package test;
|
||||||
|
import test.Foo;
|
||||||
|
public class Bar extends Foo {
|
||||||
|
@Override
|
||||||
|
@android.annotation.EnforcePermission("android.Manifest.permission.READ_CONTACTS")
|
||||||
|
public void test() throws android.os.RemoteException {
|
||||||
|
super.test();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
).indented(),
|
||||||
|
java(
|
||||||
|
"""
|
||||||
|
package test;
|
||||||
|
import test.Bar;
|
||||||
|
public class Baz extends Bar {
|
||||||
|
@Override
|
||||||
|
@android.annotation.EnforcePermission("android.Manifest.permission.READ_CONTACTS")
|
||||||
|
public void test() throws android.os.RemoteException {
|
||||||
|
super.test_enforcePermission();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
).indented(),
|
||||||
|
*stubs
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
.expectClean()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testInheritance_missingCallInChain_error() {
|
||||||
|
lint().files(
|
||||||
|
java(
|
||||||
|
"""
|
||||||
|
package test;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.test.ITest;
|
||||||
|
public class Foo extends ITest.Stub {
|
||||||
|
private Context mContext;
|
||||||
|
@Override
|
||||||
|
@android.annotation.EnforcePermission("android.Manifest.permission.READ_CONTACTS")
|
||||||
|
public void test() throws android.os.RemoteException {
|
||||||
|
super.test_enforcePermission();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
).indented(),
|
||||||
|
java(
|
||||||
|
"""
|
||||||
|
package test;
|
||||||
|
import test.Foo;
|
||||||
|
public class Bar extends Foo {
|
||||||
|
@Override
|
||||||
|
@android.annotation.EnforcePermission("android.Manifest.permission.READ_CONTACTS")
|
||||||
|
public void test() throws android.os.RemoteException {
|
||||||
|
doStuff();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
).indented(),
|
||||||
|
java(
|
||||||
|
"""
|
||||||
|
package test;
|
||||||
|
import test.Bar;
|
||||||
|
public class Baz extends Bar {
|
||||||
|
@Override
|
||||||
|
@android.annotation.EnforcePermission("android.Manifest.permission.READ_CONTACTS")
|
||||||
|
public void test() throws android.os.RemoteException {
|
||||||
|
super.test();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
).indented(),
|
||||||
|
*stubs
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
.expect(
|
||||||
|
"""
|
||||||
|
src/test/Bar.java:4: Error: Method must start with test_enforcePermission() or super.test(), if applicable [MissingEnforcePermissionHelper]
|
||||||
|
@Override
|
||||||
|
^
|
||||||
|
1 errors, 0 warnings
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testInheritance_missingCall_error() {
|
||||||
|
lint().files(
|
||||||
|
java(
|
||||||
|
"""
|
||||||
|
package test;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.test.ITest;
|
||||||
|
public class Foo extends ITest.Stub {
|
||||||
|
private Context mContext;
|
||||||
|
@Override
|
||||||
|
@android.annotation.EnforcePermission("android.Manifest.permission.READ_CONTACTS")
|
||||||
|
public void test() throws android.os.RemoteException {
|
||||||
|
super.test_enforcePermission();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
).indented(),
|
||||||
|
java(
|
||||||
|
"""
|
||||||
|
package test;
|
||||||
|
import test.Foo;
|
||||||
|
public class Bar extends Foo {
|
||||||
|
@Override
|
||||||
|
@android.annotation.EnforcePermission("android.Manifest.permission.READ_CONTACTS")
|
||||||
|
public void test() throws android.os.RemoteException {
|
||||||
|
super.test();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
).indented(),
|
||||||
|
java(
|
||||||
|
"""
|
||||||
|
package test;
|
||||||
|
import test.Bar;
|
||||||
|
public class Baz extends Bar {
|
||||||
|
@Override
|
||||||
|
@android.annotation.EnforcePermission("android.Manifest.permission.READ_CONTACTS")
|
||||||
|
public void test() throws android.os.RemoteException {
|
||||||
|
doStuff();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
).indented(),
|
||||||
|
*stubs
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
.expect(
|
||||||
|
"""
|
||||||
|
src/test/Baz.java:4: Error: Method must start with test_enforcePermission() or super.test(), if applicable [MissingEnforcePermissionHelper]
|
||||||
|
@Override
|
||||||
|
^
|
||||||
|
1 errors, 0 warnings
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testRandomClass_notStubAncestor_error() {
|
||||||
|
lint().files(
|
||||||
|
java(
|
||||||
|
"""
|
||||||
|
public class Foo {
|
||||||
|
@android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE)
|
||||||
|
void PermissionProtected() throws android.os.RemoteException {
|
||||||
|
String foo = "bar";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
).indented(),
|
||||||
|
*stubs
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
.expect(
|
||||||
|
"""
|
||||||
|
src/Foo.java:2: Error: The class of PermissionProtected does not inherit from an AIDL generated Stub class [MisusingEnforcePermissionAnnotation]
|
||||||
|
@android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE)
|
||||||
|
^
|
||||||
|
1 errors, 0 warnings
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val stubs = arrayOf(aidlStub, contextStub, binderStub)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,843 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2022 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.google.android.lint.aidl
|
||||||
|
|
||||||
|
import com.android.tools.lint.checks.infrastructure.LintDetectorTest
|
||||||
|
import com.android.tools.lint.checks.infrastructure.TestLintTask
|
||||||
|
import com.android.tools.lint.detector.api.Detector
|
||||||
|
import com.android.tools.lint.detector.api.Issue
|
||||||
|
|
||||||
|
@Suppress("UnstableApiUsage")
|
||||||
|
class SimpleManualPermissionEnforcementDetectorTest : LintDetectorTest() {
|
||||||
|
override fun getDetector(): Detector = SimpleManualPermissionEnforcementDetector()
|
||||||
|
override fun getIssues(): List<Issue> = listOf(
|
||||||
|
SimpleManualPermissionEnforcementDetector
|
||||||
|
.ISSUE_SIMPLE_MANUAL_PERMISSION_ENFORCEMENT
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun lint(): TestLintTask = super.lint().allowMissingSdk()
|
||||||
|
|
||||||
|
fun testClass() {
|
||||||
|
lint().files(
|
||||||
|
java(
|
||||||
|
"""
|
||||||
|
import android.content.Context;
|
||||||
|
import android.test.ITest;
|
||||||
|
public class Foo extends ITest.Stub {
|
||||||
|
private Context mContext;
|
||||||
|
@Override
|
||||||
|
public void test() throws android.os.RemoteException {
|
||||||
|
mContext.enforceCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
).indented(),
|
||||||
|
*stubs
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
.expect(
|
||||||
|
"""
|
||||||
|
src/Foo.java:7: Warning: ITest permission check should be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement]
|
||||||
|
mContext.enforceCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo");
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
0 errors, 1 warnings
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
.expectFixDiffs(
|
||||||
|
"""
|
||||||
|
Fix for src/Foo.java line 7: Annotate with @EnforcePermission:
|
||||||
|
@@ -5 +5
|
||||||
|
+ @android.annotation.EnforcePermission("android.permission.READ_CONTACTS")
|
||||||
|
@@ -7 +8
|
||||||
|
- mContext.enforceCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo");
|
||||||
|
+ test_enforcePermission();
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testClass_orSelfFalse_warning() {
|
||||||
|
lint().files(
|
||||||
|
java(
|
||||||
|
"""
|
||||||
|
import android.content.Context;
|
||||||
|
import android.test.ITest;
|
||||||
|
public class Foo extends ITest.Stub {
|
||||||
|
private Context mContext;
|
||||||
|
@Override
|
||||||
|
public void test() throws android.os.RemoteException {
|
||||||
|
mContext.enforceCallingPermission("android.permission.READ_CONTACTS", "foo");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
).indented(),
|
||||||
|
*stubs
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
.expect(
|
||||||
|
"""
|
||||||
|
src/Foo.java:7: Warning: ITest permission check can be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement]
|
||||||
|
mContext.enforceCallingPermission("android.permission.READ_CONTACTS", "foo");
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
0 errors, 1 warnings
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
.expectFixDiffs(
|
||||||
|
"""
|
||||||
|
Fix for src/Foo.java line 7: Annotate with @EnforcePermission:
|
||||||
|
@@ -5 +5
|
||||||
|
+ @android.annotation.EnforcePermission("android.permission.READ_CONTACTS")
|
||||||
|
@@ -7 +8
|
||||||
|
- mContext.enforceCallingPermission("android.permission.READ_CONTACTS", "foo");
|
||||||
|
+ test_enforcePermission();
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testClass_enforcesFalse_warning() {
|
||||||
|
lint().files(
|
||||||
|
java(
|
||||||
|
"""
|
||||||
|
import android.content.Context;
|
||||||
|
import android.test.ITest;
|
||||||
|
public class Foo extends ITest.Stub {
|
||||||
|
private Context mContext;
|
||||||
|
@Override
|
||||||
|
public void test() throws android.os.RemoteException {
|
||||||
|
mContext.checkCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
).indented(),
|
||||||
|
*stubs
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
.expect(
|
||||||
|
"""
|
||||||
|
src/Foo.java:7: Warning: ITest permission check can be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement]
|
||||||
|
mContext.checkCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo");
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
0 errors, 1 warnings
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
.expectFixDiffs(
|
||||||
|
"""
|
||||||
|
Fix for src/Foo.java line 7: Annotate with @EnforcePermission:
|
||||||
|
@@ -5 +5
|
||||||
|
+ @android.annotation.EnforcePermission("android.permission.READ_CONTACTS")
|
||||||
|
@@ -7 +8
|
||||||
|
- mContext.checkCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo");
|
||||||
|
+ test_enforcePermission();
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testAnonClass() {
|
||||||
|
lint().files(
|
||||||
|
java(
|
||||||
|
"""
|
||||||
|
import android.content.Context;
|
||||||
|
import android.test.ITest;
|
||||||
|
public class Foo {
|
||||||
|
private Context mContext;
|
||||||
|
private ITest itest = new ITest.Stub() {
|
||||||
|
@Override
|
||||||
|
public void test() throws android.os.RemoteException {
|
||||||
|
mContext.enforceCallingOrSelfPermission(
|
||||||
|
"android.permission.READ_CONTACTS", "foo");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
).indented(),
|
||||||
|
*stubs
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
.expect(
|
||||||
|
"""
|
||||||
|
src/Foo.java:8: Warning: ITest permission check should be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement]
|
||||||
|
mContext.enforceCallingOrSelfPermission(
|
||||||
|
^
|
||||||
|
0 errors, 1 warnings
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
.expectFixDiffs(
|
||||||
|
"""
|
||||||
|
Fix for src/Foo.java line 8: Annotate with @EnforcePermission:
|
||||||
|
@@ -6 +6
|
||||||
|
+ @android.annotation.EnforcePermission("android.permission.READ_CONTACTS")
|
||||||
|
@@ -8 +9
|
||||||
|
- mContext.enforceCallingOrSelfPermission(
|
||||||
|
- "android.permission.READ_CONTACTS", "foo");
|
||||||
|
+ test_enforcePermission();
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testConstantEvaluation() {
|
||||||
|
lint().files(
|
||||||
|
java(
|
||||||
|
"""
|
||||||
|
import android.content.Context;
|
||||||
|
import android.test.ITest;
|
||||||
|
|
||||||
|
public class Foo extends ITest.Stub {
|
||||||
|
private Context mContext;
|
||||||
|
@Override
|
||||||
|
public void test() throws android.os.RemoteException {
|
||||||
|
mContext.enforceCallingOrSelfPermission(android.Manifest.permission.READ_CONTACTS, "foo");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
).indented(),
|
||||||
|
*stubs,
|
||||||
|
manifestStub
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
.expect(
|
||||||
|
"""
|
||||||
|
src/Foo.java:8: Warning: ITest permission check should be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement]
|
||||||
|
mContext.enforceCallingOrSelfPermission(android.Manifest.permission.READ_CONTACTS, "foo");
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
0 errors, 1 warnings
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
.expectFixDiffs(
|
||||||
|
"""
|
||||||
|
Fix for src/Foo.java line 8: Annotate with @EnforcePermission:
|
||||||
|
@@ -6 +6
|
||||||
|
+ @android.annotation.EnforcePermission("android.permission.READ_CONTACTS")
|
||||||
|
@@ -8 +9
|
||||||
|
- mContext.enforceCallingOrSelfPermission(android.Manifest.permission.READ_CONTACTS, "foo");
|
||||||
|
+ test_enforcePermission();
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testAllOf() {
|
||||||
|
lint().files(
|
||||||
|
java(
|
||||||
|
"""
|
||||||
|
import android.content.Context;
|
||||||
|
import android.test.ITest;
|
||||||
|
public class Foo {
|
||||||
|
private Context mContext;
|
||||||
|
private ITest itest = new ITest.Stub() {
|
||||||
|
@Override
|
||||||
|
public void test() throws android.os.RemoteException {
|
||||||
|
mContext.enforceCallingOrSelfPermission(
|
||||||
|
"android.permission.READ_CONTACTS", "foo");
|
||||||
|
mContext.enforceCallingOrSelfPermission(
|
||||||
|
"android.permission.WRITE_CONTACTS", "foo");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
).indented(),
|
||||||
|
*stubs
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
.expect(
|
||||||
|
"""
|
||||||
|
src/Foo.java:10: Warning: ITest permission check should be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement]
|
||||||
|
mContext.enforceCallingOrSelfPermission(
|
||||||
|
^
|
||||||
|
0 errors, 1 warnings
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
.expectFixDiffs(
|
||||||
|
"""
|
||||||
|
Fix for src/Foo.java line 10: Annotate with @EnforcePermission:
|
||||||
|
@@ -6 +6
|
||||||
|
+ @android.annotation.EnforcePermission(allOf={"android.permission.READ_CONTACTS", "android.permission.WRITE_CONTACTS"})
|
||||||
|
@@ -8 +9
|
||||||
|
- mContext.enforceCallingOrSelfPermission(
|
||||||
|
- "android.permission.READ_CONTACTS", "foo");
|
||||||
|
- mContext.enforceCallingOrSelfPermission(
|
||||||
|
- "android.permission.WRITE_CONTACTS", "foo");
|
||||||
|
+ test_enforcePermission();
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testAllOf_mixedOrSelf_warning() {
|
||||||
|
lint().files(
|
||||||
|
java(
|
||||||
|
"""
|
||||||
|
import android.content.Context;
|
||||||
|
import android.test.ITest;
|
||||||
|
public class Foo {
|
||||||
|
private Context mContext;
|
||||||
|
private ITest itest = new ITest.Stub() {
|
||||||
|
@Override
|
||||||
|
public void test() throws android.os.RemoteException {
|
||||||
|
mContext.enforceCallingOrSelfPermission(
|
||||||
|
"android.permission.READ_CONTACTS", "foo");
|
||||||
|
mContext.enforceCallingPermission(
|
||||||
|
"android.permission.WRITE_CONTACTS", "foo");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
).indented(),
|
||||||
|
*stubs
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
.expect(
|
||||||
|
"""
|
||||||
|
src/Foo.java:10: Warning: ITest permission check can be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement]
|
||||||
|
mContext.enforceCallingPermission(
|
||||||
|
^
|
||||||
|
0 errors, 1 warnings
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
.expectFixDiffs(
|
||||||
|
"""
|
||||||
|
Fix for src/Foo.java line 10: Annotate with @EnforcePermission:
|
||||||
|
@@ -6 +6
|
||||||
|
+ @android.annotation.EnforcePermission(allOf={"android.permission.READ_CONTACTS", "android.permission.WRITE_CONTACTS"})
|
||||||
|
@@ -8 +9
|
||||||
|
- mContext.enforceCallingOrSelfPermission(
|
||||||
|
- "android.permission.READ_CONTACTS", "foo");
|
||||||
|
- mContext.enforceCallingPermission(
|
||||||
|
- "android.permission.WRITE_CONTACTS", "foo");
|
||||||
|
+ test_enforcePermission();
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testAllOf_mixedEnforces_warning() {
|
||||||
|
lint().files(
|
||||||
|
java(
|
||||||
|
"""
|
||||||
|
import android.content.Context;
|
||||||
|
import android.test.ITest;
|
||||||
|
public class Foo {
|
||||||
|
private Context mContext;
|
||||||
|
private ITest itest = new ITest.Stub() {
|
||||||
|
@Override
|
||||||
|
public void test() throws android.os.RemoteException {
|
||||||
|
mContext.enforceCallingOrSelfPermission(
|
||||||
|
"android.permission.READ_CONTACTS", "foo");
|
||||||
|
mContext.checkCallingOrSelfPermission(
|
||||||
|
"android.permission.WRITE_CONTACTS", "foo");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
).indented(),
|
||||||
|
*stubs
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
.expect(
|
||||||
|
"""
|
||||||
|
src/Foo.java:10: Warning: ITest permission check can be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement]
|
||||||
|
mContext.checkCallingOrSelfPermission(
|
||||||
|
^
|
||||||
|
0 errors, 1 warnings
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
.expectFixDiffs(
|
||||||
|
"""
|
||||||
|
Fix for src/Foo.java line 10: Annotate with @EnforcePermission:
|
||||||
|
@@ -6 +6
|
||||||
|
+ @android.annotation.EnforcePermission(allOf={"android.permission.READ_CONTACTS", "android.permission.WRITE_CONTACTS"})
|
||||||
|
@@ -8 +9
|
||||||
|
- mContext.enforceCallingOrSelfPermission(
|
||||||
|
- "android.permission.READ_CONTACTS", "foo");
|
||||||
|
- mContext.checkCallingOrSelfPermission(
|
||||||
|
- "android.permission.WRITE_CONTACTS", "foo");
|
||||||
|
+ test_enforcePermission();
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testPrecedingExpressions() {
|
||||||
|
lint().files(
|
||||||
|
java(
|
||||||
|
"""
|
||||||
|
import android.os.Binder;
|
||||||
|
import android.test.ITest;
|
||||||
|
public class Foo extends ITest.Stub {
|
||||||
|
private mContext Context;
|
||||||
|
@Override
|
||||||
|
public void test() throws android.os.RemoteException {
|
||||||
|
long uid = Binder.getCallingUid();
|
||||||
|
mContext.enforceCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
).indented(),
|
||||||
|
*stubs
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
.expectClean()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testPermissionHelper() {
|
||||||
|
lint().files(
|
||||||
|
java(
|
||||||
|
"""
|
||||||
|
import android.content.Context;
|
||||||
|
import android.test.ITest;
|
||||||
|
|
||||||
|
public class Foo extends ITest.Stub {
|
||||||
|
private Context mContext;
|
||||||
|
|
||||||
|
@android.annotation.PermissionMethod(orSelf = true)
|
||||||
|
private void helper() {
|
||||||
|
mContext.enforceCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void test() throws android.os.RemoteException {
|
||||||
|
helper();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
).indented(),
|
||||||
|
*stubs
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
.expect(
|
||||||
|
"""
|
||||||
|
src/Foo.java:14: Warning: ITest permission check should be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement]
|
||||||
|
helper();
|
||||||
|
~~~~~~~~~
|
||||||
|
0 errors, 1 warnings
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
.expectFixDiffs(
|
||||||
|
"""
|
||||||
|
Fix for src/Foo.java line 14: Annotate with @EnforcePermission:
|
||||||
|
@@ -12 +12
|
||||||
|
+ @android.annotation.EnforcePermission("android.permission.READ_CONTACTS")
|
||||||
|
@@ -14 +15
|
||||||
|
- helper();
|
||||||
|
+ test_enforcePermission();
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testPermissionHelper_orSelfNotBubbledUp_warning() {
|
||||||
|
lint().files(
|
||||||
|
java(
|
||||||
|
"""
|
||||||
|
import android.content.Context;
|
||||||
|
import android.test.ITest;
|
||||||
|
|
||||||
|
public class Foo extends ITest.Stub {
|
||||||
|
private Context mContext;
|
||||||
|
|
||||||
|
@android.annotation.PermissionMethod
|
||||||
|
private void helper() {
|
||||||
|
mContext.enforceCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void test() throws android.os.RemoteException {
|
||||||
|
helper();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
).indented(),
|
||||||
|
*stubs
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
.expect(
|
||||||
|
"""
|
||||||
|
src/Foo.java:14: Warning: ITest permission check can be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement]
|
||||||
|
helper();
|
||||||
|
~~~~~~~~~
|
||||||
|
0 errors, 1 warnings
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
.expectFixDiffs(
|
||||||
|
"""
|
||||||
|
Fix for src/Foo.java line 14: Annotate with @EnforcePermission:
|
||||||
|
@@ -12 +12
|
||||||
|
+ @android.annotation.EnforcePermission("android.permission.READ_CONTACTS")
|
||||||
|
@@ -14 +15
|
||||||
|
- helper();
|
||||||
|
+ test_enforcePermission();
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testPermissionHelperAllOf() {
|
||||||
|
lint().files(
|
||||||
|
java(
|
||||||
|
"""
|
||||||
|
import android.content.Context;
|
||||||
|
import android.test.ITest;
|
||||||
|
|
||||||
|
public class Foo extends ITest.Stub {
|
||||||
|
private Context mContext;
|
||||||
|
|
||||||
|
@android.annotation.PermissionMethod(orSelf = true)
|
||||||
|
private void helper() {
|
||||||
|
mContext.enforceCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo");
|
||||||
|
mContext.enforceCallingOrSelfPermission("android.permission.WRITE_CONTACTS", "foo");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void test() throws android.os.RemoteException {
|
||||||
|
helper();
|
||||||
|
mContext.enforceCallingOrSelfPermission("FOO", "foo");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
).indented(),
|
||||||
|
*stubs
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
.expect(
|
||||||
|
"""
|
||||||
|
src/Foo.java:16: Warning: ITest permission check should be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement]
|
||||||
|
mContext.enforceCallingOrSelfPermission("FOO", "foo");
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
0 errors, 1 warnings
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
.expectFixDiffs(
|
||||||
|
"""
|
||||||
|
Fix for src/Foo.java line 16: Annotate with @EnforcePermission:
|
||||||
|
@@ -13 +13
|
||||||
|
+ @android.annotation.EnforcePermission(allOf={"android.permission.READ_CONTACTS", "android.permission.WRITE_CONTACTS", "FOO"})
|
||||||
|
@@ -15 +16
|
||||||
|
- helper();
|
||||||
|
- mContext.enforceCallingOrSelfPermission("FOO", "foo");
|
||||||
|
+ test_enforcePermission();
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun testPermissionHelperNested() {
|
||||||
|
lint().files(
|
||||||
|
java(
|
||||||
|
"""
|
||||||
|
import android.content.Context;
|
||||||
|
import android.test.ITest;
|
||||||
|
|
||||||
|
public class Foo extends ITest.Stub {
|
||||||
|
private Context mContext;
|
||||||
|
|
||||||
|
@android.annotation.PermissionMethod(orSelf = true)
|
||||||
|
private void helperHelper() {
|
||||||
|
helper("android.permission.WRITE_CONTACTS");
|
||||||
|
}
|
||||||
|
|
||||||
|
@android.annotation.PermissionMethod(orSelf = true)
|
||||||
|
private void helper(@android.annotation.PermissionName String extraPermission) {
|
||||||
|
mContext.enforceCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void test() throws android.os.RemoteException {
|
||||||
|
helperHelper();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
).indented(),
|
||||||
|
*stubs
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
.expect(
|
||||||
|
"""
|
||||||
|
src/Foo.java:19: Warning: ITest permission check should be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement]
|
||||||
|
helperHelper();
|
||||||
|
~~~~~~~~~~~~~~~
|
||||||
|
0 errors, 1 warnings
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
.expectFixDiffs(
|
||||||
|
"""
|
||||||
|
Fix for src/Foo.java line 19: Annotate with @EnforcePermission:
|
||||||
|
@@ -17 +17
|
||||||
|
+ @android.annotation.EnforcePermission(allOf={"android.permission.WRITE_CONTACTS", "android.permission.READ_CONTACTS"})
|
||||||
|
@@ -19 +20
|
||||||
|
- helperHelper();
|
||||||
|
+ test_enforcePermission();
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testIfExpression() {
|
||||||
|
lint().files(
|
||||||
|
java(
|
||||||
|
"""
|
||||||
|
import android.content.Context;
|
||||||
|
import android.test.ITest;
|
||||||
|
public class Foo extends ITest.Stub {
|
||||||
|
private Context mContext;
|
||||||
|
@Override
|
||||||
|
public void test() throws android.os.RemoteException {
|
||||||
|
if (mContext.checkCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo")
|
||||||
|
!= PackageManager.PERMISSION_GRANTED) {
|
||||||
|
throw new SecurityException("yikes!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
).indented(),
|
||||||
|
*stubs
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
.expect(
|
||||||
|
"""
|
||||||
|
src/Foo.java:7: Warning: ITest permission check should be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement]
|
||||||
|
if (mContext.checkCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo")
|
||||||
|
^
|
||||||
|
0 errors, 1 warnings
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
.expectFixDiffs(
|
||||||
|
"""
|
||||||
|
Fix for src/Foo.java line 7: Annotate with @EnforcePermission:
|
||||||
|
@@ -5 +5
|
||||||
|
+ @android.annotation.EnforcePermission("android.permission.READ_CONTACTS")
|
||||||
|
@@ -7 +8
|
||||||
|
- if (mContext.checkCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo")
|
||||||
|
- != PackageManager.PERMISSION_GRANTED) {
|
||||||
|
- throw new SecurityException("yikes!");
|
||||||
|
- }
|
||||||
|
+ test_enforcePermission();
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testIfExpression_orSelfFalse_warning() {
|
||||||
|
lint().files(
|
||||||
|
java(
|
||||||
|
"""
|
||||||
|
import android.content.Context;
|
||||||
|
import android.test.ITest;
|
||||||
|
public class Foo extends ITest.Stub {
|
||||||
|
private Context mContext;
|
||||||
|
@Override
|
||||||
|
public void test() throws android.os.RemoteException {
|
||||||
|
if (mContext.checkCallingPermission("android.permission.READ_CONTACTS", "foo")
|
||||||
|
!= PackageManager.PERMISSION_GRANTED) {
|
||||||
|
throw new SecurityException("yikes!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
).indented(),
|
||||||
|
*stubs
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
.expect(
|
||||||
|
"""
|
||||||
|
src/Foo.java:7: Warning: ITest permission check can be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement]
|
||||||
|
if (mContext.checkCallingPermission("android.permission.READ_CONTACTS", "foo")
|
||||||
|
^
|
||||||
|
0 errors, 1 warnings
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
.expectFixDiffs(
|
||||||
|
"""
|
||||||
|
Fix for src/Foo.java line 7: Annotate with @EnforcePermission:
|
||||||
|
@@ -5 +5
|
||||||
|
+ @android.annotation.EnforcePermission("android.permission.READ_CONTACTS")
|
||||||
|
@@ -7 +8
|
||||||
|
- if (mContext.checkCallingPermission("android.permission.READ_CONTACTS", "foo")
|
||||||
|
- != PackageManager.PERMISSION_GRANTED) {
|
||||||
|
- throw new SecurityException("yikes!");
|
||||||
|
- }
|
||||||
|
+ test_enforcePermission();
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testIfExpression_otherSideEffect_ignored() {
|
||||||
|
lint().files(
|
||||||
|
java(
|
||||||
|
"""
|
||||||
|
import android.content.Context;
|
||||||
|
import android.test.ITest;
|
||||||
|
public class Foo extends ITest.Stub {
|
||||||
|
private Context mContext;
|
||||||
|
@Override
|
||||||
|
public void test() throws android.os.RemoteException {
|
||||||
|
if (mContext.checkCallingPermission("android.permission.READ_CONTACTS", "foo")
|
||||||
|
!= PackageManager.PERMISSION_GRANTED) {
|
||||||
|
doSomethingElse();
|
||||||
|
throw new SecurityException("yikes!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
).indented(),
|
||||||
|
*stubs
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
.expectClean()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testIfExpression_inlinedWithSideEffect_ignored() {
|
||||||
|
lint().files(
|
||||||
|
java(
|
||||||
|
"""
|
||||||
|
import android.content.Context;
|
||||||
|
import android.test.ITest;
|
||||||
|
public class Foo extends ITest.Stub {
|
||||||
|
private Context mContext;
|
||||||
|
@Override
|
||||||
|
public void test() throws android.os.RemoteException {
|
||||||
|
if (somethingElse() && mContext.checkCallingPermission("android.permission.READ_CONTACTS", "foo")
|
||||||
|
!= PackageManager.PERMISSION_GRANTED) {
|
||||||
|
throw new SecurityException("yikes!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean somethingElse() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
).indented(),
|
||||||
|
*stubs
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
.expectClean()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testAnyOf_hardCodedAndVarArgs() {
|
||||||
|
lint().files(
|
||||||
|
java(
|
||||||
|
"""
|
||||||
|
import android.content.Context;
|
||||||
|
import android.test.ITest;
|
||||||
|
|
||||||
|
public class Foo extends ITest.Stub {
|
||||||
|
private Context mContext;
|
||||||
|
|
||||||
|
@android.annotation.PermissionMethod(anyOf = true)
|
||||||
|
private void helperHelper() {
|
||||||
|
helper("FOO", "BAR");
|
||||||
|
}
|
||||||
|
|
||||||
|
@android.annotation.PermissionMethod(anyOf = true, value = {"BAZ", "BUZZ"})
|
||||||
|
private void helper(@android.annotation.PermissionName String... extraPermissions) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void test() throws android.os.RemoteException {
|
||||||
|
helperHelper();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
).indented(),
|
||||||
|
*stubs
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
.expect(
|
||||||
|
"""
|
||||||
|
src/Foo.java:17: Warning: ITest permission check can be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement]
|
||||||
|
helperHelper();
|
||||||
|
~~~~~~~~~~~~~~~
|
||||||
|
0 errors, 1 warnings
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
.expectFixDiffs(
|
||||||
|
"""
|
||||||
|
Fix for src/Foo.java line 17: Annotate with @EnforcePermission:
|
||||||
|
@@ -15 +15
|
||||||
|
+ @android.annotation.EnforcePermission(anyOf={"BAZ", "BUZZ", "FOO", "BAR"})
|
||||||
|
@@ -17 +18
|
||||||
|
- helperHelper();
|
||||||
|
+ test_enforcePermission();
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun testAnyOfAllOf_mixedConsecutiveCalls_ignored() {
|
||||||
|
lint().files(
|
||||||
|
java(
|
||||||
|
"""
|
||||||
|
import android.content.Context;
|
||||||
|
import android.test.ITest;
|
||||||
|
|
||||||
|
public class Foo extends ITest.Stub {
|
||||||
|
private Context mContext;
|
||||||
|
|
||||||
|
@android.annotation.PermissionMethod
|
||||||
|
private void allOfhelper() {
|
||||||
|
mContext.enforceCallingOrSelfPermission("FOO");
|
||||||
|
mContext.enforceCallingOrSelfPermission("BAR");
|
||||||
|
}
|
||||||
|
|
||||||
|
@android.annotation.PermissionMethod(anyOf = true, permissions = {"BAZ", "BUZZ"})
|
||||||
|
private void anyOfHelper() {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void test() throws android.os.RemoteException {
|
||||||
|
allOfhelper();
|
||||||
|
anyOfHelper();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
).indented(),
|
||||||
|
*stubs
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
.expectClean()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testAnyOfAllOf_mixedNestedCalls_ignored() {
|
||||||
|
lint().files(
|
||||||
|
java(
|
||||||
|
"""
|
||||||
|
import android.content.Context;
|
||||||
|
import android.annotation.PermissionName;
|
||||||
|
import android.test.ITest;
|
||||||
|
|
||||||
|
public class Foo extends ITest.Stub {
|
||||||
|
private Context mContext;
|
||||||
|
|
||||||
|
@android.annotation.PermissionMethod(anyOf = true)
|
||||||
|
private void anyOfCheck(@PermissionName String... permissions) {
|
||||||
|
allOfCheck("BAZ", "BUZZ");
|
||||||
|
}
|
||||||
|
|
||||||
|
@android.annotation.PermissionMethod
|
||||||
|
private void allOfCheck(@PermissionName String... permissions) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void test() throws android.os.RemoteException {
|
||||||
|
anyOfCheck("FOO", "BAR");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
).indented(),
|
||||||
|
*stubs
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
.expectClean()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val stubs = arrayOf(
|
||||||
|
aidlStub,
|
||||||
|
contextStub,
|
||||||
|
binderStub,
|
||||||
|
permissionMethodStub,
|
||||||
|
permissionNameStub
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
package com.google.android.lint.aidl
|
||||||
|
|
||||||
|
import com.android.tools.lint.checks.infrastructure.LintDetectorTest.java
|
||||||
|
import com.android.tools.lint.checks.infrastructure.TestFile
|
||||||
|
|
||||||
|
val aidlStub: TestFile = java(
|
||||||
|
"""
|
||||||
|
package android.test;
|
||||||
|
public interface ITest extends android.os.IInterface {
|
||||||
|
public static abstract class Stub extends android.os.Binder implements android.test.ITest {
|
||||||
|
protected void test_enforcePermission() throws SecurityException {}
|
||||||
|
}
|
||||||
|
public void test() throws android.os.RemoteException;
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
).indented()
|
||||||
|
|
||||||
|
val contextStub: TestFile = java(
|
||||||
|
"""
|
||||||
|
package android.content;
|
||||||
|
public class Context {
|
||||||
|
@android.annotation.PermissionMethod(orSelf = true)
|
||||||
|
public void enforceCallingOrSelfPermission(@android.annotation.PermissionName String permission, String message) {}
|
||||||
|
@android.annotation.PermissionMethod
|
||||||
|
public void enforceCallingPermission(@android.annotation.PermissionName String permission, String message) {}
|
||||||
|
@android.annotation.PermissionMethod(orSelf = true)
|
||||||
|
public int checkCallingOrSelfPermission(@android.annotation.PermissionName String permission, String message) {}
|
||||||
|
@android.annotation.PermissionMethod
|
||||||
|
public int checkCallingPermission(@android.annotation.PermissionName String permission, String message) {}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
).indented()
|
||||||
|
|
||||||
|
val binderStub: TestFile = java(
|
||||||
|
"""
|
||||||
|
package android.os;
|
||||||
|
public class Binder {
|
||||||
|
public static int getCallingUid() {}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
).indented()
|
||||||
|
|
||||||
|
val permissionMethodStub: TestFile = java(
|
||||||
|
"""
|
||||||
|
package android.annotation;
|
||||||
|
|
||||||
|
import static java.lang.annotation.ElementType.METHOD;
|
||||||
|
import static java.lang.annotation.RetentionPolicy.CLASS;
|
||||||
|
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.Target;
|
||||||
|
|
||||||
|
@Retention(CLASS)
|
||||||
|
@Target({METHOD})
|
||||||
|
public @interface PermissionMethod {}
|
||||||
|
"""
|
||||||
|
).indented()
|
||||||
|
|
||||||
|
val permissionNameStub: TestFile = java(
|
||||||
|
"""
|
||||||
|
package android.annotation;
|
||||||
|
|
||||||
|
import static java.lang.annotation.ElementType.FIELD;
|
||||||
|
import static java.lang.annotation.ElementType.LOCAL_VARIABLE;
|
||||||
|
import static java.lang.annotation.ElementType.METHOD;
|
||||||
|
import static java.lang.annotation.ElementType.PARAMETER;
|
||||||
|
import static java.lang.annotation.RetentionPolicy.CLASS;
|
||||||
|
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.Target;
|
||||||
|
|
||||||
|
@Retention(CLASS)
|
||||||
|
@Target({PARAMETER, METHOD, LOCAL_VARIABLE, FIELD})
|
||||||
|
public @interface PermissionName {}
|
||||||
|
"""
|
||||||
|
).indented()
|
||||||
|
|
||||||
|
val manifestStub: TestFile = java(
|
||||||
|
"""
|
||||||
|
package android;
|
||||||
|
|
||||||
|
public final class Manifest {
|
||||||
|
public static final class permission {
|
||||||
|
public static final String READ_CONTACTS="android.permission.READ_CONTACTS";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user