1
0
mirror of https://github.com/bitwarden/server.git synced 2024-11-24 12:35:25 +01:00

[SM-1150] Add secret sync endpoint (#3906)

* Add SecretsSyncQuery

* Add SecretsSync to controller

* Add unit tests

* Add integration tests

* update repo layer
This commit is contained in:
Thomas Avery 2024-04-25 10:34:08 -05:00 committed by GitHub
parent f7aa56b324
commit a7b992d424
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 711 additions and 138 deletions

View File

@ -0,0 +1,50 @@
#nullable enable
using Bit.Core.Exceptions;
using Bit.Core.SecretsManager.Entities;
using Bit.Core.SecretsManager.Models.Data;
using Bit.Core.SecretsManager.Queries.Secrets.Interfaces;
using Bit.Core.SecretsManager.Repositories;
namespace Bit.Commercial.Core.SecretsManager.Queries.Secrets;
public class SecretsSyncQuery : ISecretsSyncQuery
{
private readonly ISecretRepository _secretRepository;
private readonly IServiceAccountRepository _serviceAccountRepository;
public SecretsSyncQuery(
ISecretRepository secretRepository,
IServiceAccountRepository serviceAccountRepository)
{
_secretRepository = secretRepository;
_serviceAccountRepository = serviceAccountRepository;
}
public async Task<(bool HasChanges, IEnumerable<Secret>? Secrets)> GetAsync(SecretsSyncRequest syncRequest)
{
if (syncRequest.LastSyncedDate == null)
{
return await GetSecretsAsync(syncRequest);
}
var serviceAccount = await _serviceAccountRepository.GetByIdAsync(syncRequest.ServiceAccountId);
if (serviceAccount == null)
{
throw new NotFoundException();
}
if (syncRequest.LastSyncedDate.Value <= serviceAccount.RevisionDate)
{
return await GetSecretsAsync(syncRequest);
}
return (HasChanges: false, null);
}
private async Task<(bool HasChanges, IEnumerable<Secret>? Secrets)> GetSecretsAsync(SecretsSyncRequest syncRequest)
{
var secrets = await _secretRepository.GetManyByOrganizationIdAsync(syncRequest.OrganizationId,
syncRequest.ServiceAccountId, syncRequest.AccessClientType);
return (HasChanges: true, Secrets: secrets);
}
}

View File

@ -12,6 +12,7 @@ using Bit.Commercial.Core.SecretsManager.Commands.Trash;
using Bit.Commercial.Core.SecretsManager.Queries;
using Bit.Commercial.Core.SecretsManager.Queries.AccessPolicies;
using Bit.Commercial.Core.SecretsManager.Queries.Projects;
using Bit.Commercial.Core.SecretsManager.Queries.Secrets;
using Bit.Commercial.Core.SecretsManager.Queries.ServiceAccounts;
using Bit.Core.SecretsManager.Commands.AccessPolicies.Interfaces;
using Bit.Core.SecretsManager.Commands.AccessTokens.Interfaces;
@ -23,6 +24,7 @@ using Bit.Core.SecretsManager.Commands.Trash.Interfaces;
using Bit.Core.SecretsManager.Queries.AccessPolicies.Interfaces;
using Bit.Core.SecretsManager.Queries.Interfaces;
using Bit.Core.SecretsManager.Queries.Projects.Interfaces;
using Bit.Core.SecretsManager.Queries.Secrets.Interfaces;
using Bit.Core.SecretsManager.Queries.ServiceAccounts.Interfaces;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.DependencyInjection;
@ -43,6 +45,7 @@ public static class SecretsManagerCollectionExtensions
services.AddScoped<IMaxProjectsQuery, MaxProjectsQuery>();
services.AddScoped<ISameOrganizationQuery, SameOrganizationQuery>();
services.AddScoped<IServiceAccountSecretsDetailsQuery, ServiceAccountSecretsDetailsQuery>();
services.AddScoped<ISecretsSyncQuery, SecretsSyncQuery>();
services.AddScoped<ICreateSecretCommand, CreateSecretCommand>();
services.AddScoped<IUpdateSecretCommand, UpdateSecretCommand>();
services.AddScoped<IDeleteSecretCommand, DeleteSecretCommand>();

View File

@ -25,10 +25,14 @@ public class AccessPolicyRepository : BaseEntityFrameworkRepository, IAccessPoli
policy.GrantedProject.GroupAccessPolicies.Any(ap =>
ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.User.Id == userId && ap.Write));
public async Task<List<Core.SecretsManager.Entities.BaseAccessPolicy>> CreateManyAsync(List<Core.SecretsManager.Entities.BaseAccessPolicy> baseAccessPolicies)
public async Task<List<Core.SecretsManager.Entities.BaseAccessPolicy>> CreateManyAsync(
List<Core.SecretsManager.Entities.BaseAccessPolicy> baseAccessPolicies)
{
using var scope = ServiceScopeFactory.CreateScope();
await using var scope = ServiceScopeFactory.CreateAsyncScope();
var dbContext = GetDatabaseContext(scope);
await using var transaction = await dbContext.Database.BeginTransactionAsync();
var serviceAccountIds = new List<Guid>();
foreach (var baseAccessPolicy in baseAccessPolicies)
{
baseAccessPolicy.SetNewId();
@ -64,12 +68,22 @@ public class AccessPolicyRepository : BaseEntityFrameworkRepository, IAccessPoli
{
var entity = Mapper.Map<ServiceAccountProjectAccessPolicy>(accessPolicy);
await dbContext.AddAsync(entity);
serviceAccountIds.Add(entity.ServiceAccountId!.Value);
break;
}
}
}
if (serviceAccountIds.Count > 0)
{
var utcNow = DateTime.UtcNow;
await dbContext.ServiceAccount
.Where(sa => serviceAccountIds.Contains(sa.Id))
.ExecuteUpdateAsync(setters => setters.SetProperty(sa => sa.RevisionDate, utcNow));
}
await dbContext.SaveChangesAsync();
await transaction.CommitAsync();
return baseAccessPolicies;
}
@ -190,6 +204,16 @@ public class AccessPolicyRepository : BaseEntityFrameworkRepository, IAccessPoli
var entity = await dbContext.AccessPolicies.FindAsync(id);
if (entity != null)
{
if (entity is ServiceAccountProjectAccessPolicy serviceAccountProjectAccessPolicy)
{
var serviceAccount =
await dbContext.ServiceAccount.FindAsync(serviceAccountProjectAccessPolicy.ServiceAccountId);
if (serviceAccount != null)
{
serviceAccount.RevisionDate = DateTime.UtcNow;
}
}
dbContext.Remove(entity);
await dbContext.SaveChangesAsync();
}

