Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions util/Seeder/Factories/LoginCipherSeeder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ internal static Cipher Create(
string? password = null,
string? uri = null,
string? notes = null,
bool reprompt = false,
bool deleted = false,
IEnumerable<(string name, string value, int type)>? fields = null)
{
var cipherView = new CipherViewDto
Expand All @@ -29,6 +31,8 @@ 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,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ IMPORTANT: DeletedDate is set on CipherViewDto but never propagated to the Cipher entity -- setting deleted: true silently has no effect

Details and fix

CipherEncryption.CreateEntity (in CipherEncryption.cs lines 46-57) does not copy DeletedDate from EncryptedCipherDto to the returned Cipher entity. It only sets Id, OrganizationId, UserId, Type, Data, Key, Reprompt, CreationDate, and RevisionDate.

The EncryptedCipherDto does carry DeletedDate through encryption, but it is discarded when building the final entity.

Fix in CipherEncryption.CreateEntity:

return new Cipher
{
    Id = CoreHelpers.GenerateComb(),
    OrganizationId = organizationId,
    UserId = userId,
    Type = cipherType,
    Data = dataJson,
    Key = encrypted.Key,
    Reprompt = (CipherRepromptType?)encrypted.Reprompt,
    CreationDate = DateTime.UtcNow,
    RevisionDate = DateTime.UtcNow,
    DeletedDate = encrypted.DeletedDate,
};

This requires adding a DeletedDate parameter to CreateEntity or reading it from the EncryptedCipherDto that is already passed in.

Fields = fields?.Select(f => new FieldViewDto
{
Name = f.name,
Expand Down
6 changes: 4 additions & 2 deletions util/Seeder/Factories/SshKeyCipherSeeder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,17 @@ 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
{
OrganizationId = organizationId,
Name = name,
Notes = notes,
Type = CipherTypes.SshKey,
SshKey = sshKey
SshKey = sshKey,
Reprompt = reprompt ? RepromptTypes.Password : RepromptTypes.None,
};

var encrypted = CipherEncryption.Encrypt(cipherView, encryptionKey);
Expand Down
60 changes: 60 additions & 0 deletions util/Seeder/Scenes/UserCardCipherScene.cs
Original file line number Diff line number Diff line change
@@ -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<UserCardCipherScene.Request, UserCardCipherScene.Result>
{
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<SceneResult<Result>> 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>(
result: new Result
{
CipherId = cipher.Id
},
mangleMap: manglerService.GetMangleMap());
}
}
58 changes: 58 additions & 0 deletions util/Seeder/Scenes/UserIdentityCipherScene.cs
Original file line number Diff line number Diff line change
@@ -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<UserIdentityCipherScene.Request, UserIdentityCipherScene.Result>
{
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<SceneResult<Result>> 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>(
result: new Result
{
CipherId = cipher.Id
},
mangleMap: manglerService.GetMangleMap());
}
}
52 changes: 52 additions & 0 deletions util/Seeder/Scenes/UserLoginCipherScene.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
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 UserLoginCipherScene(IUserRepository userRepository, ICipherRepository cipherRepository, IManglerService manglerService) : IScene<UserLoginCipherScene.Request, UserLoginCipherScene.Result>
{
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 bool Reprompt { get; set; }
public bool Deleted { get; set; }
public IEnumerable<(string name, string value, int type)>? Fields { get; set; }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 [SUGGESTION] C# value tuples (ValueTuple) don't round-trip through System.Text.Json with named elements — at runtime the names are Item1, Item2, Item3, not name, value, type. Since SceneExecutor deserializes requests via JsonSerializer.Deserialize, callers would need to send {"Item1": ..., "Item2": ..., "Item3": ...} for this to work, which is unintuitive.

Consider using FieldViewDto (or a dedicated DTO) instead of the tuple so the JSON contract is explicit and matches the field names callers would expect:

Suggested change
public IEnumerable<(string name, string value, int type)>? Fields { get; set; }
public IEnumerable<FieldViewDto>? Fields { get; set; }

This would require a small adapter in SeedAsync to convert to the tuple the factory expects, or updating LoginCipherSeeder.Create to accept FieldViewDto directly.

}

public class Result
{
public required Guid CipherId { get; set; }
}

public async Task<SceneResult<Result>> 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, reprompt: request.Reprompt, deleted: request.Deleted);

await cipherRepository.CreateAsync(cipher);

return new SceneResult<Result>(
result: new Result
{
CipherId = cipher.Id
},
mangleMap: manglerService.GetMangleMap());
}
}
46 changes: 46 additions & 0 deletions util/Seeder/Scenes/UserSecureNoteCipherScene.cs
Original file line number Diff line number Diff line change
@@ -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<UserSecureNoteCipherScene.Request, UserSecureNoteCipherScene.Result>
{
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<SceneResult<Result>> 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>(
result: new Result
{
CipherId = cipher.Id
},
mangleMap: manglerService.GetMangleMap());
}
}
57 changes: 57 additions & 0 deletions util/Seeder/Scenes/UserSshKeyCipherScene.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
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<UserSshKeyCipherScene.Request, UserSshKeyCipherScene.Result>
{
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 bool Reprompt { get; set; }
public string? Notes { get; set; }
}

public class Result
{
public required Guid CipherId { get; set; }
}

public async Task<SceneResult<Result>> 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, notes: request.Notes, reprompt: request.Reprompt);

await cipherRepository.CreateAsync(cipher);

return new SceneResult<Result>(
result: new Result
{
CipherId = cipher.Id
},
mangleMap: manglerService.GetMangleMap());
}
}
11 changes: 3 additions & 8 deletions util/SeederApi/Commands/DestroyBatchScenesCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,18 @@ public async Task DestroyAsync(IEnumerable<string> playIds)
{
var exceptions = new List<Exception>();

var deleteTasks = playIds.Select(async playId =>
foreach (var playId in playIds)
{
try
{
await destroySceneCommand.DestroyAsync(playId);
}
catch (Exception ex)
{
lock (exceptions)
{
exceptions.Add(ex);
}
exceptions.Add(ex);
logger.LogError(ex, "Error deleting seeded data: {PlayId}", playId);
}
});

await Task.WhenAll(deleteTasks);
}

if (exceptions.Count > 0)
{
Expand Down
Loading