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:
parent
f7aa56b324
commit
a7b992d424
@ -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);
|
||||
}
|
||||
}
|
@ -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>();
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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);
|
||||
|
||||
}
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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>());
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
{
|
||||
}
|
||||
}
|
12
src/Core/SecretsManager/Models/Data/SecretsSyncRequest.cs
Normal file
12
src/Core/SecretsManager/Models/Data/SecretsSyncRequest.cs
Normal 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; }
|
||||
}
|
@ -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);
|
||||
}
|
@ -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);
|
||||
|
@ -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));
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user