View File

@ -70,23 +70,43 @@ public class ProjectRepository : Repository<Core.SecretsManager.Entities.Project
public async Task DeleteManyByIdAsync(IEnumerable<Guid> ids)
{
using var scope = ServiceScopeFactory.CreateScope();
var utcNow = DateTime.UtcNow;
await using var scope = ServiceScopeFactory.CreateAsyncScope();
var dbContext = GetDatabaseContext(scope);
var projects = dbContext.Project
.Where(c => ids.Contains(c.Id))
.Include(p => p.Secrets);
await projects.ForEachAsync(project =>
await using var transaction = await dbContext.Database.BeginTransactionAsync();
var serviceAccountIds = await dbContext.Project
.Where(p => ids.Contains(p.Id))
.Include(p => p.ServiceAccountAccessPolicies)
.SelectMany(p => p.ServiceAccountAccessPolicies.Select(ap => ap.ServiceAccountId!.Value))
.Distinct()
.ToListAsync();
var secretIds = await dbContext.Project
.Where(p => ids.Contains(p.Id))
.Include(p => p.Secrets)
.SelectMany(p => p.Secrets.Select(s => s.Id))
.Distinct()
.ToListAsync();
var utcNow = DateTime.UtcNow;
if (serviceAccountIds.Count > 0)
{
foreach (var projectSecret in project.Secrets)
{
projectSecret.RevisionDate = utcNow;
await dbContext.ServiceAccount
.Where(sa => serviceAccountIds.Contains(sa.Id))
.ExecuteUpdateAsync(setters =>
setters.SetProperty(sa => sa.RevisionDate, utcNow));
}
dbContext.Remove(project);
});
if (secretIds.Count > 0)
{
await dbContext.Secret
.Where(s => secretIds.Contains(s.Id))
.ExecuteUpdateAsync(setters =>
setters.SetProperty(s => s.RevisionDate, utcNow));
}
await dbContext.SaveChangesAsync();
await dbContext.Project.Where(p => ids.Contains(p.Id)).ExecuteDeleteAsync();
await transaction.CommitAsync();
}
public async Task<IEnumerable<Core.SecretsManager.Entities.Project>> GetManyWithSecretsByIds(IEnumerable<Guid> ids)
@ -199,8 +219,4 @@ public class ProjectRepository : Repository<Core.SecretsManager.Entities.Project
private static Expression<Func<Project, bool>> ServiceAccountHasReadAccessToProject(Guid serviceAccountId) => p =>
p.ServiceAccountAccessPolicies.Any(ap => ap.ServiceAccount.Id == serviceAccountId && ap.Read);
private static Expression<Func<Project, bool>> ServiceAccountHasWriteAccessToProject(Guid serviceAccountId) => p =>
p.ServiceAccountAccessPolicies.Any(ap => ap.ServiceAccount.Id == serviceAccountId && ap.Write);
}

View File

