Add Domains database (#784)

This commit is contained in:
Lucas Kivi
2024-01-25 19:17:03 -06:00
committed by Álison Fernandes
parent 5fa49c8b53
commit 52acc2fa47
15 changed files with 416 additions and 5 deletions

View File

@@ -34,6 +34,11 @@ interface VaultDiskSource {
*/
fun getCollections(userId: String): Flow<List<SyncResponseJson.Collection>>
/**
* Retrieves all domains from the data source for a given [userId].
*/
fun getDomains(userId: String): Flow<SyncResponseJson.Domains>
/**
* Saves a folder to the data source for the given [userId].
*/

View File

@@ -3,10 +3,12 @@ package com.x8bit.bitwarden.data.vault.datasource.disk
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
import com.x8bit.bitwarden.data.vault.datasource.disk.dao.CiphersDao
import com.x8bit.bitwarden.data.vault.datasource.disk.dao.CollectionsDao
import com.x8bit.bitwarden.data.vault.datasource.disk.dao.DomainsDao
import com.x8bit.bitwarden.data.vault.datasource.disk.dao.FoldersDao
import com.x8bit.bitwarden.data.vault.datasource.disk.dao.SendsDao
import com.x8bit.bitwarden.data.vault.datasource.disk.entity.CipherEntity
import com.x8bit.bitwarden.data.vault.datasource.disk.entity.CollectionEntity
import com.x8bit.bitwarden.data.vault.datasource.disk.entity.DomainsEntity
import com.x8bit.bitwarden.data.vault.datasource.disk.entity.FolderEntity
import com.x8bit.bitwarden.data.vault.datasource.disk.entity.SendEntity
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
@@ -16,6 +18,7 @@ import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.launch
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
@@ -26,6 +29,7 @@ import kotlinx.serialization.json.Json
class VaultDiskSourceImpl(
private val ciphersDao: CiphersDao,
private val collectionsDao: CollectionsDao,
private val domainsDao: DomainsDao,
private val foldersDao: FoldersDao,
private val sendsDao: SendsDao,
private val json: Json,
@@ -103,6 +107,13 @@ class VaultDiskSourceImpl(
},
)
override fun getDomains(userId: String): Flow<SyncResponseJson.Domains> =
domainsDao
.getDomains(userId)
.map { entity ->
json.decodeFromString<SyncResponseJson.Domains>(entity.domainsJson)
}
override suspend fun saveFolder(userId: String, folder: SyncResponseJson.Folder) {
foldersDao.insertFolder(
folder = FolderEntity(
@@ -198,6 +209,14 @@ class VaultDiskSourceImpl(
},
)
}
launch {
domainsDao.insertDomains(
domains = DomainsEntity(
userId = userId,
domainsJson = json.encodeToString(vault.domains),
),
)
}
val deferredFolders = async {
foldersDao.replaceAllFolders(
userId = userId,
@@ -245,11 +264,13 @@ class VaultDiskSourceImpl(
coroutineScope {
val deferredCiphers = async { ciphersDao.deleteAllCiphers(userId = userId) }
val deferredCollections = async { collectionsDao.deleteAllCollections(userId = userId) }
val deferredDomains = async { domainsDao.deleteDomains(userId = userId) }
val deferredFolders = async { foldersDao.deleteAllFolders(userId = userId) }
val deferredSends = async { sendsDao.deleteAllSends(userId = userId) }
awaitAll(
deferredCiphers,
deferredCollections,
deferredDomains,
deferredFolders,
deferredSends,
)

View File

@@ -0,0 +1,35 @@
package com.x8bit.bitwarden.data.vault.datasource.disk.dao
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.x8bit.bitwarden.data.vault.datasource.disk.entity.DomainsEntity
import kotlinx.coroutines.flow.Flow
/**
* Provides methods for inserting, retrieving, and deleting domains from the database using the
* [DomainsEntity].
*/
@Dao
interface DomainsDao {
/**
* Deletes the stored domains associated with the given [userId].
*/
@Query("DELETE FROM domains WHERE user_id = :userId")
suspend fun deleteDomains(userId: String)
/**
* Retrieves domains from the database for a given [userId].
*/
@Query("SELECT * FROM domains WHERE user_id = :userId")
fun getDomains(
userId: String,
): Flow<DomainsEntity>
/**
* Inserts domains into the database.
*/
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertDomains(domains: DomainsEntity)
}

View File

@@ -6,10 +6,12 @@ import androidx.room.TypeConverters
import com.x8bit.bitwarden.data.vault.datasource.disk.convertor.ZonedDateTimeTypeConverter
import com.x8bit.bitwarden.data.vault.datasource.disk.dao.CiphersDao
import com.x8bit.bitwarden.data.vault.datasource.disk.dao.CollectionsDao
import com.x8bit.bitwarden.data.vault.datasource.disk.dao.DomainsDao
import com.x8bit.bitwarden.data.vault.datasource.disk.dao.FoldersDao
import com.x8bit.bitwarden.data.vault.datasource.disk.dao.SendsDao
import com.x8bit.bitwarden.data.vault.datasource.disk.entity.CipherEntity
import com.x8bit.bitwarden.data.vault.datasource.disk.entity.CollectionEntity
import com.x8bit.bitwarden.data.vault.datasource.disk.entity.DomainsEntity
import com.x8bit.bitwarden.data.vault.datasource.disk.entity.FolderEntity
import com.x8bit.bitwarden.data.vault.datasource.disk.entity.SendEntity
@@ -20,10 +22,11 @@ import com.x8bit.bitwarden.data.vault.datasource.disk.entity.SendEntity
entities = [
CipherEntity::class,
CollectionEntity::class,
DomainsEntity::class,
FolderEntity::class,
SendEntity::class,
],
version = 2,
version = 3,
)
@TypeConverters(ZonedDateTimeTypeConverter::class)
abstract class VaultDatabase : RoomDatabase() {
@@ -38,6 +41,11 @@ abstract class VaultDatabase : RoomDatabase() {
*/
abstract fun collectionDao(): CollectionsDao
/**
* Provides the DAO for accessing domains data.
*/
abstract fun domainsDao(): DomainsDao
/**
* Provides the DAO for accessing folder data.
*/

View File

@@ -7,6 +7,7 @@ import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSourceImpl
import com.x8bit.bitwarden.data.vault.datasource.disk.convertor.ZonedDateTimeTypeConverter
import com.x8bit.bitwarden.data.vault.datasource.disk.dao.CiphersDao
import com.x8bit.bitwarden.data.vault.datasource.disk.dao.CollectionsDao
import com.x8bit.bitwarden.data.vault.datasource.disk.dao.DomainsDao
import com.x8bit.bitwarden.data.vault.datasource.disk.dao.FoldersDao
import com.x8bit.bitwarden.data.vault.datasource.disk.dao.SendsDao
import com.x8bit.bitwarden.data.vault.datasource.disk.database.VaultDatabase
@@ -45,6 +46,10 @@ class VaultDiskModule {
@Singleton
fun provideCollectionDao(database: VaultDatabase): CollectionsDao = database.collectionDao()
@Provides
@Singleton
fun provideDomainsDao(database: VaultDatabase): DomainsDao = database.domainsDao()
@Provides
@Singleton
fun provideFolderDao(database: VaultDatabase): FoldersDao = database.folderDao()
@@ -58,12 +63,14 @@ class VaultDiskModule {
fun provideVaultDiskSource(
ciphersDao: CiphersDao,
collectionsDao: CollectionsDao,
domainsDao: DomainsDao,
foldersDao: FoldersDao,
sendsDao: SendsDao,
json: Json,
): VaultDiskSource = VaultDiskSourceImpl(
ciphersDao = ciphersDao,
collectionsDao = collectionsDao,
domainsDao = domainsDao,
foldersDao = foldersDao,
sendsDao = sendsDao,
json = json,

View File

@@ -0,0 +1,18 @@
package com.x8bit.bitwarden.data.vault.datasource.disk.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
/**
* Entity representing a set of domains in the database.
*/
@Entity(tableName = "domains")
data class DomainsEntity(
@PrimaryKey(autoGenerate = false)
@ColumnInfo(name = "user_id")
val userId: String,
@ColumnInfo(name = "domains_json")
val domainsJson: String,
)

View File

@@ -17,6 +17,7 @@ import com.x8bit.bitwarden.data.vault.repository.model.CreateSendResult
import com.x8bit.bitwarden.data.vault.repository.model.DeleteAttachmentResult
import com.x8bit.bitwarden.data.vault.repository.model.DeleteCipherResult
import com.x8bit.bitwarden.data.vault.repository.model.DeleteSendResult
import com.x8bit.bitwarden.data.vault.repository.model.DomainsData
import com.x8bit.bitwarden.data.vault.repository.model.GenerateTotpResult
import com.x8bit.bitwarden.data.vault.repository.model.RemovePasswordSendResult
import com.x8bit.bitwarden.data.vault.repository.model.RestoreCipherResult
@@ -69,6 +70,14 @@ interface VaultRepository : VaultLockManager {
*/
val collectionsStateFlow: StateFlow<DataState<List<CollectionView>>>
/**
* Flow that represents all domains for the active user.
*
* Note that the [StateFlow.value] will return the last known value but the [StateFlow] itself
* must be collected in order to trigger state changes.
*/
val domainsStateFlow: StateFlow<DataState<DomainsData>>
/**
* Flow that represents all folders for the active user.
*

View File

@@ -46,6 +46,7 @@ import com.x8bit.bitwarden.data.vault.repository.model.CreateSendResult
import com.x8bit.bitwarden.data.vault.repository.model.DeleteAttachmentResult
import com.x8bit.bitwarden.data.vault.repository.model.DeleteCipherResult
import com.x8bit.bitwarden.data.vault.repository.model.DeleteSendResult
import com.x8bit.bitwarden.data.vault.repository.model.DomainsData
import com.x8bit.bitwarden.data.vault.repository.model.GenerateTotpResult
import com.x8bit.bitwarden.data.vault.repository.model.RemovePasswordSendResult
import com.x8bit.bitwarden.data.vault.repository.model.RestoreCipherResult
@@ -56,6 +57,7 @@ import com.x8bit.bitwarden.data.vault.repository.model.UpdateCipherResult
import com.x8bit.bitwarden.data.vault.repository.model.UpdateSendResult
import com.x8bit.bitwarden.data.vault.repository.model.VaultData
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult
import com.x8bit.bitwarden.data.vault.repository.util.toDomainsData
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedNetworkCipher
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedNetworkCipherResponse
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedNetworkSend
@@ -136,6 +138,9 @@ class VaultRepositoryImpl(
private val mutableCollectionsStateFlow =
MutableStateFlow<DataState<List<CollectionView>>>(DataState.Loading)
private val mutableDomainsStateFlow =
MutableStateFlow<DataState<DomainsData>>(DataState.Loading)
override var vaultFilterType: VaultFilterType = VaultFilterType.AllVaults
override val vaultDataStateFlow: StateFlow<DataState<VaultData>> =
@@ -171,6 +176,9 @@ class VaultRepositoryImpl(
override val ciphersStateFlow: StateFlow<DataState<List<CipherView>>>
get() = mutableCiphersStateFlow.asStateFlow()
override val domainsStateFlow: StateFlow<DataState<DomainsData>>
get() = mutableDomainsStateFlow.asStateFlow()
override val foldersStateFlow: StateFlow<DataState<List<FolderView>>>
get() = mutableFoldersStateFlow.asStateFlow()
@@ -187,6 +195,12 @@ class VaultRepositoryImpl(
observeVaultDiskCiphers(activeUserId)
}
.launchIn(unconfinedScope)
// Setup domains MutableStateFlow
mutableDomainsStateFlow
.observeWhenSubscribedAndLoggedIn(authDiskSource.userStateFlow) { activeUserId ->
observeVaultDiskDomains(activeUserId)
}
.launchIn(unconfinedScope)
// Setup folders MutableStateFlow
mutableFoldersStateFlow
.observeWhenSubscribedAndLoggedIn(authDiskSource.userStateFlow) { activeUserId ->
@@ -209,6 +223,7 @@ class VaultRepositoryImpl(
override fun clearUnlockedData() {
mutableCiphersStateFlow.update { DataState.Loading }
mutableDomainsStateFlow.update { DataState.Loading }
mutableFoldersStateFlow.update { DataState.Loading }
mutableCollectionsStateFlow.update { DataState.Loading }
mutableSendDataStateFlow.update { DataState.Loading }
@@ -224,6 +239,7 @@ class VaultRepositoryImpl(
val userId = activeUserId ?: return
if (!syncJob.isCompleted || isVaultUnlocking(userId)) return
mutableCiphersStateFlow.updateToPendingOrLoading()
mutableDomainsStateFlow.updateToPendingOrLoading()
mutableFoldersStateFlow.updateToPendingOrLoading()
mutableCollectionsStateFlow.updateToPendingOrLoading()
mutableSendDataStateFlow.updateToPendingOrLoading()
@@ -250,6 +266,11 @@ class VaultRepositoryImpl(
data = currentState.data,
)
}
mutableDomainsStateFlow.update { currentState ->
throwable.toNetworkOrErrorState(
data = currentState.data,
)
}
mutableFoldersStateFlow.update { currentState ->
throwable.toNetworkOrErrorState(
data = currentState.data,
@@ -993,6 +1014,19 @@ class VaultRepositoryImpl(
}
.onEach { mutableCiphersStateFlow.value = it }
private fun observeVaultDiskDomains(
userId: String,
): Flow<DataState<DomainsData>> =
vaultDiskSource
.getDomains(userId = userId)
.onStart { mutableDomainsStateFlow.value = DataState.Loading }
.map {
DataState.Loaded(
data = it.toDomainsData(),
)
}
.onEach { mutableDomainsStateFlow.value = it }
private fun observeVaultDiskFolders(
userId: String,
): Flow<DataState<List<FolderView>>> =

View File

@@ -0,0 +1,25 @@
package com.x8bit.bitwarden.data.vault.repository.model
/**
* Model for equivalent domain details.
*
* @param equivalentDomains A list of equivalent domains to compare URIs to.
* @param globalEquivalentDomains A list of global equivalent domains to compare URIs to.
*/
data class DomainsData(
val equivalentDomains: List<List<String>>,
val globalEquivalentDomains: List<GlobalEquivalentDomain>,
) {
/**
* Model for a group of domains that should be matched together.
*
* @property isExcluded If the global equivalent domain should be excluded.
* @property domains A list of domains that should all match a URI.
* @property type The domain type identifier.
*/
data class GlobalEquivalentDomain(
val isExcluded: Boolean,
val domains: List<String>,
val type: Int,
)
}

View File

@@ -0,0 +1,30 @@
package com.x8bit.bitwarden.data.vault.repository.util
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson.Domains
import com.x8bit.bitwarden.data.vault.repository.model.DomainsData
/**
* Map the API [Domains] model to the internal [DomainsData] model.
*/
fun Domains.toDomainsData(): DomainsData {
val globalEquivalentDomains = this
.globalEquivalentDomains
?.map { it.toInternalModel() }
.orEmpty()
return DomainsData(
equivalentDomains = this.equivalentDomains.orEmpty(),
globalEquivalentDomains = globalEquivalentDomains,
)
}
/**
* Map the API [Domains.GlobalEquivalentDomain] model to the internal
* [DomainsData.GlobalEquivalentDomain] model.
*/
private fun Domains.GlobalEquivalentDomain.toInternalModel(): DomainsData.GlobalEquivalentDomain =
DomainsData.GlobalEquivalentDomain(
domains = this.domains.orEmpty(),
isExcluded = this.isExcluded,
type = this.type,
)