From 89a0eb2068d4e97fe3132804a2d8f097c2a0d532 Mon Sep 17 00:00:00 2001 From: jaasen-livefront Date: Thu, 13 Nov 2025 18:30:30 -0800 Subject: [PATCH] create ArchiveCipher table --- src/Core/Vault/Entities/Cipher.cs | 6 ++ src/Core/Vault/Entities/CipherArchive.cs | 12 ++++ .../Repositories/DatabaseContext.cs | 2 + .../CipherArchiveConfigurations.cs | 23 ++++++++ .../Vault/Models/CipherArchive.cs | 13 +++++ src/Sql/dbo/Vault/Tables/CipherArchive.sql | 55 ++++++++++++++++++ .../2025-11-13_00_AddCipherArchive.sql | 58 +++++++++++++++++++ 7 files changed, 169 insertions(+) create mode 100644 src/Core/Vault/Entities/CipherArchive.cs create mode 100644 src/Infrastructure.EntityFramework/Vault/Configurations/CipherArchiveConfigurations.cs create mode 100644 src/Infrastructure.EntityFramework/Vault/Models/CipherArchive.cs create mode 100644 src/Sql/dbo/Vault/Tables/CipherArchive.sql create mode 100644 util/Migrator/DbScripts/2025-11-13_00_AddCipherArchive.sql diff --git a/src/Core/Vault/Entities/Cipher.cs b/src/Core/Vault/Entities/Cipher.cs index f6afc090bb..d585c93288 100644 --- a/src/Core/Vault/Entities/Cipher.cs +++ b/src/Core/Vault/Entities/Cipher.cs @@ -25,6 +25,12 @@ public class Cipher : ITableObject, ICloneable public DateTime? DeletedDate { get; set; } public Enums.CipherRepromptType? Reprompt { get; set; } public string Key { get; set; } + + /// + /// Deprecated as of Nov 2025. + /// Source of truth is now CipherArchive (CipherId + UserId). + /// Kept for backward compatibility during phased migration. + /// public DateTime? ArchivedDate { get; set; } public void SetNewId() diff --git a/src/Core/Vault/Entities/CipherArchive.cs b/src/Core/Vault/Entities/CipherArchive.cs new file mode 100644 index 0000000000..e3cd339824 --- /dev/null +++ b/src/Core/Vault/Entities/CipherArchive.cs @@ -0,0 +1,12 @@ +#nullable disable + +namespace Bit.Core.Vault.Entities; + +public class CipherArchive +{ + public Guid CipherId { get; set; } + public Guid UserId { get; set; } + public DateTime ArchivedDate { get; set; } + + public Cipher Cipher { get; set; } +} diff --git a/src/Infrastructure.EntityFramework/Repositories/DatabaseContext.cs b/src/Infrastructure.EntityFramework/Repositories/DatabaseContext.cs index b748a26db2..e9bd3af278 100644 --- a/src/Infrastructure.EntityFramework/Repositories/DatabaseContext.cs +++ b/src/Infrastructure.EntityFramework/Repositories/DatabaseContext.cs @@ -41,6 +41,7 @@ public class DatabaseContext : DbContext public DbSet ApiKeys { get; set; } public DbSet Cache { get; set; } public DbSet Ciphers { get; set; } + public DbSet CipherArchives { get; set; } public DbSet Collections { get; set; } public DbSet CollectionCiphers { get; set; } public DbSet CollectionGroups { get; set; } @@ -162,6 +163,7 @@ public class DatabaseContext : DbContext } eCipher.ToTable(nameof(Cipher)); + eCipherArchive.ToTable(nameof(CipherArchive)); eCollection.ToTable(nameof(Collection)); eCollectionCipher.ToTable(nameof(CollectionCipher)); eEmergencyAccess.ToTable(nameof(EmergencyAccess)); diff --git a/src/Infrastructure.EntityFramework/Vault/Configurations/CipherArchiveConfigurations.cs b/src/Infrastructure.EntityFramework/Vault/Configurations/CipherArchiveConfigurations.cs new file mode 100644 index 0000000000..50db798353 --- /dev/null +++ b/src/Infrastructure.EntityFramework/Vault/Configurations/CipherArchiveConfigurations.cs @@ -0,0 +1,23 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Bit.Infrastructure.EntityFramework.Vault.Configurations; + +public class CipherArchiveConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable(nameof(CipherArchive)); + + builder.HasKey(ca => new { ca.CipherId, ca.UserId }); + + builder + .HasOne(ca => ca.Cipher) + .WithMany() + .HasForeignKey(ca => ca.CipherId) + .OnDelete(DeleteBehavior.Cascade); + + // If you want explicit mapping: + builder.Property(ca => ca.ArchivedDate).IsRequired(); + } +} diff --git a/src/Infrastructure.EntityFramework/Vault/Models/CipherArchive.cs b/src/Infrastructure.EntityFramework/Vault/Models/CipherArchive.cs new file mode 100644 index 0000000000..1b3baf0d3d --- /dev/null +++ b/src/Infrastructure.EntityFramework/Vault/Models/CipherArchive.cs @@ -0,0 +1,13 @@ +#nullable disable + +using Bit.Infrastructure.EntityFramework.Vault.Models; + +public class CipherArchive +{ + public Guid CipherId { get; set; } + public Guid UserId { get; set; } + public DateTime ArchivedDate { get; set; } + + // Optional navigation props – you can drop these if you don't want them. + public Cipher Cipher { get; set; } +} diff --git a/src/Sql/dbo/Vault/Tables/CipherArchive.sql b/src/Sql/dbo/Vault/Tables/CipherArchive.sql new file mode 100644 index 0000000000..d4a50f3520 --- /dev/null +++ b/src/Sql/dbo/Vault/Tables/CipherArchive.sql @@ -0,0 +1,55 @@ +IF OBJECT_ID(N'[dbo].[CipherArchive]', N'U') IS NULL +BEGIN + CREATE TABLE [dbo].[CipherArchive] + ( + [CipherId] UNIQUEIDENTIFIER NOT NULL, + [UserId] UNIQUEIDENTIFIER NOT NULL, + [ArchivedDate] DATETIME2(7) NOT NULL, + + CONSTRAINT [PK_CipherArchive] + PRIMARY KEY CLUSTERED ([CipherId], [UserId]) + ); +END; +GO + +IF NOT EXISTS ( + SELECT 1 + FROM sys.foreign_keys + WHERE name = N'FK_CipherArchive_Cipher' + AND parent_object_id = OBJECT_ID(N'[dbo].[CipherArchive]', N'U') +) +BEGIN + ALTER TABLE [dbo].[CipherArchive] + ADD CONSTRAINT [FK_CipherArchive_Cipher] + FOREIGN KEY ([CipherId]) + REFERENCES [dbo].[Cipher]([Id]) + ON DELETE CASCADE; +END; +GO + +IF NOT EXISTS ( + SELECT 1 + FROM sys.foreign_keys + WHERE name = N'FK_CipherArchive_User' + AND parent_object_id = OBJECT_ID(N'[dbo].[CipherArchive]', N'U') +) +BEGIN + ALTER TABLE [dbo].[CipherArchive] + ADD CONSTRAINT [FK_CipherArchive_User] + FOREIGN KEY ([UserId]) + REFERENCES [dbo].[User]([Id]) + ON DELETE CASCADE; +END; +GO + +IF NOT EXISTS ( + SELECT 1 + FROM sys.indexes + WHERE name = N'IX_CipherArchive_UserId' + AND object_id = OBJECT_ID(N'[dbo].[CipherArchive]', N'U') +) +BEGIN + CREATE NONCLUSTERED INDEX [IX_CipherArchive_UserId] + ON [dbo].[CipherArchive]([UserId]); +END; +GO diff --git a/util/Migrator/DbScripts/2025-11-13_00_AddCipherArchive.sql b/util/Migrator/DbScripts/2025-11-13_00_AddCipherArchive.sql new file mode 100644 index 0000000000..e2001f3ef0 --- /dev/null +++ b/util/Migrator/DbScripts/2025-11-13_00_AddCipherArchive.sql @@ -0,0 +1,58 @@ +IF OBJECT_ID(N'[dbo].[CipherArchive]', N'U') IS NULL +BEGIN + CREATE TABLE [dbo].[CipherArchive] + ( + [CipherId] UNIQUEIDENTIFIER NOT NULL, + [UserId] UNIQUEIDENTIFIER NOT NULL, + [ArchivedDate] DATETIME2(7) NOT NULL, + + CONSTRAINT [PK_CipherArchive] + PRIMARY KEY CLUSTERED ([CipherId], [UserId]) + ); +END; +GO + +-- FK → Cipher +IF NOT EXISTS ( + SELECT 1 + FROM sys.foreign_keys + WHERE name = N'FK_CipherArchive_Cipher' + AND parent_object_id = OBJECT_ID(N'[dbo].[CipherArchive]', N'U') +) +BEGIN + ALTER TABLE [dbo].[CipherArchive] + ADD CONSTRAINT [FK_CipherArchive_Cipher] + FOREIGN KEY ([CipherId]) + REFERENCES [dbo].[Cipher]([Id]) + ON DELETE CASCADE; +END; +GO + +-- FK → User +IF NOT EXISTS ( + SELECT 1 + FROM sys.foreign_keys + WHERE name = N'FK_CipherArchive_User' + AND parent_object_id = OBJECT_ID(N'[dbo].[CipherArchive]', N'U') +) +BEGIN + ALTER TABLE [dbo].[CipherArchive] + ADD CONSTRAINT [FK_CipherArchive_User] + FOREIGN KEY ([UserId]) + REFERENCES [dbo].[User]([Id]) + ON DELETE CASCADE; +END; +GO + +-- Optional index for queries by user +IF NOT EXISTS ( + SELECT 1 + FROM sys.indexes + WHERE name = N'IX_CipherArchive_UserId' + AND object_id = OBJECT_ID(N'[dbo].[CipherArchive]', N'U') +) +BEGIN + CREATE NONCLUSTERED INDEX [IX_CipherArchive_UserId] + ON [dbo].[CipherArchive]([UserId]); +END; +GO