From fccc71a3b39349985c998dfdbc8454b55c33fea8 Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Mon, 9 Mar 2026 12:38:53 -0700 Subject: [PATCH 1/9] User Cipher scene For now only supports one login cipher --- util/Seeder/Scenes/UserCipherScene.cs | 50 +++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 util/Seeder/Scenes/UserCipherScene.cs diff --git a/util/Seeder/Scenes/UserCipherScene.cs b/util/Seeder/Scenes/UserCipherScene.cs new file mode 100644 index 000000000000..430e617264ef --- /dev/null +++ b/util/Seeder/Scenes/UserCipherScene.cs @@ -0,0 +1,50 @@ +using System.ComponentModel.DataAnnotations; +using Bit.Core.Repositories; +using Bit.Core.Vault.Repositories; +using Bit.Seeder.Factories; +using Bit.Seeder.Services; + +namespace Bit.Seeder.Scenes; + +public class UserCipherScene(IUserRepository userRepository, ICipherRepository cipherRepository, IManglerService manglerService) : IScene +{ + public class Request + { + [Required] + public required Guid UserId { get; set; } + [Required] + public required string UserKeyB64 { get; set; } + [Required] + public required string Name { get; set; } + public string? Username { get; set; } + public string? Password { get; set; } + public string? Uri { get; set; } + public string? Notes { get; set; } + public IEnumerable<(string name, string value, int type)>? Fields { get; set; } + } + + public class Result + { + public required Guid CipherId { get; set; } + } + + public async Task> SeedAsync(Request request) + { + var user = await userRepository.GetByIdAsync(request.UserId); + if (user == null) + { + throw new Exception($"User with ID {request.UserId} not found."); + } + + var cipher = LoginCipherSeeder.Create(request.UserKeyB64, request.Name, userId: request.UserId, username: request.Username, password: request.Password, uri: request.Uri, notes: request.Notes, fields: request.Fields); + + await cipherRepository.CreateAsync(cipher); + + return new SceneResult( + result: new Result + { + CipherId = cipher.Id + }, + mangleMap: manglerService.GetMangleMap()); + } +} From a7d282fa58d44173e7d3f0ee6b36cf6d472f213a Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Mon, 16 Mar 2026 16:00:26 -0700 Subject: [PATCH 2/9] Fixup batch delete, which fails due to db collisions --- util/SeederApi/Commands/DestroyBatchScenesCommand.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/util/SeederApi/Commands/DestroyBatchScenesCommand.cs b/util/SeederApi/Commands/DestroyBatchScenesCommand.cs index 50f6142a988d..9fcbb624009a 100644 --- a/util/SeederApi/Commands/DestroyBatchScenesCommand.cs +++ b/util/SeederApi/Commands/DestroyBatchScenesCommand.cs @@ -10,7 +10,7 @@ public async Task DestroyAsync(IEnumerable playIds) { var exceptions = new List(); - var deleteTasks = playIds.Select(async playId => + foreach (var playId in playIds) { try { @@ -24,9 +24,7 @@ public async Task DestroyAsync(IEnumerable playIds) } logger.LogError(ex, "Error deleting seeded data: {PlayId}", playId); } - }); - - await Task.WhenAll(deleteTasks); + } if (exceptions.Count > 0) { From 9c1a957b7204366297f4f6d52f478f85fd7b84e2 Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Tue, 17 Mar 2026 16:19:01 -0700 Subject: [PATCH 3/9] Create cipher scenes for each cipher type --- util/Seeder/Scenes/UserCardCipherScene.cs | 60 +++++++++++++++++++ util/Seeder/Scenes/UserIdentityCipherScene.cs | 58 ++++++++++++++++++ ...CipherScene.cs => UserLoginCipherScene.cs} | 2 +- .../Scenes/UserSecureNoteCipherScene.cs | 46 ++++++++++++++ util/Seeder/Scenes/UserSshKeyCipherScene.cs | 55 +++++++++++++++++ 5 files changed, 220 insertions(+), 1 deletion(-) create mode 100644 util/Seeder/Scenes/UserCardCipherScene.cs create mode 100644 util/Seeder/Scenes/UserIdentityCipherScene.cs rename util/Seeder/Scenes/{UserCipherScene.cs => UserLoginCipherScene.cs} (88%) create mode 100644 util/Seeder/Scenes/UserSecureNoteCipherScene.cs create mode 100644 util/Seeder/Scenes/UserSshKeyCipherScene.cs diff --git a/util/Seeder/Scenes/UserCardCipherScene.cs b/util/Seeder/Scenes/UserCardCipherScene.cs new file mode 100644 index 000000000000..93a0e0e26b10 --- /dev/null +++ b/util/Seeder/Scenes/UserCardCipherScene.cs @@ -0,0 +1,60 @@ +using System.ComponentModel.DataAnnotations; +using Bit.Core.Repositories; +using Bit.Core.Vault.Repositories; +using Bit.Seeder.Factories; +using Bit.Seeder.Models; +using Bit.Seeder.Services; + +namespace Bit.Seeder.Scenes; + +public class UserCardCipherScene(IUserRepository userRepository, ICipherRepository cipherRepository, IManglerService manglerService) : IScene +{ + public class Request + { + [Required] + public required Guid UserId { get; set; } + [Required] + public required string UserKeyB64 { get; set; } + [Required] + public required string Name { get; set; } + public required string CardholderName { get; set; } + public required string Number { get; set; } + public required string ExpMonth { get; set; } + public required string ExpYear { get; set; } + public required string Code { get; set; } + public string? Notes { get; set; } + } + + public class Result + { + public required Guid CipherId { get; set; } + } + + public async Task> SeedAsync(Request request) + { + var user = await userRepository.GetByIdAsync(request.UserId); + if (user == null) + { + throw new Exception($"User with ID {request.UserId} not found."); + } + + var card = new CardViewDto + { + CardholderName = request.CardholderName, + Number = request.Number, + ExpMonth = request.ExpMonth, + ExpYear = request.ExpYear, + Code = request.Code + }; + var cipher = CardCipherSeeder.Create(request.UserKeyB64, request.Name, card: card, userId: request.UserId, notes: request.Notes); + + await cipherRepository.CreateAsync(cipher); + + return new SceneResult( + result: new Result + { + CipherId = cipher.Id + }, + mangleMap: manglerService.GetMangleMap()); + } +} diff --git a/util/Seeder/Scenes/UserIdentityCipherScene.cs b/util/Seeder/Scenes/UserIdentityCipherScene.cs new file mode 100644 index 000000000000..7adf9662d28a --- /dev/null +++ b/util/Seeder/Scenes/UserIdentityCipherScene.cs @@ -0,0 +1,58 @@ +using System.ComponentModel.DataAnnotations; +using Bit.Core.Repositories; +using Bit.Core.Vault.Repositories; +using Bit.Seeder.Factories; +using Bit.Seeder.Models; +using Bit.Seeder.Services; + +namespace Bit.Seeder.Scenes; + +public class UserIdentityCipherScene(IUserRepository userRepository, ICipherRepository cipherRepository, IManglerService manglerService) : IScene +{ + public class Request + { + [Required] + public required Guid UserId { get; set; } + [Required] + public required string UserKeyB64 { get; set; } + [Required] + public required string Name { get; set; } + public string? Title { get; set; } + public string? FirstName { get; set; } + public string? MiddleName { get; set; } + public string? LastName { get; set; } + public string? Notes { get; set; } + } + + public class Result + { + public required Guid CipherId { get; set; } + } + + public async Task> SeedAsync(Request request) + { + var user = await userRepository.GetByIdAsync(request.UserId); + if (user == null) + { + throw new Exception($"User with ID {request.UserId} not found."); + } + + var identity = new IdentityViewDto + { + Title = request.Title, + FirstName = request.FirstName, + MiddleName = request.MiddleName, + LastName = request.LastName + }; + var cipher = IdentityCipherSeeder.Create(request.UserKeyB64, request.Name, userId: request.UserId, identity: identity, notes: request.Notes); + + await cipherRepository.CreateAsync(cipher); + + return new SceneResult( + result: new Result + { + CipherId = cipher.Id + }, + mangleMap: manglerService.GetMangleMap()); + } +} diff --git a/util/Seeder/Scenes/UserCipherScene.cs b/util/Seeder/Scenes/UserLoginCipherScene.cs similarity index 88% rename from util/Seeder/Scenes/UserCipherScene.cs rename to util/Seeder/Scenes/UserLoginCipherScene.cs index 430e617264ef..8588a10e108a 100644 --- a/util/Seeder/Scenes/UserCipherScene.cs +++ b/util/Seeder/Scenes/UserLoginCipherScene.cs @@ -6,7 +6,7 @@ namespace Bit.Seeder.Scenes; -public class UserCipherScene(IUserRepository userRepository, ICipherRepository cipherRepository, IManglerService manglerService) : IScene +public class UserLoginCipherScene(IUserRepository userRepository, ICipherRepository cipherRepository, IManglerService manglerService) : IScene { public class Request { diff --git a/util/Seeder/Scenes/UserSecureNoteCipherScene.cs b/util/Seeder/Scenes/UserSecureNoteCipherScene.cs new file mode 100644 index 000000000000..ec99ab9d680c --- /dev/null +++ b/util/Seeder/Scenes/UserSecureNoteCipherScene.cs @@ -0,0 +1,46 @@ +using System.ComponentModel.DataAnnotations; +using Bit.Core.Repositories; +using Bit.Core.Vault.Repositories; +using Bit.Seeder.Factories; +using Bit.Seeder.Services; + +namespace Bit.Seeder.Scenes; + +public class UserSecureNoteCipherScene(IUserRepository userRepository, ICipherRepository cipherRepository, IManglerService manglerService) : IScene +{ + public class Request + { + [Required] + public required Guid UserId { get; set; } + [Required] + public required string UserKeyB64 { get; set; } + [Required] + public required string Name { get; set; } + public string? Notes { get; set; } + } + + public class Result + { + public required Guid CipherId { get; set; } + } + + public async Task> SeedAsync(Request request) + { + var user = await userRepository.GetByIdAsync(request.UserId); + if (user == null) + { + throw new Exception($"User with ID {request.UserId} not found."); + } + + var cipher = SecureNoteCipherSeeder.Create(request.UserKeyB64, request.Name, userId: request.UserId, notes: request.Notes); + + await cipherRepository.CreateAsync(cipher); + + return new SceneResult( + result: new Result + { + CipherId = cipher.Id + }, + mangleMap: manglerService.GetMangleMap()); + } +} diff --git a/util/Seeder/Scenes/UserSshKeyCipherScene.cs b/util/Seeder/Scenes/UserSshKeyCipherScene.cs new file mode 100644 index 000000000000..7a90c20346ed --- /dev/null +++ b/util/Seeder/Scenes/UserSshKeyCipherScene.cs @@ -0,0 +1,55 @@ +using System.ComponentModel.DataAnnotations; +using Bit.Core.Repositories; +using Bit.Core.Vault.Repositories; +using Bit.Seeder.Factories; +using Bit.Seeder.Models; +using Bit.Seeder.Services; + +namespace Bit.Seeder.Scenes; + +public class UserSshKeyCipherScene(IUserRepository userRepository, ICipherRepository cipherRepository, IManglerService manglerService) : IScene +{ + public class Request + { + [Required] + public required Guid UserId { get; set; } + [Required] + public required string UserKeyB64 { get; set; } + [Required] + public required string Name { get; set; } + public string? PrivateKey { get; set; } + public string? PublicKey { get; set; } + public string? Fingerprint { get; set; } + } + + public class Result + { + public required Guid CipherId { get; set; } + } + + public async Task> SeedAsync(Request request) + { + var user = await userRepository.GetByIdAsync(request.UserId); + if (user == null) + { + throw new Exception($"User with ID {request.UserId} not found."); + } + + var sshKey = new SshKeyViewDto + { + PrivateKey = request.PrivateKey, + PublicKey = request.PublicKey, + Fingerprint = request.Fingerprint + }; + var cipher = SshKeyCipherSeeder.Create(request.UserKeyB64, request.Name, userId: request.UserId, sshKey: sshKey); + + await cipherRepository.CreateAsync(cipher); + + return new SceneResult( + result: new Result + { + CipherId = cipher.Id + }, + mangleMap: manglerService.GetMangleMap()); + } +} From 56c74288fb34d8b31861046fb110e543aaff345a Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Wed, 18 Mar 2026 07:35:50 -0700 Subject: [PATCH 4/9] Remove unnecessary mutex locking --- util/SeederApi/Commands/DestroyBatchScenesCommand.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/util/SeederApi/Commands/DestroyBatchScenesCommand.cs b/util/SeederApi/Commands/DestroyBatchScenesCommand.cs index 9fcbb624009a..aeb7e05aca16 100644 --- a/util/SeederApi/Commands/DestroyBatchScenesCommand.cs +++ b/util/SeederApi/Commands/DestroyBatchScenesCommand.cs @@ -18,10 +18,7 @@ public async Task DestroyAsync(IEnumerable playIds) } catch (Exception ex) { - lock (exceptions) - { - exceptions.Add(ex); - } + exceptions.Add(ex); logger.LogError(ex, "Error deleting seeded data: {PlayId}", playId); } } From 06415933977865514b284af997a42b1dbc4717d3 Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Wed, 18 Mar 2026 07:41:43 -0700 Subject: [PATCH 5/9] Include notes in ssh key ciphers --- util/Seeder/Scenes/UserSshKeyCipherScene.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/util/Seeder/Scenes/UserSshKeyCipherScene.cs b/util/Seeder/Scenes/UserSshKeyCipherScene.cs index 7a90c20346ed..78b0758207c0 100644 --- a/util/Seeder/Scenes/UserSshKeyCipherScene.cs +++ b/util/Seeder/Scenes/UserSshKeyCipherScene.cs @@ -20,6 +20,7 @@ public class Request public string? PrivateKey { get; set; } public string? PublicKey { get; set; } public string? Fingerprint { get; set; } + public string? Notes { get; set; } } public class Result @@ -41,7 +42,7 @@ public async Task> SeedAsync(Request request) PublicKey = request.PublicKey, Fingerprint = request.Fingerprint }; - var cipher = SshKeyCipherSeeder.Create(request.UserKeyB64, request.Name, userId: request.UserId, sshKey: sshKey); + var cipher = SshKeyCipherSeeder.Create(request.UserKeyB64, request.Name, userId: request.UserId, sshKey: sshKey, notes: request.Notes); await cipherRepository.CreateAsync(cipher); From 234b79cc3971bc82b24c2a0c80e1f9fdb7670212 Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Wed, 18 Mar 2026 10:55:22 -0700 Subject: [PATCH 6/9] Add reprompt to ssh keys --- util/Seeder/Factories/SshKeyCipherSeeder.cs | 6 ++++-- util/Seeder/Scenes/UserSshKeyCipherScene.cs | 3 ++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/util/Seeder/Factories/SshKeyCipherSeeder.cs b/util/Seeder/Factories/SshKeyCipherSeeder.cs index fa70a8bcbc2f..66d4f6d38843 100644 --- a/util/Seeder/Factories/SshKeyCipherSeeder.cs +++ b/util/Seeder/Factories/SshKeyCipherSeeder.cs @@ -12,7 +12,8 @@ internal static Cipher Create( SshKeyViewDto sshKey, Guid? organizationId = null, Guid? userId = null, - string? notes = null) + string? notes = null, + bool reprompt = false) { var cipherView = new CipherViewDto { @@ -20,7 +21,8 @@ internal static Cipher Create( Name = name, Notes = notes, Type = CipherTypes.SshKey, - SshKey = sshKey + SshKey = sshKey, + Reprompt = reprompt ? 1 : 0, }; var encrypted = CipherEncryption.Encrypt(cipherView, encryptionKey); diff --git a/util/Seeder/Scenes/UserSshKeyCipherScene.cs b/util/Seeder/Scenes/UserSshKeyCipherScene.cs index 78b0758207c0..726424df8675 100644 --- a/util/Seeder/Scenes/UserSshKeyCipherScene.cs +++ b/util/Seeder/Scenes/UserSshKeyCipherScene.cs @@ -20,6 +20,7 @@ public class Request public string? PrivateKey { get; set; } public string? PublicKey { get; set; } public string? Fingerprint { get; set; } + public bool Reprompt { get; set; } public string? Notes { get; set; } } @@ -42,7 +43,7 @@ public async Task> SeedAsync(Request request) PublicKey = request.PublicKey, Fingerprint = request.Fingerprint }; - var cipher = SshKeyCipherSeeder.Create(request.UserKeyB64, request.Name, userId: request.UserId, sshKey: sshKey, notes: request.Notes); + var cipher = SshKeyCipherSeeder.Create(request.UserKeyB64, request.Name, userId: request.UserId, sshKey: sshKey, notes: request.Notes, reprompt: request.Reprompt); await cipherRepository.CreateAsync(cipher); From 728090a1dd3a78dd34766838d500dc87b63e307f Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Wed, 18 Mar 2026 11:42:56 -0700 Subject: [PATCH 7/9] Add deleted and archived options to login cipher seeder --- util/Seeder/Factories/LoginCipherSeeder.cs | 6 ++++++ util/Seeder/Scenes/UserLoginCipherScene.cs | 5 ++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/util/Seeder/Factories/LoginCipherSeeder.cs b/util/Seeder/Factories/LoginCipherSeeder.cs index 4683a2c7b6b4..d9b33eeffbfb 100644 --- a/util/Seeder/Factories/LoginCipherSeeder.cs +++ b/util/Seeder/Factories/LoginCipherSeeder.cs @@ -15,6 +15,9 @@ internal static Cipher Create( string? password = null, string? uri = null, string? notes = null, + bool reprompt = false, + bool deleted = false, + bool archived = false, IEnumerable<(string name, string value, int type)>? fields = null) { var cipherView = new CipherViewDto @@ -29,6 +32,9 @@ internal static Cipher Create( Password = password, Uris = string.IsNullOrEmpty(uri) ? null : [new LoginUriViewDto { Uri = uri }] }, + Reprompt = reprompt ? RepromptTypes.Password : RepromptTypes.None, + DeletedDate = deleted ? DateTime.UtcNow.AddDays(-1) : null, + ArchivedDate = archived ? DateTime.UtcNow.AddDays(-1) : null, Fields = fields?.Select(f => new FieldViewDto { Name = f.name, diff --git a/util/Seeder/Scenes/UserLoginCipherScene.cs b/util/Seeder/Scenes/UserLoginCipherScene.cs index 8588a10e108a..f836e04c6f1a 100644 --- a/util/Seeder/Scenes/UserLoginCipherScene.cs +++ b/util/Seeder/Scenes/UserLoginCipherScene.cs @@ -20,6 +20,9 @@ public class Request public string? Password { get; set; } public string? Uri { get; set; } public string? Notes { get; set; } + public bool Reprompt { get; set; } + public bool Deleted { get; set; } + public bool Archived { get; set; } public IEnumerable<(string name, string value, int type)>? Fields { get; set; } } @@ -36,7 +39,7 @@ public async Task> SeedAsync(Request request) throw new Exception($"User with ID {request.UserId} not found."); } - var cipher = LoginCipherSeeder.Create(request.UserKeyB64, request.Name, userId: request.UserId, username: request.Username, password: request.Password, uri: request.Uri, notes: request.Notes, fields: request.Fields); + var cipher = LoginCipherSeeder.Create(request.UserKeyB64, request.Name, userId: request.UserId, username: request.Username, password: request.Password, uri: request.Uri, notes: request.Notes, fields: request.Fields, reprompt: request.Reprompt, deleted: request.Deleted, archived: request.Archived); await cipherRepository.CreateAsync(cipher); From 1807b461a705b865026f58423e0272c059b45e99 Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Wed, 18 Mar 2026 12:36:21 -0700 Subject: [PATCH 8/9] Remove ArchivedDate for now --- util/Seeder/Factories/LoginCipherSeeder.cs | 2 -- util/Seeder/Scenes/UserLoginCipherScene.cs | 3 +-- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/util/Seeder/Factories/LoginCipherSeeder.cs b/util/Seeder/Factories/LoginCipherSeeder.cs index d9b33eeffbfb..6e557476fb76 100644 --- a/util/Seeder/Factories/LoginCipherSeeder.cs +++ b/util/Seeder/Factories/LoginCipherSeeder.cs @@ -17,7 +17,6 @@ internal static Cipher Create( string? notes = null, bool reprompt = false, bool deleted = false, - bool archived = false, IEnumerable<(string name, string value, int type)>? fields = null) { var cipherView = new CipherViewDto @@ -34,7 +33,6 @@ internal static Cipher Create( }, Reprompt = reprompt ? RepromptTypes.Password : RepromptTypes.None, DeletedDate = deleted ? DateTime.UtcNow.AddDays(-1) : null, - ArchivedDate = archived ? DateTime.UtcNow.AddDays(-1) : null, Fields = fields?.Select(f => new FieldViewDto { Name = f.name, diff --git a/util/Seeder/Scenes/UserLoginCipherScene.cs b/util/Seeder/Scenes/UserLoginCipherScene.cs index f836e04c6f1a..2cbbd24bd223 100644 --- a/util/Seeder/Scenes/UserLoginCipherScene.cs +++ b/util/Seeder/Scenes/UserLoginCipherScene.cs @@ -22,7 +22,6 @@ public class Request public string? Notes { get; set; } public bool Reprompt { get; set; } public bool Deleted { get; set; } - public bool Archived { get; set; } public IEnumerable<(string name, string value, int type)>? Fields { get; set; } } @@ -39,7 +38,7 @@ public async Task> SeedAsync(Request request) throw new Exception($"User with ID {request.UserId} not found."); } - var cipher = LoginCipherSeeder.Create(request.UserKeyB64, request.Name, userId: request.UserId, username: request.Username, password: request.Password, uri: request.Uri, notes: request.Notes, fields: request.Fields, reprompt: request.Reprompt, deleted: request.Deleted, archived: request.Archived); + var cipher = LoginCipherSeeder.Create(request.UserKeyB64, request.Name, userId: request.UserId, username: request.Username, password: request.Password, uri: request.Uri, notes: request.Notes, fields: request.Fields, reprompt: request.Reprompt, deleted: request.Deleted); await cipherRepository.CreateAsync(cipher); From 221ae1220fe6c0cb60bc650a8cd09f06ea2414d0 Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Wed, 18 Mar 2026 12:49:04 -0700 Subject: [PATCH 9/9] Update util/Seeder/Factories/SshKeyCipherSeeder.cs Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> --- util/Seeder/Factories/SshKeyCipherSeeder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/util/Seeder/Factories/SshKeyCipherSeeder.cs b/util/Seeder/Factories/SshKeyCipherSeeder.cs index 66d4f6d38843..10a6a1bba254 100644 --- a/util/Seeder/Factories/SshKeyCipherSeeder.cs +++ b/util/Seeder/Factories/SshKeyCipherSeeder.cs @@ -22,7 +22,7 @@ internal static Cipher Create( Notes = notes, Type = CipherTypes.SshKey, SshKey = sshKey, - Reprompt = reprompt ? 1 : 0, + Reprompt = reprompt ? RepromptTypes.Password : RepromptTypes.None, }; var encrypted = CipherEncryption.Encrypt(cipherView, encryptionKey);