-
Notifications
You must be signed in to change notification settings - Fork 393
@W-21933885: [MSDK Android] App Attestation Implementation #2868
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from 95 commits
79e735e
e912590
00a2924
9c62dbe
d1376e5
e7f94f6
763515c
91e6079
62660cf
ba416fd
4a40216
2d2ca63
80771f6
7842f19
3ebedd4
5219194
09651ad
1ab0d06
44849dc
fb5c845
86377f5
a2a451f
0b158b5
4076ab2
b0b7370
e374d58
b1e5bad
d488ff0
1325829
1ebcfe2
9bf16d1
e3398ad
6e06e9e
3a0e417
91a434c
466b058
5845735
9a2aca8
5035087
b8f1790
7e5c233
f984d45
1304062
ab2af33
4029772
f3fad52
48f98cc
484e620
b6c4354
86f4b77
0f15179
9ddfa9e
aeb7405
e9d4890
6a36d44
6cdb0d1
ffce923
1b994fb
e9b32a4
97228a7
f93c9e5
8975f32
732a127
9a39ef0
7131666
15a9b04
d2998a3
9994387
29beb71
2c44b99
966c13a
c724a1a
2ef4eca
3673200
f674131
a44d1f0
30d3ca8
ecdd9d2
e2070f0
3e822b6
98efcc6
d154ec6
55d370f
89400d3
8a60818
261f874
63172a1
6946375
2ba67fb
5f968fa
d526e06
06c3719
beb685d
8d48205
da8bce9
76c91d8
b1e380e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -27,6 +27,10 @@ jobs: | |
| - name: Install Dependencies | ||
| env: | ||
| TEST_CREDENTIALS: ${{ secrets.TEST_CREDENTIALS }} | ||
| # On PR runs, only SalesforceReact consumes the bundled index.android.bundle, | ||
| # so skip the yarn install + react-native bundle step for every other lib to | ||
| # save ~3-5 min per matrix job. Nightly runs still produce the bundle. | ||
| SKIP_REACT_NATIVE_BUNDLE: ${{ (inputs.is_pr && inputs.lib != 'SalesforceReact') && '1' || '0' }} | ||
| run: | | ||
| ./install.sh | ||
| echo $TEST_CREDENTIALS > ./shared/test/test_credentials.json | ||
|
|
@@ -68,6 +72,42 @@ jobs: | |
| run: | | ||
| ./gradlew libs:${{ inputs.lib }}:assembleAndroidTest | ||
| ./gradlew native:NativeSampleApps:RestExplorer:assembleDebug | ||
| - name: Run Unit Tests | ||
| if: success() || failure() | ||
| continue-on-error: true | ||
| run: ./gradlew libs:${{ inputs.lib }}:testDebugUnitTest --continue | ||
| - name: Publish Unit Test Results | ||
| uses: EnricoMi/publish-unit-test-result-action@v2 | ||
| if: success() || failure() | ||
| with: | ||
| check_name: ${{ inputs.lib }} Unit Test Results | ||
| files: | | ||
| libs/${{ inputs.lib }}/build/test-results/testDebugUnitTest/*.xml | ||
| comment_mode: off | ||
| fail_on: nothing | ||
| - name: Archive Unit Test Results | ||
| uses: actions/upload-artifact@v4 | ||
| if: success() || failure() | ||
| with: | ||
| name: unit-test-results-${{ inputs.lib }} | ||
| path: libs/${{ inputs.lib }}/build/test-results/testDebugUnitTest/*.xml | ||
| - name: Generate Unit Test Coverage Report | ||
| if: success() || failure() | ||
| run: ./gradlew libs:${{ inputs.lib }}:unitTestCoverageReport | ||
| - uses: codecov/codecov-action@v5 | ||
| if: success() || failure() | ||
| with: | ||
| files: libs/${{ inputs.lib }}/build/reports/jacoco/unitTestCoverageReport/unitTestCoverageReport.xml | ||
| flags: ${{ inputs.lib }}-unit-tests | ||
| name: ${{ inputs.lib }}-unit-test-coverage | ||
| env: | ||
| CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} | ||
| - name: Archive Unit Test Coverage Report | ||
| uses: actions/upload-artifact@v4 | ||
| if: success() || failure() | ||
| with: | ||
| name: unit-test-coverage-${{ inputs.lib }} | ||
| path: libs/${{ inputs.lib }}/build/reports/jacoco/unitTestCoverageReport/ | ||
| - uses: 'google-github-actions/auth@v2' | ||
| if: success() || failure() | ||
| with: | ||
|
|
@@ -109,7 +149,6 @@ jobs: | |
|
|
||
| if $IS_PR ; then | ||
| LEVELS_TO_TEST=$PR_API_VERSION | ||
| RETRIES=1 | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @brandonpage, a little later you'll see a fix in the |
||
| fi | ||
|
|
||
| # Build test-targets-for-shard arguments from config file | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||
|---|---|---|---|---|
|
|
@@ -23,6 +23,7 @@ dependencies { | |||
| api("androidx.browser:browser:1.8.0") // Update requires API 36 compileSdk | ||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||||
| api("androidx.work:work-runtime-ktx:2.10.3") | ||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||||
|
|
||||
| implementation("com.google.android.play:integrity:1.6.0") | ||||
| implementation("com.google.accompanist:accompanist-drawablepainter:0.37.3") | ||||
| implementation("com.google.android.material:material:1.13.0") // remove this when all xml is gone | ||||
| implementation("androidx.appcompat:appcompat:1.7.1") | ||||
|
|
@@ -55,6 +56,14 @@ dependencies { | |||
| androidTestImplementation("androidx.arch.core:core-testing:2.2.0") | ||||
| androidTestImplementation("androidx.compose.ui:ui-test-junit4:$composeVersion") | ||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||||
| androidTestImplementation("io.mockk:mockk-android:1.14.0") // Update requires Kotlin 2 | ||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||||
|
|
||||
| testImplementation("junit:junit:4.13.2") | ||||
| testImplementation("io.mockk:mockk:1.14.0") // Update requires Kotlin 2 | ||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||||
| testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.1") // Update requires Kotlin 2 | ||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||||
| testImplementation("androidx.test:core:1.7.0") | ||||
| testImplementation("androidx.test.ext:junit:1.3.0") | ||||
| testImplementation("androidx.arch.core:core-testing:2.2.0") | ||||
| testImplementation("org.robolectric:robolectric:4.14.1") | ||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||||
| } | ||||
|
|
||||
| android { | ||||
|
|
@@ -78,27 +87,35 @@ android { | |||
| sourceSets { | ||||
| getByName("main") { | ||||
| manifest.srcFile("AndroidManifest.xml") | ||||
| java.srcDirs(arrayOf("src")) | ||||
| resources.srcDirs(arrayOf("src")) | ||||
| aidl.srcDirs(arrayOf("src")) | ||||
| renderscript.srcDirs(arrayOf("src")) | ||||
| // Non-standard layout: production code is in src/com and src/debug (excludes src/test and src/androidTest) | ||||
| java.srcDirs(arrayOf("src/com", "src/debug")) | ||||
| resources.srcDirs(arrayOf("src/com", "src/debug")) | ||||
| aidl.srcDirs(arrayOf("src/com", "src/debug")) | ||||
| renderscript.srcDirs(arrayOf("src/com", "src/debug")) | ||||
| res.srcDirs(arrayOf("res")) | ||||
| assets.srcDirs(arrayOf("assets")) | ||||
| } | ||||
|
|
||||
| getByName("androidTest") { | ||||
| setRoot("../test/SalesforceSDKTest") | ||||
| // Manifest is in standard location: src/androidTest/AndroidManifest.xml | ||||
| // Test code remains in external ../test/SalesforceSDKTest directory | ||||
| java.srcDirs(arrayOf("../test/SalesforceSDKTest/src")) | ||||
| resources.srcDirs(arrayOf("../test/SalesforceSDKTest/src")) | ||||
| res.srcDirs(arrayOf("../test/SalesforceSDKTest/res")) | ||||
| @Suppress("UnstableApiUsage") | ||||
| assets.directories.add("../../shared/test") | ||||
| } | ||||
|
|
||||
| getByName("test") { | ||||
| // Standard layout for JVM unit tests: src/test/kotlin | ||||
| java.srcDirs(arrayOf("src/test/kotlin")) | ||||
| resources.srcDirs(arrayOf("src/test/resources")) | ||||
| } | ||||
| } | ||||
|
|
||||
| packaging { | ||||
| resources { | ||||
| excludes += setOf("META-INF/LICENSE*", "META-INF/LICENSE.txt", "META-INF/DEPENDENCIES", "META-INF/NOTICE") | ||||
| excludes += setOf("META-INF/LICENSE*", "META-INF/LICENSE.txt", "META-INF/DEPENDENCIES", "META-INF/NOTICE", "AndroidManifest.xml") | ||||
| } | ||||
| } | ||||
|
|
||||
|
|
@@ -142,6 +159,34 @@ android { | |||
| classDirectories.setFrom(javaTree, kotlinTree) | ||||
| executionData.setFrom(fileTree("$rootDir/firebase") { setIncludes(arrayListOf("**/coverage.ec")) }) | ||||
| } | ||||
|
|
||||
| val unitTestCoverage: TaskProvider<JacocoReport> = tasks.register<JacocoReport>("unitTestCoverageReport") { | ||||
| group = "Coverage" | ||||
| description = "Generate JaCoCo coverage report for unit tests." | ||||
| dependsOn("testDebugUnitTest") | ||||
| } | ||||
|
|
||||
| unitTestCoverage { | ||||
| reports { | ||||
| xml.required = true | ||||
| html.required = true | ||||
| } | ||||
|
|
||||
| sourceDirectories.setFrom(files("${project.projectDir}/src", "${project.projectDir}/src/debug")) | ||||
| val fileFilter = arrayListOf("**/R.class", "**/R$*.class", "**/BuildConfig.*", "**/Manifest*.*", "**/*Test*.*", "android/**/*.*") | ||||
| val javaTree = fileTree("${project.projectDir}/build/intermediates/javac/debug") { setExcludes(fileFilter) } | ||||
| val kotlinTree = fileTree("${project.projectDir}/build/tmp/kotlin-classes/debug") { setExcludes(fileFilter) } | ||||
| classDirectories.setFrom(javaTree, kotlinTree) | ||||
| executionData.setFrom(fileTree("${project.projectDir}/build/jacoco") { setIncludes(arrayListOf("testDebugUnitTest.exec")) }) | ||||
| } | ||||
| } | ||||
|
|
||||
| // Configure JaCoCo for unit tests | ||||
| tasks.withType<Test> { | ||||
| configure<JacocoTaskExtension> { | ||||
| isIncludeNoLocationClasses = true | ||||
| excludes = listOf("jdk.internal.*") | ||||
| } | ||||
| } | ||||
|
|
||||
| kotlin { | ||||
|
|
||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -89,6 +89,7 @@ import com.salesforce.androidsdk.app.Features.FEATURE_BROWSER_LOGIN | |
| import com.salesforce.androidsdk.app.Features.FEATURE_NATIVE_LOGIN | ||
| import com.salesforce.androidsdk.app.SalesforceSDKManager.Theme.DARK | ||
| import com.salesforce.androidsdk.app.SalesforceSDKManager.Theme.SYSTEM_DEFAULT | ||
| import com.salesforce.androidsdk.auth.AppAttestationClient | ||
| import com.salesforce.androidsdk.auth.AuthenticatorService.KEY_INSTANCE_URL | ||
| import com.salesforce.androidsdk.auth.HttpAccess | ||
| import com.salesforce.androidsdk.auth.HttpAccess.DEFAULT | ||
|
|
@@ -226,6 +227,54 @@ open class SalesforceSDKManager protected constructor( | |
| */ | ||
| val loginActivityClass: Class<out Activity> = nativeLoginActivity ?: webViewLoginActivityClass | ||
|
|
||
| /** | ||
| * The client side implementation of the Salesforce App Attestation External | ||
| * Client App (ECA) Plugin or null when app attestation is disabled. | ||
| * | ||
| * This property is not intended for public use outside of Salesforce Mobile | ||
| * SDK | ||
| * | ||
| * TODO: Make this Kotlin-internal once it is no longer referenced by Java. ECJ20260420 | ||
| */ | ||
| @Volatile | ||
| var appAttestationClient: AppAttestationClient? = null | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A crux change is creating a new object to encapsulate all the things for the new Salesforce "Challenge" API, the Integrity Token Provider, the Token and providing that in the "Attestation" format the auth and token refresh endpoints now expect. That's here. Our tools had some great suggestion around making this property thread safe, so I added @volatile, the private setter and a dedicated lock object based on tool feedback. |
||
| @VisibleForTesting | ||
| internal set | ||
|
|
||
| /** Lock object for synchronized access to the app Attestation Client */ | ||
| private val appAttestationClientLock = Any() | ||
|
|
||
| /** | ||
| * Updates the Salesforce App Attestation ECA Plugin Client for the selected | ||
| * login server and matching Google Cloud Project ID. When using App | ||
| * Attestation, this value must match the linked Google Cloud Project ID | ||
| * for the app in Google Play Console's Play Integrity API and provided to | ||
| * the Salesforce App Attestation External Client App Plugin. | ||
| * | ||
| * @param apiHostName The Salesforce App Attestation External Client App | ||
| * (ECA) Plugin Challenge API Host Name. This usually matches the selected | ||
| * login server | ||
| * @param googleCloudProjectId The Google Cloud Project ID or null to | ||
| * disable Salesforce App Attestation | ||
| */ | ||
| fun updateAppAttestationClient( | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If one reads the description of this pull request, this is the entry point for an app to actually enable App Attestation. |
||
| apiHostName: String, | ||
| googleCloudProjectId: Long? = null | ||
| ) { | ||
| synchronized(appAttestationClientLock) { | ||
| appAttestationClient = googleCloudProjectId?.let { appAttestationGoogleCloudProjectId -> | ||
| AppAttestationClient( | ||
| context = appContext, | ||
| apiHostName = apiHostName, | ||
| deviceId = deviceId, | ||
| googleCloudProjectId = appAttestationGoogleCloudProjectId, | ||
| remoteAccessConsumerKey = getBootConfig(appContext).remoteAccessConsumerKey, | ||
| restClient = clientManager.peekUnauthenticatedRestClient() | ||
| ) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * ViewModel Factory the SDK will use in LoginActivity and composable functions. Setting this will allow for | ||
| * visual customization without overriding LoginActivity. | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is unrelated to the feature, but could be useful to consider. While spending a lot of time watching ~25m CI runs, I asked our tools where a quick win would be in trimming that time. The analysis found that we're re-running yarn from scratch for all the modules instead of just for
SalesforceReact. I believe this brought my run down to ~12m! I need to verify that over a few more runs, but if that's the case this could be a nice optimization. @brandonpage?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice find!