mirror of
https://github.com/bitwarden/server.git
synced 2024-11-21 12:05:42 +01:00
[SM-1256] Add BulkSecretAuthorizationHandler (#4099)
* Add AccessToSecretsAsync to the repository * Add BulkSecretAuthorizationHandler * Update controller to use the new authz handler * Add integration test coverage
This commit is contained in:
parent
313eef49f0
commit
acc4808509
@ -0,0 +1,63 @@
|
||||
#nullable enable
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.SecretsManager.AuthorizationRequirements;
|
||||
using Bit.Core.SecretsManager.Entities;
|
||||
using Bit.Core.SecretsManager.Queries.Interfaces;
|
||||
using Bit.Core.SecretsManager.Repositories;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
namespace Bit.Commercial.Core.SecretsManager.AuthorizationHandlers.Secrets;
|
||||
|
||||
public class
|
||||
BulkSecretAuthorizationHandler : AuthorizationHandler<BulkSecretOperationRequirement, IReadOnlyList<Secret>>
|
||||
{
|
||||
private readonly IAccessClientQuery _accessClientQuery;
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly ISecretRepository _secretRepository;
|
||||
|
||||
public BulkSecretAuthorizationHandler(ICurrentContext currentContext, IAccessClientQuery accessClientQuery,
|
||||
ISecretRepository secretRepository)
|
||||
{
|
||||
_currentContext = currentContext;
|
||||
_accessClientQuery = accessClientQuery;
|
||||
_secretRepository = secretRepository;
|
||||
}
|
||||
|
||||
|
||||
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context,
|
||||
BulkSecretOperationRequirement requirement,
|
||||
IReadOnlyList<Secret> resources)
|
||||
{
|
||||
// Ensure all secrets belong to the same organization.
|
||||
var organizationId = resources[0].OrganizationId;
|
||||
if (resources.Any(secret => secret.OrganizationId != organizationId) ||
|
||||
!_currentContext.AccessSecretsManager(organizationId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
switch (requirement)
|
||||
{
|
||||
case not null when requirement == BulkSecretOperations.ReadAll:
|
||||
await CanReadAllAsync(context, requirement, resources, organizationId);
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentException("Unsupported operation requirement type provided.", nameof(requirement));
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CanReadAllAsync(AuthorizationHandlerContext context,
|
||||
BulkSecretOperationRequirement requirement, IReadOnlyList<Secret> resources, Guid organizationId)
|
||||
{
|
||||
var (accessClient, userId) = await _accessClientQuery.GetAccessClientAsync(context.User, organizationId);
|
||||
|
||||
var secretsAccess =
|
||||
await _secretRepository.AccessToSecretsAsync(resources.Select(s => s.Id), userId, accessClient);
|
||||
|
||||
if (secretsAccess.Count == resources.Count &&
|
||||
secretsAccess.All(a => a.Value.Read))
|
||||
{
|
||||
context.Succeed(requirement);
|
||||
}
|
||||
}
|
||||
}
|
@ -43,6 +43,7 @@ public static class SecretsManagerCollectionExtensions
|
||||
services.AddScoped<IAuthorizationHandler, ServiceAccountGrantedPoliciesAuthorizationHandler>();
|
||||
services.AddScoped<IAuthorizationHandler, ProjectServiceAccountsAccessPoliciesAuthorizationHandler>();
|
||||
services.AddScoped<IAuthorizationHandler, SecretAccessPoliciesUpdatesAuthorizationHandler>();
|
||||
services.AddScoped<IAuthorizationHandler, BulkSecretAuthorizationHandler>();
|
||||
services.AddScoped<IAccessClientQuery, AccessClientQuery>();
|
||||
services.AddScoped<IMaxProjectsQuery, MaxProjectsQuery>();
|
||||
services.AddScoped<ISameOrganizationQuery, SameOrganizationQuery>();
|
||||
|
@ -299,6 +299,22 @@ public class SecretRepository : Repository<Core.SecretsManager.Entities.Secret,
|
||||
return policy == null ? (false, false) : (policy.Read, policy.Write);
|
||||
}
|
||||
|
||||
public async Task<Dictionary<Guid, (bool Read, bool Write)>> AccessToSecretsAsync(
|
||||
IEnumerable<Guid> ids,
|
||||
Guid userId,
|
||||
AccessClientType accessType)
|
||||
{
|
||||
await using var scope = ServiceScopeFactory.CreateAsyncScope();
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
|
||||
var secrets = dbContext.Secret
|
||||
.Where(s => ids.Contains(s.Id));
|
||||
|
||||
var accessQuery = BuildSecretAccessQuery(secrets, userId, accessType);
|
||||
|
||||
return await accessQuery.ToDictionaryAsync(sa => sa.Id, sa => (sa.Read, sa.Write));
|
||||
}
|
||||
|
||||
public async Task EmptyTrash(DateTime currentDate, uint deleteAfterThisNumberOfDays)
|
||||
{
|
||||
using var scope = ServiceScopeFactory.CreateScope();
|
||||
|
@ -0,0 +1,224 @@
|
||||
#nullable enable
|
||||
using System.Reflection;
|
||||
using System.Security.Claims;
|
||||
using Bit.Commercial.Core.SecretsManager.AuthorizationHandlers.Secrets;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.SecretsManager.AuthorizationRequirements;
|
||||
using Bit.Core.SecretsManager.Entities;
|
||||
using Bit.Core.SecretsManager.Queries.Interfaces;
|
||||
using Bit.Core.SecretsManager.Repositories;
|
||||
using Bit.Core.Test.SecretsManager.AutoFixture.ProjectsFixture;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Commercial.Core.Test.SecretsManager.AuthorizationHandlers.Secrets;
|
||||
|
||||
[SutProviderCustomize]
|
||||
[ProjectCustomize]
|
||||
public class BulkSecretAuthorizationHandlerTests
|
||||
{
|
||||
[Fact]
|
||||
public void BulkSecretOperations_OnlyPublicStatic()
|
||||
{
|
||||
var publicStaticFields = typeof(BulkSecretOperations).GetFields(BindingFlags.Public | BindingFlags.Static);
|
||||
var allFields = typeof(BulkSecretOperations).GetFields();
|
||||
Assert.Equal(publicStaticFields.Length, allFields.Length);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task Handler_MisMatchedOrganizations_DoesNotSucceed(
|
||||
SutProvider<BulkSecretAuthorizationHandler> sutProvider, List<Secret> resources,
|
||||
ClaimsPrincipal claimsPrincipal)
|
||||
{
|
||||
var requirement = BulkSecretOperations.ReadAll;
|
||||
resources[0].OrganizationId = Guid.NewGuid();
|
||||
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(Arg.Any<Guid>())
|
||||
.ReturnsForAnyArgs(true);
|
||||
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
|
||||
claimsPrincipal, resources);
|
||||
|
||||
await sutProvider.Sut.HandleAsync(authzContext);
|
||||
|
||||
Assert.False(authzContext.HasSucceeded);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task Handler_NoAccessToSecretsManager_DoesNotSucceed(
|
||||
SutProvider<BulkSecretAuthorizationHandler> sutProvider, List<Secret> resources,
|
||||
ClaimsPrincipal claimsPrincipal)
|
||||
{
|
||||
var requirement = BulkSecretOperations.ReadAll;
|
||||
resources = SetSameOrganization(resources);
|
||||
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(Arg.Any<Guid>())
|
||||
.ReturnsForAnyArgs(false);
|
||||
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
|
||||
claimsPrincipal, resources);
|
||||
|
||||
await sutProvider.Sut.HandleAsync(authzContext);
|
||||
|
||||
Assert.False(authzContext.HasSucceeded);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task Handler_UnsupportedSecretOperationRequirement_Throws(
|
||||
SutProvider<BulkSecretAuthorizationHandler> sutProvider, List<Secret> resources,
|
||||
ClaimsPrincipal claimsPrincipal)
|
||||
{
|
||||
var requirement = new BulkSecretOperationRequirement();
|
||||
resources = SetSameOrganization(resources);
|
||||
SetupUserSubstitutes(sutProvider, AccessClientType.User, resources.First().OrganizationId);
|
||||
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
|
||||
claimsPrincipal, resources);
|
||||
|
||||
await Assert.ThrowsAsync<ArgumentException>(() => sutProvider.Sut.HandleAsync(authzContext));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(AccessClientType.User)]
|
||||
[BitAutoData(AccessClientType.NoAccessCheck)]
|
||||
[BitAutoData(AccessClientType.ServiceAccount)]
|
||||
public async Task Handler_NoAccessToSecrets_DoesNotSucceed(
|
||||
AccessClientType accessClientType,
|
||||
SutProvider<BulkSecretAuthorizationHandler> sutProvider, List<Secret> resources,
|
||||
ClaimsPrincipal claimsPrincipal)
|
||||
{
|
||||
var requirement = BulkSecretOperations.ReadAll;
|
||||
resources = SetSameOrganization(resources);
|
||||
var secretIds =
|
||||
SetupSecretAccessRequest(sutProvider, resources, accessClientType, resources.First().OrganizationId);
|
||||
sutProvider.GetDependency<ISecretRepository>()
|
||||
.AccessToSecretsAsync(Arg.Any<IEnumerable<Guid>>(), Arg.Any<Guid>(), Arg.Any<AccessClientType>())
|
||||
.Returns(secretIds.ToDictionary(id => id, _ => (false, false)));
|
||||
|
||||
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
|
||||
claimsPrincipal, resources);
|
||||
|
||||
await sutProvider.Sut.HandleAsync(authzContext);
|
||||
|
||||
Assert.False(authzContext.HasSucceeded);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(AccessClientType.User)]
|
||||
[BitAutoData(AccessClientType.NoAccessCheck)]
|
||||
[BitAutoData(AccessClientType.ServiceAccount)]
|
||||
public async Task Handler_HasAccessToSomeSecrets_DoesNotSucceed(
|
||||
AccessClientType accessClientType,
|
||||
SutProvider<BulkSecretAuthorizationHandler> sutProvider, List<Secret> resources,
|
||||
ClaimsPrincipal claimsPrincipal)
|
||||
{
|
||||
var requirement = BulkSecretOperations.ReadAll;
|
||||
resources = SetSameOrganization(resources);
|
||||
var secretIds =
|
||||
SetupSecretAccessRequest(sutProvider, resources, accessClientType, resources.First().OrganizationId);
|
||||
|
||||
var accessResult = secretIds.ToDictionary(secretId => secretId, _ => (false, false));
|
||||
accessResult[secretIds.First()] = (true, true);
|
||||
sutProvider.GetDependency<ISecretRepository>()
|
||||
.AccessToSecretsAsync(Arg.Any<IEnumerable<Guid>>(), Arg.Any<Guid>(), Arg.Any<AccessClientType>())
|
||||
.Returns(accessResult);
|
||||
|
||||
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
|
||||
claimsPrincipal, resources);
|
||||
|
||||
await sutProvider.Sut.HandleAsync(authzContext);
|
||||
|
||||
Assert.False(authzContext.HasSucceeded);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(AccessClientType.User)]
|
||||
[BitAutoData(AccessClientType.NoAccessCheck)]
|
||||
[BitAutoData(AccessClientType.ServiceAccount)]
|
||||
public async Task Handler_PartialAccessReturn_DoesNotSucceed(
|
||||
AccessClientType accessClientType,
|
||||
SutProvider<BulkSecretAuthorizationHandler> sutProvider, List<Secret> resources,
|
||||
ClaimsPrincipal claimsPrincipal)
|
||||
{
|
||||
var requirement = BulkSecretOperations.ReadAll;
|
||||
resources = SetSameOrganization(resources);
|
||||
var secretIds =
|
||||
SetupSecretAccessRequest(sutProvider, resources, accessClientType, resources.First().OrganizationId);
|
||||
|
||||
var accessResult = secretIds.ToDictionary(secretId => secretId, _ => (false, false));
|
||||
accessResult.Remove(secretIds.First());
|
||||
sutProvider.GetDependency<ISecretRepository>()
|
||||
.AccessToSecretsAsync(Arg.Any<IEnumerable<Guid>>(), Arg.Any<Guid>(), Arg.Any<AccessClientType>())
|
||||
.Returns(accessResult);
|
||||
|
||||
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
|
||||
claimsPrincipal, resources);
|
||||
|
||||
await sutProvider.Sut.HandleAsync(authzContext);
|
||||
|
||||
Assert.False(authzContext.HasSucceeded);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(AccessClientType.User)]
|
||||
[BitAutoData(AccessClientType.NoAccessCheck)]
|
||||
[BitAutoData(AccessClientType.ServiceAccount)]
|
||||
public async Task Handler_HasAccessToAllSecrets_Success(
|
||||
AccessClientType accessClientType,
|
||||
SutProvider<BulkSecretAuthorizationHandler> sutProvider, List<Secret> resources,
|
||||
ClaimsPrincipal claimsPrincipal)
|
||||
{
|
||||
var requirement = BulkSecretOperations.ReadAll;
|
||||
resources = SetSameOrganization(resources);
|
||||
var secretIds =
|
||||
SetupSecretAccessRequest(sutProvider, resources, accessClientType, resources.First().OrganizationId);
|
||||
|
||||
var accessResult = secretIds.ToDictionary(secretId => secretId, _ => (true, true));
|
||||
sutProvider.GetDependency<ISecretRepository>()
|
||||
.AccessToSecretsAsync(Arg.Any<IEnumerable<Guid>>(), Arg.Any<Guid>(), Arg.Any<AccessClientType>())
|
||||
.Returns(accessResult);
|
||||
|
||||
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
|
||||
claimsPrincipal, resources);
|
||||
|
||||
await sutProvider.Sut.HandleAsync(authzContext);
|
||||
|
||||
Assert.True(authzContext.HasSucceeded);
|
||||
}
|
||||
|
||||
private static List<Secret> SetSameOrganization(List<Secret> secrets)
|
||||
{
|
||||
var organizationId = secrets.First().OrganizationId;
|
||||
foreach (var secret in secrets)
|
||||
{
|
||||
secret.OrganizationId = organizationId;
|
||||
}
|
||||
|
||||
return secrets;
|
||||
}
|
||||
|
||||
private static void SetupUserSubstitutes(
|
||||
SutProvider<BulkSecretAuthorizationHandler> sutProvider,
|
||||
AccessClientType accessClientType,
|
||||
Guid organizationId,
|
||||
Guid userId = new())
|
||||
{
|
||||
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(organizationId)
|
||||
.Returns(true);
|
||||
sutProvider.GetDependency<IAccessClientQuery>().GetAccessClientAsync(default, organizationId)
|
||||
.ReturnsForAnyArgs((accessClientType, userId));
|
||||
}
|
||||
|
||||
private static List<Guid> SetupSecretAccessRequest(
|
||||
SutProvider<BulkSecretAuthorizationHandler> sutProvider,
|
||||
IEnumerable<Secret> resources,
|
||||
AccessClientType accessClientType,
|
||||
Guid organizationId,
|
||||
Guid userId = new())
|
||||
{
|
||||
SetupUserSubstitutes(sutProvider, accessClientType, organizationId, userId);
|
||||
return resources.Select(s => s.Id).ToList();
|
||||
}
|
||||
}
|
@ -260,25 +260,13 @@ public class SecretsController : Controller
|
||||
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))
|
||||
var authorizationResult = await _authorizationService.AuthorizeAsync(User, secrets, BulkSecretOperations.ReadAll);
|
||||
if (!authorizationResult.Succeeded)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
|
||||
foreach (var secret in secrets)
|
||||
{
|
||||
var authorizationResult = await _authorizationService.AuthorizeAsync(User, secret, SecretOperations.Read);
|
||||
if (!authorizationResult.Succeeded)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
}
|
||||
|
||||
await LogSecretsRetrievalAsync(organizationId, secrets);
|
||||
await LogSecretsRetrievalAsync(secrets.First().OrganizationId, secrets);
|
||||
|
||||
var responses = secrets.Select(s => new BaseSecretResponseModel(s));
|
||||
return new ListResponseModel<BaseSecretResponseModel>(responses);
|
||||
|
@ -0,0 +1,12 @@
|
||||
using Microsoft.AspNetCore.Authorization.Infrastructure;
|
||||
|
||||
namespace Bit.Core.SecretsManager.AuthorizationRequirements;
|
||||
|
||||
public class BulkSecretOperationRequirement : OperationAuthorizationRequirement
|
||||
{
|
||||
}
|
||||
|
||||
public static class BulkSecretOperations
|
||||
{
|
||||
public static readonly BulkSecretOperationRequirement ReadAll = new() { Name = nameof(ReadAll) };
|
||||
}
|
@ -21,6 +21,7 @@ public interface ISecretRepository
|
||||
Task RestoreManyByIdAsync(IEnumerable<Guid> ids);
|
||||
Task<IEnumerable<Secret>> ImportAsync(IEnumerable<Secret> secrets);
|
||||
Task<(bool Read, bool Write)> AccessToSecretAsync(Guid id, Guid userId, AccessClientType accessType);
|
||||
Task<Dictionary<Guid, (bool Read, bool Write)>> AccessToSecretsAsync(IEnumerable<Guid> ids, Guid userId, AccessClientType accessType);
|
||||
Task EmptyTrash(DateTime nowTime, uint deleteAfterThisNumberOfDays);
|
||||
Task<int> GetSecretsCountByOrganizationIdAsync(Guid organizationId);
|
||||
}
|
||||
|
@ -81,6 +81,12 @@ public class NoopSecretRepository : ISecretRepository
|
||||
return Task.FromResult((false, false));
|
||||
}
|
||||
|
||||
public Task<Dictionary<Guid, (bool Read, bool Write)>> AccessToSecretsAsync(IEnumerable<Guid> ids,
|
||||
Guid userId, AccessClientType accessType)
|
||||
{
|
||||
return Task.FromResult(null as Dictionary<Guid, (bool Read, bool Write)>);
|
||||
}
|
||||
|
||||
public Task EmptyTrash(DateTime nowTime, uint deleteAfterThisNumberOfDays)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
|
@ -741,44 +741,83 @@ public class SecretsControllerTests : IClassFixture<ApiApplicationFactory>, IAsy
|
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(PermissionType.RunAsAdmin)]
|
||||
[InlineData(PermissionType.RunAsUserWithPermission)]
|
||||
public async Task GetSecretsByIds_Success(PermissionType permissionType)
|
||||
[Fact]
|
||||
public async Task GetSecretsByIds_SecretsNotInTheSameOrganization_NotFound()
|
||||
{
|
||||
var (org, _) = await _organizationHelper.Initialize(true, true, true);
|
||||
await _loginHelper.LoginAsync(_email);
|
||||
|
||||
var (project, secretIds) = await CreateSecretsAsync(org.Id);
|
||||
|
||||
if (permissionType == PermissionType.RunAsUserWithPermission)
|
||||
{
|
||||
var (email, orgUser) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);
|
||||
await _loginHelper.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 _loginHelper.LoginAsync(email);
|
||||
}
|
||||
var otherOrg = await _organizationHelper.CreateSmOrganizationAsync();
|
||||
var (_, secretIds) = await CreateSecretsAsync(org.Id);
|
||||
var (_, diffOrgSecrets) = await CreateSecretsAsync(otherOrg.Id, 1);
|
||||
secretIds.AddRange(diffOrgSecrets);
|
||||
|
||||
var request = new GetSecretsRequestModel { Ids = secretIds };
|
||||
|
||||
var response = await _client.PostAsJsonAsync("/secrets/get-by-ids", request);
|
||||
|
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public async Task GetSecretsByIds_SecretsNonExistent_NotFound(bool partial)
|
||||
{
|
||||
var (org, _) = await _organizationHelper.Initialize(true, true, true);
|
||||
await _loginHelper.LoginAsync(_email);
|
||||
var ids = new List<Guid>();
|
||||
|
||||
if (partial)
|
||||
{
|
||||
var (_, secretIds) = await CreateSecretsAsync(org.Id);
|
||||
ids = secretIds;
|
||||
ids.Add(Guid.NewGuid());
|
||||
}
|
||||
|
||||
var request = new GetSecretsRequestModel { Ids = ids };
|
||||
|
||||
var response = await _client.PostAsJsonAsync("/secrets/get-by-ids", request);
|
||||
|
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(true, false)]
|
||||
[InlineData(true, true)]
|
||||
[InlineData(false, false)]
|
||||
[InlineData(false, true)]
|
||||
public async Task GetSecretsByIds_NoAccess_NotFound(bool runAsServiceAccount, bool partialAccess)
|
||||
{
|
||||
var (org, _) = await _organizationHelper.Initialize(true, true, true);
|
||||
|
||||
var request = await SetupNoAccessRequestAsync(org.Id, runAsServiceAccount, partialAccess);
|
||||
|
||||
var response = await _client.PostAsJsonAsync("/secrets/get-by-ids", request);
|
||||
|
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(PermissionType.RunAsAdmin)]
|
||||
[InlineData(PermissionType.RunAsUserWithPermission)]
|
||||
[InlineData(PermissionType.RunAsServiceAccountWithPermission)]
|
||||
public async Task GetSecretsByIds_Success(PermissionType permissionType)
|
||||
{
|
||||
var (org, _) = await _organizationHelper.Initialize(true, true, true);
|
||||
await _loginHelper.LoginAsync(_email);
|
||||
var request = await SetupGetSecretsByIdsRequestAsync(org.Id, permissionType);
|
||||
|
||||
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());
|
||||
Assert.Equal(request.Ids.Count(), result.Data.Count());
|
||||
Assert.All(result.Data, data => Assert.Equal(_mockEncryptedString, data.Value));
|
||||
Assert.All(result.Data, data => Assert.Equal(_mockEncryptedString, data.Key));
|
||||
Assert.All(result.Data, data => Assert.Equal(_mockEncryptedString, data.Note));
|
||||
Assert.All(result.Data, data => Assert.Equal(org.Id, data.OrganizationId));
|
||||
}
|
||||
|
||||
|
||||
@ -1161,4 +1200,94 @@ public class SecretsControllerTests : IClassFixture<ApiApplicationFactory>, IAsy
|
||||
|
||||
return (secret, request);
|
||||
}
|
||||
|
||||
private async Task<GetSecretsRequestModel> SetupGetSecretsByIdsRequestAsync(Guid organizationId,
|
||||
PermissionType permissionType)
|
||||
{
|
||||
var (project, secretIds) = await CreateSecretsAsync(organizationId);
|
||||
|
||||
if (permissionType == PermissionType.RunAsUserWithPermission)
|
||||
{
|
||||
var (email, orgUser) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);
|
||||
await _loginHelper.LoginAsync(email);
|
||||
|
||||
var accessPolicies = new List<BaseAccessPolicy>
|
||||
{
|
||||
new UserProjectAccessPolicy
|
||||
{
|
||||
GrantedProjectId = project.Id, OrganizationUserId = orgUser.Id, Read = true, Write = true
|
||||
}
|
||||
};
|
||||
await _accessPolicyRepository.CreateManyAsync(accessPolicies);
|
||||
}
|
||||
|
||||
if (permissionType == PermissionType.RunAsServiceAccountWithPermission)
|
||||
{
|
||||
var apiKeyDetails = await _organizationHelper.CreateNewServiceAccountApiKeyAsync();
|
||||
await _loginHelper.LoginWithApiKeyAsync(apiKeyDetails);
|
||||
|
||||
var accessPolicies = new List<BaseAccessPolicy>
|
||||
{
|
||||
new ServiceAccountProjectAccessPolicy
|
||||
{
|
||||
GrantedProjectId = project.Id,
|
||||
ServiceAccountId = apiKeyDetails.ApiKey.ServiceAccountId,
|
||||
Read = true,
|
||||
Write = true
|
||||
}
|
||||
};
|
||||
await _accessPolicyRepository.CreateManyAsync(accessPolicies);
|
||||
}
|
||||
|
||||
return new GetSecretsRequestModel { Ids = secretIds };
|
||||
}
|
||||
|
||||
private async Task<GetSecretsRequestModel> SetupNoAccessRequestAsync(Guid organizationId, bool runAsServiceAccount,
|
||||
bool partialAccess)
|
||||
{
|
||||
var (_, secretIds) = await CreateSecretsAsync(organizationId);
|
||||
|
||||
if (runAsServiceAccount)
|
||||
{
|
||||
var apiKeyDetails = await _organizationHelper.CreateNewServiceAccountApiKeyAsync();
|
||||
await _loginHelper.LoginWithApiKeyAsync(apiKeyDetails);
|
||||
|
||||
if (partialAccess)
|
||||
{
|
||||
var accessPolicies = new List<BaseAccessPolicy>
|
||||
{
|
||||
new ServiceAccountSecretAccessPolicy
|
||||
{
|
||||
GrantedSecretId = secretIds[0],
|
||||
ServiceAccountId = apiKeyDetails.ApiKey.ServiceAccountId,
|
||||
Read = true,
|
||||
Write = true
|
||||
}
|
||||
};
|
||||
await _accessPolicyRepository.CreateManyAsync(accessPolicies);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var (email, orgUser) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);
|
||||
await _loginHelper.LoginAsync(email);
|
||||
|
||||
if (partialAccess)
|
||||
{
|
||||
var accessPolicies = new List<BaseAccessPolicy>
|
||||
{
|
||||
new UserSecretAccessPolicy
|
||||
{
|
||||
GrantedSecretId = secretIds[0],
|
||||
OrganizationUserId = orgUser.Id,
|
||||
Read = true,
|
||||
Write = true
|
||||
}
|
||||
};
|
||||
await _accessPolicyRepository.CreateManyAsync(accessPolicies);
|
||||
}
|
||||
}
|
||||
|
||||
return new GetSecretsRequestModel { Ids = secretIds };
|
||||
}
|
||||
}
|
||||
|
@ -412,30 +412,6 @@ public class SecretsControllerTests
|
||||
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.GetSecretsByIdsAsync(request));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task 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 Task 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 Task GetSecretsByIds_AccessDenied_ThrowsNotFound(SutProvider<SecretsController> sutProvider,
|
||||
@ -445,10 +421,8 @@ public class SecretsControllerTests
|
||||
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(),
|
||||
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), data,
|
||||
Arg.Any<IEnumerable<IAuthorizationRequirement>>()).ReturnsForAnyArgs(AuthorizationResult.Failed());
|
||||
|
||||
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.GetSecretsByIdsAsync(request));
|
||||
@ -462,10 +436,8 @@ public class SecretsControllerTests
|
||||
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(),
|
||||
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), data,
|
||||
Arg.Any<IEnumerable<IAuthorizationRequirement>>()).ReturnsForAnyArgs(AuthorizationResult.Success());
|
||||
|
||||
var results = await sutProvider.Sut.GetSecretsByIdsAsync(request);
|
||||
|
Loading…
Reference in New Issue
Block a user