mirror of
https://github.com/bitwarden/server.git
synced 2025-04-05 21:18:13 -05:00
[SM-863] Add endpoint for fetching multiple secrets by IDs (#3134)
* Add support CanReadSecret authorization * Extract base response model for secret * Add support for SA bulk fetching event logging * secret repository bug fix * Add endpoint and request for bulk fetching secrets * Swap to original reference event * Add unit tests * Add integration tests * Add unit tests for authz handler * update authz handler tests ---------
This commit is contained in:
parent
8eee9b330d
commit
640cb68d51
@ -38,6 +38,9 @@ public class SecretAuthorizationHandler : AuthorizationHandler<SecretOperationRe
|
|||||||
case not null when requirement == SecretOperations.Create:
|
case not null when requirement == SecretOperations.Create:
|
||||||
await CanCreateSecretAsync(context, requirement, resource);
|
await CanCreateSecretAsync(context, requirement, resource);
|
||||||
break;
|
break;
|
||||||
|
case not null when requirement == SecretOperations.Read:
|
||||||
|
await CanReadSecretAsync(context, requirement, resource);
|
||||||
|
break;
|
||||||
case not null when requirement == SecretOperations.Update:
|
case not null when requirement == SecretOperations.Update:
|
||||||
await CanUpdateSecretAsync(context, requirement, resource);
|
await CanUpdateSecretAsync(context, requirement, resource);
|
||||||
break;
|
break;
|
||||||
@ -85,6 +88,18 @@ public class SecretAuthorizationHandler : AuthorizationHandler<SecretOperationRe
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task CanReadSecretAsync(AuthorizationHandlerContext context,
|
||||||
|
SecretOperationRequirement requirement, Secret resource)
|
||||||
|
{
|
||||||
|
var (accessClient, userId) = await _accessClientQuery.GetAccessClientAsync(context.User, resource.OrganizationId);
|
||||||
|
|
||||||
|
var access = await _secretRepository.AccessToSecretAsync(resource.Id, userId, accessClient);
|
||||||
|
|
||||||
|
if (access.Read)
|
||||||
|
{
|
||||||
|
context.Succeed(requirement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async Task CanUpdateSecretAsync(AuthorizationHandlerContext context,
|
private async Task CanUpdateSecretAsync(AuthorizationHandlerContext context,
|
||||||
SecretOperationRequirement requirement, Secret resource)
|
SecretOperationRequirement requirement, Secret resource)
|
||||||
|
@ -329,7 +329,8 @@ public class SecretRepository : Repository<Core.SecretsManager.Entities.Secret,
|
|||||||
{
|
{
|
||||||
Secret = Mapper.Map<Bit.Core.SecretsManager.Entities.Secret>(s),
|
Secret = Mapper.Map<Bit.Core.SecretsManager.Entities.Secret>(s),
|
||||||
Read = true,
|
Read = true,
|
||||||
Write = false,
|
Write = s.Projects.Any(p =>
|
||||||
|
p.ServiceAccountAccessPolicies.Any(ap => ap.ServiceAccountId == userId && ap.Write)),
|
||||||
}),
|
}),
|
||||||
_ => throw new ArgumentOutOfRangeException(nameof(accessType), accessType, null),
|
_ => throw new ArgumentOutOfRangeException(nameof(accessType), accessType, null),
|
||||||
};
|
};
|
||||||
|
@ -232,6 +232,69 @@ public class SecretAuthorizationHandlerTests
|
|||||||
Assert.True(authzContext.HasSucceeded);
|
Assert.True(authzContext.HasSucceeded);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task CanReadSecret_AccessToSecretsManagerFalse_DoesNotSucceed(
|
||||||
|
SutProvider<SecretAuthorizationHandler> sutProvider, Secret secret,
|
||||||
|
ClaimsPrincipal claimsPrincipal)
|
||||||
|
{
|
||||||
|
var requirement = SecretOperations.Read;
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(secret.OrganizationId)
|
||||||
|
.Returns(false);
|
||||||
|
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
|
||||||
|
claimsPrincipal, secret);
|
||||||
|
|
||||||
|
await sutProvider.Sut.HandleAsync(authzContext);
|
||||||
|
|
||||||
|
Assert.False(authzContext.HasSucceeded);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task CanReadSecret_NullResource_DoesNotSucceed(
|
||||||
|
SutProvider<SecretAuthorizationHandler> sutProvider, Secret secret,
|
||||||
|
ClaimsPrincipal claimsPrincipal,
|
||||||
|
Guid userId)
|
||||||
|
{
|
||||||
|
var requirement = SecretOperations.Read;
|
||||||
|
SetupPermission(sutProvider, PermissionType.RunAsAdmin, secret.OrganizationId, userId);
|
||||||
|
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
|
||||||
|
claimsPrincipal, null);
|
||||||
|
|
||||||
|
await sutProvider.Sut.HandleAsync(authzContext);
|
||||||
|
|
||||||
|
Assert.False(authzContext.HasSucceeded);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData(PermissionType.RunAsAdmin, true, true, true)]
|
||||||
|
[BitAutoData(PermissionType.RunAsUserWithPermission, false, false, false)]
|
||||||
|
[BitAutoData(PermissionType.RunAsUserWithPermission, false, true, false)]
|
||||||
|
[BitAutoData(PermissionType.RunAsUserWithPermission, true, false, true)]
|
||||||
|
[BitAutoData(PermissionType.RunAsUserWithPermission, true, true, true)]
|
||||||
|
[BitAutoData(PermissionType.RunAsServiceAccountWithPermission, false, false, false)]
|
||||||
|
[BitAutoData(PermissionType.RunAsServiceAccountWithPermission, false, true, false)]
|
||||||
|
[BitAutoData(PermissionType.RunAsServiceAccountWithPermission, true, false, true)]
|
||||||
|
[BitAutoData(PermissionType.RunAsServiceAccountWithPermission, true, true, true)]
|
||||||
|
public async Task CanReadSecret_AccessCheck(PermissionType permissionType, bool read, bool write,
|
||||||
|
bool expected,
|
||||||
|
SutProvider<SecretAuthorizationHandler> sutProvider, Secret secret,
|
||||||
|
ClaimsPrincipal claimsPrincipal,
|
||||||
|
Guid userId)
|
||||||
|
{
|
||||||
|
var requirement = SecretOperations.Read;
|
||||||
|
SetupPermission(sutProvider, permissionType, secret.OrganizationId, userId);
|
||||||
|
sutProvider.GetDependency<ISecretRepository>()
|
||||||
|
.AccessToSecretAsync(secret.Id, userId, Arg.Any<AccessClientType>())
|
||||||
|
.Returns((read, write));
|
||||||
|
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
|
||||||
|
claimsPrincipal, secret);
|
||||||
|
|
||||||
|
await sutProvider.Sut.HandleAsync(authzContext);
|
||||||
|
|
||||||
|
Assert.Equal(expected, authzContext.HasSucceeded);
|
||||||
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[BitAutoData]
|
[BitAutoData]
|
||||||
public async Task CanUpdateSecret_AccessToSecretsManagerFalse_DoesNotSucceed(
|
public async Task CanUpdateSecret_AccessToSecretsManagerFalse_DoesNotSucceed(
|
||||||
|
@ -207,4 +207,45 @@ public class SecretsController : Controller
|
|||||||
var responses = results.Select(r => new BulkDeleteResponseModel(r.Secret.Id, r.Error));
|
var responses = results.Select(r => new BulkDeleteResponseModel(r.Secret.Id, r.Error));
|
||||||
return new ListResponseModel<BulkDeleteResponseModel>(responses);
|
return new ListResponseModel<BulkDeleteResponseModel>(responses);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpPost("secrets/get-by-ids")]
|
||||||
|
public async Task<ListResponseModel<BaseSecretResponseModel>> GetSecretsByIdsAsync(
|
||||||
|
[FromBody] GetSecretsRequestModel request)
|
||||||
|
{
|
||||||
|
var secrets = (await _secretRepository.GetManyByIds(request.Ids)).ToList();
|
||||||
|
if (!secrets.Any() || secrets.Count != request.Ids.Count())
|
||||||
|
{
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure all secrets belong to the same organization.
|
||||||
|
var organizationId = secrets.First().OrganizationId;
|
||||||
|
if (secrets.Any(secret => secret.OrganizationId != organizationId) ||
|
||||||
|
!_currentContext.AccessSecretsManager(organizationId))
|
||||||
|
{
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
foreach (var secret in secrets)
|
||||||
|
{
|
||||||
|
var authorizationResult = await _authorizationService.AuthorizeAsync(User, secret, SecretOperations.Read);
|
||||||
|
if (!authorizationResult.Succeeded)
|
||||||
|
{
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_currentContext.ClientType == ClientType.ServiceAccount)
|
||||||
|
{
|
||||||
|
var userId = _userService.GetProperUserId(User).Value;
|
||||||
|
var org = await _organizationRepository.GetByIdAsync(organizationId);
|
||||||
|
await _eventService.LogServiceAccountSecretsEventAsync(userId, secrets, EventType.Secret_Retrieved);
|
||||||
|
await _referenceEventService.RaiseEventAsync(
|
||||||
|
new ReferenceEvent(ReferenceEventType.SmServiceAccountAccessedSecret, org, _currentContext));
|
||||||
|
}
|
||||||
|
|
||||||
|
var responses = secrets.Select(s => new BaseSecretResponseModel(s));
|
||||||
|
return new ListResponseModel<BaseSecretResponseModel>(responses);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,9 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace Bit.Api.SecretsManager.Models.Request;
|
||||||
|
|
||||||
|
public class GetSecretsRequestModel
|
||||||
|
{
|
||||||
|
[Required]
|
||||||
|
public IEnumerable<Guid> Ids { get; set; }
|
||||||
|
}
|
@ -0,0 +1,66 @@
|
|||||||
|
using Bit.Core.Models.Api;
|
||||||
|
using Bit.Core.SecretsManager.Entities;
|
||||||
|
|
||||||
|
namespace Bit.Api.SecretsManager.Models.Response;
|
||||||
|
|
||||||
|
public class BaseSecretResponseModel : ResponseModel
|
||||||
|
{
|
||||||
|
private const string _objectName = "baseSecret";
|
||||||
|
|
||||||
|
public BaseSecretResponseModel(Secret secret, string objectName = _objectName) : base(objectName)
|
||||||
|
{
|
||||||
|
if (secret == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(secret));
|
||||||
|
}
|
||||||
|
|
||||||
|
Id = secret.Id;
|
||||||
|
OrganizationId = secret.OrganizationId;
|
||||||
|
Key = secret.Key;
|
||||||
|
Value = secret.Value;
|
||||||
|
Note = secret.Note;
|
||||||
|
CreationDate = secret.CreationDate;
|
||||||
|
RevisionDate = secret.RevisionDate;
|
||||||
|
Projects = secret.Projects?.Select(p => new SecretResponseInnerProject(p));
|
||||||
|
}
|
||||||
|
|
||||||
|
public BaseSecretResponseModel(string objectName = _objectName) : base(objectName)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public BaseSecretResponseModel() : base(_objectName)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
|
||||||
|
public Guid OrganizationId { get; set; }
|
||||||
|
|
||||||
|
public string Key { get; set; }
|
||||||
|
|
||||||
|
public string Value { get; set; }
|
||||||
|
|
||||||
|
public string Note { get; set; }
|
||||||
|
|
||||||
|
public DateTime CreationDate { get; set; }
|
||||||
|
|
||||||
|
public DateTime RevisionDate { get; set; }
|
||||||
|
|
||||||
|
public IEnumerable<SecretResponseInnerProject> Projects { get; set; }
|
||||||
|
|
||||||
|
public class SecretResponseInnerProject
|
||||||
|
{
|
||||||
|
public SecretResponseInnerProject(Project project)
|
||||||
|
{
|
||||||
|
Id = project.Id;
|
||||||
|
Name = project.Name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SecretResponseInnerProject()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public string Name { get; set; }
|
||||||
|
}
|
||||||
|
}
|
@ -1,28 +1,13 @@
|
|||||||
using Bit.Core.Models.Api;
|
using Bit.Core.SecretsManager.Entities;
|
||||||
using Bit.Core.SecretsManager.Entities;
|
|
||||||
|
|
||||||
namespace Bit.Api.SecretsManager.Models.Response;
|
namespace Bit.Api.SecretsManager.Models.Response;
|
||||||
|
|
||||||
public class SecretResponseModel : ResponseModel
|
public class SecretResponseModel : BaseSecretResponseModel
|
||||||
{
|
{
|
||||||
private const string _objectName = "secret";
|
private const string _objectName = "secret";
|
||||||
|
|
||||||
public SecretResponseModel(Secret secret, bool read, bool write) : base(_objectName)
|
public SecretResponseModel(Secret secret, bool read, bool write) : base(secret, _objectName)
|
||||||
{
|
{
|
||||||
if (secret == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(secret));
|
|
||||||
}
|
|
||||||
|
|
||||||
Id = secret.Id;
|
|
||||||
OrganizationId = secret.OrganizationId;
|
|
||||||
Key = secret.Key;
|
|
||||||
Value = secret.Value;
|
|
||||||
Note = secret.Note;
|
|
||||||
CreationDate = secret.CreationDate;
|
|
||||||
RevisionDate = secret.RevisionDate;
|
|
||||||
Projects = secret.Projects?.Select(p => new SecretResponseInnerProject(p));
|
|
||||||
|
|
||||||
Read = read;
|
Read = read;
|
||||||
Write = write;
|
Write = write;
|
||||||
}
|
}
|
||||||
@ -31,39 +16,7 @@ public class SecretResponseModel : ResponseModel
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public Guid Id { get; set; }
|
|
||||||
|
|
||||||
public Guid OrganizationId { get; set; }
|
|
||||||
|
|
||||||
public string Key { get; set; }
|
|
||||||
|
|
||||||
public string Value { get; set; }
|
|
||||||
|
|
||||||
public string Note { get; set; }
|
|
||||||
|
|
||||||
public DateTime CreationDate { get; set; }
|
|
||||||
|
|
||||||
public DateTime RevisionDate { get; set; }
|
|
||||||
|
|
||||||
public IEnumerable<SecretResponseInnerProject> Projects { get; set; }
|
|
||||||
|
|
||||||
public bool Read { get; set; }
|
public bool Read { get; set; }
|
||||||
|
|
||||||
public bool Write { get; set; }
|
public bool Write { get; set; }
|
||||||
|
|
||||||
public class SecretResponseInnerProject
|
|
||||||
{
|
|
||||||
public SecretResponseInnerProject(Project project)
|
|
||||||
{
|
|
||||||
Id = project.Id;
|
|
||||||
Name = project.Name;
|
|
||||||
}
|
|
||||||
|
|
||||||
public SecretResponseInnerProject()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public Guid Id { get; set; }
|
|
||||||
public string Name { get; set; }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,7 @@ public class SecretOperationRequirement : OperationAuthorizationRequirement
|
|||||||
public static class SecretOperations
|
public static class SecretOperations
|
||||||
{
|
{
|
||||||
public static readonly SecretOperationRequirement Create = new() { Name = nameof(Create) };
|
public static readonly SecretOperationRequirement Create = new() { Name = nameof(Create) };
|
||||||
|
public static readonly SecretOperationRequirement Read = new() { Name = nameof(Read) };
|
||||||
public static readonly SecretOperationRequirement Update = new() { Name = nameof(Update) };
|
public static readonly SecretOperationRequirement Update = new() { Name = nameof(Update) };
|
||||||
public static readonly SecretOperationRequirement Delete = new() { Name = nameof(Delete) };
|
public static readonly SecretOperationRequirement Delete = new() { Name = nameof(Delete) };
|
||||||
}
|
}
|
||||||
|
@ -29,4 +29,5 @@ public interface IEventService
|
|||||||
Task LogOrganizationDomainEventAsync(OrganizationDomain organizationDomain, EventType type, DateTime? date = null);
|
Task LogOrganizationDomainEventAsync(OrganizationDomain organizationDomain, EventType type, DateTime? date = null);
|
||||||
Task LogOrganizationDomainEventAsync(OrganizationDomain organizationDomain, EventType type, EventSystemUser systemUser, DateTime? date = null);
|
Task LogOrganizationDomainEventAsync(OrganizationDomain organizationDomain, EventType type, EventSystemUser systemUser, DateTime? date = null);
|
||||||
Task LogServiceAccountSecretEventAsync(Guid serviceAccountId, Secret secret, EventType type, DateTime? date = null);
|
Task LogServiceAccountSecretEventAsync(Guid serviceAccountId, Secret secret, EventType type, DateTime? date = null);
|
||||||
|
Task LogServiceAccountSecretsEventAsync(Guid serviceAccountId, IEnumerable<Secret> secrets, EventType type, DateTime? date = null);
|
||||||
}
|
}
|
||||||
|
@ -406,22 +406,34 @@ public class EventService : IEventService
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async Task LogServiceAccountSecretEventAsync(Guid serviceAccountId, Secret secret, EventType type, DateTime? date = null)
|
public async Task LogServiceAccountSecretEventAsync(Guid serviceAccountId, Secret secret, EventType type, DateTime? date = null)
|
||||||
|
{
|
||||||
|
await LogServiceAccountSecretsEventAsync(serviceAccountId, new[] { secret }, type, date);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task LogServiceAccountSecretsEventAsync(Guid serviceAccountId, IEnumerable<Secret> secrets, EventType type, DateTime? date = null)
|
||||||
{
|
{
|
||||||
var orgAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync();
|
var orgAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync();
|
||||||
if (!CanUseEvents(orgAbilities, secret.OrganizationId))
|
var eventMessages = new List<IEvent>();
|
||||||
|
|
||||||
|
foreach (var secret in secrets)
|
||||||
{
|
{
|
||||||
return;
|
if (!CanUseEvents(orgAbilities, secret.OrganizationId))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var e = new EventMessage(_currentContext)
|
||||||
|
{
|
||||||
|
OrganizationId = secret.OrganizationId,
|
||||||
|
Type = type,
|
||||||
|
SecretId = secret.Id,
|
||||||
|
ServiceAccountId = serviceAccountId,
|
||||||
|
Date = date.GetValueOrDefault(DateTime.UtcNow)
|
||||||
|
};
|
||||||
|
eventMessages.Add(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
var e = new EventMessage(_currentContext)
|
await _eventWriteService.CreateManyAsync(eventMessages);
|
||||||
{
|
|
||||||
OrganizationId = secret.OrganizationId,
|
|
||||||
Type = type,
|
|
||||||
SecretId = secret.Id,
|
|
||||||
ServiceAccountId = serviceAccountId,
|
|
||||||
Date = date.GetValueOrDefault(DateTime.UtcNow)
|
|
||||||
};
|
|
||||||
await _eventWriteService.CreateAsync(e);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<Guid?> GetProviderIdAsync(Guid? orgId)
|
private async Task<Guid?> GetProviderIdAsync(Guid? orgId)
|
||||||
|
@ -119,4 +119,10 @@ public class NoopEventService : IEventService
|
|||||||
{
|
{
|
||||||
return Task.FromResult(0);
|
return Task.FromResult(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Task LogServiceAccountSecretsEventAsync(Guid serviceAccountId, IEnumerable<Secret> secrets, EventType type,
|
||||||
|
DateTime? date = null)
|
||||||
|
{
|
||||||
|
return Task.FromResult(0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -709,6 +709,69 @@ public class SecretsControllerTests : IClassFixture<ApiApplicationFactory>, IAsy
|
|||||||
Assert.Empty(secrets);
|
Assert.Empty(secrets);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(false, false)]
|
||||||
|
[InlineData(true, false)]
|
||||||
|
[InlineData(false, true)]
|
||||||
|
public async Task GetSecretsByIds_SmNotEnabled_NotFound(bool useSecrets, bool accessSecrets)
|
||||||
|
{
|
||||||
|
var (org, _) = await _organizationHelper.Initialize(useSecrets, accessSecrets);
|
||||||
|
await LoginAsync(_email);
|
||||||
|
|
||||||
|
var secret = await _secretRepository.CreateAsync(new Secret
|
||||||
|
{
|
||||||
|
OrganizationId = org.Id,
|
||||||
|
Key = _mockEncryptedString,
|
||||||
|
Value = _mockEncryptedString,
|
||||||
|
Note = _mockEncryptedString,
|
||||||
|
});
|
||||||
|
|
||||||
|
var request = new GetSecretsRequestModel { Ids = new[] { secret.Id } };
|
||||||
|
|
||||||
|
var response = await _client.PostAsJsonAsync("/secrets/get-by-ids", request);
|
||||||
|
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(PermissionType.RunAsAdmin)]
|
||||||
|
[InlineData(PermissionType.RunAsUserWithPermission)]
|
||||||
|
public async Task GetSecretsByIds_Success(PermissionType permissionType)
|
||||||
|
{
|
||||||
|
var (org, _) = await _organizationHelper.Initialize(true, true);
|
||||||
|
await LoginAsync(_email);
|
||||||
|
|
||||||
|
var (project, secretIds) = await CreateSecretsAsync(org.Id);
|
||||||
|
|
||||||
|
if (permissionType == PermissionType.RunAsUserWithPermission)
|
||||||
|
{
|
||||||
|
var (email, orgUser) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);
|
||||||
|
await LoginAsync(email);
|
||||||
|
|
||||||
|
var accessPolicies = new List<BaseAccessPolicy>
|
||||||
|
{
|
||||||
|
new UserProjectAccessPolicy
|
||||||
|
{
|
||||||
|
GrantedProjectId = project.Id, OrganizationUserId = orgUser.Id, Read = true, Write = true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
await _accessPolicyRepository.CreateManyAsync(accessPolicies);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var (email, _) = await _organizationHelper.CreateNewUser(OrganizationUserType.Admin, true);
|
||||||
|
await LoginAsync(email);
|
||||||
|
}
|
||||||
|
|
||||||
|
var request = new GetSecretsRequestModel { Ids = secretIds };
|
||||||
|
|
||||||
|
var response = await _client.PostAsJsonAsync("/secrets/get-by-ids", request);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
var result = await response.Content.ReadFromJsonAsync<ListResponseModel<BaseSecretResponseModel>>();
|
||||||
|
Assert.NotNull(result);
|
||||||
|
Assert.NotEmpty(result!.Data);
|
||||||
|
Assert.Equal(secretIds.Count, result!.Data.Count());
|
||||||
|
}
|
||||||
|
|
||||||
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
|
||||||
|
@ -346,4 +346,105 @@ public class SecretsControllerTests
|
|||||||
Assert.Null(result.Error);
|
Assert.Null(result.Error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async void GetSecretsByIds_NoSecretsFound_ThrowsNotFound(SutProvider<SecretsController> sutProvider,
|
||||||
|
List<Secret> data)
|
||||||
|
{
|
||||||
|
var (ids, request) = BuildGetSecretsRequestModel(data);
|
||||||
|
sutProvider.GetDependency<ISecretRepository>().GetManyByIds(Arg.Is(ids)).ReturnsForAnyArgs(new List<Secret>());
|
||||||
|
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.GetSecretsByIdsAsync(request));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async void GetSecretsByIds_SecretsFoundMisMatch_ThrowsNotFound(SutProvider<SecretsController> sutProvider,
|
||||||
|
List<Secret> data, Secret mockSecret)
|
||||||
|
{
|
||||||
|
var (ids, request) = BuildGetSecretsRequestModel(data);
|
||||||
|
ids.Add(mockSecret.Id);
|
||||||
|
sutProvider.GetDependency<ISecretRepository>().GetManyByIds(Arg.Is(ids))
|
||||||
|
.ReturnsForAnyArgs(new List<Secret> { mockSecret });
|
||||||
|
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.GetSecretsByIdsAsync(request));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async void GetSecretsByIds_OrganizationMisMatch_ThrowsNotFound(SutProvider<SecretsController> sutProvider,
|
||||||
|
List<Secret> data)
|
||||||
|
{
|
||||||
|
var (ids, request) = BuildGetSecretsRequestModel(data);
|
||||||
|
sutProvider.GetDependency<ISecretRepository>().GetManyByIds(Arg.Is(ids)).ReturnsForAnyArgs(data);
|
||||||
|
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.GetSecretsByIdsAsync(request));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async void GetSecretsByIds_NoAccessToSecretsManager_ThrowsNotFound(
|
||||||
|
SutProvider<SecretsController> sutProvider, List<Secret> data)
|
||||||
|
{
|
||||||
|
var (ids, request) = BuildGetSecretsRequestModel(data);
|
||||||
|
var organizationId = SetOrganizations(ref data);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(Arg.Is(organizationId))
|
||||||
|
.ReturnsForAnyArgs(false);
|
||||||
|
sutProvider.GetDependency<ISecretRepository>().GetManyByIds(Arg.Is(ids)).ReturnsForAnyArgs(data);
|
||||||
|
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.GetSecretsByIdsAsync(request));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async void GetSecretsByIds_AccessDenied_ThrowsNotFound(SutProvider<SecretsController> sutProvider,
|
||||||
|
List<Secret> data)
|
||||||
|
{
|
||||||
|
var (ids, request) = BuildGetSecretsRequestModel(data);
|
||||||
|
var organizationId = SetOrganizations(ref data);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ISecretRepository>().GetManyByIds(Arg.Is(ids)).ReturnsForAnyArgs(data);
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(Arg.Is(organizationId))
|
||||||
|
.ReturnsForAnyArgs(true);
|
||||||
|
sutProvider.GetDependency<IAuthorizationService>()
|
||||||
|
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), data.First(),
|
||||||
|
Arg.Any<IEnumerable<IAuthorizationRequirement>>()).ReturnsForAnyArgs(AuthorizationResult.Failed());
|
||||||
|
|
||||||
|
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.GetSecretsByIdsAsync(request));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async void GetSecretsByIds_Success(SutProvider<SecretsController> sutProvider, List<Secret> data)
|
||||||
|
{
|
||||||
|
var (ids, request) = BuildGetSecretsRequestModel(data);
|
||||||
|
var organizationId = SetOrganizations(ref data);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ISecretRepository>().GetManyByIds(Arg.Is(ids)).ReturnsForAnyArgs(data);
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(Arg.Is(organizationId))
|
||||||
|
.ReturnsForAnyArgs(true);
|
||||||
|
sutProvider.GetDependency<IAuthorizationService>()
|
||||||
|
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), data.First(),
|
||||||
|
Arg.Any<IEnumerable<IAuthorizationRequirement>>()).ReturnsForAnyArgs(AuthorizationResult.Success());
|
||||||
|
|
||||||
|
var results = await sutProvider.Sut.GetSecretsByIdsAsync(request);
|
||||||
|
Assert.Equal(data.Count, results.Data.Count());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (List<Guid> Ids, GetSecretsRequestModel request) BuildGetSecretsRequestModel(
|
||||||
|
IEnumerable<Secret> data)
|
||||||
|
{
|
||||||
|
var ids = data.Select(s => s.Id).ToList();
|
||||||
|
var request = new GetSecretsRequestModel { Ids = ids };
|
||||||
|
return (ids, request);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Guid SetOrganizations(ref List<Secret> data)
|
||||||
|
{
|
||||||
|
var organizationId = data.First().OrganizationId;
|
||||||
|
foreach (var s in data)
|
||||||
|
{
|
||||||
|
s.OrganizationId = organizationId;
|
||||||
|
}
|
||||||
|
|
||||||
|
return organizationId;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user