[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
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
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,
date = event.date,
organizationId = event.organizationId,
),
)
}
@ -48,6 +49,7 @@ class EventDiskSourceImpl(
},
cipherId = it.cipherId,
date = it.date,
organizationId = it.organizationId,
)
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -158,7 +158,9 @@ class MigrateToMyItemsViewModelTest : BaseViewModelTest() {
verify {
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("cipherId") val cipherId: String?,
@SerialName("date") @Contextual val date: ZonedDateTime,
@SerialName("organizationId") val organizationId: String?,
)

View File

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