@ -43,7 +43,28 @@ public class SecretRepository : Repository<Core.SecretsManager.Entities.Secret,
}
}
public async Task<IEnumerable<SecretPermissionDetails>> GetManyByOrganizationIdAsync(Guid organizationId, Guid userId, AccessClientType accessType)
public async Task<IEnumerable<Core.SecretsManager.Entities.Secret>> GetManyByOrganizationIdAsync(
Guid organizationId, Guid userId, AccessClientType accessType)
{
await using var scope = ServiceScopeFactory.CreateAsyncScope();
var dbContext = GetDatabaseContext(scope);
var query = dbContext.Secret
.Include(c => c.Projects)
.Where(c => c.OrganizationId == organizationId && c.DeletedDate == null);
query = accessType switch
{
AccessClientType.NoAccessCheck => query,
AccessClientType.User => query.Where(UserHasReadAccessToSecret(userId)),
AccessClientType.ServiceAccount => query.Where(ServiceAccountHasReadAccessToSecret(userId)),
_ => throw new ArgumentOutOfRangeException(nameof(accessType), accessType, null)
};
var secrets = await query.OrderBy(c => c.RevisionDate).ToListAsync();
return Mapper.Map<List<Core.SecretsManager.Entities.Secret>>(secrets);
}
public async Task<IEnumerable<SecretPermissionDetails>> GetManyDetailsByOrganizationIdAsync(Guid organizationId, Guid userId, AccessClientType accessType)
{
using var scope = ServiceScopeFactory.CreateScope();
var dbContext = GetDatabaseContext(scope);
@ -82,7 +103,7 @@ public class SecretRepository : Repository<Core.SecretsManager.Entities.Secret,
}
}
public async Task<IEnumerable<SecretPermissionDetails>> GetManyByOrganizationIdInTrashAsync(Guid organizationId)
public async Task<IEnumerable<SecretPermissionDetails>> GetManyDetailsByOrganizationIdInTrashAsync(Guid organizationId)
{
using (var scope = ServiceScopeFactory.CreateScope())
{
@ -103,7 +124,7 @@ public class SecretRepository : Repository<Core.SecretsManager.Entities.Secret,
}
}
public async Task<IEnumerable<SecretPermissionDetails>> GetManyByProjectIdAsync(Guid projectId, Guid userId, AccessClientType accessType)
public async Task<IEnumerable<SecretPermissionDetails>> GetManyDetailsByProjectIdAsync(Guid projectId, Guid userId, AccessClientType accessType)
{
using var scope = ServiceScopeFactory.CreateScope();
var dbContext = GetDatabaseContext(scope);
@ -115,106 +136,124 @@ public class SecretRepository : Repository<Core.SecretsManager.Entities.Secret,
return await secrets.ToListAsync();
}
public override async Task<Core.SecretsManager.Entities.Secret> CreateAsync(Core.SecretsManager.Entities.Secret secret)
{
using (var scope = ServiceScopeFactory.CreateScope())
public override async Task<Core.SecretsManager.Entities.Secret> CreateAsync(
Core.SecretsManager.Entities.Secret secret)
{
await using var scope = ServiceScopeFactory.CreateAsyncScope();
var dbContext = GetDatabaseContext(scope);
secret.SetNewId();
var entity = Mapper.Map<Secret>(secret);
await using var transaction = await dbContext.Database.BeginTransactionAsync();
if (secret.Projects?.Count > 0)
{
foreach (var p in entity.Projects)
foreach (var project in entity.Projects)
{
dbContext.Attach(p);
dbContext.Attach(project);
}
var projectIds = entity.Projects.Select(p => p.Id).ToList();
await UpdateServiceAccountRevisionsByProjectIdsAsync(dbContext, projectIds);
}
await dbContext.AddAsync(entity);
await dbContext.SaveChangesAsync();
await transaction.CommitAsync();
secret.Id = entity.Id;
return secret;
}
}
public async Task<Core.SecretsManager.Entities.Secret> UpdateAsync(Core.SecretsManager.Entities.Secret secret)
{
using (var scope = ServiceScopeFactory.CreateScope())
{
await using var scope = ServiceScopeFactory.CreateAsyncScope();
var dbContext = GetDatabaseContext(scope);
var mappedEntity = Mapper.Map<Secret>(secret);
await using var transaction = await dbContext.Database.BeginTransactionAsync();
var entity = await dbContext.Secret
.Include("Projects")
.Include(s => s.Projects)
.FirstAsync(s => s.Id == secret.Id);
foreach (var p in entity.Projects.Where(p => mappedEntity.Projects.All(mp => mp.Id != p.Id)))
var projectsToRemove = entity.Projects.Where(p => mappedEntity.Projects.All(mp => mp.Id != p.Id)).ToList();
var projectsToAdd = mappedEntity.Projects.Where(p => entity.Projects.All(ep => ep.Id != p.Id)).ToList();
foreach (var p in projectsToRemove)
{
entity.Projects.Remove(p);
}
// Add new relationships
foreach (var project in mappedEntity.Projects.Where(p => entity.Projects.All(ep => ep.Id != p.Id)))
foreach (var project in projectsToAdd)
{
var p = dbContext.AttachToOrGet<Project>(_ => _.Id == project.Id, () => project);
var p = dbContext.AttachToOrGet<Project>(x => x.Id == project.Id, () => project);
entity.Projects.Add(p);
}
var projectIds = projectsToRemove.Select(p => p.Id).Concat(projectsToAdd.Select(p => p.Id)).ToList();
if (projectIds.Count > 0)
{
await UpdateServiceAccountRevisionsByProjectIdsAsync(dbContext, projectIds);
}
await UpdateServiceAccountRevisionsBySecretIdsAsync(dbContext, [entity.Id]);
dbContext.Entry(entity).CurrentValues.SetValues(mappedEntity);
await dbContext.SaveChangesAsync();
}
await transaction.CommitAsync();
return secret;
}
public async Task SoftDeleteManyByIdAsync(IEnumerable<Guid> ids)
{
using (var scope = ServiceScopeFactory.CreateScope())
{
await using var scope = ServiceScopeFactory.CreateAsyncScope();
var dbContext = GetDatabaseContext(scope);
await using var transaction = await dbContext.Database.BeginTransactionAsync();
var secretIds = ids.ToList();
await UpdateServiceAccountRevisionsBySecretIdsAsync(dbContext, secretIds);
var utcNow = DateTime.UtcNow;
var secrets = dbContext.Secret.Where(c => ids.Contains(c.Id));
await secrets.ForEachAsync(secret =>
{
dbContext.Attach(secret);
secret.DeletedDate = utcNow;
secret.RevisionDate = utcNow;
});
await dbContext.SaveChangesAsync();
}
await dbContext.Secret.Where(c => secretIds.Contains(c.Id))
.ExecuteUpdateAsync(setters =>
setters.SetProperty(s => s.RevisionDate, utcNow)
.SetProperty(s => s.DeletedDate, utcNow));
await transaction.CommitAsync();
}
public async Task HardDeleteManyByIdAsync(IEnumerable<Guid> ids)
{
using (var scope = ServiceScopeFactory.CreateScope())
{
await using var scope = ServiceScopeFactory.CreateAsyncScope();
var dbContext = GetDatabaseContext(scope);
var secrets = dbContext.Secret.Where(c => ids.Contains(c.Id));
await secrets.ForEachAsync(secret =>
{
dbContext.Attach(secret);
dbContext.Remove(secret);
});
await dbContext.SaveChangesAsync();
}
await using var transaction = await dbContext.Database.BeginTransactionAsync();
var secretIds = ids.ToList();
await UpdateServiceAccountRevisionsBySecretIdsAsync(dbContext, secretIds);
await dbContext.Secret.Where(c => secretIds.Contains(c.Id))
.ExecuteDeleteAsync();
await transaction.CommitAsync();
}
public async Task RestoreManyByIdAsync(IEnumerable<Guid> ids)
{
using (var scope = ServiceScopeFactory.CreateScope())
{
await using var scope = ServiceScopeFactory.CreateAsyncScope();
var dbContext = GetDatabaseContext(scope);
await using var transaction = await dbContext.Database.BeginTransactionAsync();
var secretIds = ids.ToList();
await UpdateServiceAccountRevisionsBySecretIdsAsync(dbContext, secretIds);
var utcNow = DateTime.UtcNow;
var secrets = dbContext.Secret.Where(c => ids.Contains(c.Id));
await secrets.ForEachAsync(secret =>
{
dbContext.Attach(secret);
secret.DeletedDate = null;
secret.RevisionDate = utcNow;
});
await dbContext.SaveChangesAsync();
}
await dbContext.Secret.Where(c => secretIds.Contains(c.Id))
.ExecuteUpdateAsync(setters =>
setters.SetProperty(s => s.RevisionDate, utcNow)
.SetProperty(s => s.DeletedDate, (DateTime?)null));
await transaction.CommitAsync();
}
public async Task<IEnumerable<Core.SecretsManager.Entities.Secret>> ImportAsync(IEnumerable<Core.SecretsManager.Entities.Secret> secrets)
@ -248,24 +287,6 @@ public class SecretRepository : Repository<Core.SecretsManager.Entities.Secret,
return secrets;
}
public async Task UpdateRevisionDates(IEnumerable<Guid> ids)
{
using (var scope = ServiceScopeFactory.CreateScope())
{
var dbContext = GetDatabaseContext(scope);
var utcNow = DateTime.UtcNow;
var secrets = dbContext.Secret.Where(s => ids.Contains(s.Id));
await secrets.ForEachAsync(secret =>
{
dbContext.Attach(secret);
secret.RevisionDate = utcNow;
});
await dbContext.SaveChangesAsync();
}
}
public async Task<(bool Read, bool Write)> AccessToSecretAsync(Guid id, Guid userId, AccessClientType accessType)
{
using var scope = ServiceScopeFactory.CreateScope();
@ -357,4 +378,60 @@ public class SecretRepository : Repository<Core.SecretsManager.Entities.Secret,
p.UserAccessPolicies.Any(ap => ap.OrganizationUser.UserId == userId && ap.Read) ||
p.GroupAccessPolicies.Any(ap =>
ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.UserId == userId && ap.Read)));
private static async Task UpdateServiceAccountRevisionsByProjectIdsAsync(DatabaseContext dbContext,
List<Guid> projectIds)
{
if (projectIds.Count == 0)
{
return;
}
var serviceAccountIds = await dbContext.Project.Where(p => projectIds.Contains(p.Id))
.Include(p => p.ServiceAccountAccessPolicies)
.SelectMany(p => p.ServiceAccountAccessPolicies.Select(ap => ap.ServiceAccountId!.Value))
.Distinct()
.ToListAsync();
await UpdateServiceAccountRevisionsAsync(dbContext, serviceAccountIds);
}
private static async Task UpdateServiceAccountRevisionsBySecretIdsAsync(DatabaseContext dbContext,
List<Guid> secretIds)
{
if (secretIds.Count == 0)
{
return;
}
var projectAccessServiceAccountIds = await dbContext.Secret
.Where(s => secretIds.Contains(s.Id))
.SelectMany(s =>
s.Projects.SelectMany(p => p.ServiceAccountAccessPolicies.Select(ap => ap.ServiceAccountId!.Value)))
.Distinct()
.ToListAsync();
var directAccessServiceAccountIds = await dbContext.Secret
.Where(s => secretIds.Contains(s.Id))
.SelectMany(s => s.ServiceAccountAccessPolicies.Select(ap => ap.ServiceAccountId!.Value))
.Distinct()
.ToListAsync();
var serviceAccountIds =
directAccessServiceAccountIds.Concat(projectAccessServiceAccountIds).Distinct().ToList();
await UpdateServiceAccountRevisionsAsync(dbContext, serviceAccountIds);
}
private static async Task UpdateServiceAccountRevisionsAsync(DatabaseContext dbContext,
List<Guid> serviceAccountIds)
{
if (serviceAccountIds.Count > 0)
{
var utcNow = DateTime.UtcNow;
await dbContext.ServiceAccount
.Where(sa => serviceAccountIds.Contains(sa.Id))
.ExecuteUpdateAsync(setters => setters.SetProperty(b => b.RevisionDate, utcNow));
}
}
}

