Skip to content

Commit e26c43a

Browse files
committed
[PM-19108] Add privileged app management screen
Allow users to manage trusted privileged applications and view privileged applications that are trusted by external sources.
1 parent 465c5ce commit e26c43a

File tree

13 files changed

+1038
-22
lines changed

13 files changed

+1038
-22
lines changed

app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/di/Fido2ProviderModule.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,12 +87,16 @@ object Fido2ProviderModule {
8787

8888
@Provides
8989
@Singleton
90-
fun provideFido2PrivilegedAppRepository(
90+
fun providePrivilegedAppRepository(
9191
fido2PrivilegedAppDiskSource: Fido2PrivilegedAppDiskSource,
92+
assetManager: AssetManager,
93+
dispatcherManager: DispatcherManager,
9294
json: Json,
9395
): PrivilegedAppRepository =
9496
PrivilegedAppRepositoryImpl(
9597
fido2PrivilegedAppDiskSource = fido2PrivilegedAppDiskSource,
98+
assetManager = assetManager,
99+
dispatcherManager = dispatcherManager,
96100
json = json,
97101
)
98102

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package com.x8bit.bitwarden.data.autofill.fido2.model
2+
3+
/**
4+
* Represents privileged applications that are trusted by various sources.
5+
*/
6+
data class PrivilegedAppData(
7+
val googleTrustedApps: PrivilegedAppAllowListJson,
8+
val communityTrustedApps: PrivilegedAppAllowListJson,
9+
val userTrustedApps: PrivilegedAppAllowListJson,
10+
)

app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/repository/PrivilegedAppRepository.kt

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,49 @@
11
package com.x8bit.bitwarden.data.autofill.fido2.repository
22

33
import com.x8bit.bitwarden.data.autofill.fido2.model.PrivilegedAppAllowListJson
4-
import kotlinx.coroutines.flow.Flow
4+
import com.x8bit.bitwarden.data.autofill.fido2.model.PrivilegedAppData
5+
import com.x8bit.bitwarden.data.platform.repository.model.DataState
6+
import kotlinx.coroutines.flow.StateFlow
57

68
/**
79
* Repository for managing privileged apps trusted by the user.
810
*/
911
interface PrivilegedAppRepository {
1012

13+
/**
14+
* Flow that represents the trusted privileged apps data.
15+
*/
16+
val trustedAppDataStateFlow: StateFlow<DataState<PrivilegedAppData>>
17+
1118
/**
1219
* Flow of the user's trusted privileged apps.
1320
*/
14-
val userTrustedPrivilegedAppsFlow: Flow<PrivilegedAppAllowListJson>
21+
val userTrustedAppsFlow: StateFlow<DataState<PrivilegedAppAllowListJson>>
22+
23+
/**
24+
* Flow of the Google's trusted privileged apps.
25+
*/
26+
val googleTrustedPrivilegedAppsFlow: StateFlow<DataState<PrivilegedAppAllowListJson>>
27+
28+
/**
29+
* Flow of the community's trusted privileged apps.
30+
*/
31+
val communityTrustedAppsFlow: StateFlow<DataState<PrivilegedAppAllowListJson>>
1532

1633
/**
1734
* List the user's trusted privileged apps.
1835
*/
19-
suspend fun getAllUserTrustedPrivilegedApps(): PrivilegedAppAllowListJson
36+
suspend fun getUserTrustedPrivilegedAppsOrNull(): PrivilegedAppAllowListJson?
37+
38+
/**
39+
* List Google's trusted privileged apps.
40+
*/
41+
suspend fun getGoogleTrustedPrivilegedAppsOrNull(): PrivilegedAppAllowListJson?
2042

2143
/**
22-
* Returns true if the given [packageName] and [signature] are trusted.
44+
* List community's trusted privileged apps.
2345
*/
24-
suspend fun isPrivilegedAppAllowed(packageName: String, signature: String): Boolean
46+
suspend fun getCommunityTrustedPrivilegedAppsOrNull(): PrivilegedAppAllowListJson?
2547

2648
/**
2749
* Adds the given [packageName] and [signature] to the list of trusted privileged apps.

app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/repository/PrivilegedAppRepositoryImpl.kt

Lines changed: 116 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,33 @@ package com.x8bit.bitwarden.data.autofill.fido2.repository
33
import com.x8bit.bitwarden.data.autofill.fido2.datasource.disk.Fido2PrivilegedAppDiskSource
44
import com.x8bit.bitwarden.data.autofill.fido2.datasource.disk.entity.Fido2PrivilegedAppInfoEntity
55
import com.x8bit.bitwarden.data.autofill.fido2.model.PrivilegedAppAllowListJson
6-
import kotlinx.coroutines.flow.Flow
6+
import com.x8bit.bitwarden.data.autofill.fido2.model.PrivilegedAppData
7+
import com.x8bit.bitwarden.data.platform.manager.AssetManager
8+
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
9+
import com.x8bit.bitwarden.data.platform.repository.model.DataState
10+
import com.x8bit.bitwarden.data.platform.repository.util.combineDataStates
11+
import com.x8bit.bitwarden.data.platform.util.decodeFromStringOrNull
12+
import kotlinx.coroutines.CoroutineScope
13+
import kotlinx.coroutines.flow.MutableStateFlow
14+
import kotlinx.coroutines.flow.SharingStarted
15+
import kotlinx.coroutines.flow.StateFlow
16+
import kotlinx.coroutines.flow.asStateFlow
17+
import kotlinx.coroutines.flow.combine
18+
import kotlinx.coroutines.flow.launchIn
719
import kotlinx.coroutines.flow.map
20+
import kotlinx.coroutines.flow.onEach
21+
import kotlinx.coroutines.flow.stateIn
22+
import kotlinx.coroutines.launch
23+
import kotlinx.coroutines.withContext
824
import kotlinx.serialization.json.Json
925

26+
/**
27+
* A "stop timeout delay" in milliseconds used to let a shared coroutine continue to run for the
28+
* specified period of time after it no longer has subscribers.
29+
*/
30+
private const val STOP_TIMEOUT_DELAY_MS: Long = 1000L
31+
private const val GOOGLE_ALLOW_LIST_FILE_NAME = "fido2_privileged_google.json"
32+
private const val COMMUNITY_ALLOW_LIST_FILE_NAME = "fido2_privileged_community.json"
1033
private const val ANDROID_TYPE = "android"
1134
private const val RELEASE_BUILD = "release"
1235

@@ -15,25 +38,104 @@ private const val RELEASE_BUILD = "release"
1538
*/
1639
class PrivilegedAppRepositoryImpl(
1740
private val fido2PrivilegedAppDiskSource: Fido2PrivilegedAppDiskSource,
41+
private val assetManager: AssetManager,
42+
dispatcherManager: DispatcherManager,
1843
private val json: Json,
1944
) : PrivilegedAppRepository {
2045

21-
override val userTrustedPrivilegedAppsFlow: Flow<PrivilegedAppAllowListJson> =
22-
fido2PrivilegedAppDiskSource.userTrustedPrivilegedAppsFlow
23-
.map { it.toFido2PrivilegedAppAllowListJson() }
46+
private val unconfinedScope = CoroutineScope(dispatcherManager.unconfined)
47+
private val ioScope = CoroutineScope(dispatcherManager.io)
48+
49+
private val mutableUserTrustedAppsFlow =
50+
MutableStateFlow<DataState<PrivilegedAppAllowListJson>>(DataState.Loading)
51+
private val mutableGoogleTrustedAppsFlow =
52+
MutableStateFlow<DataState<PrivilegedAppAllowListJson>>(DataState.Loading)
53+
private val mutableCommunityTrustedPrivilegedAppsFlow =
54+
MutableStateFlow<DataState<PrivilegedAppAllowListJson>>(DataState.Loading)
55+
56+
override val trustedAppDataStateFlow: StateFlow<DataState<PrivilegedAppData>> =
57+
combine(
58+
userTrustedAppsFlow,
59+
googleTrustedPrivilegedAppsFlow,
60+
communityTrustedAppsFlow,
61+
) { userAppsState, googleAppsState, communityAppsState ->
62+
combineDataStates(
63+
userAppsState,
64+
googleAppsState,
65+
communityAppsState,
66+
) { userApps, googleApps, communityApps ->
67+
PrivilegedAppData(
68+
googleTrustedApps = googleApps,
69+
communityTrustedApps = communityApps,
70+
userTrustedApps = userApps,
71+
)
72+
}
73+
}
74+
.stateIn(
75+
scope = unconfinedScope,
76+
started = SharingStarted.WhileSubscribed(stopTimeoutMillis = STOP_TIMEOUT_DELAY_MS),
77+
initialValue = DataState.Loading,
78+
)
79+
80+
override val userTrustedAppsFlow: StateFlow<DataState<PrivilegedAppAllowListJson>>
81+
get() = mutableUserTrustedAppsFlow.asStateFlow()
82+
83+
override val googleTrustedPrivilegedAppsFlow: StateFlow<DataState<PrivilegedAppAllowListJson>>
84+
get() = mutableGoogleTrustedAppsFlow.asStateFlow()
85+
86+
override val communityTrustedAppsFlow: StateFlow<DataState<PrivilegedAppAllowListJson>>
87+
get() = mutableCommunityTrustedPrivilegedAppsFlow.asStateFlow()
88+
89+
init {
90+
ioScope.launch {
91+
val googleAppsDataState = assetManager.readAsset(fileName = GOOGLE_ALLOW_LIST_FILE_NAME)
92+
.map { json.decodeFromString<PrivilegedAppAllowListJson>(it) }
93+
.fold(
94+
onSuccess = { DataState.Loaded(it) },
95+
onFailure = { DataState.Error(it) },
96+
)
97+
98+
val communityAppsDataState =
99+
assetManager.readAsset(fileName = COMMUNITY_ALLOW_LIST_FILE_NAME)
100+
.map { json.decodeFromString<PrivilegedAppAllowListJson>(it) }
101+
.fold(
102+
onSuccess = { DataState.Loaded(it) },
103+
onFailure = { DataState.Error(it) },
104+
)
24105

25-
override suspend fun getAllUserTrustedPrivilegedApps(): PrivilegedAppAllowListJson =
26-
fido2PrivilegedAppDiskSource.getAllUserTrustedPrivilegedApps()
106+
mutableGoogleTrustedAppsFlow.value = googleAppsDataState
107+
mutableCommunityTrustedPrivilegedAppsFlow.value = communityAppsDataState
108+
109+
fido2PrivilegedAppDiskSource.userTrustedPrivilegedAppsFlow
110+
.map { DataState.Loaded(it.toFido2PrivilegedAppAllowListJson()) }
111+
.onEach {
112+
mutableUserTrustedAppsFlow.value = it
113+
}
114+
.launchIn(ioScope)
115+
}
116+
}
117+
118+
override suspend fun getUserTrustedPrivilegedAppsOrNull(): PrivilegedAppAllowListJson =
119+
fido2PrivilegedAppDiskSource
120+
.getAllUserTrustedPrivilegedApps()
27121
.toFido2PrivilegedAppAllowListJson()
28122

29-
override suspend fun isPrivilegedAppAllowed(
30-
packageName: String,
31-
signature: String,
32-
): Boolean = fido2PrivilegedAppDiskSource
33-
.isPrivilegedAppTrustedByUser(
34-
packageName = packageName,
35-
signature = signature,
36-
)
123+
override suspend fun getGoogleTrustedPrivilegedAppsOrNull(): PrivilegedAppAllowListJson? =
124+
withContext(ioScope.coroutineContext) {
125+
assetManager
126+
.readAsset(fileName = GOOGLE_ALLOW_LIST_FILE_NAME)
127+
.map { json.decodeFromStringOrNull<PrivilegedAppAllowListJson>(it) }
128+
.getOrNull()
129+
}
130+
131+
override suspend fun getCommunityTrustedPrivilegedAppsOrNull(): PrivilegedAppAllowListJson? {
132+
return withContext(ioScope.coroutineContext) {
133+
assetManager
134+
.readAsset(fileName = COMMUNITY_ALLOW_LIST_FILE_NAME)
135+
.map { json.decodeFromStringOrNull<PrivilegedAppAllowListJson>(it) }
136+
.getOrNull()
137+
}
138+
}
37139

38140
override suspend fun addTrustedPrivilegedApp(
39141
packageName: String,

app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/SettingsNavigation.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.navigateToApp
1414
import com.x8bit.bitwarden.ui.platform.feature.settings.autofill.autoFillDestination
1515
import com.x8bit.bitwarden.ui.platform.feature.settings.autofill.blockautofill.blockAutoFillDestination
1616
import com.x8bit.bitwarden.ui.platform.feature.settings.autofill.blockautofill.navigateToBlockAutoFillScreen
17+
import com.x8bit.bitwarden.ui.platform.feature.settings.autofill.privilegedappslist.privilegedAppsListDestination
18+
import com.x8bit.bitwarden.ui.platform.feature.settings.autofill.privilegedappslist.navigateToPrivilegedAppsList
1719
import com.x8bit.bitwarden.ui.platform.feature.settings.autofill.navigateToAutoFill
1820
import com.x8bit.bitwarden.ui.platform.feature.settings.other.navigateToOther
1921
import com.x8bit.bitwarden.ui.platform.feature.settings.other.otherDestination
@@ -66,6 +68,7 @@ fun NavGraphBuilder.settingsGraph(
6668
onNavigateBack = { navController.popBackStack() },
6769
onNavigateToBlockAutoFillScreen = { navController.navigateToBlockAutoFillScreen() },
6870
onNavigateToSetupAutofill = onNavigateToSetupAutoFillScreen,
71+
onNavigateToTrustedAppsScreen = { navController.navigateToPrivilegedAppsList() },
6972
)
7073
otherDestination(onNavigateBack = { navController.popBackStack() })
7174
vaultSettingsDestination(
@@ -75,6 +78,7 @@ fun NavGraphBuilder.settingsGraph(
7578
onNavigateToImportLogins = onNavigateToImportLogins,
7679
)
7780
blockAutoFillDestination(onNavigateBack = { navController.popBackStack() })
81+
privilegedAppsListDestination(onNavigateBack = { navController.popBackStack() })
7882
}
7983
}
8084

app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillNavigation.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ fun NavGraphBuilder.autoFillDestination(
1414
onNavigateBack: () -> Unit,
1515
onNavigateToBlockAutoFillScreen: () -> Unit,
1616
onNavigateToSetupAutofill: () -> Unit,
17+
onNavigateToTrustedAppsScreen: () -> Unit,
1718
) {
1819
composableWithPushTransitions(
1920
route = AUTO_FILL_ROUTE,
@@ -22,6 +23,7 @@ fun NavGraphBuilder.autoFillDestination(
2223
onNavigateBack = onNavigateBack,
2324
onNavigateToBlockAutoFillScreen = onNavigateToBlockAutoFillScreen,
2425
onNavigateToSetupAutofill = onNavigateToSetupAutofill,
26+
onNavigateToPrivilegedAppsScreen = onNavigateToTrustedAppsScreen,
2527
)
2628
}
2729
}

app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillScreen.kt

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,11 @@ import androidx.compose.foundation.layout.fillMaxSize
99
import androidx.compose.foundation.layout.fillMaxWidth
1010
import androidx.compose.foundation.layout.height
1111
import androidx.compose.foundation.layout.padding
12+
import androidx.compose.foundation.layout.size
1213
import androidx.compose.foundation.rememberScrollState
1314
import androidx.compose.foundation.verticalScroll
1415
import androidx.compose.material3.ExperimentalMaterial3Api
16+
import androidx.compose.material3.Icon
1517
import androidx.compose.material3.TopAppBarDefaults
1618
import androidx.compose.material3.rememberTopAppBarState
1719
import androidx.compose.runtime.Composable
@@ -31,6 +33,9 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
3133
import com.x8bit.bitwarden.R
3234
import com.x8bit.bitwarden.data.platform.repository.model.UriMatchType
3335
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
36+
import com.x8bit.bitwarden.ui.platform.base.util.Text
37+
import com.x8bit.bitwarden.ui.platform.base.util.asText
38+
import com.x8bit.bitwarden.ui.platform.base.util.mirrorIfRtl
3439
import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin
3540
import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
3641
import com.x8bit.bitwarden.ui.platform.components.badge.NotificationBadge
@@ -50,6 +55,7 @@ import com.x8bit.bitwarden.ui.platform.composition.LocalIntentManager
5055
import com.x8bit.bitwarden.ui.platform.feature.settings.autofill.chrome.ChromeAutofillSettingsCard
5156
import com.x8bit.bitwarden.ui.platform.feature.settings.autofill.util.displayLabel
5257
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
58+
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
5359
import kotlinx.collections.immutable.toImmutableList
5460

5561
/**
@@ -64,6 +70,7 @@ fun AutoFillScreen(
6470
intentManager: IntentManager = LocalIntentManager.current,
6571
onNavigateToBlockAutoFillScreen: () -> Unit,
6672
onNavigateToSetupAutofill: () -> Unit,
73+
onNavigateToPrivilegedAppsScreen: () -> Unit,
6774
) {
6875
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
6976
val context = LocalContext.current
@@ -101,6 +108,9 @@ fun AutoFillScreen(
101108
releaseChannel = event.releaseChannel,
102109
)
103110
}
111+
AutoFillEvent.NavigateToPrivilegedApps -> {
112+
onNavigateToPrivilegedAppsScreen()
113+
}
104114
}
105115
}
106116

@@ -230,12 +240,21 @@ fun AutoFillScreen(
230240
id = R.string.set_bitwarden_as_passkey_manager_description,
231241
),
232242
withDivider = false,
233-
cardStyle = CardStyle.Full,
243+
cardStyle = CardStyle.Top(hasDivider = true),
234244
modifier = Modifier
235245
.fillMaxWidth()
236246
.standardHorizontalMargin(),
237247
)
238-
Spacer(modifier = Modifier.height(height = 8.dp))
248+
PrivilegedAppsRow(
249+
text = R.string.privileged_apps.asText(),
250+
onClick = remember(viewModel) {
251+
{ viewModel.trySendAction(AutoFillAction.TrustedAppsClick) }
252+
},
253+
modifier = Modifier
254+
.standardHorizontalMargin()
255+
.fillMaxWidth(),
256+
)
257+
Spacer(modifier = Modifier.height(8.dp))
239258
}
240259
AccessibilityAutofillSwitch(
241260
isAccessibilityAutoFillEnabled = state.isAccessibilityAutofillEnabled,
@@ -373,3 +392,27 @@ private fun DefaultUriMatchTypeRow(
373392
modifier = modifier,
374393
)
375394
}
395+
396+
@Composable
397+
private fun PrivilegedAppsRow(
398+
text: Text,
399+
onClick: () -> Unit,
400+
modifier: Modifier = Modifier,
401+
) {
402+
BitwardenTextRow(
403+
text = text(),
404+
description = stringResource(R.string.privileged_apps_description),
405+
onClick = onClick,
406+
cardStyle = CardStyle.Bottom,
407+
modifier = modifier,
408+
) {
409+
Icon(
410+
painter = rememberVectorPainter(id = R.drawable.ic_chevron_right),
411+
contentDescription = null,
412+
tint = BitwardenTheme.colorScheme.icon.primary,
413+
modifier = Modifier
414+
.mirrorIfRtl()
415+
.size(size = 16.dp),
416+
)
417+
}
418+
}

0 commit comments

Comments
 (0)