Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
9daaa15
Adding SecretVersion table to server
cd-bitwarden Oct 1, 2025
768eeb0
Merge branch 'main' into SM-1591-HistoryOfSecrets
cd-bitwarden Oct 1, 2025
989f594
Merge branch 'main' into SM-1591-HistoryOfSecrets
cd-bitwarden Oct 3, 2025
4fa76c9
making the names singular not plural for new table
cd-bitwarden Oct 7, 2025
140724c
removing migration
cd-bitwarden Oct 7, 2025
74e32db
fixing migration
cd-bitwarden Oct 7, 2025
46d456e
Merge branch 'main' into SM-1591-HistoryOfSecrets
cd-bitwarden Oct 7, 2025
cf2b260
Merge branch 'main' into SM-1591-HistoryOfSecrets
cd-bitwarden Oct 8, 2025
4405818
Adding indexes for serviceacct and orguserId
cd-bitwarden Oct 9, 2025
95d2f96
indexes for sqllite
cd-bitwarden Oct 9, 2025
7272a77
fixing migrations
cd-bitwarden Oct 9, 2025
0dc1396
adding indexes to secretVeriosn.sql
cd-bitwarden Oct 9, 2025
e71b6ed
tests
cd-bitwarden Oct 9, 2025
39576d7
removing tests
cd-bitwarden Oct 10, 2025
a7384e2
Merge branch 'main' into SM-1591-HistoryOfSecrets
cd-bitwarden Oct 10, 2025
2d91b0c
adding GO
cd-bitwarden Oct 10, 2025
58b5066
Merge branch 'SM-1591-HistoryOfSecrets' of https://github.com/bitwardโ€ฆ
cd-bitwarden Oct 10, 2025
f42cd8a
api repository and controller additions for SecretVersion table, as wโ€ฆ
cd-bitwarden Oct 10, 2025
af32d61
Merge branch 'main' into SM-1592-API
cd-bitwarden Oct 10, 2025
f50cbaa
Merge branch 'SM-1591-HistoryOfSecrets' into SM-1592-API
cd-bitwarden Oct 10, 2025
61563bc
Merge branch 'SM-1592-API' of https://github.com/bitwarden/server intโ€ฆ
cd-bitwarden Oct 14, 2025
0b0e3e0
Merge branch 'main' into SM-1592-API
cd-bitwarden Oct 14, 2025
cf5bfdf
Merge branch 'SM-1592-API' of https://github.com/bitwarden/server intโ€ฆ
cd-bitwarden Oct 14, 2025
b271de6
test fix sqllite
cd-bitwarden Oct 14, 2025
3399de7
Merge branch 'main' into SM-1592-API
cd-bitwarden Oct 16, 2025
30c12f8
improvements
cd-bitwarden Oct 17, 2025
7078b61
Merge branch 'main' into SM-1592-API
cd-bitwarden Oct 23, 2025
72063b1
removing comments
cd-bitwarden Oct 23, 2025
0aeeb5b
Merge branch 'SM-1592-API' of https://github.com/bitwarden/server intโ€ฆ
cd-bitwarden Oct 23, 2025
4877f13
making files nullable safe
cd-bitwarden Oct 23, 2025
e99702c
Merge branch 'main' into SM-1592-API
cd-bitwarden Oct 30, 2025
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
๏ปฟusing AutoMapper;
using Bit.Core.SecretsManager.Repositories;
using Bit.Infrastructure.EntityFramework.Repositories;
using Bit.Infrastructure.EntityFramework.SecretsManager.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;

namespace Bit.Commercial.Infrastructure.EntityFramework.SecretsManager.Repositories;

public class SecretVersionRepository : Repository<Core.SecretsManager.Entities.SecretVersion, SecretVersion, Guid>, ISecretVersionRepository
{
public SecretVersionRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper)
: base(serviceScopeFactory, mapper, db => db.SecretVersion)
{ }

public override async Task<Core.SecretsManager.Entities.SecretVersion?> GetByIdAsync(Guid id)
{
using var scope = ServiceScopeFactory.CreateScope();
var dbContext = GetDatabaseContext(scope);
var secretVersion = await dbContext.SecretVersion
.Where(sv => sv.Id == id)
.FirstOrDefaultAsync();
return Mapper.Map<Core.SecretsManager.Entities.SecretVersion>(secretVersion);
}

public async Task<IEnumerable<Core.SecretsManager.Entities.SecretVersion>> GetManyBySecretIdAsync(Guid secretId)
{
using var scope = ServiceScopeFactory.CreateScope();
var dbContext = GetDatabaseContext(scope);
var secretVersions = await dbContext.SecretVersion
.Where(sv => sv.SecretId == secretId)
.OrderByDescending(sv => sv.VersionDate)
.ToListAsync();
return Mapper.Map<List<Core.SecretsManager.Entities.SecretVersion>>(secretVersions);
}

public override async Task<Core.SecretsManager.Entities.SecretVersion> CreateAsync(Core.SecretsManager.Entities.SecretVersion secretVersion)
{
const int maxVersionsToKeep = 10;

await using var scope = ServiceScopeFactory.CreateAsyncScope();
var dbContext = GetDatabaseContext(scope);

await using var transaction = await dbContext.Database.BeginTransactionAsync();

// Get the IDs of the most recent (maxVersionsToKeep - 1) versions to keep
var versionsToKeepIds = await dbContext.SecretVersion
.Where(sv => sv.SecretId == secretVersion.SecretId)
.OrderByDescending(sv => sv.VersionDate)
.Take(maxVersionsToKeep - 1)
.Select(sv => sv.Id)
.ToListAsync();

// Delete all versions for this secret that are not in the "keep" list
if (versionsToKeepIds.Any())
{
await dbContext.SecretVersion
.Where(sv => sv.SecretId == secretVersion.SecretId && !versionsToKeepIds.Contains(sv.Id))
.ExecuteDeleteAsync();
}

secretVersion.SetNewId();
var entity = Mapper.Map<SecretVersion>(secretVersion);

await dbContext.AddAsync(entity);
await dbContext.SaveChangesAsync();
await transaction.CommitAsync();

return secretVersion;
}