View File

@ -0,0 +1,96 @@
#nullable enable
using Bit.Commercial.Core.SecretsManager.Queries.Secrets;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.SecretsManager.Entities;
using Bit.Core.SecretsManager.Models.Data;
using Bit.Core.SecretsManager.Repositories;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
namespace Bit.Commercial.Core.Test.SecretsManager.Queries.Secrets;
[SutProviderCustomize]
public class SecretsSyncQueryTests
{
[Theory, BitAutoData]
public async Task GetAsync_NullLastSyncedDate_ReturnsHasChanges(
SutProvider<SecretsSyncQuery> sutProvider,
SecretsSyncRequest data)
{
data.LastSyncedDate = null;
var result = await sutProvider.Sut.GetAsync(data);
Assert.True(result.HasChanges);
await sutProvider.GetDependency<ISecretRepository>().Received(1)
.GetManyByOrganizationIdAsync(Arg.Is(data.OrganizationId),
Arg.Is(data.ServiceAccountId),
Arg.Is(data.AccessClientType));
}
[Theory, BitAutoData]
public async Task GetAsync_HasLastSyncedDateServiceAccountNotFound_Throws(
SutProvider<SecretsSyncQuery> sutProvider,
SecretsSyncRequest data)
{
data.LastSyncedDate = DateTime.UtcNow;
sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(data.ServiceAccountId)
.Returns((ServiceAccount?)null);
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.GetAsync(data));
await sutProvider.GetDependency<ISecretRepository>()
.DidNotReceiveWithAnyArgs()
.GetManyByOrganizationIdAsync(Arg.Any<Guid>(), Arg.Any<Guid>(), Arg.Any<AccessClientType>());
}
[Theory]
[BitAutoData(true)]
[BitAutoData(false)]
public async Task GetAsync_HasLastSyncedDateServiceAccountWithLaterOrEqualRevisionDate_ReturnsChanges(
bool datesEqual,
SutProvider<SecretsSyncQuery> sutProvider,
SecretsSyncRequest data,
ServiceAccount serviceAccount)
{
data.LastSyncedDate = DateTime.UtcNow.AddDays(-1);
serviceAccount.Id = data.ServiceAccountId;
serviceAccount.RevisionDate = datesEqual ? data.LastSyncedDate.Value : data.LastSyncedDate.Value.AddSeconds(600);
sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(data.ServiceAccountId)
.Returns(serviceAccount);
var result = await sutProvider.Sut.GetAsync(data);
Assert.True(result.HasChanges);
await sutProvider.GetDependency<ISecretRepository>().Received(1)
.GetManyByOrganizationIdAsync(Arg.Is(data.OrganizationId),
Arg.Is(data.ServiceAccountId),
Arg.Is(data.AccessClientType));
}
[Theory, BitAutoData]
public async Task GetAsync_HasLastSyncedDateServiceAccountWithEarlierRevisionDate_ReturnsNoChanges(
SutProvider<SecretsSyncQuery> sutProvider,
SecretsSyncRequest data,
ServiceAccount serviceAccount)
{
data.LastSyncedDate = DateTime.UtcNow.AddDays(-1);
serviceAccount.Id = data.ServiceAccountId;
serviceAccount.RevisionDate = data.LastSyncedDate.Value.AddDays(-2);
sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(data.ServiceAccountId)
.Returns(serviceAccount);
var result = await sutProvider.Sut.GetAsync(data);
Assert.False(result.HasChanges);
Assert.Null(result.Secrets);
await sutProvider.GetDependency<ISecretRepository>()
.DidNotReceiveWithAnyArgs()
.GetManyByOrganizationIdAsync(Arg.Any<Guid>(), Arg.Any<Guid>(), Arg.Any<AccessClientType>());
}
}

View File

