[PM-31069] Add OrganizationId support for Vault Migration operations (#6397)

This commit is contained in:
aj-rosado
2026-01-23 16:05:55 +00:00
committed by GitHub
parent 2acf429f67
commit 0395d489c2
14 changed files with 129 additions and 9 deletions

View File

@@ -0,0 +1,70 @@
{
"formatVersion": 1,
"database": {
"version": 2,
"identityHash": "2835802f9de260f6f5109c81081e9b46",
"entities": [
{
"tableName": "organization_events",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `user_id` TEXT NOT NULL, `organization_event_type` TEXT NOT NULL, `cipher_id` TEXT, `date` INTEGER NOT NULL, `organization_id` TEXT)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "userId",
"columnName": "user_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "organizationEventType",
"columnName": "organization_event_type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "cipherId",
"columnName": "cipher_id",
"affinity": "TEXT"
},
{
"fieldPath": "date",
"columnName": "date",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "organizationId",
"columnName": "organization_id",
"affinity": "TEXT"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_organization_events_user_id",
"unique": false,
"columnNames": [
"user_id"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_organization_events_user_id` ON `${TABLE_NAME}` (`user_id`)"
}
]
}
],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '2835802f9de260f6f5109c81081e9b46')"
]
}
}

View File

@@ -30,6 +30,7 @@ class EventDiskSourceImpl(
}, },
cipherId = event.cipherId, cipherId = event.cipherId,
date = event.date, date = event.date,
organizationId = event.organizationId,
), ),
) )
} }
@@ -48,6 +49,7 @@ class EventDiskSourceImpl(
}, },
cipherId = it.cipherId, cipherId = it.cipherId,
date = it.date, date = it.date,
organizationId = it.organizationId,
) )
} }
} }

View File

@@ -1,5 +1,6 @@
package com.x8bit.bitwarden.data.platform.datasource.disk.database package com.x8bit.bitwarden.data.platform.datasource.disk.database
import androidx.room.AutoMigration
import androidx.room.Database import androidx.room.Database
import androidx.room.RoomDatabase import androidx.room.RoomDatabase
import androidx.room.TypeConverters import androidx.room.TypeConverters
@@ -14,8 +15,11 @@ import com.x8bit.bitwarden.data.vault.datasource.disk.convertor.ZonedDateTimeTyp
entities = [ entities = [
OrganizationEventEntity::class, OrganizationEventEntity::class,
], ],
version = 1, version = 2,
exportSchema = true, exportSchema = true,
autoMigrations = [
AutoMigration(from = 1, to = 2),
],
) )
@TypeConverters(ZonedDateTimeTypeConverter::class) @TypeConverters(ZonedDateTimeTypeConverter::class)
abstract class PlatformDatabase : RoomDatabase() { abstract class PlatformDatabase : RoomDatabase() {

View File

@@ -25,4 +25,7 @@ data class OrganizationEventEntity(
@ColumnInfo(name = "date") @ColumnInfo(name = "date")
val date: ZonedDateTime, val date: ZonedDateTime,
@ColumnInfo(name = "organization_id")
val organizationId: String?,
) )

View File

@@ -79,6 +79,7 @@ class OrganizationEventManagerImpl(
type = event.type, type = event.type,
cipherId = event.cipherId, cipherId = event.cipherId,
date = ZonedDateTime.now(clock), date = ZonedDateTime.now(clock),
organizationId = event.organizationId,
), ),
) )
} }

View File

@@ -16,11 +16,17 @@ sealed class OrganizationEvent {
*/ */
abstract val cipherId: String? abstract val cipherId: String?
/**
* The optional organization ID.
*/
abstract val organizationId: String?
/** /**
* Tracks when a value is successfully auto-filled * Tracks when a value is successfully auto-filled
*/ */
data class CipherClientAutoFilled( data class CipherClientAutoFilled(
override val cipherId: String, override val cipherId: String,
override val organizationId: String? = null,
) : OrganizationEvent() { ) : OrganizationEvent() {
override val type: OrganizationEventType override val type: OrganizationEventType
get() = OrganizationEventType.CIPHER_CLIENT_AUTO_FILLED get() = OrganizationEventType.CIPHER_CLIENT_AUTO_FILLED
@@ -31,6 +37,7 @@ sealed class OrganizationEvent {
*/ */
data class CipherClientCopiedCardCode( data class CipherClientCopiedCardCode(
override val cipherId: String, override val cipherId: String,
override val organizationId: String? = null,
) : OrganizationEvent() { ) : OrganizationEvent() {
override val type: OrganizationEventType override val type: OrganizationEventType
get() = OrganizationEventType.CIPHER_CLIENT_COPIED_CARD_CODE get() = OrganizationEventType.CIPHER_CLIENT_COPIED_CARD_CODE
@@ -41,6 +48,7 @@ sealed class OrganizationEvent {
*/ */
data class CipherClientCopiedHiddenField( data class CipherClientCopiedHiddenField(
override val cipherId: String, override val cipherId: String,
override val organizationId: String? = null,
) : OrganizationEvent() { ) : OrganizationEvent() {
override val type: OrganizationEventType override val type: OrganizationEventType
get() = OrganizationEventType.CIPHER_CLIENT_COPIED_HIDDEN_FIELD get() = OrganizationEventType.CIPHER_CLIENT_COPIED_HIDDEN_FIELD
@@ -51,6 +59,7 @@ sealed class OrganizationEvent {
*/ */
data class CipherClientCopiedPassword( data class CipherClientCopiedPassword(
override val cipherId: String, override val cipherId: String,
override val organizationId: String? = null,
) : OrganizationEvent() { ) : OrganizationEvent() {
override val type: OrganizationEventType override val type: OrganizationEventType
get() = OrganizationEventType.CIPHER_CLIENT_COPIED_PASSWORD get() = OrganizationEventType.CIPHER_CLIENT_COPIED_PASSWORD
@@ -61,6 +70,7 @@ sealed class OrganizationEvent {
*/ */
data class CipherClientToggledCardCodeVisible( data class CipherClientToggledCardCodeVisible(
override val cipherId: String, override val cipherId: String,
override val organizationId: String? = null,
) : OrganizationEvent() { ) : OrganizationEvent() {
override val type: OrganizationEventType override val type: OrganizationEventType
get() = OrganizationEventType.CIPHER_CLIENT_TOGGLED_CARD_CODE_VISIBLE get() = OrganizationEventType.CIPHER_CLIENT_TOGGLED_CARD_CODE_VISIBLE
@@ -71,6 +81,7 @@ sealed class OrganizationEvent {
*/ */
data class CipherClientToggledCardNumberVisible( data class CipherClientToggledCardNumberVisible(
override val cipherId: String, override val cipherId: String,
override val organizationId: String? = null,
) : OrganizationEvent() { ) : OrganizationEvent() {
override val type: OrganizationEventType override val type: OrganizationEventType
get() = OrganizationEventType.CIPHER_CLIENT_TOGGLED_CARD_NUMBER_VISIBLE get() = OrganizationEventType.CIPHER_CLIENT_TOGGLED_CARD_NUMBER_VISIBLE
@@ -81,6 +92,7 @@ sealed class OrganizationEvent {
*/ */
data class CipherClientToggledHiddenFieldVisible( data class CipherClientToggledHiddenFieldVisible(
override val cipherId: String, override val cipherId: String,
override val organizationId: String? = null,
) : OrganizationEvent() { ) : OrganizationEvent() {
override val type: OrganizationEventType override val type: OrganizationEventType
get() = OrganizationEventType.CIPHER_CLIENT_TOGGLED_HIDDEN_FIELD_VISIBLE get() = OrganizationEventType.CIPHER_CLIENT_TOGGLED_HIDDEN_FIELD_VISIBLE
@@ -91,6 +103,7 @@ sealed class OrganizationEvent {
*/ */
data class CipherClientToggledPasswordVisible( data class CipherClientToggledPasswordVisible(
override val cipherId: String, override val cipherId: String,
override val organizationId: String? = null,
) : OrganizationEvent() { ) : OrganizationEvent() {
override val type: OrganizationEventType override val type: OrganizationEventType
get() = OrganizationEventType.CIPHER_CLIENT_TOGGLED_PASSWORD_VISIBLE get() = OrganizationEventType.CIPHER_CLIENT_TOGGLED_PASSWORD_VISIBLE
@@ -101,6 +114,7 @@ sealed class OrganizationEvent {
*/ */
data class CipherClientViewed( data class CipherClientViewed(
override val cipherId: String, override val cipherId: String,
override val organizationId: String? = null,
) : OrganizationEvent() { ) : OrganizationEvent() {
override val type: OrganizationEventType override val type: OrganizationEventType
get() = OrganizationEventType.CIPHER_CLIENT_VIEWED get() = OrganizationEventType.CIPHER_CLIENT_VIEWED
@@ -111,6 +125,7 @@ sealed class OrganizationEvent {
*/ */
data object UserClientExportedVault : OrganizationEvent() { data object UserClientExportedVault : OrganizationEvent() {
override val cipherId: String? = null override val cipherId: String? = null
override val organizationId: String? = null
override val type: OrganizationEventType override val type: OrganizationEventType
get() = OrganizationEventType.USER_CLIENT_EXPORTED_VAULT get() = OrganizationEventType.USER_CLIENT_EXPORTED_VAULT
} }
@@ -119,8 +134,10 @@ sealed class OrganizationEvent {
* Tracks when a user's personal ciphers have been migrated to their organization's My Items * Tracks when a user's personal ciphers have been migrated to their organization's My Items
* folder as required by the organization's personal vault ownership policy. * folder as required by the organization's personal vault ownership policy.
*/ */
data object ItemOrganizationAccepted : OrganizationEvent() { data class ItemOrganizationAccepted(
override val cipherId: String? = null override val cipherId: String? = null,
override val organizationId: String,
) : OrganizationEvent() {
override val type: OrganizationEventType override val type: OrganizationEventType
get() = OrganizationEventType.ORGANIZATION_ITEM_ORGANIZATION_ACCEPTED get() = OrganizationEventType.ORGANIZATION_ITEM_ORGANIZATION_ACCEPTED
} }
@@ -129,8 +146,10 @@ sealed class OrganizationEvent {
* Tracks when a user chooses to leave an organization instead of migrating their personal * Tracks when a user chooses to leave an organization instead of migrating their personal
* ciphers to their organization's My Items folder. * ciphers to their organization's My Items folder.
*/ */
data object ItemOrganizationDeclined : OrganizationEvent() { data class ItemOrganizationDeclined(
override val cipherId: String? = null override val cipherId: String? = null,
override val organizationId: String,
) : OrganizationEvent() {
override val type: OrganizationEventType override val type: OrganizationEventType
get() = OrganizationEventType.ORGANIZATION_ITEM_ORGANIZATION_DECLINED get() = OrganizationEventType.ORGANIZATION_ITEM_ORGANIZATION_DECLINED
} }

View File

@@ -106,7 +106,9 @@ class LeaveOrganizationViewModel @Inject constructor(
), ),
) )
organizationEventManager.trackEvent( organizationEventManager.trackEvent(
event = OrganizationEvent.ItemOrganizationDeclined, event = OrganizationEvent.ItemOrganizationDeclined(
organizationId = state.organizationId,
),
) )
mutableStateFlow.update { mutableStateFlow.update {
it.copy(dialogState = null) it.copy(dialogState = null)

View File

@@ -139,7 +139,9 @@ class MigrateToMyItemsViewModel @Inject constructor(
when (val result = action.result) { when (val result = action.result) {
is MigratePersonalVaultResult.Success -> { is MigratePersonalVaultResult.Success -> {
organizationEventManager.trackEvent( organizationEventManager.trackEvent(
event = OrganizationEvent.ItemOrganizationAccepted, event = OrganizationEvent.ItemOrganizationAccepted(
organizationId = state.organizationId,
),
) )
clearDialog() clearDialog()
sendEvent(MigrateToMyItemsEvent.NavigateToVault) sendEvent(MigrateToMyItemsEvent.NavigateToVault)

View File

@@ -39,6 +39,7 @@ class EventDiskSourceTest {
type = OrganizationEventType.CIPHER_DELETED, type = OrganizationEventType.CIPHER_DELETED,
cipherId = "cipherId-1", cipherId = "cipherId-1",
date = ZonedDateTime.now(fixedClock), date = ZonedDateTime.now(fixedClock),
organizationId = null,
) )
eventDiskSource.addOrganizationEvent( eventDiskSource.addOrganizationEvent(
@@ -54,6 +55,7 @@ class EventDiskSourceTest {
organizationEventType = "1102", organizationEventType = "1102",
cipherId = "cipherId-1", cipherId = "cipherId-1",
date = ZonedDateTime.now(fixedClock), date = ZonedDateTime.now(fixedClock),
organizationId = null,
), ),
), ),
fakeOrganizationEventDao.storedEvents, fakeOrganizationEventDao.storedEvents,
@@ -73,6 +75,7 @@ class EventDiskSourceTest {
organizationEventType = "1102", organizationEventType = "1102",
cipherId = "cipherId-1", cipherId = "cipherId-1",
date = ZonedDateTime.now(fixedClock), date = ZonedDateTime.now(fixedClock),
organizationId = null,
), ),
OrganizationEventEntity( OrganizationEventEntity(
id = 2, id = 2,
@@ -80,6 +83,7 @@ class EventDiskSourceTest {
organizationEventType = "1102", organizationEventType = "1102",
cipherId = "cipherId-2", cipherId = "cipherId-2",
date = ZonedDateTime.now(fixedClock), date = ZonedDateTime.now(fixedClock),
organizationId = null,
), ),
), ),
) )
@@ -94,6 +98,7 @@ class EventDiskSourceTest {
organizationEventType = "1102", organizationEventType = "1102",
cipherId = "cipherId-2", cipherId = "cipherId-2",
date = ZonedDateTime.now(fixedClock), date = ZonedDateTime.now(fixedClock),
organizationId = null,
), ),
), ),
fakeOrganizationEventDao.storedEvents, fakeOrganizationEventDao.storedEvents,
@@ -113,6 +118,7 @@ class EventDiskSourceTest {
organizationEventType = "1102", organizationEventType = "1102",
cipherId = "cipherId-1", cipherId = "cipherId-1",
date = ZonedDateTime.now(fixedClock), date = ZonedDateTime.now(fixedClock),
organizationId = null,
), ),
OrganizationEventEntity( OrganizationEventEntity(
id = 2, id = 2,
@@ -120,6 +126,7 @@ class EventDiskSourceTest {
organizationEventType = "1102", organizationEventType = "1102",
cipherId = "cipherId-2", cipherId = "cipherId-2",
date = ZonedDateTime.now(fixedClock), date = ZonedDateTime.now(fixedClock),
organizationId = null,
), ),
), ),
) )
@@ -132,6 +139,7 @@ class EventDiskSourceTest {
type = OrganizationEventType.CIPHER_DELETED, type = OrganizationEventType.CIPHER_DELETED,
cipherId = "cipherId-1", cipherId = "cipherId-1",
date = ZonedDateTime.now(fixedClock), date = ZonedDateTime.now(fixedClock),
organizationId = null,
), ),
), ),
result, result,

View File

@@ -74,6 +74,7 @@ class OrganizationEventManagerTest {
type = OrganizationEventType.CIPHER_UPDATED, type = OrganizationEventType.CIPHER_UPDATED,
cipherId = CIPHER_ID, cipherId = CIPHER_ID,
date = ZonedDateTime.now(fixedClock), date = ZonedDateTime.now(fixedClock),
organizationId = null,
) )
val events = listOf(organizationEvent) val events = listOf(organizationEvent)
coEvery { eventDiskSource.getOrganizationEvents(userId = USER_ID) } returns events coEvery { eventDiskSource.getOrganizationEvents(userId = USER_ID) } returns events
@@ -105,6 +106,7 @@ class OrganizationEventManagerTest {
type = OrganizationEventType.CIPHER_UPDATED, type = OrganizationEventType.CIPHER_UPDATED,
cipherId = CIPHER_ID, cipherId = CIPHER_ID,
date = ZonedDateTime.now(fixedClock), date = ZonedDateTime.now(fixedClock),
organizationId = null,
) )
val events = listOf(organizationEvent) val events = listOf(organizationEvent)
coEvery { eventDiskSource.getOrganizationEvents(userId = USER_ID) } returns events coEvery { eventDiskSource.getOrganizationEvents(userId = USER_ID) } returns events
@@ -209,6 +211,7 @@ class OrganizationEventManagerTest {
type = OrganizationEventType.CIPHER_CLIENT_AUTO_FILLED, type = OrganizationEventType.CIPHER_CLIENT_AUTO_FILLED,
cipherId = CIPHER_ID, cipherId = CIPHER_ID,
date = ZonedDateTime.now(fixedClock), date = ZonedDateTime.now(fixedClock),
organizationId = null,
), ),
) )
} }

View File

@@ -138,7 +138,9 @@ class LeaveOrganizationViewModelTest : BaseViewModelTest() {
), ),
) )
mockOrganizationEventManager.trackEvent( mockOrganizationEventManager.trackEvent(
event = OrganizationEvent.ItemOrganizationDeclined, event = OrganizationEvent.ItemOrganizationDeclined(
organizationId = ORGANIZATION_ID,
),
) )
mockVaultMigrationManager.clearMigrationState() mockVaultMigrationManager.clearMigrationState()
} }

View File

@@ -158,7 +158,9 @@ class MigrateToMyItemsViewModelTest : BaseViewModelTest() {
verify { verify {
mockOrganizationEventManager.trackEvent( mockOrganizationEventManager.trackEvent(
event = OrganizationEvent.ItemOrganizationAccepted, event = OrganizationEvent.ItemOrganizationAccepted(
organizationId = ORGANIZATION_ID,
),
) )
} }
} }

View File

@@ -13,4 +13,5 @@ data class OrganizationEventJson(
@SerialName("type") val type: OrganizationEventType, @SerialName("type") val type: OrganizationEventType,
@SerialName("cipherId") val cipherId: String?, @SerialName("cipherId") val cipherId: String?,
@SerialName("date") @Contextual val date: ZonedDateTime, @SerialName("date") @Contextual val date: ZonedDateTime,
@SerialName("organizationId") val organizationId: String?,
) )

View File

@@ -35,6 +35,7 @@ class EventServiceTest : BaseServiceTest() {
type = OrganizationEventType.CIPHER_CREATED, type = OrganizationEventType.CIPHER_CREATED,
cipherId = "cipher-id", cipherId = "cipher-id",
date = ZonedDateTime.now(fixedClock), date = ZonedDateTime.now(fixedClock),
organizationId = null,
), ),
), ),
) )