mirror of
https://github.com/bitwarden/server.git
synced 2025-04-05 21:18:13 -05: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, ServiceAccountGrantedPoliciesAuthorizationHandler>();
|
||||||
services.AddScoped<IAuthorizationHandler, ProjectServiceAccountsAccessPoliciesAuthorizationHandler>();
|
services.AddScoped<IAuthorizationHandler, ProjectServiceAccountsAccessPoliciesAuthorizationHandler>();
|
||||||
services.AddScoped<IAuthorizationHandler, SecretAccessPoliciesUpdatesAuthorizationHandler>();
|
services.AddScoped<IAuthorizationHandler, SecretAccessPoliciesUpdatesAuthorizationHandler>();
|
||||||
|
services.AddScoped<IAuthorizationHandler, BulkSecretAuthorizationHandler>();
|
||||||
services.AddScoped<IAccessClientQuery, AccessClientQuery>();
|
services.AddScoped<IAccessClientQuery, AccessClientQuery>();
|
||||||
services.AddScoped<IMaxProjectsQuery, MaxProjectsQuery>();
|
services.AddScoped<IMaxProjectsQuery, MaxProjectsQuery>();
|
||||||
services.AddScoped<ISameOrganizationQuery, SameOrganizationQuery>();
|
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);
|
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)
|
public async Task EmptyTrash(DateTime currentDate, uint deleteAfterThisNumberOfDays)
|
||||||
{
|
{
|
||||||
using var scope = ServiceScopeFactory.CreateScope();
|
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();
|
throw new NotFoundException();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure all secrets belong to the same organization.
|
var authorizationResult = await _authorizationService.AuthorizeAsync(User, secrets, BulkSecretOperations.ReadAll);
|
||||||
var organizationId = secrets.First().OrganizationId;
|
if (!authorizationResult.Succeeded)
|
||||||
if (secrets.Any(secret => secret.OrganizationId != organizationId) ||
|
|
||||||
!_currentContext.AccessSecretsManager(organizationId))
|
|
||||||
{
|
{
|
||||||
throw new NotFoundException();
|
throw new NotFoundException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await LogSecretsRetrievalAsync(secrets.First().OrganizationId, secrets);
|
||||||
foreach (var secret in secrets)
|
|
||||||
{
|
|
||||||
var authorizationResult = await _authorizationService.AuthorizeAsync(User, secret, SecretOperations.Read);
|
|
||||||
if (!authorizationResult.Succeeded)
|
|
||||||
{
|
|
||||||
throw new NotFoundException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await LogSecretsRetrievalAsync(organizationId, secrets);
|
|
||||||
|
|
||||||
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);
|
||||||
|
@ -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 RestoreManyByIdAsync(IEnumerable<Guid> ids);
|
||||||
Task<IEnumerable<Secret>> ImportAsync(IEnumerable<Secret> secrets);
|
Task<IEnumerable<Secret>> ImportAsync(IEnumerable<Secret> secrets);
|
||||||
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<Dictionary<Guid, (bool Read, bool Write)>> AccessToSecretsAsync(IEnumerable<Guid> ids, 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);
|
||||||
}
|
}
|
||||||
|
@ -81,6 +81,12 @@ public class NoopSecretRepository : ISecretRepository
|
|||||||
return Task.FromResult((false, false));
|
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)
|
public Task EmptyTrash(DateTime nowTime, uint deleteAfterThisNumberOfDays)
|
||||||
{
|
{
|
||||||
return Task.FromResult(0);
|
return Task.FromResult(0);
|
||||||
|
@ -741,44 +741,83 @@ public class SecretsControllerTests : IClassFixture<ApiApplicationFactory>, IAsy
|
|||||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Fact]
|
||||||
[InlineData(PermissionType.RunAsAdmin)]
|
public async Task GetSecretsByIds_SecretsNotInTheSameOrganization_NotFound()
|
||||||
[InlineData(PermissionType.RunAsUserWithPermission)]
|
|
||||||
public async Task GetSecretsByIds_Success(PermissionType permissionType)
|
|
||||||
{
|
{
|
||||||
var (org, _) = await _organizationHelper.Initialize(true, true, true);
|
var (org, _) = await _organizationHelper.Initialize(true, true, true);
|
||||||
await _loginHelper.LoginAsync(_email);
|
await _loginHelper.LoginAsync(_email);
|
||||||
|
var otherOrg = await _organizationHelper.CreateSmOrganizationAsync();
|
||||||
var (project, secretIds) = await CreateSecretsAsync(org.Id);
|
var (_, secretIds) = await CreateSecretsAsync(org.Id);
|
||||||
|
var (_, diffOrgSecrets) = await CreateSecretsAsync(otherOrg.Id, 1);
|
||||||
if (permissionType == PermissionType.RunAsUserWithPermission)
|
secretIds.AddRange(diffOrgSecrets);
|
||||||
{
|
|
||||||
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 request = new GetSecretsRequestModel { Ids = secretIds };
|
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);
|
var response = await _client.PostAsJsonAsync("/secrets/get-by-ids", request);
|
||||||
response.EnsureSuccessStatusCode();
|
response.EnsureSuccessStatusCode();
|
||||||
var result = await response.Content.ReadFromJsonAsync<ListResponseModel<BaseSecretResponseModel>>();
|
var result = await response.Content.ReadFromJsonAsync<ListResponseModel<BaseSecretResponseModel>>();
|
||||||
|
|
||||||
Assert.NotNull(result);
|
Assert.NotNull(result);
|
||||||
Assert.NotEmpty(result.Data);
|
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);
|
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));
|
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]
|
[Theory]
|
||||||
[BitAutoData]
|
[BitAutoData]
|
||||||
public async Task GetSecretsByIds_AccessDenied_ThrowsNotFound(SutProvider<SecretsController> sutProvider,
|
public async Task GetSecretsByIds_AccessDenied_ThrowsNotFound(SutProvider<SecretsController> sutProvider,
|
||||||
@ -445,10 +421,8 @@ public class SecretsControllerTests
|
|||||||
var organizationId = SetOrganizations(ref data);
|
var organizationId = SetOrganizations(ref data);
|
||||||
|
|
||||||
sutProvider.GetDependency<ISecretRepository>().GetManyByIds(Arg.Is(ids)).ReturnsForAnyArgs(data);
|
sutProvider.GetDependency<ISecretRepository>().GetManyByIds(Arg.Is(ids)).ReturnsForAnyArgs(data);
|
||||||
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(Arg.Is(organizationId))
|
|
||||||
.ReturnsForAnyArgs(true);
|
|
||||||
sutProvider.GetDependency<IAuthorizationService>()
|
sutProvider.GetDependency<IAuthorizationService>()
|
||||||
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), data.First(),
|
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), data,
|
||||||
Arg.Any<IEnumerable<IAuthorizationRequirement>>()).ReturnsForAnyArgs(AuthorizationResult.Failed());
|
Arg.Any<IEnumerable<IAuthorizationRequirement>>()).ReturnsForAnyArgs(AuthorizationResult.Failed());
|
||||||
|
|
||||||
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.GetSecretsByIdsAsync(request));
|
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.GetSecretsByIdsAsync(request));
|
||||||
@ -462,10 +436,8 @@ public class SecretsControllerTests
|
|||||||
var organizationId = SetOrganizations(ref data);
|
var organizationId = SetOrganizations(ref data);
|
||||||
|
|
||||||
sutProvider.GetDependency<ISecretRepository>().GetManyByIds(Arg.Is(ids)).ReturnsForAnyArgs(data);
|
sutProvider.GetDependency<ISecretRepository>().GetManyByIds(Arg.Is(ids)).ReturnsForAnyArgs(data);
|
||||||
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(Arg.Is(organizationId))
|
|
||||||
.ReturnsForAnyArgs(true);
|
|
||||||
sutProvider.GetDependency<IAuthorizationService>()
|
sutProvider.GetDependency<IAuthorizationService>()
|
||||||
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), data.First(),
|
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), data,
|
||||||
Arg.Any<IEnumerable<IAuthorizationRequirement>>()).ReturnsForAnyArgs(AuthorizationResult.Success());
|
Arg.Any<IEnumerable<IAuthorizationRequirement>>()).ReturnsForAnyArgs(AuthorizationResult.Success());
|
||||||
|
|
||||||
var results = await sutProvider.Sut.GetSecretsByIdsAsync(request);
|
var results = await sutProvider.Sut.GetSecretsByIdsAsync(request);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user