@ -9,6 +9,9 @@ using Bit.Core.Repositories;
using Bit.Core.SecretsManager.AuthorizationRequirements;
using Bit.Core.SecretsManager.Commands.Secrets.Interfaces;
using Bit.Core.SecretsManager.Entities;
using Bit.Core.SecretsManager.Models.Data;
using Bit.Core.SecretsManager.Queries.Interfaces;
using Bit.Core.SecretsManager.Queries.Secrets.Interfaces;
using Bit.Core.SecretsManager.Repositories;
using Bit.Core.Services;
using Bit.Core.Tools.Enums;
@ -29,6 +32,8 @@ public class SecretsController : Controller
private readonly ICreateSecretCommand _createSecretCommand;
private readonly IUpdateSecretCommand _updateSecretCommand;
private readonly IDeleteSecretCommand _deleteSecretCommand;
private readonly IAccessClientQuery _accessClientQuery;
private readonly ISecretsSyncQuery _secretsSyncQuery;
private readonly IUserService _userService;
private readonly IEventService _eventService;
private readonly IReferenceEventService _referenceEventService;
@ -42,6 +47,8 @@ public class SecretsController : Controller
ICreateSecretCommand createSecretCommand,
IUpdateSecretCommand updateSecretCommand,
IDeleteSecretCommand deleteSecretCommand,
IAccessClientQuery accessClientQuery,
ISecretsSyncQuery secretsSyncQuery,
IUserService userService,
IEventService eventService,
IReferenceEventService referenceEventService,
@ -54,6 +61,8 @@ public class SecretsController : Controller
_createSecretCommand = createSecretCommand;
_updateSecretCommand = updateSecretCommand;
_deleteSecretCommand = deleteSecretCommand;
_accessClientQuery = accessClientQuery;
_secretsSyncQuery = secretsSyncQuery;
_userService = userService;
_eventService = eventService;
_referenceEventService = referenceEventService;
@ -73,7 +82,7 @@ public class SecretsController : Controller
var orgAdmin = await _currentContext.OrganizationAdmin(organizationId);
var accessClient = AccessClientHelper.ToAccessClient(_currentContext.ClientType, orgAdmin);
var secrets = await _secretRepository.GetManyByOrganizationIdAsync(organizationId, userId, accessClient);
var secrets = await _secretRepository.GetManyDetailsByOrganizationIdAsync(organizationId, userId, accessClient);
return new SecretWithProjectsListResponseModel(secrets);
}
@ -139,7 +148,7 @@ public class SecretsController : Controller
var orgAdmin = await _currentContext.OrganizationAdmin(project.OrganizationId);
var accessClient = AccessClientHelper.ToAccessClient(_currentContext.ClientType, orgAdmin);
var secrets = await _secretRepository.GetManyByProjectIdAsync(projectId, userId, accessClient);
var secrets = await _secretRepository.GetManyDetailsByProjectIdAsync(projectId, userId, accessClient);
return new SecretWithProjectsListResponseModel(secrets);
}
@ -246,4 +255,35 @@ public class SecretsController : Controller
var responses = secrets.Select(s => new BaseSecretResponseModel(s));
return new ListResponseModel<BaseSecretResponseModel>(responses);
}
[HttpGet("/organizations/{organizationId}/secrets/sync")]
public async Task<SecretsSyncResponseModel> GetSecretsSyncAsync([FromRoute] Guid organizationId,
[FromQuery] DateTime? lastSyncedDate = null)
{
if (lastSyncedDate.HasValue && lastSyncedDate.Value > DateTime.UtcNow)
{
throw new BadRequestException("Last synced date must be in the past.");
}
if (!_currentContext.AccessSecretsManager(organizationId))
{
throw new NotFoundException();
}
var (accessClient, serviceAccountId) = await _accessClientQuery.GetAccessClientAsync(User, organizationId);
if (accessClient != AccessClientType.ServiceAccount)
{
throw new BadRequestException("Only service accounts can sync secrets.");
}
var syncRequest = new SecretsSyncRequest
{
AccessClientType = accessClient,
OrganizationId = organizationId,
ServiceAccountId = serviceAccountId,
LastSyncedDate = lastSyncedDate
};
var (hasChanges, secrets) = await _secretsSyncQuery.GetAsync(syncRequest);
return new SecretsSyncResponseModel(hasChanges, secrets);
}
}

View File

@ -44,7 +44,7 @@ public class SecretsManagerPortingController : Controller
var userId = _userService.GetProperUserId(User).Value;
var projects = await _projectRepository.GetManyByOrganizationIdAsync(organizationId, userId, AccessClientType.NoAccessCheck);
var secrets = await _secretRepository.GetManyByOrganizationIdAsync(organizationId, userId, AccessClientType.NoAccessCheck);
var secrets = await _secretRepository.GetManyDetailsByOrganizationIdAsync(organizationId, userId, AccessClientType.NoAccessCheck);
if (projects == null && secrets == null)
{

View File

@ -41,7 +41,7 @@ public class TrashController : Controller
throw new UnauthorizedAccessException();
}
var secrets = await _secretRepository.GetManyByOrganizationIdInTrashAsync(organizationId);
var secrets = await _secretRepository.GetManyDetailsByOrganizationIdInTrashAsync(organizationId);
return new SecretWithProjectsListResponseModel(secrets);
}

View File

@ -0,0 +1,27 @@
#nullable enable
using Bit.Api.Models.Response;
using Bit.Core.Models.Api;
using Bit.Core.SecretsManager.Entities;
namespace Bit.Api.SecretsManager.Models.Response;
public class SecretsSyncResponseModel : ResponseModel
{
private const string _objectName = "secretsSync";
public bool HasChanges { get; set; }
public ListResponseModel<BaseSecretResponseModel>? Secrets { get; set; }
public SecretsSyncResponseModel(bool hasChanges, IEnumerable<Secret>? secrets, string obj = _objectName)
: base(obj)
{
Secrets = secrets != null
? new ListResponseModel<BaseSecretResponseModel>(secrets.Select(s => new BaseSecretResponseModel(s)))
: null;
HasChanges = hasChanges;
}
public SecretsSyncResponseModel() : base(_objectName)
{
}
}

View File

@ -0,0 +1,12 @@
#nullable enable
using Bit.Core.Enums;
namespace Bit.Core.SecretsManager.Models.Data;
public class SecretsSyncRequest
{
public AccessClientType AccessClientType { get; set; }
public Guid OrganizationId { get; set; }
public Guid ServiceAccountId { get; set; }
public DateTime? LastSyncedDate { get; set; }
}

View File

@ -0,0 +1,10 @@
#nullable enable
using Bit.Core.SecretsManager.Entities;
using Bit.Core.SecretsManager.Models.Data;
namespace Bit.Core.SecretsManager.Queries.Secrets.Interfaces;
public interface ISecretsSyncQuery
{
Task<(bool HasChanges, IEnumerable<Secret>? Secrets)> GetAsync(SecretsSyncRequest syncRequest);
}

View File

