mirror of
https://github.com/bitwarden/android.git
synced 2025-12-10 00:06:22 -06:00
[PM-25663] Introduce CredentialExchangeImporter (#5868)
This commit is contained in:
parent
8235045dad
commit
80b3a7e675
@ -52,6 +52,7 @@ dependencies {
|
||||
implementation(libs.androidx.credentials.providerevents)
|
||||
implementation(libs.androidx.credentials.providerevents.play.services)
|
||||
implementation(libs.kotlinx.coroutines.android)
|
||||
implementation(libs.timber)
|
||||
|
||||
testImplementation(platform(libs.junit.bom))
|
||||
testRuntimeOnly(libs.junit.platform.launcher)
|
||||
|
||||
@ -0,0 +1,17 @@
|
||||
package com.bitwarden.cxf.importer
|
||||
|
||||
import com.bitwarden.cxf.importer.model.ImportCredentialsSelectionResult
|
||||
|
||||
/**
|
||||
* Responsible for importing credentials from other apps.
|
||||
*/
|
||||
interface CredentialExchangeImporter {
|
||||
/**
|
||||
* Starts the import process by requesting selection of a source credential provider.
|
||||
*
|
||||
* @param credentialTypes The types of credentials to import.
|
||||
*/
|
||||
suspend fun importCredentials(
|
||||
credentialTypes: List<String>,
|
||||
): ImportCredentialsSelectionResult
|
||||
}
|
||||
@ -0,0 +1,57 @@
|
||||
package com.bitwarden.cxf.importer
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.credentials.providerevents.ProviderEventsManager
|
||||
import androidx.credentials.providerevents.exception.ImportCredentialsCancellationException
|
||||
import androidx.credentials.providerevents.exception.ImportCredentialsException
|
||||
import androidx.credentials.providerevents.transfer.ImportCredentialsRequest
|
||||
import com.bitwarden.cxf.importer.model.ImportCredentialsSelectionResult
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* Default implementation of [CredentialExchangeImporter].
|
||||
*
|
||||
* @param activity The context of the activity that is importing credentials.
|
||||
* @param providerEventsManager The [ProviderEventsManager] instance used for managing provider
|
||||
* events. If not provided, a default instance will be created using the provided [activity].
|
||||
* It is only meant to be used for testing purposes.
|
||||
*/
|
||||
internal class CredentialExchangeImporterImpl(
|
||||
private val activity: Context,
|
||||
@param:VisibleForTesting
|
||||
private val providerEventsManager: ProviderEventsManager =
|
||||
ProviderEventsManager.create(activity),
|
||||
) : CredentialExchangeImporter {
|
||||
|
||||
override suspend fun importCredentials(
|
||||
credentialTypes: List<String>,
|
||||
): ImportCredentialsSelectionResult {
|
||||
return try {
|
||||
val response = providerEventsManager.importCredentials(
|
||||
context = activity,
|
||||
request = ImportCredentialsRequest(
|
||||
// TODO: [PM-25663] Link to the correct documentation once it's available.
|
||||
requestJson = """
|
||||
{
|
||||
"importer": "${activity.packageName}",
|
||||
"credentialTypes": [
|
||||
${credentialTypes.joinToString { "\"$it\"" }}
|
||||
]
|
||||
}
|
||||
"""
|
||||
.trimIndent(),
|
||||
),
|
||||
)
|
||||
ImportCredentialsSelectionResult.Success(
|
||||
response = response.response.responseJson,
|
||||
callingAppInfo = response.callingAppInfo,
|
||||
)
|
||||
} catch (_: ImportCredentialsCancellationException) {
|
||||
ImportCredentialsSelectionResult.Cancelled
|
||||
} catch (e: ImportCredentialsException) {
|
||||
Timber.e(e, "Failed to import items from selected credential manager.")
|
||||
ImportCredentialsSelectionResult.Failure(error = e)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,47 @@
|
||||
@file:OmitFromCoverage
|
||||
|
||||
package com.bitwarden.cxf.importer.dsl
|
||||
|
||||
import android.app.Activity
|
||||
import com.bitwarden.annotation.OmitFromCoverage
|
||||
import com.bitwarden.cxf.importer.CredentialExchangeImporter
|
||||
import com.bitwarden.cxf.importer.CredentialExchangeImporterImpl
|
||||
|
||||
/**
|
||||
* A builder class for constructing an instance of [CredentialExchangeImporter].
|
||||
*
|
||||
* This builder is invoked within the [credentialExchangeImporter] function to configure and create
|
||||
* the importer. It is not intended to be instantiated directly.
|
||||
*/
|
||||
@OmitFromCoverage
|
||||
class CredentialExchangeImporterBuilder internal constructor() {
|
||||
internal fun build(activity: Activity): CredentialExchangeImporter =
|
||||
CredentialExchangeImporterImpl(activity = activity)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an instance of [CredentialExchangeImporter] using the provided [activity]
|
||||
* and an optional [config] lambda to configure the importer.
|
||||
*
|
||||
* This function acts as a DSL entry point for building a [CredentialExchangeImporter].
|
||||
*
|
||||
* Example usage:
|
||||
* ```kotlin
|
||||
* val importer = credentialExchangeImporter(activity) {
|
||||
* // Configuration options for the builder can be set here if any are added in the future.
|
||||
* logsManager = myLogsManager // Optional: pass your LogsManager instance
|
||||
* }
|
||||
* importer.importCredentials()
|
||||
* ```
|
||||
*
|
||||
* @param activity The Android [Activity] triggering the import process.
|
||||
* @param config A lambda with [CredentialExchangeImporterBuilder] as its receiver,
|
||||
* allowing for declarative configuration of the importer.
|
||||
* @return A fully configured instance of [CredentialExchangeImporter].
|
||||
*/
|
||||
fun credentialExchangeImporter(
|
||||
activity: Activity,
|
||||
config: CredentialExchangeImporterBuilder.() -> Unit = {},
|
||||
): CredentialExchangeImporter = CredentialExchangeImporterBuilder()
|
||||
.apply(config)
|
||||
.build(activity = activity)
|
||||
@ -0,0 +1,36 @@
|
||||
package com.bitwarden.cxf.importer.model
|
||||
|
||||
import androidx.credentials.provider.CallingAppInfo
|
||||
import androidx.credentials.providerevents.exception.ImportCredentialsException
|
||||
|
||||
/**
|
||||
* Represents the result of the credential selection step of the import process.
|
||||
*/
|
||||
sealed class ImportCredentialsSelectionResult {
|
||||
|
||||
/**
|
||||
* Represents a successful response from the selected credential provider.
|
||||
*
|
||||
* @property response The response from the import. This is a JSON string containing
|
||||
* credentials to import and is compliant with the FIDO2 CXF standard.
|
||||
* @property callingAppInfo The calling app information.
|
||||
*/
|
||||
data class Success(
|
||||
val response: String,
|
||||
val callingAppInfo: CallingAppInfo,
|
||||
) : ImportCredentialsSelectionResult()
|
||||
|
||||
/**
|
||||
* Represents a cancellation of the import process.
|
||||
*/
|
||||
data object Cancelled : ImportCredentialsSelectionResult()
|
||||
|
||||
/**
|
||||
* Represents a failure during the credential selection step of the import process.
|
||||
*
|
||||
* @property error The exception that caused the failure.
|
||||
*/
|
||||
data class Failure(
|
||||
val error: ImportCredentialsException,
|
||||
) : ImportCredentialsSelectionResult()
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
@file:OmitFromCoverage
|
||||
|
||||
package com.bitwarden.cxf.ui.composition
|
||||
|
||||
import androidx.compose.runtime.ProvidableCompositionLocal
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
import com.bitwarden.annotation.OmitFromCoverage
|
||||
import com.bitwarden.cxf.importer.CredentialExchangeImporter
|
||||
|
||||
/**
|
||||
* Provides access to the Credential Exchange importer throughout the app.
|
||||
*/
|
||||
val LocalCredentialExchangeImporter: ProvidableCompositionLocal<CredentialExchangeImporter> =
|
||||
compositionLocalOf {
|
||||
error("CompositionLocal LocalPermissionsManager not present")
|
||||
}
|
||||
@ -0,0 +1,106 @@
|
||||
package com.bitwarden.cxf.importer
|
||||
|
||||
import android.app.Activity
|
||||
import androidx.credentials.provider.CallingAppInfo
|
||||
import androidx.credentials.providerevents.ProviderEventsManager
|
||||
import androidx.credentials.providerevents.exception.ImportCredentialsCancellationException
|
||||
import androidx.credentials.providerevents.exception.ImportCredentialsException
|
||||
import androidx.credentials.providerevents.transfer.ImportCredentialsRequest
|
||||
import androidx.credentials.providerevents.transfer.ImportCredentialsResponse
|
||||
import androidx.credentials.providerevents.transfer.ProviderImportCredentialsResponse
|
||||
import com.bitwarden.cxf.importer.model.ImportCredentialsSelectionResult
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class CredentialExchangeImporterTest {
|
||||
|
||||
private val mockActivity = mockk<Activity>(relaxed = true) {
|
||||
every { packageName } returns "mockPackageName"
|
||||
}
|
||||
private val mockProviderEventsManager = mockk<ProviderEventsManager>()
|
||||
private val importer = CredentialExchangeImporterImpl(
|
||||
activity = mockActivity,
|
||||
providerEventsManager = mockProviderEventsManager,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `importCredentials should construct request correctly and return a success result`() =
|
||||
runTest {
|
||||
val mockCallingAppInfo = mockk<CallingAppInfo>()
|
||||
val capturedRequestJson = mutableListOf<ImportCredentialsRequest>()
|
||||
val expectedRequestJson = """
|
||||
{
|
||||
"importer": "mockPackageName",
|
||||
"credentialTypes": [
|
||||
"basic-auth"
|
||||
]
|
||||
}
|
||||
"""
|
||||
.trimIndent()
|
||||
coEvery {
|
||||
mockProviderEventsManager.importCredentials(
|
||||
context = mockActivity,
|
||||
request = capture(capturedRequestJson),
|
||||
)
|
||||
} returns ProviderImportCredentialsResponse(
|
||||
response = ImportCredentialsResponse(
|
||||
responseJson = "mockResponse",
|
||||
),
|
||||
callingAppInfo = mockCallingAppInfo,
|
||||
)
|
||||
|
||||
val result = importer.importCredentials(listOf("basic-auth"))
|
||||
assertEquals(
|
||||
expectedRequestJson,
|
||||
capturedRequestJson.firstOrNull()?.requestJson,
|
||||
)
|
||||
assertEquals(
|
||||
ImportCredentialsSelectionResult.Success(
|
||||
response = "mockResponse",
|
||||
callingAppInfo = mockCallingAppInfo,
|
||||
),
|
||||
result,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `importCredentials should return ImportCredentialsSelectionResult Cancelled when ImportCredentialsCancellationException is thrown`() =
|
||||
runTest {
|
||||
coEvery {
|
||||
mockProviderEventsManager.importCredentials(
|
||||
context = mockActivity,
|
||||
request = any(),
|
||||
)
|
||||
} throws ImportCredentialsCancellationException()
|
||||
|
||||
assertEquals(
|
||||
ImportCredentialsSelectionResult.Cancelled,
|
||||
importer.importCredentials(listOf("basic-auth")),
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `importCredentials should return ImportCredentialsSelectionResult Failure when ImportCredentialsException is thrown`() =
|
||||
runTest {
|
||||
val importException = mockk<ImportCredentialsException>()
|
||||
coEvery {
|
||||
mockProviderEventsManager.importCredentials(
|
||||
context = mockActivity,
|
||||
request = any(),
|
||||
)
|
||||
} throws importException
|
||||
|
||||
val result = importer.importCredentials(listOf("basic-auth"))
|
||||
|
||||
assertEquals(
|
||||
ImportCredentialsSelectionResult.Failure(error = importException),
|
||||
result,
|
||||
)
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user