mirror of
https://github.com/bitwarden/server.git
synced 2025-04-05 21:18:13 -05: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;
|
||||||
using Bit.Commercial.Core.SecretsManager.Queries.AccessPolicies;
|
using Bit.Commercial.Core.SecretsManager.Queries.AccessPolicies;
|
||||||
using Bit.Commercial.Core.SecretsManager.Queries.Projects;
|
using Bit.Commercial.Core.SecretsManager.Queries.Projects;
|
||||||
|
using Bit.Commercial.Core.SecretsManager.Queries.Secrets;
|
||||||
using Bit.Commercial.Core.SecretsManager.Queries.ServiceAccounts;
|
using Bit.Commercial.Core.SecretsManager.Queries.ServiceAccounts;
|
||||||
using Bit.Core.SecretsManager.Commands.AccessPolicies.Interfaces;
|
using Bit.Core.SecretsManager.Commands.AccessPolicies.Interfaces;
|
||||||
using Bit.Core.SecretsManager.Commands.AccessTokens.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.AccessPolicies.Interfaces;
|
||||||
using Bit.Core.SecretsManager.Queries.Interfaces;
|
using Bit.Core.SecretsManager.Queries.Interfaces;
|
||||||
using Bit.Core.SecretsManager.Queries.Projects.Interfaces;
|
using Bit.Core.SecretsManager.Queries.Projects.Interfaces;
|
||||||
|
using Bit.Core.SecretsManager.Queries.Secrets.Interfaces;
|
||||||
using Bit.Core.SecretsManager.Queries.ServiceAccounts.Interfaces;
|
using Bit.Core.SecretsManager.Queries.ServiceAccounts.Interfaces;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
@ -43,6 +45,7 @@ public static class SecretsManagerCollectionExtensions
|
|||||||
services.AddScoped<IMaxProjectsQuery, MaxProjectsQuery>();
|
services.AddScoped<IMaxProjectsQuery, MaxProjectsQuery>();
|
||||||
services.AddScoped<ISameOrganizationQuery, SameOrganizationQuery>();
|
services.AddScoped<ISameOrganizationQuery, SameOrganizationQuery>();
|
||||||
services.AddScoped<IServiceAccountSecretsDetailsQuery, ServiceAccountSecretsDetailsQuery>();
|
services.AddScoped<IServiceAccountSecretsDetailsQuery, ServiceAccountSecretsDetailsQuery>();
|
||||||
|
services.AddScoped<ISecretsSyncQuery, SecretsSyncQuery>();
|
||||||
services.AddScoped<ICreateSecretCommand, CreateSecretCommand>();
|
services.AddScoped<ICreateSecretCommand, CreateSecretCommand>();
|
||||||
services.AddScoped<IUpdateSecretCommand, UpdateSecretCommand>();
|
services.AddScoped<IUpdateSecretCommand, UpdateSecretCommand>();
|
||||||
services.AddScoped<IDeleteSecretCommand, DeleteSecretCommand>();
|
services.AddScoped<IDeleteSecretCommand, DeleteSecretCommand>();
|
||||||
|
@ -25,10 +25,14 @@ public class AccessPolicyRepository : BaseEntityFrameworkRepository, IAccessPoli
|
|||||||
policy.GrantedProject.GroupAccessPolicies.Any(ap =>
|
policy.GrantedProject.GroupAccessPolicies.Any(ap =>
|
||||||
ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.User.Id == userId && ap.Write));
|
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);
|
var dbContext = GetDatabaseContext(scope);
|
||||||
|
await using var transaction = await dbContext.Database.BeginTransactionAsync();
|
||||||
|
|
||||||
|
var serviceAccountIds = new List<Guid>();
|
||||||
foreach (var baseAccessPolicy in baseAccessPolicies)
|
foreach (var baseAccessPolicy in baseAccessPolicies)
|
||||||
{
|
{
|
||||||
baseAccessPolicy.SetNewId();
|
baseAccessPolicy.SetNewId();
|
||||||
@ -64,12 +68,22 @@ public class AccessPolicyRepository : BaseEntityFrameworkRepository, IAccessPoli
|
|||||||
{
|
{
|
||||||
var entity = Mapper.Map<ServiceAccountProjectAccessPolicy>(accessPolicy);
|
var entity = Mapper.Map<ServiceAccountProjectAccessPolicy>(accessPolicy);
|
||||||
await dbContext.AddAsync(entity);
|
await dbContext.AddAsync(entity);
|
||||||
|
serviceAccountIds.Add(entity.ServiceAccountId!.Value);
|
||||||
break;
|
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 dbContext.SaveChangesAsync();
|
||||||
|
await transaction.CommitAsync();
|
||||||
return baseAccessPolicies;
|
return baseAccessPolicies;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -190,6 +204,16 @@ public class AccessPolicyRepository : BaseEntityFrameworkRepository, IAccessPoli
|
|||||||
var entity = await dbContext.AccessPolicies.FindAsync(id);
|
var entity = await dbContext.AccessPolicies.FindAsync(id);
|
||||||
if (entity != null)
|
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);
|
dbContext.Remove(entity);
|
||||||
await dbContext.SaveChangesAsync();
|
await dbContext.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
|
@ -70,23 +70,43 @@ public class ProjectRepository : Repository<Core.SecretsManager.Entities.Project
|
|||||||
|
|
||||||
public async Task DeleteManyByIdAsync(IEnumerable<Guid> ids)
|
public async Task DeleteManyByIdAsync(IEnumerable<Guid> ids)
|
||||||
{
|
{
|
||||||
using var scope = ServiceScopeFactory.CreateScope();
|
await using var scope = ServiceScopeFactory.CreateAsyncScope();
|
||||||
var utcNow = DateTime.UtcNow;
|
|
||||||
var dbContext = GetDatabaseContext(scope);
|
var dbContext = GetDatabaseContext(scope);
|
||||||
var projects = dbContext.Project
|
await using var transaction = await dbContext.Database.BeginTransactionAsync();
|
||||||
.Where(c => ids.Contains(c.Id))
|
|
||||||
.Include(p => p.Secrets);
|
var serviceAccountIds = await dbContext.Project
|
||||||
await projects.ForEachAsync(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)
|
await dbContext.ServiceAccount
|
||||||
{
|
.Where(sa => serviceAccountIds.Contains(sa.Id))
|
||||||
projectSecret.RevisionDate = utcNow;
|
.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)
|
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 =>
|
private static Expression<Func<Project, bool>> ServiceAccountHasReadAccessToProject(Guid serviceAccountId) => p =>
|
||||||
p.ServiceAccountAccessPolicies.Any(ap => ap.ServiceAccount.Id == serviceAccountId && ap.Read);
|
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();
|
using var scope = ServiceScopeFactory.CreateScope();
|
||||||
var dbContext = GetDatabaseContext(scope);
|
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())
|
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();
|
using var scope = ServiceScopeFactory.CreateScope();
|
||||||
var dbContext = GetDatabaseContext(scope);
|
var dbContext = GetDatabaseContext(scope);
|
||||||
@ -115,106 +136,124 @@ public class SecretRepository : Repository<Core.SecretsManager.Entities.Secret,
|
|||||||
return await secrets.ToListAsync();
|
return await secrets.ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task<Core.SecretsManager.Entities.Secret> CreateAsync(Core.SecretsManager.Entities.Secret secret)
|
public override async Task<Core.SecretsManager.Entities.Secret> CreateAsync(
|
||||||
|
Core.SecretsManager.Entities.Secret secret)
|
||||||
{
|
{
|
||||||
using (var scope = ServiceScopeFactory.CreateScope())
|
await using var scope = ServiceScopeFactory.CreateAsyncScope();
|
||||||
{
|
var dbContext = GetDatabaseContext(scope);
|
||||||
var dbContext = GetDatabaseContext(scope);
|
secret.SetNewId();
|
||||||
secret.SetNewId();
|
var entity = Mapper.Map<Secret>(secret);
|
||||||
var entity = Mapper.Map<Secret>(secret);
|
|
||||||
|
|
||||||
if (secret.Projects?.Count > 0)
|
await using var transaction = await dbContext.Database.BeginTransactionAsync();
|
||||||
|
|
||||||
|
if (secret.Projects?.Count > 0)
|
||||||
|
{
|
||||||
|
foreach (var project in entity.Projects)
|
||||||
{
|
{
|
||||||
foreach (var p in entity.Projects)
|
dbContext.Attach(project);
|
||||||
{
|
|
||||||
dbContext.Attach(p);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await dbContext.AddAsync(entity);
|
var projectIds = entity.Projects.Select(p => p.Id).ToList();
|
||||||
await dbContext.SaveChangesAsync();
|
await UpdateServiceAccountRevisionsByProjectIdsAsync(dbContext, projectIds);
|
||||||
secret.Id = entity.Id;
|
|
||||||
return secret;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
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(s => s.Projects)
|
||||||
|
.FirstAsync(s => s.Id == secret.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)
|
||||||
{
|
{
|
||||||
var dbContext = GetDatabaseContext(scope);
|
entity.Projects.Remove(p);
|
||||||
var mappedEntity = Mapper.Map<Secret>(secret);
|
|
||||||
|
|
||||||
var entity = await dbContext.Secret
|
|
||||||
.Include("Projects")
|
|
||||||
.FirstAsync(s => s.Id == secret.Id);
|
|
||||||
|
|
||||||
foreach (var p in entity.Projects.Where(p => mappedEntity.Projects.All(mp => mp.Id != p.Id)))
|
|
||||||
{
|
|
||||||
entity.Projects.Remove(p);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add new relationships
|
|
||||||
foreach (var project in mappedEntity.Projects.Where(p => entity.Projects.All(ep => ep.Id != p.Id)))
|
|
||||||
{
|
|
||||||
var p = dbContext.AttachToOrGet<Project>(_ => _.Id == project.Id, () => project);
|
|
||||||
entity.Projects.Add(p);
|
|
||||||
}
|
|
||||||
|
|
||||||
dbContext.Entry(entity).CurrentValues.SetValues(mappedEntity);
|
|
||||||
await dbContext.SaveChangesAsync();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
foreach (var project in projectsToAdd)
|
||||||
|
{
|
||||||
|
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;
|
return secret;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SoftDeleteManyByIdAsync(IEnumerable<Guid> ids)
|
public async Task SoftDeleteManyByIdAsync(IEnumerable<Guid> ids)
|
||||||
{
|
{
|
||||||
using (var scope = ServiceScopeFactory.CreateScope())
|
await using var scope = ServiceScopeFactory.CreateAsyncScope();
|
||||||
{
|
var dbContext = GetDatabaseContext(scope);
|
||||||
var dbContext = GetDatabaseContext(scope);
|
await using var transaction = await dbContext.Database.BeginTransactionAsync();
|
||||||
var utcNow = DateTime.UtcNow;
|
|
||||||
var secrets = dbContext.Secret.Where(c => ids.Contains(c.Id));
|
var secretIds = ids.ToList();
|
||||||
await secrets.ForEachAsync(secret =>
|
await UpdateServiceAccountRevisionsBySecretIdsAsync(dbContext, secretIds);
|
||||||
{
|
|
||||||
dbContext.Attach(secret);
|
var utcNow = DateTime.UtcNow;
|
||||||
secret.DeletedDate = utcNow;
|
|
||||||
secret.RevisionDate = utcNow;
|
await dbContext.Secret.Where(c => secretIds.Contains(c.Id))
|
||||||
});
|
.ExecuteUpdateAsync(setters =>
|
||||||
await dbContext.SaveChangesAsync();
|
setters.SetProperty(s => s.RevisionDate, utcNow)
|
||||||
}
|
.SetProperty(s => s.DeletedDate, utcNow));
|
||||||
|
|
||||||
|
await transaction.CommitAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task HardDeleteManyByIdAsync(IEnumerable<Guid> ids)
|
public async Task HardDeleteManyByIdAsync(IEnumerable<Guid> ids)
|
||||||
{
|
{
|
||||||
using (var scope = ServiceScopeFactory.CreateScope())
|
await using var scope = ServiceScopeFactory.CreateAsyncScope();
|
||||||
{
|
var dbContext = GetDatabaseContext(scope);
|
||||||
var dbContext = GetDatabaseContext(scope);
|
await using var transaction = await dbContext.Database.BeginTransactionAsync();
|
||||||
var secrets = dbContext.Secret.Where(c => ids.Contains(c.Id));
|
|
||||||
await secrets.ForEachAsync(secret =>
|
var secretIds = ids.ToList();
|
||||||
{
|
await UpdateServiceAccountRevisionsBySecretIdsAsync(dbContext, secretIds);
|
||||||
dbContext.Attach(secret);
|
|
||||||
dbContext.Remove(secret);
|
await dbContext.Secret.Where(c => secretIds.Contains(c.Id))
|
||||||
});
|
.ExecuteDeleteAsync();
|
||||||
await dbContext.SaveChangesAsync();
|
|
||||||
}
|
await transaction.CommitAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task RestoreManyByIdAsync(IEnumerable<Guid> ids)
|
public async Task RestoreManyByIdAsync(IEnumerable<Guid> ids)
|
||||||
{
|
{
|
||||||
using (var scope = ServiceScopeFactory.CreateScope())
|
await using var scope = ServiceScopeFactory.CreateAsyncScope();
|
||||||
{
|
var dbContext = GetDatabaseContext(scope);
|
||||||
var dbContext = GetDatabaseContext(scope);
|
await using var transaction = await dbContext.Database.BeginTransactionAsync();
|
||||||
var utcNow = DateTime.UtcNow;
|
|
||||||
var secrets = dbContext.Secret.Where(c => ids.Contains(c.Id));
|
var secretIds = ids.ToList();
|
||||||
await secrets.ForEachAsync(secret =>
|
await UpdateServiceAccountRevisionsBySecretIdsAsync(dbContext, secretIds);
|
||||||
{
|
|
||||||
dbContext.Attach(secret);
|
var utcNow = DateTime.UtcNow;
|
||||||
secret.DeletedDate = null;
|
|
||||||
secret.RevisionDate = utcNow;
|
await dbContext.Secret.Where(c => secretIds.Contains(c.Id))
|
||||||
});
|
.ExecuteUpdateAsync(setters =>
|
||||||
await dbContext.SaveChangesAsync();
|
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)
|
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;
|
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)
|
public async Task<(bool Read, bool Write)> AccessToSecretAsync(Guid id, Guid userId, AccessClientType accessType)
|
||||||
{
|
{
|
||||||
using var scope = ServiceScopeFactory.CreateScope();
|
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.UserAccessPolicies.Any(ap => ap.OrganizationUser.UserId == userId && ap.Read) ||
|
||||||
p.GroupAccessPolicies.Any(ap =>
|
p.GroupAccessPolicies.Any(ap =>
|
||||||
ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.UserId == userId && ap.Read)));
|
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.AuthorizationRequirements;
|
||||||
using Bit.Core.SecretsManager.Commands.Secrets.Interfaces;
|
using Bit.Core.SecretsManager.Commands.Secrets.Interfaces;
|
||||||
using Bit.Core.SecretsManager.Entities;
|
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.SecretsManager.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Tools.Enums;
|
using Bit.Core.Tools.Enums;
|
||||||
@ -29,6 +32,8 @@ public class SecretsController : Controller
|
|||||||
private readonly ICreateSecretCommand _createSecretCommand;
|
private readonly ICreateSecretCommand _createSecretCommand;
|
||||||
private readonly IUpdateSecretCommand _updateSecretCommand;
|
private readonly IUpdateSecretCommand _updateSecretCommand;
|
||||||
private readonly IDeleteSecretCommand _deleteSecretCommand;
|
private readonly IDeleteSecretCommand _deleteSecretCommand;
|
||||||
|
private readonly IAccessClientQuery _accessClientQuery;
|
||||||
|
private readonly ISecretsSyncQuery _secretsSyncQuery;
|
||||||
private readonly IUserService _userService;
|
private readonly IUserService _userService;
|
||||||
private readonly IEventService _eventService;
|
private readonly IEventService _eventService;
|
||||||
private readonly IReferenceEventService _referenceEventService;
|
private readonly IReferenceEventService _referenceEventService;
|
||||||
@ -42,6 +47,8 @@ public class SecretsController : Controller
|
|||||||
ICreateSecretCommand createSecretCommand,
|
ICreateSecretCommand createSecretCommand,
|
||||||
IUpdateSecretCommand updateSecretCommand,
|
IUpdateSecretCommand updateSecretCommand,
|
||||||
IDeleteSecretCommand deleteSecretCommand,
|
IDeleteSecretCommand deleteSecretCommand,
|
||||||
|
IAccessClientQuery accessClientQuery,
|
||||||
|
ISecretsSyncQuery secretsSyncQuery,
|
||||||
IUserService userService,
|
IUserService userService,
|
||||||
IEventService eventService,
|
IEventService eventService,
|
||||||
IReferenceEventService referenceEventService,
|
IReferenceEventService referenceEventService,
|
||||||
@ -54,6 +61,8 @@ public class SecretsController : Controller
|
|||||||
_createSecretCommand = createSecretCommand;
|
_createSecretCommand = createSecretCommand;
|
||||||
_updateSecretCommand = updateSecretCommand;
|
_updateSecretCommand = updateSecretCommand;
|
||||||
_deleteSecretCommand = deleteSecretCommand;
|
_deleteSecretCommand = deleteSecretCommand;
|
||||||
|
_accessClientQuery = accessClientQuery;
|
||||||
|
_secretsSyncQuery = secretsSyncQuery;
|
||||||
_userService = userService;
|
_userService = userService;
|
||||||
_eventService = eventService;
|
_eventService = eventService;
|
||||||
_referenceEventService = referenceEventService;
|
_referenceEventService = referenceEventService;
|
||||||
@ -73,7 +82,7 @@ public class SecretsController : Controller
|
|||||||
var orgAdmin = await _currentContext.OrganizationAdmin(organizationId);
|
var orgAdmin = await _currentContext.OrganizationAdmin(organizationId);
|
||||||
var accessClient = AccessClientHelper.ToAccessClient(_currentContext.ClientType, orgAdmin);
|
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);
|
return new SecretWithProjectsListResponseModel(secrets);
|
||||||
}
|
}
|
||||||
@ -139,7 +148,7 @@ public class SecretsController : Controller
|
|||||||
var orgAdmin = await _currentContext.OrganizationAdmin(project.OrganizationId);
|
var orgAdmin = await _currentContext.OrganizationAdmin(project.OrganizationId);
|
||||||
var accessClient = AccessClientHelper.ToAccessClient(_currentContext.ClientType, orgAdmin);
|
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);
|
return new SecretWithProjectsListResponseModel(secrets);
|
||||||
}
|
}
|
||||||
@ -246,4 +255,35 @@ public class SecretsController : Controller
|
|||||||
var responses = secrets.Select(s => new BaseSecretResponseModel(s));
|
var responses = secrets.Select(s => new BaseSecretResponseModel(s));
|
||||||
return new ListResponseModel<BaseSecretResponseModel>(responses);
|
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 userId = _userService.GetProperUserId(User).Value;
|
||||||
var projects = await _projectRepository.GetManyByOrganizationIdAsync(organizationId, userId, AccessClientType.NoAccessCheck);
|
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)
|
if (projects == null && secrets == null)
|
||||||
{
|
{
|
||||||
|
@ -41,7 +41,7 @@ public class TrashController : Controller
|
|||||||
throw new UnauthorizedAccessException();
|
throw new UnauthorizedAccessException();
|
||||||
}
|
}
|
||||||
|
|
||||||
var secrets = await _secretRepository.GetManyByOrganizationIdInTrashAsync(organizationId);
|
var secrets = await _secretRepository.GetManyDetailsByOrganizationIdInTrashAsync(organizationId);
|
||||||
return new SecretWithProjectsListResponseModel(secrets);
|
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
|
public interface ISecretRepository
|
||||||
{
|
{
|
||||||
Task<IEnumerable<SecretPermissionDetails>> GetManyByOrganizationIdAsync(Guid organizationId, Guid userId, AccessClientType accessType);
|
Task<IEnumerable<SecretPermissionDetails>> GetManyDetailsByOrganizationIdAsync(Guid organizationId, Guid userId, AccessClientType accessType);
|
||||||
Task<IEnumerable<SecretPermissionDetails>> GetManyByOrganizationIdInTrashAsync(Guid organizationId);
|
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>> GetManyByOrganizationIdInTrashByIdsAsync(Guid organizationId, IEnumerable<Guid> ids);
|
||||||
Task<IEnumerable<Secret>> GetManyByIds(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> GetByIdAsync(Guid id);
|
||||||
Task<Secret> CreateAsync(Secret secret);
|
Task<Secret> CreateAsync(Secret secret);
|
||||||
Task<Secret> UpdateAsync(Secret secret);
|
Task<Secret> UpdateAsync(Secret secret);
|
||||||
@ -18,7 +19,6 @@ public interface ISecretRepository
|
|||||||
Task HardDeleteManyByIdAsync(IEnumerable<Guid> ids);
|
Task HardDeleteManyByIdAsync(IEnumerable<Guid> ids);
|
||||||
Task RestoreManyByIdAsync(IEnumerable<Guid> ids);
|
Task RestoreManyByIdAsync(IEnumerable<Guid> ids);
|
||||||
Task<IEnumerable<Secret>> ImportAsync(IEnumerable<Secret> secrets);
|
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<(bool Read, bool Write)> AccessToSecretAsync(Guid id, Guid userId, AccessClientType accessType);
|
||||||
Task EmptyTrash(DateTime nowTime, uint deleteAfterThisNumberOfDays);
|
Task EmptyTrash(DateTime nowTime, uint deleteAfterThisNumberOfDays);
|
||||||
Task<int> GetSecretsCountByOrganizationIdAsync(Guid organizationId);
|
Task<int> GetSecretsCountByOrganizationIdAsync(Guid organizationId);
|
||||||
|
@ -6,17 +6,23 @@ namespace Bit.Core.SecretsManager.Repositories.Noop;
|
|||||||
|
|
||||||
public class NoopSecretRepository : ISecretRepository
|
public class NoopSecretRepository : ISecretRepository
|
||||||
{
|
{
|
||||||
public Task<IEnumerable<SecretPermissionDetails>> GetManyByOrganizationIdAsync(Guid organizationId, Guid userId,
|
public Task<IEnumerable<SecretPermissionDetails>> GetManyDetailsByOrganizationIdAsync(Guid organizationId, Guid userId,
|
||||||
AccessClientType accessType)
|
AccessClientType accessType)
|
||||||
{
|
{
|
||||||
return Task.FromResult(null as IEnumerable<SecretPermissionDetails>);
|
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>);
|
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,
|
public Task<IEnumerable<Secret>> GetManyByOrganizationIdInTrashByIdsAsync(Guid organizationId,
|
||||||
IEnumerable<Guid> ids)
|
IEnumerable<Guid> ids)
|
||||||
{
|
{
|
||||||
@ -28,7 +34,7 @@ public class NoopSecretRepository : ISecretRepository
|
|||||||
return Task.FromResult(null as IEnumerable<Secret>);
|
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)
|
AccessClientType accessType)
|
||||||
{
|
{
|
||||||
return Task.FromResult(null as IEnumerable<SecretPermissionDetails>);
|
return Task.FromResult(null as IEnumerable<SecretPermissionDetails>);
|
||||||
@ -69,11 +75,6 @@ public class NoopSecretRepository : ISecretRepository
|
|||||||
return Task.FromResult(null as IEnumerable<Secret>);
|
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)
|
public Task<(bool Read, bool Write)> AccessToSecretAsync(Guid id, Guid userId, AccessClientType accessType)
|
||||||
{
|
{
|
||||||
return Task.FromResult((false, false));
|
return Task.FromResult((false, false));
|
||||||
|
@ -22,6 +22,7 @@ public class SecretsControllerTests : IClassFixture<ApiApplicationFactory>, IAsy
|
|||||||
private readonly ApiApplicationFactory _factory;
|
private readonly ApiApplicationFactory _factory;
|
||||||
private readonly ISecretRepository _secretRepository;
|
private readonly ISecretRepository _secretRepository;
|
||||||
private readonly IProjectRepository _projectRepository;
|
private readonly IProjectRepository _projectRepository;
|
||||||
|
private readonly IServiceAccountRepository _serviceAccountRepository;
|
||||||
private readonly IAccessPolicyRepository _accessPolicyRepository;
|
private readonly IAccessPolicyRepository _accessPolicyRepository;
|
||||||
private readonly LoginHelper _loginHelper;
|
private readonly LoginHelper _loginHelper;
|
||||||
|
|
||||||
@ -35,6 +36,7 @@ public class SecretsControllerTests : IClassFixture<ApiApplicationFactory>, IAsy
|
|||||||
_secretRepository = _factory.GetService<ISecretRepository>();
|
_secretRepository = _factory.GetService<ISecretRepository>();
|
||||||
_projectRepository = _factory.GetService<IProjectRepository>();
|
_projectRepository = _factory.GetService<IProjectRepository>();
|
||||||
_accessPolicyRepository = _factory.GetService<IAccessPolicyRepository>();
|
_accessPolicyRepository = _factory.GetService<IAccessPolicyRepository>();
|
||||||
|
_serviceAccountRepository = _factory.GetService<IServiceAccountRepository>();
|
||||||
_loginHelper = new LoginHelper(_factory, _client);
|
_loginHelper = new LoginHelper(_factory, _client);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -264,6 +266,7 @@ public class SecretsControllerTests : IClassFixture<ApiApplicationFactory>, IAsy
|
|||||||
{
|
{
|
||||||
var (email, orgUser) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);
|
var (email, orgUser) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);
|
||||||
await _loginHelper.LoginAsync(email);
|
await _loginHelper.LoginAsync(email);
|
||||||
|
await _loginHelper.LoginAsync(email);
|
||||||
accessType = AccessClientType.User;
|
accessType = AccessClientType.User;
|
||||||
|
|
||||||
var accessPolicies = new List<BaseAccessPolicy>
|
var accessPolicies = new List<BaseAccessPolicy>
|
||||||
@ -288,7 +291,7 @@ public class SecretsControllerTests : IClassFixture<ApiApplicationFactory>, IAsy
|
|||||||
secretResponse.EnsureSuccessStatusCode();
|
secretResponse.EnsureSuccessStatusCode();
|
||||||
var secretResult = await secretResponse.Content.ReadFromJsonAsync<SecretResponseModel>();
|
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;
|
var secret = result.Secret;
|
||||||
|
|
||||||
Assert.NotNull(secretResult);
|
Assert.NotNull(secretResult);
|
||||||
@ -392,6 +395,7 @@ public class SecretsControllerTests : IClassFixture<ApiApplicationFactory>, IAsy
|
|||||||
{
|
{
|
||||||
var (org, _) = await _organizationHelper.Initialize(useSecrets, accessSecrets, organizationEnabled);
|
var (org, _) = await _organizationHelper.Initialize(useSecrets, accessSecrets, organizationEnabled);
|
||||||
await _loginHelper.LoginAsync(_email);
|
await _loginHelper.LoginAsync(_email);
|
||||||
|
await _loginHelper.LoginAsync(_email);
|
||||||
|
|
||||||
var project = await _projectRepository.CreateAsync(new Project
|
var project = await _projectRepository.CreateAsync(new Project
|
||||||
{
|
{
|
||||||
@ -784,6 +788,106 @@ public class SecretsControllerTests : IClassFixture<ApiApplicationFactory>, IAsy
|
|||||||
Assert.Equal(secretIds.Count, result.Data.Count());
|
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)
|
private async Task<(Project Project, List<Guid> secretIds)> CreateSecretsAsync(Guid orgId, int numberToCreate = 3)
|
||||||
{
|
{
|
||||||
var project = await _projectRepository.CreateAsync(new Project
|
var project = await _projectRepository.CreateAsync(new Project
|
||||||
@ -853,4 +957,25 @@ public class SecretsControllerTests : IClassFixture<ApiApplicationFactory>, IAsy
|
|||||||
throw new ArgumentOutOfRangeException(nameof(permissionType), permissionType, null);
|
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.Commands.Secrets.Interfaces;
|
||||||
using Bit.Core.SecretsManager.Entities;
|
using Bit.Core.SecretsManager.Entities;
|
||||||
using Bit.Core.SecretsManager.Models.Data;
|
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.SecretsManager.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Test.SecretsManager.AutoFixture.SecretsFixture;
|
using Bit.Core.Test.SecretsManager.AutoFixture.SecretsFixture;
|
||||||
@ -37,7 +39,7 @@ public class SecretsControllerTests
|
|||||||
var result = await sutProvider.Sut.ListByOrganizationAsync(id);
|
var result = await sutProvider.Sut.ListByOrganizationAsync(id);
|
||||||
|
|
||||||
await sutProvider.GetDependency<ISecretRepository>().Received(1)
|
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);
|
Assert.Empty(result.Secrets);
|
||||||
}
|
}
|
||||||
@ -45,10 +47,10 @@ public class SecretsControllerTests
|
|||||||
[Theory]
|
[Theory]
|
||||||
[BitAutoData(PermissionType.RunAsAdmin)]
|
[BitAutoData(PermissionType.RunAsAdmin)]
|
||||||
[BitAutoData(PermissionType.RunAsUserWithPermission)]
|
[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<ICurrentContext>().AccessSecretsManager(default).ReturnsForAnyArgs(true);
|
||||||
sutProvider.GetDependency<ISecretRepository>().GetManyByOrganizationIdAsync(default, default, default)
|
sutProvider.GetDependency<ISecretRepository>().GetManyDetailsByOrganizationIdAsync(default, default, default)
|
||||||
.ReturnsForAnyArgs(new List<SecretPermissionDetails>
|
.ReturnsForAnyArgs(new List<SecretPermissionDetails>
|
||||||
{
|
{
|
||||||
new() { Secret = resultSecret, Read = true, Write = true },
|
new() { Secret = resultSecret, Read = true, Write = true },
|
||||||
@ -61,22 +63,22 @@ public class SecretsControllerTests
|
|||||||
}
|
}
|
||||||
else
|
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<ICurrentContext>().OrganizationAdmin(organizationId).Returns(false);
|
||||||
sutProvider.GetDependency<IProjectRepository>().AccessToProjectAsync(default, default, default)
|
sutProvider.GetDependency<IProjectRepository>().AccessToProjectAsync(default, default, default)
|
||||||
.Returns((true, true));
|
.Returns((true, true));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
var result = await sutProvider.Sut.ListByOrganizationAsync(resultSecret.OrganizationId);
|
await sutProvider.Sut.ListByOrganizationAsync(resultSecret.OrganizationId);
|
||||||
|
|
||||||
await sutProvider.GetDependency<ISecretRepository>().Received(1)
|
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]
|
[Theory]
|
||||||
[BitAutoData]
|
[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);
|
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(default).ReturnsForAnyArgs(false);
|
||||||
|
|
||||||
@ -429,6 +431,76 @@ public class SecretsControllerTests
|
|||||||
Assert.Equal(data.Count, results.Data.Count());
|
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(
|
private static (List<Guid> Ids, GetSecretsRequestModel request) BuildGetSecretsRequestModel(
|
||||||
IEnumerable<Secret> data)
|
IEnumerable<Secret> data)
|
||||||
{
|
{
|
||||||
@ -447,4 +519,24 @@ public class SecretsControllerTests
|
|||||||
|
|
||||||
return organizationId;
|
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…
x
Reference in New Issue
Block a user