@ -6,11 +6,12 @@ namespace Bit.Core.SecretsManager.Repositories;
public interface ISecretRepository
{
Task<IEnumerable<SecretPermissionDetails>> GetManyByOrganizationIdAsync(Guid organizationId, Guid userId, AccessClientType accessType);
Task<IEnumerable<SecretPermissionDetails>> GetManyByOrganizationIdInTrashAsync(Guid organizationId);
Task<IEnumerable<SecretPermissionDetails>> GetManyDetailsByOrganizationIdAsync(Guid organizationId, Guid userId, AccessClientType accessType);
Task<IEnumerable<SecretPermissionDetails>> GetManyDetailsByOrganizationIdInTrashAsync(Guid organizationId);
Task<IEnumerable<SecretPermissionDetails>> GetManyDetailsByProjectIdAsync(Guid projectId, Guid userId, AccessClientType accessType);
Task<IEnumerable<Secret>> GetManyByOrganizationIdAsync(Guid organizationId, Guid userId, AccessClientType accessType);
Task<IEnumerable<Secret>> GetManyByOrganizationIdInTrashByIdsAsync(Guid organizationId, IEnumerable<Guid> ids);
Task<IEnumerable<Secret>> GetManyByIds(IEnumerable<Guid> ids);
Task<IEnumerable<SecretPermissionDetails>> GetManyByProjectIdAsync(Guid projectId, Guid userId, AccessClientType accessType);
Task<Secret> GetByIdAsync(Guid id);
Task<Secret> CreateAsync(Secret secret);
Task<Secret> UpdateAsync(Secret secret);
@ -18,7 +19,6 @@ public interface ISecretRepository
Task HardDeleteManyByIdAsync(IEnumerable<Guid> ids);
Task RestoreManyByIdAsync(IEnumerable<Guid> ids);
Task<IEnumerable<Secret>> ImportAsync(IEnumerable<Secret> secrets);
Task UpdateRevisionDates(IEnumerable<Guid> ids);
Task<(bool Read, bool Write)> AccessToSecretAsync(Guid id, Guid userId, AccessClientType accessType);
Task EmptyTrash(DateTime nowTime, uint deleteAfterThisNumberOfDays);
Task<int> GetSecretsCountByOrganizationIdAsync(Guid organizationId);

View File

@ -6,17 +6,23 @@ namespace Bit.Core.SecretsManager.Repositories.Noop;
public class NoopSecretRepository : ISecretRepository
{
public Task<IEnumerable<SecretPermissionDetails>> GetManyByOrganizationIdAsync(Guid organizationId, Guid userId,
public Task<IEnumerable<SecretPermissionDetails>> GetManyDetailsByOrganizationIdAsync(Guid organizationId, Guid userId,
AccessClientType accessType)
{
return Task.FromResult(null as IEnumerable<SecretPermissionDetails>);
}
public Task<IEnumerable<SecretPermissionDetails>> GetManyByOrganizationIdInTrashAsync(Guid organizationId)
public Task<IEnumerable<SecretPermissionDetails>> GetManyDetailsByOrganizationIdInTrashAsync(Guid organizationId)
{
return Task.FromResult(null as IEnumerable<SecretPermissionDetails>);
}
public Task<IEnumerable<Secret>> GetManyByOrganizationIdAsync(Guid organizationId, Guid userId,
AccessClientType accessType)
{
return Task.FromResult(null as IEnumerable<Secret>);
}
public Task<IEnumerable<Secret>> GetManyByOrganizationIdInTrashByIdsAsync(Guid organizationId,
IEnumerable<Guid> ids)
{
@ -28,7 +34,7 @@ public class NoopSecretRepository : ISecretRepository
return Task.FromResult(null as IEnumerable<Secret>);
}
public Task<IEnumerable<SecretPermissionDetails>> GetManyByProjectIdAsync(Guid projectId, Guid userId,
public Task<IEnumerable<SecretPermissionDetails>> GetManyDetailsByProjectIdAsync(Guid projectId, Guid userId,
AccessClientType accessType)
{
return Task.FromResult(null as IEnumerable<SecretPermissionDetails>);
@ -69,11 +75,6 @@ public class NoopSecretRepository : ISecretRepository
return Task.FromResult(null as IEnumerable<Secret>);
}
public Task UpdateRevisionDates(IEnumerable<Guid> ids)
{
return Task.FromResult(0);
}
public Task<(bool Read, bool Write)> AccessToSecretAsync(Guid id, Guid userId, AccessClientType accessType)
{
return Task.FromResult((false, false));

View File

@ -22,6 +22,7 @@ public class SecretsControllerTests : IClassFixture<ApiApplicationFactory>, IAsy
private readonly ApiApplicationFactory _factory;
private readonly ISecretRepository _secretRepository;
private readonly IProjectRepository _projectRepository;
private readonly IServiceAccountRepository _serviceAccountRepository;
private readonly IAccessPolicyRepository _accessPolicyRepository;
private readonly LoginHelper _loginHelper;
@ -35,6 +36,7 @@ public class SecretsControllerTests : IClassFixture<ApiApplicationFactory>, IAsy
_secretRepository = _factory.GetService<ISecretRepository>();
_projectRepository = _factory.GetService<IProjectRepository>();
_accessPolicyRepository = _factory.GetService<IAccessPolicyRepository>();
_serviceAccountRepository = _factory.GetService<IServiceAccountRepository>();
_loginHelper = new LoginHelper(_factory, _client);
}
@ -264,6 +266,7 @@ public class SecretsControllerTests : IClassFixture<ApiApplicationFactory>, IAsy
{
var (email, orgUser) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);
await _loginHelper.LoginAsync(email);
await _loginHelper.LoginAsync(email);
accessType = AccessClientType.User;
var accessPolicies = new List<BaseAccessPolicy>
@ -288,7 +291,7 @@ public class SecretsControllerTests : IClassFixture<ApiApplicationFactory>, IAsy
secretResponse.EnsureSuccessStatusCode();
var secretResult = await secretResponse.Content.ReadFromJsonAsync<SecretResponseModel>();
var result = (await _secretRepository.GetManyByProjectIdAsync(project.Id, orgUserId, accessType)).First();
var result = (await _secretRepository.GetManyDetailsByProjectIdAsync(project.Id, orgUserId, accessType)).First();
var secret = result.Secret;
Assert.NotNull(secretResult);
@ -392,6 +395,7 @@ public class SecretsControllerTests : IClassFixture<ApiApplicationFactory>, IAsy
{
var (org, _) = await _organizationHelper.Initialize(useSecrets, accessSecrets, organizationEnabled);
await _loginHelper.LoginAsync(_email);
await _loginHelper.LoginAsync(_email);
var project = await _projectRepository.CreateAsync(new Project
{
@ -784,6 +788,106 @@ public class SecretsControllerTests : IClassFixture<ApiApplicationFactory>, IAsy
Assert.Equal(secretIds.Count, result.Data.Count());
}
[Theory]
[InlineData(false, false, false)]
[InlineData(false, false, true)]
[InlineData(false, true, false)]
[InlineData(false, true, true)]
[InlineData(true, false, false)]
[InlineData(true, false, true)]
[InlineData(true, true, false)]
public async Task GetSecretsSyncAsync_SmAccessDenied_NotFound(bool useSecrets, bool accessSecrets,
bool organizationEnabled)
{
var (org, _) = await _organizationHelper.Initialize(useSecrets, accessSecrets, organizationEnabled);
await _loginHelper.LoginAsync(_email);
var response = await _client.GetAsync($"/organizations/{org.Id}/secrets/sync");
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Fact]
public async Task GetSecretsSyncAsync_UserClient_BadRequest()
{
var (org, _) = await _organizationHelper.Initialize(true, true, true);
await _loginHelper.LoginAsync(_email);
var response = await _client.GetAsync($"/organizations/{org.Id}/secrets/sync");
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public async Task GetSecretsSyncAsync_NoSecrets_ReturnsEmptyList(bool useLastSyncedDate)
{
var (org, _) = await _organizationHelper.Initialize(true, true, true);
var apiKeyDetails = await _organizationHelper.CreateNewServiceAccountApiKeyAsync();
await _loginHelper.LoginWithApiKeyAsync(apiKeyDetails);
var requestUrl = $"/organizations/{org.Id}/secrets/sync";
if (useLastSyncedDate)
{
requestUrl = $"/organizations/{org.Id}/secrets/sync?lastSyncedDate={DateTime.UtcNow.AddDays(-1)}";
}
var response = await _client.GetAsync(requestUrl);
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<SecretsSyncResponseModel>();
Assert.NotNull(result);
Assert.True(result.HasChanges);
Assert.NotNull(result.Secrets);
Assert.Empty(result.Secrets.Data);
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public async Task GetSecretsSyncAsync_HasSecrets_ReturnsAll(bool useLastSyncedDate)
{
var (org, _) = await _organizationHelper.Initialize(true, true, true);
var apiKeyDetails = await _organizationHelper.CreateNewServiceAccountApiKeyAsync();
await _loginHelper.LoginWithApiKeyAsync(apiKeyDetails);
var secretIds = await SetupSecretsSyncRequestAsync(org.Id, apiKeyDetails.ApiKey.ServiceAccountId!.Value);
var requestUrl = $"/organizations/{org.Id}/secrets/sync";
if (useLastSyncedDate)
{
requestUrl = $"/organizations/{org.Id}/secrets/sync?lastSyncedDate={DateTime.UtcNow.AddDays(-1)}";
}
var response = await _client.GetAsync(requestUrl);
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<SecretsSyncResponseModel>();
Assert.NotNull(result);
Assert.True(result.HasChanges);
Assert.NotNull(result.Secrets);
Assert.NotEmpty(result.Secrets.Data);
Assert.Equal(secretIds.Count, result.Secrets.Data.Count());
Assert.All(result.Secrets.Data, item => Assert.Contains(item.Id, secretIds));
}
[Fact]
public async Task GetSecretsSyncAsync_ServiceAccountNotRevised_ReturnsNoChanges()
{
var (org, _) = await _organizationHelper.Initialize(true, true, true);
var apiKeyDetails = await _organizationHelper.CreateNewServiceAccountApiKeyAsync();
var serviceAccountId = apiKeyDetails.ApiKey.ServiceAccountId!.Value;
await _loginHelper.LoginWithApiKeyAsync(apiKeyDetails);
await SetupSecretsSyncRequestAsync(org.Id, serviceAccountId);
await UpdateServiceAccountRevisionAsync(serviceAccountId, DateTime.UtcNow.AddDays(-1));
var response = await _client.GetAsync($"/organizations/{org.Id}/secrets/sync?lastSyncedDate={DateTime.UtcNow}");
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<SecretsSyncResponseModel>();
Assert.NotNull(result);
Assert.False(result.HasChanges);
Assert.Null(result.Secrets);
}
private async Task<(Project Project, List<Guid> secretIds)> CreateSecretsAsync(Guid orgId, int numberToCreate = 3)
{
var project = await _projectRepository.CreateAsync(new Project
@ -853,4 +957,25 @@ public class SecretsControllerTests : IClassFixture<ApiApplicationFactory>, IAsy
throw new ArgumentOutOfRangeException(nameof(permissionType), permissionType, null);
}
}
private async Task<List<Guid>> SetupSecretsSyncRequestAsync(Guid organizationId, Guid serviceAccountId)
{
var (project, secretIds) = await CreateSecretsAsync(organizationId);
var accessPolicies = new List<BaseAccessPolicy>
{
new ServiceAccountProjectAccessPolicy
{
GrantedProjectId = project.Id, ServiceAccountId = serviceAccountId, Read = true, Write = true
}
};
await _accessPolicyRepository.CreateManyAsync(accessPolicies);
return secretIds;
}
private async Task UpdateServiceAccountRevisionAsync(Guid serviceAccountId, DateTime revisionDate)
{
var sa = await _serviceAccountRepository.GetByIdAsync(serviceAccountId);
sa.RevisionDate = revisionDate;
await _serviceAccountRepository.ReplaceAsync(sa);
}
}

View File

@ -8,6 +8,8 @@ using Bit.Core.Exceptions;
using Bit.Core.SecretsManager.Commands.Secrets.Interfaces;
using Bit.Core.SecretsManager.Entities;
using Bit.Core.SecretsManager.Models.Data;
using Bit.Core.SecretsManager.Queries.Interfaces;
using Bit.Core.SecretsManager.Queries.Secrets.Interfaces;
using Bit.Core.SecretsManager.Repositories;
using Bit.Core.Services;
using Bit.Core.Test.SecretsManager.AutoFixture.SecretsFixture;
@ -37,7 +39,7 @@ public class SecretsControllerTests
var result = await sutProvider.Sut.ListByOrganizationAsync(id);
await sutProvider.GetDependency<ISecretRepository>().Received(1)
.GetManyByOrganizationIdAsync(Arg.Is(AssertHelper.AssertPropertyEqual(id)), userId, accessType);
.GetManyDetailsByOrganizationIdAsync(Arg.Is(AssertHelper.AssertPropertyEqual(id)), userId, accessType);
Assert.Empty(result.Secrets);
}
@ -45,10 +47,10 @@ public class SecretsControllerTests
[Theory]
[BitAutoData(PermissionType.RunAsAdmin)]
[BitAutoData(PermissionType.RunAsUserWithPermission)]
public async Task GetSecretsByOrganization_Success(PermissionType permissionType, SutProvider<SecretsController> sutProvider, Core.SecretsManager.Entities.Secret resultSecret, Guid organizationId, Guid userId, Core.SecretsManager.Entities.Project mockProject, AccessClientType accessType)
public async Task GetSecretsByOrganization_Success(PermissionType permissionType, SutProvider<SecretsController> sutProvider, Secret resultSecret, Guid organizationId, Guid userId, Project mockProject, AccessClientType accessType)
{
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(default).ReturnsForAnyArgs(true);
sutProvider.GetDependency<ISecretRepository>().GetManyByOrganizationIdAsync(default, default, default)
sutProvider.GetDependency<ISecretRepository>().GetManyDetailsByOrganizationIdAsync(default, default, default)
.ReturnsForAnyArgs(new List<SecretPermissionDetails>
{
new() { Secret = resultSecret, Read = true, Write = true },
@ -61,22 +63,22 @@ public class SecretsControllerTests
}
else
{
resultSecret.Projects = new List<Core.SecretsManager.Entities.Project>() { mockProject };
resultSecret.Projects = new List<Project>() { mockProject };
sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(organizationId).Returns(false);
sutProvider.GetDependency<IProjectRepository>().AccessToProjectAsync(default, default, default)
.Returns((true, true));
}
var result = await sutProvider.Sut.ListByOrganizationAsync(resultSecret.OrganizationId);
await sutProvider.Sut.ListByOrganizationAsync(resultSecret.OrganizationId);
await sutProvider.GetDependency<ISecretRepository>().Received(1)
.GetManyByOrganizationIdAsync(Arg.Is(AssertHelper.AssertPropertyEqual(resultSecret.OrganizationId)), userId, accessType);
.GetManyDetailsByOrganizationIdAsync(Arg.Is(AssertHelper.AssertPropertyEqual(resultSecret.OrganizationId)), userId, accessType);
}
[Theory]
[BitAutoData]
public async Task GetSecretsByOrganization_AccessDenied_Throws(SutProvider<SecretsController> sutProvider, Core.SecretsManager.Entities.Secret resultSecret)
public async Task GetSecretsByOrganization_AccessDenied_Throws(SutProvider<SecretsController> sutProvider, Secret resultSecret)
{
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(default).ReturnsForAnyArgs(false);
@ -429,6 +431,76 @@ public class SecretsControllerTests
Assert.Equal(data.Count, results.Data.Count());
}
[Theory]
[BitAutoData(true)]
[BitAutoData(false)]
public async Task GetSecretsSyncAsync_AccessSecretsManagerFalse_ThrowsNotFound(
bool nullLastSyncedDate,
SutProvider<SecretsController> sutProvider, Guid organizationId)
{
var lastSyncedDate = GetLastSyncedDate(nullLastSyncedDate);
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(Arg.Is(organizationId))
.ReturnsForAnyArgs(false);
await Assert.ThrowsAsync<NotFoundException>(() =>
sutProvider.Sut.GetSecretsSyncAsync(organizationId, lastSyncedDate));
}
[Theory]
[BitAutoData(true, AccessClientType.NoAccessCheck)]
[BitAutoData(true, AccessClientType.User)]
[BitAutoData(true, AccessClientType.Organization)]
[BitAutoData(false, AccessClientType.NoAccessCheck)]
[BitAutoData(false, AccessClientType.User)]
[BitAutoData(false, AccessClientType.Organization)]
public async Task GetSecretsSyncAsync_AccessClientIsNotAServiceAccount_ThrowsBadRequest(
bool nullLastSyncedDate,
AccessClientType accessClientType,
SutProvider<SecretsController> sutProvider, Guid organizationId)
{
var lastSyncedDate = GetLastSyncedDate(nullLastSyncedDate);
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(Arg.Is(organizationId))
.ReturnsForAnyArgs(true);
sutProvider.GetDependency<IAccessClientQuery>()
.GetAccessClientAsync(Arg.Any<ClaimsPrincipal>(), Arg.Any<Guid>())
.Returns((accessClientType, new Guid()));
await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.GetSecretsSyncAsync(organizationId, lastSyncedDate));
}
[Theory]
[BitAutoData]
public async Task GetSecretsSyncAsync_LastSyncedInFuture_ThrowsBadRequest(
List<Secret> secrets,
SutProvider<SecretsController> sutProvider, Guid organizationId)
{
DateTime? lastSyncedDate = DateTime.UtcNow.AddDays(3);
SetupSecretsSyncRequest(false, secrets, sutProvider, organizationId);
await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.GetSecretsSyncAsync(organizationId, lastSyncedDate));
}
[Theory]
[BitAutoData(true)]
[BitAutoData(false)]
public async Task GetSecretsSyncAsync_AccessClientIsAServiceAccount_Success(
bool nullLastSyncedDate,
List<Secret> secrets,
SutProvider<SecretsController> sutProvider, Guid organizationId)
{
var lastSyncedDate = SetupSecretsSyncRequest(nullLastSyncedDate, secrets, sutProvider, organizationId);
var result = await sutProvider.Sut.GetSecretsSyncAsync(organizationId, lastSyncedDate);
Assert.True(result.HasChanges);
Assert.NotNull(result.Secrets);
Assert.NotEmpty(result.Secrets.Data);
}
private static (List<Guid> Ids, GetSecretsRequestModel request) BuildGetSecretsRequestModel(
IEnumerable<Secret> data)
{
@ -447,4 +519,24 @@ public class SecretsControllerTests
return organizationId;
}
private static DateTime? SetupSecretsSyncRequest(bool nullLastSyncedDate, List<Secret> secrets,
SutProvider<SecretsController> sutProvider, Guid organizationId)
{
var lastSyncedDate = GetLastSyncedDate(nullLastSyncedDate);
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(Arg.Is(organizationId))
.ReturnsForAnyArgs(true);
sutProvider.GetDependency<IAccessClientQuery>()
.GetAccessClientAsync(Arg.Any<ClaimsPrincipal>(), Arg.Any<Guid>())
.Returns((AccessClientType.ServiceAccount, new Guid()));
sutProvider.GetDependency<ISecretsSyncQuery>().GetAsync(Arg.Any<SecretsSyncRequest>())
.Returns((true, secrets));
return lastSyncedDate;
}
private static DateTime? GetLastSyncedDate(bool nullLastSyncedDate)
{
return nullLastSyncedDate ? null : DateTime.UtcNow.AddDays(-1);
}
}