public async Task DeleteManyByIdAsync(IEnumerable<Guid> ids)
{
await using var scope = ServiceScopeFactory.CreateAsyncScope();
var dbContext = GetDatabaseContext(scope);

var secretVersionIds = ids.ToList();
await dbContext.SecretVersion
.Where(sv => secretVersionIds.Contains(sv.Id))
.ExecuteDeleteAsync();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ public static void AddSecretsManagerEfRepositories(this IServiceCollection servi
{
services.AddSingleton<IAccessPolicyRepository, AccessPolicyRepository>();
services.AddSingleton<ISecretRepository, SecretRepository>();
services.AddSingleton<ISecretVersionRepository, SecretVersionRepository>();
services.AddSingleton<IProjectRepository, ProjectRepository>();
services.AddSingleton<IServiceAccountRepository, ServiceAccountRepository>();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
๏ปฟusing Bit.Core.SecretsManager.Entities;
using Bit.Test.Common.AutoFixture.Attributes;
using Xunit;

namespace Bit.Commercial.Core.Test.SecretsManager.Repositories;

public class SecretVersionRepositoryTests
{
[Theory]
[BitAutoData]
public void SecretVersion_EntityCreation_Success(SecretVersion secretVersion)
{
// Arrange & Act
secretVersion.SetNewId();

// Assert
Assert.NotEqual(Guid.Empty, secretVersion.Id);
Assert.NotEqual(Guid.Empty, secretVersion.SecretId);
Assert.NotNull(secretVersion.Value);
Assert.NotEqual(default, secretVersion.VersionDate);
}

[Theory]
[BitAutoData]
public void SecretVersion_WithServiceAccountEditor_Success(SecretVersion secretVersion, Guid serviceAccountId)
{
// Arrange & Act
secretVersion.EditorServiceAccountId = serviceAccountId;
secretVersion.EditorOrganizationUserId = null;

// Assert
Assert.Equal(serviceAccountId, secretVersion.EditorServiceAccountId);
Assert.Null(secretVersion.EditorOrganizationUserId);
}

[Theory]
[BitAutoData]
public void SecretVersion_WithOrganizationUserEditor_Success(SecretVersion secretVersion, Guid organizationUserId)
{
// Arrange & Act
secretVersion.EditorOrganizationUserId = organizationUserId;
secretVersion.EditorServiceAccountId = null;

// Assert
Assert.Equal(organizationUserId, secretVersion.EditorOrganizationUserId);
Assert.Null(secretVersion.EditorServiceAccountId);
}

[Theory]
[BitAutoData]
public void SecretVersion_NullableEditors_Success(SecretVersion secretVersion)
{
// Arrange & Act
secretVersion.EditorServiceAccountId = null;
secretVersion.EditorOrganizationUserId = null;

// Assert
Assert.Null(secretVersion.EditorServiceAccountId);
Assert.Null(secretVersion.EditorOrganizationUserId);
}

[Theory]
[BitAutoData]
public void SecretVersion_VersionDateSet_Success(SecretVersion secretVersion)
{
// Arrange
var versionDate = DateTime.UtcNow;

// Act
secretVersion.VersionDate = versionDate;

// Assert
Assert.Equal(versionDate, secretVersion.VersionDate);
}

[Theory]
[BitAutoData]
public void SecretVersion_ValueEncrypted_Success(SecretVersion secretVersion, string encryptedValue)
{
// Arrange & Act
secretVersion.Value = encryptedValue;

// Assert
Assert.Equal(encryptedValue, secretVersion.Value);
Assert.NotEmpty(secretVersion.Value);
}

[Theory]
[BitAutoData]
public void SecretVersion_MultipleVersions_DifferentIds(List<SecretVersion> secretVersions, Guid secretId)
{
// Arrange & Act
foreach (var version in secretVersions)
{
version.SecretId = secretId;
version.SetNewId();
}

// Assert
var distinctIds = secretVersions.Select(v => v.Id).Distinct();
Assert.Equal(secretVersions.Count, distinctIds.Count());
Assert.All(secretVersions, v => Assert.Equal(secretId, v.SecretId));
}

[Theory]
[BitAutoData]
public void SecretVersion_VersionDateOrdering_Success(SecretVersion version1, SecretVersion version2, SecretVersion version3, Guid secretId)
{
// Arrange
var now = DateTime.UtcNow;
version1.SecretId = secretId;
version1.VersionDate = now.AddDays(-2);

version2.SecretId = secretId;
version2.VersionDate = now.AddDays(-1);

version3.SecretId = secretId;
version3.VersionDate = now;

var versions = new List<SecretVersion> { version2, version3, version1 };

// Act
var orderedVersions = versions.OrderByDescending(v => v.VersionDate).ToList();

// Assert
Assert.Equal(version3.Id, orderedVersions[0].Id); // Most recent
Assert.Equal(version2.Id, orderedVersions[1].Id);
Assert.Equal(version1.Id, orderedVersions[2].Id); // Oldest
}
}
Loading
Loading