1
0
mirror of https://github.com/bitwarden/server.git synced 2024-11-21 12:05:42 +01:00

[SM-706] Extract Authorization From Create/Update Secret Commands (#2896)

* Extract authorization from commands

* Swap to request model validation.

* Swap to pattern detection
This commit is contained in:
Thomas Avery 2023-06-08 16:40:35 -05:00 committed by GitHub
parent 6a9e7a1d0a
commit 05f11a8ee1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 674 additions and 231 deletions

View File

@ -0,0 +1,123 @@
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 Microsoft.AspNetCore.Authorization;
namespace Bit.Commercial.Core.SecretsManager.AuthorizationHandlers.Secrets;
public class SecretAuthorizationHandler : AuthorizationHandler<SecretOperationRequirement, Secret>
{
private readonly ICurrentContext _currentContext;
private readonly IAccessClientQuery _accessClientQuery;
private readonly IProjectRepository _projectRepository;
private readonly ISecretRepository _secretRepository;
public SecretAuthorizationHandler(ICurrentContext currentContext, IAccessClientQuery accessClientQuery,
IProjectRepository projectRepository, ISecretRepository secretRepository)
{
_currentContext = currentContext;
_accessClientQuery = accessClientQuery;
_projectRepository = projectRepository;
_secretRepository = secretRepository;
}
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context,
SecretOperationRequirement requirement,
Secret resource)
{
if (!_currentContext.AccessSecretsManager(resource.OrganizationId))
{
return;
}
switch (requirement)
{
case not null when requirement == SecretOperations.Create:
await CanCreateSecretAsync(context, requirement, resource);
break;
case not null when requirement == SecretOperations.Update:
await CanUpdateSecretAsync(context, requirement, resource);
break;
default:
throw new ArgumentException("Unsupported operation requirement type provided.", nameof(requirement));
}
}
private async Task CanCreateSecretAsync(AuthorizationHandlerContext context,
SecretOperationRequirement requirement, Secret resource)
{
var (accessClient, userId) = await _accessClientQuery.GetAccessClientAsync(context.User, resource.OrganizationId);
var project = resource.Projects?.FirstOrDefault();
if (project == null && accessClient != AccessClientType.NoAccessCheck)
{
return;
}
// All projects should be apart of the same organization
if (resource.Projects != null
&& resource.Projects.Any()
&& !await _projectRepository.ProjectsAreInOrganization(resource.Projects.Select(p => p.Id).ToList(),
resource.OrganizationId))
{
return;
}
var hasAccess = accessClient switch
{
AccessClientType.NoAccessCheck => true,
AccessClientType.User => (await _projectRepository.AccessToProjectAsync(project!.Id, userId, accessClient))
.Write,
AccessClientType.ServiceAccount => false,
_ => false,
};
if (hasAccess)
{
context.Succeed(requirement);
}
}
private async Task CanUpdateSecretAsync(AuthorizationHandlerContext context,
SecretOperationRequirement requirement, Secret resource)
{
var (accessClient, userId) = await _accessClientQuery.GetAccessClientAsync(context.User, resource.OrganizationId);
// All projects should be apart of the same organization
if (resource.Projects != null
&& resource.Projects.Any()
&& !await _projectRepository.ProjectsAreInOrganization(resource.Projects.Select(p => p.Id).ToList(),
resource.OrganizationId))
{
return;
}
bool hasAccess;
switch (accessClient)
{
case AccessClientType.NoAccessCheck:
hasAccess = true;
break;
case AccessClientType.User:
var newProject = resource.Projects?.FirstOrDefault();
var access = (await _secretRepository.AccessToSecretAsync(resource.Id, userId, accessClient)).Write;
var accessToNew = newProject != null &&
(await _projectRepository.AccessToProjectAsync(newProject.Id, userId, accessClient))
.Write;
hasAccess = access && accessToNew;
break;
default:
hasAccess = false;
break;
}
if (hasAccess)
{
context.Succeed(requirement);
}
}
}

View File

@ -1,7 +1,4 @@
using Bit.Core.Context; using Bit.Core.SecretsManager.Commands.Secrets.Interfaces;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.SecretsManager.Commands.Secrets.Interfaces;
using Bit.Core.SecretsManager.Entities; using Bit.Core.SecretsManager.Entities;
using Bit.Core.SecretsManager.Repositories; using Bit.Core.SecretsManager.Repositories;
@ -10,44 +7,14 @@ namespace Bit.Commercial.Core.SecretsManager.Commands.Secrets;
public class CreateSecretCommand : ICreateSecretCommand public class CreateSecretCommand : ICreateSecretCommand
{ {
private readonly ISecretRepository _secretRepository; private readonly ISecretRepository _secretRepository;
private readonly IProjectRepository _projectRepository;
private readonly ICurrentContext _currentContext;
public CreateSecretCommand(ISecretRepository secretRepository, IProjectRepository projectRepository, ICurrentContext currentContext) public CreateSecretCommand(ISecretRepository secretRepository)
{ {
_secretRepository = secretRepository; _secretRepository = secretRepository;
_projectRepository = projectRepository;
_currentContext = currentContext;
} }
public async Task<Secret> CreateAsync(Secret secret, Guid userId) public async Task<Secret> CreateAsync(Secret secret)
{ {
var orgAdmin = await _currentContext.OrganizationAdmin(secret.OrganizationId);
var accessClient = AccessClientHelper.ToAccessClient(_currentContext.ClientType, orgAdmin);
var project = secret.Projects?.FirstOrDefault();
if (project == null && !orgAdmin)
{
throw new NotFoundException();
}
if (secret.Projects != null && secret.Projects.Any() && !(await _projectRepository.ProjectsAreInOrganization(secret.Projects.Select(p => p.Id).ToList(), secret.OrganizationId)))
{
throw new NotFoundException();
}
var hasAccess = accessClient switch
{
AccessClientType.NoAccessCheck => true,
AccessClientType.User => (await _projectRepository.AccessToProjectAsync(project.Id, userId, accessClient)).Write,
_ => false,
};
if (!hasAccess)
{
throw new NotFoundException();
}
return await _secretRepository.CreateAsync(secret); return await _secretRepository.CreateAsync(secret);
} }
} }

View File

@ -1,6 +1,4 @@
using Bit.Core.Context; using Bit.Core.Exceptions;
using Bit.Core.Enums;
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.Repositories; using Bit.Core.SecretsManager.Repositories;
@ -10,33 +8,16 @@ namespace Bit.Commercial.Core.SecretsManager.Commands.Secrets;
public class UpdateSecretCommand : IUpdateSecretCommand public class UpdateSecretCommand : IUpdateSecretCommand
{ {
private readonly ISecretRepository _secretRepository; private readonly ISecretRepository _secretRepository;
private readonly IProjectRepository _projectRepository;
private readonly ICurrentContext _currentContext;
public UpdateSecretCommand(ISecretRepository secretRepository, IProjectRepository projectRepository, ICurrentContext currentContext) public UpdateSecretCommand(ISecretRepository secretRepository)
{ {
_secretRepository = secretRepository; _secretRepository = secretRepository;
_projectRepository = projectRepository;
_currentContext = currentContext;
} }
public async Task<Secret> UpdateAsync(Secret updatedSecret, Guid userId) public async Task<Secret> UpdateAsync(Secret updatedSecret)
{ {
var secret = await _secretRepository.GetByIdAsync(updatedSecret.Id); var secret = await _secretRepository.GetByIdAsync(updatedSecret.Id);
if (secret == null || !_currentContext.AccessSecretsManager(secret.OrganizationId)) if (secret == null)
{
throw new NotFoundException();
}
if (updatedSecret.Projects != null && updatedSecret.Projects.Any() && !(await _projectRepository.ProjectsAreInOrganization(updatedSecret.Projects.Select(p => p.Id).ToList(), secret.OrganizationId)))
{
throw new NotFoundException();
}
var orgAdmin = await _currentContext.OrganizationAdmin(secret.OrganizationId);
var accessClient = AccessClientHelper.ToAccessClient(_currentContext.ClientType, orgAdmin);
if (!await HasAccessToOriginalAndUpdatedProject(accessClient, secret, updatedSecret, userId))
{ {
throw new NotFoundException(); throw new NotFoundException();
} }
@ -50,21 +31,4 @@ public class UpdateSecretCommand : IUpdateSecretCommand
await _secretRepository.UpdateAsync(secret); await _secretRepository.UpdateAsync(secret);
return secret; return secret;
} }
private async Task<bool> HasAccessToOriginalAndUpdatedProject(AccessClientType accessClient, Secret secret, Secret updatedSecret, Guid userId)
{
switch (accessClient)
{
case AccessClientType.NoAccessCheck:
return true;
case AccessClientType.User:
var oldProject = secret.Projects?.FirstOrDefault();
var newProject = updatedSecret.Projects?.FirstOrDefault();
var accessToOld = oldProject != null && (await _projectRepository.AccessToProjectAsync(oldProject.Id, userId, accessClient)).Write;
var accessToNew = newProject != null && (await _projectRepository.AccessToProjectAsync(newProject.Id, userId, accessClient)).Write;
return accessToOld && accessToNew;
default:
return false;
}
}
} }

View File

@ -1,4 +1,5 @@
using Bit.Commercial.Core.SecretsManager.AuthorizationHandlers.Projects; using Bit.Commercial.Core.SecretsManager.AuthorizationHandlers.Projects;
using Bit.Commercial.Core.SecretsManager.AuthorizationHandlers.Secrets;
using Bit.Commercial.Core.SecretsManager.AuthorizationHandlers.ServiceAccounts; using Bit.Commercial.Core.SecretsManager.AuthorizationHandlers.ServiceAccounts;
using Bit.Commercial.Core.SecretsManager.Commands.AccessPolicies; using Bit.Commercial.Core.SecretsManager.Commands.AccessPolicies;
using Bit.Commercial.Core.SecretsManager.Commands.AccessTokens; using Bit.Commercial.Core.SecretsManager.Commands.AccessTokens;
@ -26,6 +27,7 @@ public static class SecretsManagerCollectionExtensions
public static void AddSecretsManagerServices(this IServiceCollection services) public static void AddSecretsManagerServices(this IServiceCollection services)
{ {
services.AddScoped<IAuthorizationHandler, ProjectAuthorizationHandler>(); services.AddScoped<IAuthorizationHandler, ProjectAuthorizationHandler>();
services.AddScoped<IAuthorizationHandler, SecretAuthorizationHandler>();
services.AddScoped<IAuthorizationHandler, ServiceAccountAuthorizationHandler>(); services.AddScoped<IAuthorizationHandler, ServiceAccountAuthorizationHandler>();
services.AddScoped<IAccessClientQuery, AccessClientQuery>(); services.AddScoped<IAccessClientQuery, AccessClientQuery>();
services.AddScoped<ICreateSecretCommand, CreateSecretCommand>(); services.AddScoped<ICreateSecretCommand, CreateSecretCommand>();

View File

@ -0,0 +1,377 @@
using System.Reflection;
using System.Security.Claims;
using Bit.Commercial.Core.SecretsManager.AuthorizationHandlers.Secrets;
using Bit.Commercial.Core.Test.SecretsManager.Enums;
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 SecretAuthorizationHandlerTests
{
private static void SetupPermission(SutProvider<SecretAuthorizationHandler> sutProvider,
PermissionType permissionType, Guid organizationId, Guid userId = new(),
AccessClientType clientType = AccessClientType.User)
{
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(organizationId)
.Returns(true);
sutProvider.GetDependency<IProjectRepository>().ProjectsAreInOrganization(default, default)
.ReturnsForAnyArgs(true);
switch (permissionType)
{
case PermissionType.RunAsAdmin:
sutProvider.GetDependency<IAccessClientQuery>().GetAccessClientAsync(default, organizationId).ReturnsForAnyArgs(
(AccessClientType.NoAccessCheck, userId));
break;
case PermissionType.RunAsUserWithPermission:
sutProvider.GetDependency<IAccessClientQuery>().GetAccessClientAsync(default, organizationId).ReturnsForAnyArgs(
(clientType, userId));
break;
default:
throw new ArgumentOutOfRangeException(nameof(permissionType), permissionType, null);
}
}
[Fact]
public void SecretOperations_OnlyPublicStatic()
{
var publicStaticFields = typeof(SecretOperations).GetFields(BindingFlags.Public | BindingFlags.Static);
var allFields = typeof(SecretOperations).GetFields();
Assert.Equal(publicStaticFields.Length, allFields.Length);
}
[Theory]
[BitAutoData]
public async Task Handler_UnsupportedSecretOperationRequirement_Throws(
SutProvider<SecretAuthorizationHandler> sutProvider, Secret secret, ClaimsPrincipal claimsPrincipal)
{
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(secret.OrganizationId)
.Returns(true);
var requirement = new SecretOperationRequirement();
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, secret);
await Assert.ThrowsAsync<ArgumentException>(() => sutProvider.Sut.HandleAsync(authzContext));
}
[Theory]
[BitAutoData]
public async Task Handler_SupportedSecretOperationRequirement_Throws(
SutProvider<SecretAuthorizationHandler> sutProvider, Secret secret, ClaimsPrincipal claimsPrincipal)
{
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(secret.OrganizationId)
.Returns(true);
var requirements = typeof(SecretOperations).GetFields(BindingFlags.Public | BindingFlags.Static)
.Select(i => (SecretOperationRequirement)i.GetValue(null));
foreach (var req in requirements)
{
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { req },
claimsPrincipal, secret);
await sutProvider.Sut.HandleAsync(authzContext);
}
}
[Theory]
[BitAutoData]
public async Task CanCreateSecret_AccessToSecretsManagerFalse_DoesNotSucceed(
SutProvider<SecretAuthorizationHandler> sutProvider, Secret secret,
ClaimsPrincipal claimsPrincipal)
{
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(secret.OrganizationId)
.Returns(false);
var requirement = SecretOperations.Create;
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, secret);
await sutProvider.Sut.HandleAsync(authzContext);
Assert.False(authzContext.HasSucceeded);
}
[Theory]
[BitAutoData(AccessClientType.ServiceAccount)]
[BitAutoData(AccessClientType.Organization)]
public async Task CanCreateSecret_NotSupportedClientTypes_DoesNotSucceed(AccessClientType clientType,
SutProvider<SecretAuthorizationHandler> sutProvider, Secret secret, Guid userId,
ClaimsPrincipal claimsPrincipal)
{
var requirement = SecretOperations.Create;
SetupPermission(sutProvider, PermissionType.RunAsUserWithPermission, secret.OrganizationId, userId, clientType);
sutProvider.GetDependency<IProjectRepository>()
.AccessToProjectAsync(secret.Projects!.FirstOrDefault()!.Id, userId, default).Returns(
(true, true));
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, secret);
await sutProvider.Sut.HandleAsync(authzContext);
Assert.False(authzContext.HasSucceeded);
}
[Theory]
[BitAutoData]
public async Task CanCreateSecret_ProjectsNotInOrg_DoesNotSucceed(
SutProvider<SecretAuthorizationHandler> sutProvider, Secret secret,
Guid userId,
ClaimsPrincipal claimsPrincipal)
{
var requirement = SecretOperations.Create;
SetupPermission(sutProvider, PermissionType.RunAsUserWithPermission, secret.OrganizationId, userId);
sutProvider.GetDependency<IProjectRepository>().ProjectsAreInOrganization(default, default)
.ReturnsForAnyArgs(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 CanCreateSecret_WithoutProjectUser_DoesNotSucceed(
SutProvider<SecretAuthorizationHandler> sutProvider, Secret secret,
Guid userId,
ClaimsPrincipal claimsPrincipal)
{
secret.Projects = null;
var requirement = SecretOperations.Create;
SetupPermission(sutProvider, PermissionType.RunAsUserWithPermission, secret.OrganizationId, userId);
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, secret);
await sutProvider.Sut.HandleAsync(authzContext);
Assert.False(authzContext.HasSucceeded);
}
[Theory]
[BitAutoData]
public async Task CanCreateSecret_WithoutProjectAdmin_Success(SutProvider<SecretAuthorizationHandler> sutProvider,
Secret secret,
Guid userId,
ClaimsPrincipal claimsPrincipal)
{
secret.Projects = null;
var requirement = SecretOperations.Create;
SetupPermission(sutProvider, PermissionType.RunAsAdmin, secret.OrganizationId, userId);
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, secret);
await sutProvider.Sut.HandleAsync(authzContext);
Assert.True(authzContext.HasSucceeded);
}
[Theory]
[BitAutoData(PermissionType.RunAsUserWithPermission, true, false)]
[BitAutoData(PermissionType.RunAsUserWithPermission, false, false)]
public async Task CanCreateSecret_DoesNotSucceed(PermissionType permissionType, bool read, bool write,
SutProvider<SecretAuthorizationHandler> sutProvider, Secret secret,
Guid userId,
ClaimsPrincipal claimsPrincipal)
{
var requirement = SecretOperations.Create;
SetupPermission(sutProvider, permissionType, secret.OrganizationId, userId);
sutProvider.GetDependency<IProjectRepository>()
.AccessToProjectAsync(secret.Projects!.FirstOrDefault()!.Id, userId, default).Returns(
(read, write));
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, secret);
await sutProvider.Sut.HandleAsync(authzContext);
Assert.False(authzContext.HasSucceeded);
}
[Theory]
[BitAutoData(PermissionType.RunAsAdmin, true, true)]
[BitAutoData(PermissionType.RunAsAdmin, false, true)]
[BitAutoData(PermissionType.RunAsAdmin, true, false)]
[BitAutoData(PermissionType.RunAsAdmin, false, false)]
[BitAutoData(PermissionType.RunAsUserWithPermission, true, true)]
[BitAutoData(PermissionType.RunAsUserWithPermission, false, true)]
public async Task CanCreateSecret_Success(PermissionType permissionType, bool read, bool write,
SutProvider<SecretAuthorizationHandler> sutProvider, Secret secret,
Guid userId,
ClaimsPrincipal claimsPrincipal)
{
var requirement = SecretOperations.Create;
SetupPermission(sutProvider, permissionType, secret.OrganizationId, userId);
sutProvider.GetDependency<IProjectRepository>()
.AccessToProjectAsync(secret.Projects!.FirstOrDefault()!.Id, userId, default).Returns(
(read, write));
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, secret);
await sutProvider.Sut.HandleAsync(authzContext);
Assert.True(authzContext.HasSucceeded);
}
[Theory]
[BitAutoData]
public async Task CanUpdateSecret_AccessToSecretsManagerFalse_DoesNotSucceed(
SutProvider<SecretAuthorizationHandler> sutProvider, Secret secret,
ClaimsPrincipal claimsPrincipal)
{
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(secret.OrganizationId)
.Returns(false);
var requirement = SecretOperations.Update;
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, secret);
await sutProvider.Sut.HandleAsync(authzContext);
Assert.False(authzContext.HasSucceeded);
}
[Theory]
[BitAutoData(AccessClientType.ServiceAccount)]
[BitAutoData(AccessClientType.Organization)]
public async Task CanUpdateSecret_NotSupportedClientTypes_DoesNotSucceed(AccessClientType clientType,
SutProvider<SecretAuthorizationHandler> sutProvider, Secret secret, Guid userId,
ClaimsPrincipal claimsPrincipal)
{
var requirement = SecretOperations.Update;
SetupPermission(sutProvider, PermissionType.RunAsUserWithPermission, secret.OrganizationId, userId, clientType);
sutProvider.GetDependency<IProjectRepository>()
.AccessToProjectAsync(secret.Projects!.FirstOrDefault()!.Id, userId, default).Returns(
(true, true));
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, secret);
await sutProvider.Sut.HandleAsync(authzContext);
Assert.False(authzContext.HasSucceeded);
}
[Theory]
[BitAutoData]
public async Task CanUpdateSecret_ProjectsNotInOrg_DoesNotSucceed(
SutProvider<SecretAuthorizationHandler> sutProvider, Secret secret,
Guid userId,
ClaimsPrincipal claimsPrincipal)
{
var requirement = SecretOperations.Update;
SetupPermission(sutProvider, PermissionType.RunAsUserWithPermission, secret.OrganizationId, userId);
sutProvider.GetDependency<IProjectRepository>().ProjectsAreInOrganization(default, default)
.ReturnsForAnyArgs(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 CanUpdateSecret_WithoutProjectUser_DoesNotSucceed(
SutProvider<SecretAuthorizationHandler> sutProvider, Secret secret,
Guid userId,
ClaimsPrincipal claimsPrincipal)
{
secret.Projects = null;
var requirement = SecretOperations.Update;
SetupPermission(sutProvider, PermissionType.RunAsUserWithPermission, secret.OrganizationId, userId);
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, secret);
await sutProvider.Sut.HandleAsync(authzContext);
Assert.False(authzContext.HasSucceeded);
}
[Theory]
[BitAutoData]
public async Task CanUpdateSecret_WithoutProjectAdmin_Success(SutProvider<SecretAuthorizationHandler> sutProvider,
Secret secret,
Guid userId,
ClaimsPrincipal claimsPrincipal)
{
secret.Projects = null;
var requirement = SecretOperations.Update;
SetupPermission(sutProvider, PermissionType.RunAsAdmin, secret.OrganizationId, userId);
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, secret);
await sutProvider.Sut.HandleAsync(authzContext);
Assert.True(authzContext.HasSucceeded);
}
[Theory]
[BitAutoData(PermissionType.RunAsUserWithPermission, true, true, true, false)]
[BitAutoData(PermissionType.RunAsUserWithPermission, true, true, false, false)]
[BitAutoData(PermissionType.RunAsUserWithPermission, false, true, true, false)]
[BitAutoData(PermissionType.RunAsUserWithPermission, false, true, false, false)]
[BitAutoData(PermissionType.RunAsUserWithPermission, false, false, true, true)]
[BitAutoData(PermissionType.RunAsUserWithPermission, false, false, false, true)]
[BitAutoData(PermissionType.RunAsUserWithPermission, true, false, true, true)]
[BitAutoData(PermissionType.RunAsUserWithPermission, true, false, false, true)]
[BitAutoData(PermissionType.RunAsUserWithPermission, true, false, false, false)]
public async Task CanUpdateSecret_DoesNotSucceed(PermissionType permissionType, bool read, bool write,
bool projectRead, bool projectWrite,
SutProvider<SecretAuthorizationHandler> sutProvider, Secret secret,
Guid userId,
ClaimsPrincipal claimsPrincipal)
{
var requirement = SecretOperations.Update;
SetupPermission(sutProvider, permissionType, secret.OrganizationId, userId);
sutProvider.GetDependency<ISecretRepository>().AccessToSecretAsync(secret.Id, userId, default).Returns(
(read, write));
sutProvider.GetDependency<IProjectRepository>()
.AccessToProjectAsync(secret.Projects!.FirstOrDefault()!.Id, userId, default).Returns(
(projectRead, projectWrite));
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, secret);
await sutProvider.Sut.HandleAsync(authzContext);
Assert.False(authzContext.HasSucceeded);
}
[Theory]
[BitAutoData(PermissionType.RunAsAdmin, true, true)]
[BitAutoData(PermissionType.RunAsAdmin, false, true)]
[BitAutoData(PermissionType.RunAsAdmin, true, false)]
[BitAutoData(PermissionType.RunAsAdmin, false, false)]
[BitAutoData(PermissionType.RunAsUserWithPermission, true, true)]
[BitAutoData(PermissionType.RunAsUserWithPermission, false, true)]
public async Task CanUpdateSecret_Success(PermissionType permissionType, bool read, bool write,
SutProvider<SecretAuthorizationHandler> sutProvider, Secret secret,
Guid userId,
ClaimsPrincipal claimsPrincipal)
{
var requirement = SecretOperations.Update;
SetupPermission(sutProvider, permissionType, secret.OrganizationId, userId);
sutProvider.GetDependency<ISecretRepository>().AccessToSecretAsync(secret.Id, userId, default).Returns(
(read, write));
sutProvider.GetDependency<IProjectRepository>()
.AccessToProjectAsync(secret.Projects!.FirstOrDefault()!.Id, userId, default).Returns(
(read, write));
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, secret);
await sutProvider.Sut.HandleAsync(authzContext);
Assert.True(authzContext.HasSucceeded);
}
}

View File

@ -1,8 +1,4 @@
using Bit.Commercial.Core.SecretsManager.Commands.Secrets; using Bit.Commercial.Core.SecretsManager.Commands.Secrets;
using Bit.Commercial.Core.Test.SecretsManager.Enums;
using Bit.Core.Context;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.SecretsManager.Entities; using Bit.Core.SecretsManager.Entities;
using Bit.Core.SecretsManager.Repositories; using Bit.Core.SecretsManager.Repositories;
using Bit.Core.Test.SecretsManager.AutoFixture.SecretsFixture; using Bit.Core.Test.SecretsManager.AutoFixture.SecretsFixture;
@ -18,61 +14,15 @@ namespace Bit.Commercial.Core.Test.SecretsManager.Commands.Secrets;
public class CreateSecretCommandTests public class CreateSecretCommandTests
{ {
[Theory] [Theory]
[BitAutoData(PermissionType.RunAsAdmin)] [BitAutoData]
[BitAutoData(PermissionType.RunAsUserWithPermission)] public async Task CreateAsync_Success(Secret data,
public async Task CreateAsync_Success(PermissionType permissionType, Secret data, SutProvider<CreateSecretCommand> sutProvider, Project mockProject)
SutProvider<CreateSecretCommand> sutProvider, Guid userId, Project mockProject)
{ {
sutProvider.GetDependency<IProjectRepository>().ProjectsAreInOrganization(default, default).ReturnsForAnyArgs(true);
mockProject.OrganizationId = data.OrganizationId;
data.Projects = new List<Project>() { mockProject }; data.Projects = new List<Project>() { mockProject };
if (permissionType == PermissionType.RunAsAdmin) await sutProvider.Sut.CreateAsync(data);
{
sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(data.OrganizationId).Returns(true);
sutProvider.GetDependency<IProjectRepository>().AccessToProjectAsync((Guid)data.Projects?.First().Id, userId, AccessClientType.NoAccessCheck)
.Returns((true, true));
}
else
{
sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(data.OrganizationId).Returns(false);
sutProvider.GetDependency<IProjectRepository>().AccessToProjectAsync((Guid)data.Projects?.First().Id, userId, AccessClientType.User)
.Returns((true, true));
}
await sutProvider.Sut.CreateAsync(data, userId);
await sutProvider.GetDependency<ISecretRepository>().Received(1) await sutProvider.GetDependency<ISecretRepository>().Received(1)
.CreateAsync(data); .CreateAsync(data);
} }
[Theory]
[BitAutoData]
public async Task CreateAsync_UserWithoutPermission_ThrowsNotFound(Secret data,
SutProvider<CreateSecretCommand> sutProvider, Guid userId, Project mockProject)
{
data.Projects = new List<Project>() { mockProject };
sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(data.OrganizationId).Returns(false);
sutProvider.GetDependency<IProjectRepository>().AccessToProjectAsync((Guid)data.Projects?.First().Id, userId, AccessClientType.User)
.Returns((false, false));
await Assert.ThrowsAsync<NotFoundException>(() =>
sutProvider.Sut.CreateAsync(data, userId));
}
[Theory]
[BitAutoData]
public async Task CreateAsync_NoProjects_User_ThrowsNotFound(Secret data,
SutProvider<CreateSecretCommand> sutProvider, Guid userId)
{
data.Projects = null;
sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(data.OrganizationId).Returns(false);
await Assert.ThrowsAsync<NotFoundException>(() =>
sutProvider.Sut.CreateAsync(data, userId));
}
} }

View File

@ -1,7 +1,4 @@
using Bit.Commercial.Core.SecretsManager.Commands.Secrets; using Bit.Commercial.Core.SecretsManager.Commands.Secrets;
using Bit.Commercial.Core.Test.SecretsManager.Enums;
using Bit.Core.Context;
using Bit.Core.Enums;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.SecretsManager.Entities; using Bit.Core.SecretsManager.Entities;
using Bit.Core.SecretsManager.Repositories; using Bit.Core.SecretsManager.Repositories;
@ -24,36 +21,20 @@ public class UpdateSecretCommandTests
[BitAutoData] [BitAutoData]
public async Task UpdateAsync_SecretDoesNotExist_ThrowsNotFound(Secret data, SutProvider<UpdateSecretCommand> sutProvider) public async Task UpdateAsync_SecretDoesNotExist_ThrowsNotFound(Secret data, SutProvider<UpdateSecretCommand> sutProvider)
{ {
var exception = await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.UpdateAsync(data, default)); await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.UpdateAsync(data));
await sutProvider.GetDependency<ISecretRepository>().DidNotReceiveWithAnyArgs().UpdateAsync(default); await sutProvider.GetDependency<ISecretRepository>().DidNotReceiveWithAnyArgs().UpdateAsync(default);
} }
[Theory] [Theory]
[BitAutoData(PermissionType.RunAsAdmin)] [BitAutoData]
[BitAutoData(PermissionType.RunAsUserWithPermission)] public async Task UpdateAsync_Success(Secret existingSecret, Secret data, SutProvider<UpdateSecretCommand> sutProvider, Project mockProject)
public async Task UpdateAsync_Success(PermissionType permissionType, Secret data, SutProvider<UpdateSecretCommand> sutProvider, Guid userId, Project mockProject)
{ {
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(data.OrganizationId).Returns(true); sutProvider.GetDependency<ISecretRepository>().GetByIdAsync(existingSecret.Id).Returns(existingSecret);
sutProvider.GetDependency<IProjectRepository>().ProjectsAreInOrganization(default, default).ReturnsForAnyArgs(true);
mockProject.OrganizationId = data.OrganizationId;
data.Projects = new List<Project>() { mockProject }; data.Projects = new List<Project>() { mockProject };
if (permissionType == PermissionType.RunAsAdmin)
{
sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(data.OrganizationId).Returns(true);
}
else
{
sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(data.OrganizationId).Returns(false);
sutProvider.GetDependency<IProjectRepository>()
.AccessToProjectAsync((Guid)data.Projects?.First().Id, userId, AccessClientType.User)
.Returns((true, true));
}
sutProvider.GetDependency<ISecretRepository>().GetByIdAsync(data.Id).Returns(data); sutProvider.GetDependency<ISecretRepository>().GetByIdAsync(data.Id).Returns(data);
await sutProvider.Sut.UpdateAsync(data, userId); await sutProvider.Sut.UpdateAsync(data);
await sutProvider.GetDependency<ISecretRepository>().Received(1) await sutProvider.GetDependency<ISecretRepository>().Received(1)
.UpdateAsync(data); .UpdateAsync(data);
@ -61,13 +42,10 @@ public class UpdateSecretCommandTests
[Theory] [Theory]
[BitAutoData] [BitAutoData]
public async Task UpdateAsync_DoesNotModifyOrganizationId(Secret existingSecret, SutProvider<UpdateSecretCommand> sutProvider, Guid userId) public async Task UpdateAsync_DoesNotModifyOrganizationId(Secret existingSecret, SutProvider<UpdateSecretCommand> sutProvider)
{ {
var updatedOrgId = Guid.NewGuid(); var updatedOrgId = Guid.NewGuid();
sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(existingSecret.OrganizationId).Returns(true);
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(existingSecret.OrganizationId).Returns(true);
sutProvider.GetDependency<ISecretRepository>().GetByIdAsync(existingSecret.Id).Returns(existingSecret); sutProvider.GetDependency<ISecretRepository>().GetByIdAsync(existingSecret.Id).Returns(existingSecret);
sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(updatedOrgId).Returns(true);
var secretUpdate = new Secret() var secretUpdate = new Secret()
{ {
@ -76,7 +54,7 @@ public class UpdateSecretCommandTests
Key = existingSecret.Key, Key = existingSecret.Key,
}; };
var result = await sutProvider.Sut.UpdateAsync(secretUpdate, userId); var result = await sutProvider.Sut.UpdateAsync(secretUpdate);
Assert.Equal(existingSecret.OrganizationId, result.OrganizationId); Assert.Equal(existingSecret.OrganizationId, result.OrganizationId);
Assert.NotEqual(existingSecret.OrganizationId, updatedOrgId); Assert.NotEqual(existingSecret.OrganizationId, updatedOrgId);
@ -84,11 +62,9 @@ public class UpdateSecretCommandTests
[Theory] [Theory]
[BitAutoData] [BitAutoData]
public async Task UpdateAsync_DoesNotModifyCreationDate(Secret existingSecret, SutProvider<UpdateSecretCommand> sutProvider, Guid userId) public async Task UpdateAsync_DoesNotModifyCreationDate(Secret existingSecret, SutProvider<UpdateSecretCommand> sutProvider)
{ {
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(existingSecret.OrganizationId).Returns(true);
sutProvider.GetDependency<ISecretRepository>().GetByIdAsync(existingSecret.Id).Returns(existingSecret); sutProvider.GetDependency<ISecretRepository>().GetByIdAsync(existingSecret.Id).Returns(existingSecret);
sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(existingSecret.OrganizationId).Returns(true);
var updatedCreationDate = DateTime.UtcNow; var updatedCreationDate = DateTime.UtcNow;
var secretUpdate = new Secret() var secretUpdate = new Secret()
@ -99,7 +75,7 @@ public class UpdateSecretCommandTests
OrganizationId = existingSecret.OrganizationId OrganizationId = existingSecret.OrganizationId
}; };
var result = await sutProvider.Sut.UpdateAsync(secretUpdate, userId); var result = await sutProvider.Sut.UpdateAsync(secretUpdate);
Assert.Equal(existingSecret.CreationDate, result.CreationDate); Assert.Equal(existingSecret.CreationDate, result.CreationDate);
Assert.NotEqual(existingSecret.CreationDate, updatedCreationDate); Assert.NotEqual(existingSecret.CreationDate, updatedCreationDate);
@ -107,11 +83,9 @@ public class UpdateSecretCommandTests
[Theory] [Theory]
[BitAutoData] [BitAutoData]
public async Task UpdateAsync_DoesNotModifyDeletionDate(Secret existingSecret, SutProvider<UpdateSecretCommand> sutProvider, Guid userId) public async Task UpdateAsync_DoesNotModifyDeletionDate(Secret existingSecret, SutProvider<UpdateSecretCommand> sutProvider)
{ {
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(existingSecret.OrganizationId).Returns(true);
sutProvider.GetDependency<ISecretRepository>().GetByIdAsync(existingSecret.Id).Returns(existingSecret); sutProvider.GetDependency<ISecretRepository>().GetByIdAsync(existingSecret.Id).Returns(existingSecret);
sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(existingSecret.OrganizationId).Returns(true);
var updatedDeletionDate = DateTime.UtcNow; var updatedDeletionDate = DateTime.UtcNow;
var secretUpdate = new Secret() var secretUpdate = new Secret()
@ -122,7 +96,7 @@ public class UpdateSecretCommandTests
OrganizationId = existingSecret.OrganizationId OrganizationId = existingSecret.OrganizationId
}; };
var result = await sutProvider.Sut.UpdateAsync(secretUpdate, userId); var result = await sutProvider.Sut.UpdateAsync(secretUpdate);
Assert.Equal(existingSecret.DeletedDate, result.DeletedDate); Assert.Equal(existingSecret.DeletedDate, result.DeletedDate);
Assert.NotEqual(existingSecret.DeletedDate, updatedDeletionDate); Assert.NotEqual(existingSecret.DeletedDate, updatedDeletionDate);
@ -131,12 +105,9 @@ public class UpdateSecretCommandTests
[Theory] [Theory]
[BitAutoData] [BitAutoData]
public async Task UpdateAsync_RevisionDateIsUpdatedToUtcNow(Secret existingSecret, SutProvider<UpdateSecretCommand> sutProvider, Guid userId) public async Task UpdateAsync_RevisionDateIsUpdatedToUtcNow(Secret existingSecret, SutProvider<UpdateSecretCommand> sutProvider)
{ {
sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(existingSecret.OrganizationId).Returns(true);
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(existingSecret.OrganizationId).Returns(true);
sutProvider.GetDependency<ISecretRepository>().GetByIdAsync(existingSecret.Id).Returns(existingSecret); sutProvider.GetDependency<ISecretRepository>().GetByIdAsync(existingSecret.Id).Returns(existingSecret);
sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(existingSecret.OrganizationId).Returns(true);
var updatedRevisionDate = DateTime.UtcNow.AddDays(10); var updatedRevisionDate = DateTime.UtcNow.AddDays(10);
var secretUpdate = new Secret() var secretUpdate = new Secret()
@ -147,7 +118,7 @@ public class UpdateSecretCommandTests
OrganizationId = existingSecret.OrganizationId OrganizationId = existingSecret.OrganizationId
}; };
var result = await sutProvider.Sut.UpdateAsync(secretUpdate, userId); var result = await sutProvider.Sut.UpdateAsync(secretUpdate);
Assert.NotEqual(secretUpdate.RevisionDate, result.RevisionDate); Assert.NotEqual(secretUpdate.RevisionDate, result.RevisionDate);
AssertHelper.AssertRecent(result.RevisionDate); AssertHelper.AssertRecent(result.RevisionDate);

View File

@ -6,6 +6,7 @@ using Bit.Core.Enums;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.Identity; using Bit.Core.Identity;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.SecretsManager.AuthorizationRequirements;
using Bit.Core.SecretsManager.Commands.Secrets.Interfaces; using Bit.Core.SecretsManager.Commands.Secrets.Interfaces;
using Bit.Core.SecretsManager.Repositories; using Bit.Core.SecretsManager.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
@ -32,6 +33,7 @@ public class SecretsController : Controller
private readonly IUserService _userService; private readonly IUserService _userService;
private readonly IEventService _eventService; private readonly IEventService _eventService;
private readonly IReferenceEventService _referenceEventService; private readonly IReferenceEventService _referenceEventService;
private readonly IAuthorizationService _authorizationService;
public SecretsController( public SecretsController(
ICurrentContext currentContext, ICurrentContext currentContext,
@ -43,7 +45,8 @@ public class SecretsController : Controller
IDeleteSecretCommand deleteSecretCommand, IDeleteSecretCommand deleteSecretCommand,
IUserService userService, IUserService userService,
IEventService eventService, IEventService eventService,
IReferenceEventService referenceEventService) IReferenceEventService referenceEventService,
IAuthorizationService authorizationService)
{ {
_currentContext = currentContext; _currentContext = currentContext;
_projectRepository = projectRepository; _projectRepository = projectRepository;
@ -55,6 +58,7 @@ public class SecretsController : Controller
_userService = userService; _userService = userService;
_eventService = eventService; _eventService = eventService;
_referenceEventService = referenceEventService; _referenceEventService = referenceEventService;
_authorizationService = authorizationService;
} }
@ -78,18 +82,14 @@ public class SecretsController : Controller
[HttpPost("organizations/{organizationId}/secrets")] [HttpPost("organizations/{organizationId}/secrets")]
public async Task<SecretResponseModel> CreateAsync([FromRoute] Guid organizationId, [FromBody] SecretCreateRequestModel createRequest) public async Task<SecretResponseModel> CreateAsync([FromRoute] Guid organizationId, [FromBody] SecretCreateRequestModel createRequest)
{ {
if (!_currentContext.AccessSecretsManager(organizationId)) var secret = createRequest.ToSecret(organizationId);
var authorizationResult = await _authorizationService.AuthorizeAsync(User, secret, SecretOperations.Create);
if (!authorizationResult.Succeeded)
{ {
throw new NotFoundException(); throw new NotFoundException();
} }
if (createRequest.ProjectIds != null && createRequest.ProjectIds.Length > 1) var result = await _createSecretCommand.CreateAsync(secret);
{
throw new BadRequestException();
}
var userId = _userService.GetProperUserId(User).Value;
var result = await _createSecretCommand.CreateAsync(createRequest.ToSecret(organizationId), userId);
// Creating a secret means you have read & write permission. // Creating a secret means you have read & write permission.
return new SecretResponseModel(result, true, true); return new SecretResponseModel(result, true, true);
@ -148,14 +148,20 @@ public class SecretsController : Controller
[HttpPut("secrets/{id}")] [HttpPut("secrets/{id}")]
public async Task<SecretResponseModel> UpdateSecretAsync([FromRoute] Guid id, [FromBody] SecretUpdateRequestModel updateRequest) public async Task<SecretResponseModel> UpdateSecretAsync([FromRoute] Guid id, [FromBody] SecretUpdateRequestModel updateRequest)
{ {
if (updateRequest.ProjectIds != null && updateRequest.ProjectIds.Length > 1) var secret = await _secretRepository.GetByIdAsync(id);
if (secret == null)
{ {
throw new BadRequestException(); throw new NotFoundException();
} }
var userId = _userService.GetProperUserId(User).Value; var updatedSecret = updateRequest.ToSecret(id, secret.OrganizationId);
var secret = updateRequest.ToSecret(id); var authorizationResult = await _authorizationService.AuthorizeAsync(User, updatedSecret, SecretOperations.Update);
var result = await _updateSecretCommand.UpdateAsync(secret, userId); if (!authorizationResult.Succeeded)
{
throw new NotFoundException();
}
var result = await _updateSecretCommand.UpdateAsync(updatedSecret);
// Updating a secret means you have read & write permission. // Updating a secret means you have read & write permission.
return new SecretResponseModel(result, true, true); return new SecretResponseModel(result, true, true);

View File

@ -4,7 +4,7 @@ using Bit.Core.Utilities;
namespace Bit.Api.SecretsManager.Models.Request; namespace Bit.Api.SecretsManager.Models.Request;
public class SecretCreateRequestModel public class SecretCreateRequestModel : IValidatableObject
{ {
[Required] [Required]
[EncryptedString] [EncryptedString]
@ -32,4 +32,14 @@ public class SecretCreateRequestModel
Projects = ProjectIds != null && ProjectIds.Any() ? ProjectIds.Select(x => new Project() { Id = x }).ToList() : null, Projects = ProjectIds != null && ProjectIds.Any() ? ProjectIds.Select(x => new Project() { Id = x }).ToList() : null,
}; };
} }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (ProjectIds is { Length: > 1 })
{
yield return new ValidationResult(
$"Only one project assignment is supported.",
new[] { nameof(ProjectIds) });
}
}
} }

View File

@ -4,7 +4,7 @@ using Bit.Core.Utilities;
namespace Bit.Api.SecretsManager.Models.Request; namespace Bit.Api.SecretsManager.Models.Request;
public class SecretUpdateRequestModel public class SecretUpdateRequestModel : IValidatableObject
{ {
[Required] [Required]
[EncryptedString] [EncryptedString]
@ -20,11 +20,12 @@ public class SecretUpdateRequestModel
public Guid[] ProjectIds { get; set; } public Guid[] ProjectIds { get; set; }
public Secret ToSecret(Guid id) public Secret ToSecret(Guid id, Guid organizationId)
{ {
return new Secret() return new Secret()
{ {
Id = id, Id = id,
OrganizationId = organizationId,
Key = Key, Key = Key,
Value = Value, Value = Value,
Note = Note, Note = Note,
@ -32,4 +33,14 @@ public class SecretUpdateRequestModel
Projects = ProjectIds != null && ProjectIds.Any() ? ProjectIds.Select(x => new Project() { Id = x }).ToList() : null, Projects = ProjectIds != null && ProjectIds.Any() ? ProjectIds.Select(x => new Project() { Id = x }).ToList() : null,
}; };
} }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (ProjectIds is { Length: > 1 })
{
yield return new ValidationResult(
$"Only one project assignment is supported.",
new[] { nameof(ProjectIds) });
}
}
} }

View File

@ -0,0 +1,13 @@
using Microsoft.AspNetCore.Authorization.Infrastructure;
namespace Bit.Core.SecretsManager.AuthorizationRequirements;
public class SecretOperationRequirement : OperationAuthorizationRequirement
{
}
public static class SecretOperations
{
public static readonly SecretOperationRequirement Create = new() { Name = nameof(Create) };
public static readonly SecretOperationRequirement Update = new() { Name = nameof(Update) };
}

View File

@ -4,5 +4,5 @@ namespace Bit.Core.SecretsManager.Commands.Secrets.Interfaces;
public interface ICreateSecretCommand public interface ICreateSecretCommand
{ {
Task<Secret> CreateAsync(Secret secret, Guid userId); Task<Secret> CreateAsync(Secret secret);
} }

View File

@ -4,5 +4,5 @@ namespace Bit.Core.SecretsManager.Commands.Secrets.Interfaces;
public interface IUpdateSecretCommand public interface IUpdateSecretCommand
{ {
Task<Secret> UpdateAsync(Secret secret, Guid userId); Task<Secret> UpdateAsync(Secret secret);
} }

View File

@ -148,11 +148,8 @@ public class SecretsControllerTests : IClassFixture<ApiApplicationFactory>, IAsy
var (org, _) = await _organizationHelper.Initialize(true, true); var (org, _) = await _organizationHelper.Initialize(true, true);
await LoginAsync(_email); await LoginAsync(_email);
var project = await _projectRepository.CreateAsync(new Project { OrganizationId = org.Id, Name = "123" });
var request = new SecretCreateRequestModel var request = new SecretCreateRequestModel
{ {
ProjectIds = new Guid[] { project.Id },
Key = _mockEncryptedString, Key = _mockEncryptedString,
Value = _mockEncryptedString, Value = _mockEncryptedString,
Note = _mockEncryptedString, Note = _mockEncryptedString,

View File

@ -1,4 +1,5 @@
using Bit.Api.SecretsManager.Controllers; using System.Security.Claims;
using Bit.Api.SecretsManager.Controllers;
using Bit.Api.SecretsManager.Models.Request; using Bit.Api.SecretsManager.Models.Request;
using Bit.Api.Test.SecretsManager.Enums; using Bit.Api.Test.SecretsManager.Enums;
using Bit.Core.Context; using Bit.Core.Context;
@ -13,6 +14,7 @@ using Bit.Core.Test.SecretsManager.AutoFixture.SecretsFixture;
using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.AutoFixture.Attributes;
using Bit.Test.Common.Helpers; using Bit.Test.Common.Helpers;
using Microsoft.AspNetCore.Authorization;
using NSubstitute; using NSubstitute;
using Xunit; using Xunit;
@ -125,9 +127,8 @@ public class SecretsControllerTests
} }
[Theory] [Theory]
[BitAutoData(PermissionType.RunAsAdmin)] [BitAutoData]
[BitAutoData(PermissionType.RunAsUserWithPermission)] public async void CreateSecret_NoAccess_Throws(SutProvider<SecretsController> sutProvider, SecretCreateRequestModel data, Guid organizationId, Guid userId)
public async void CreateSecret_Success(PermissionType permissionType, SutProvider<SecretsController> sutProvider, SecretCreateRequestModel data, Guid organizationId, Project mockProject, Guid userId)
{ {
// We currently only allow a secret to be in one project at a time // We currently only allow a secret to be in one project at a time
if (data.ProjectIds != null && data.ProjectIds.Length > 1) if (data.ProjectIds != null && data.ProjectIds.Length > 1)
@ -135,33 +136,23 @@ public class SecretsControllerTests
data.ProjectIds = new Guid[] { data.ProjectIds.ElementAt(0) }; data.ProjectIds = new Guid[] { data.ProjectIds.ElementAt(0) };
} }
sutProvider.GetDependency<IAuthorizationService>()
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), data.ToSecret(organizationId),
Arg.Any<IEnumerable<IAuthorizationRequirement>>()).ReturnsForAnyArgs(AuthorizationResult.Failed());
var resultSecret = data.ToSecret(organizationId); var resultSecret = data.ToSecret(organizationId);
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId); sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
if (permissionType == PermissionType.RunAsAdmin) sutProvider.GetDependency<ICreateSecretCommand>().CreateAsync(default).ReturnsForAnyArgs(resultSecret);
{
sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(organizationId).Returns(true);
}
else
{
resultSecret.Projects = new List<Core.SecretsManager.Entities.Project>() { mockProject };
sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(organizationId).Returns(false);
sutProvider.GetDependency<IProjectRepository>().AccessToProjectAsync(Arg.Any<Guid>(), Arg.Any<Guid>(), AccessClientType.User)
.Returns((true, true));
}
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(organizationId).Returns(true); await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.CreateAsync(organizationId, data));
sutProvider.GetDependency<ICreateSecretCommand>().CreateAsync(default, userId).ReturnsForAnyArgs(resultSecret); await sutProvider.GetDependency<ICreateSecretCommand>().DidNotReceiveWithAnyArgs()
.CreateAsync(Arg.Any<Secret>());
var result = await sutProvider.Sut.CreateAsync(organizationId, data);
await sutProvider.GetDependency<ICreateSecretCommand>().Received(1)
.CreateAsync(Arg.Any<Secret>(), userId);
} }
[Theory] [Theory]
[BitAutoData(PermissionType.RunAsAdmin)] [BitAutoData]
[BitAutoData(PermissionType.RunAsUserWithPermission)] public async void CreateSecret_Success(SutProvider<SecretsController> sutProvider, SecretCreateRequestModel data, Guid organizationId, Guid userId)
public async void UpdateSecret_Success(PermissionType permissionType, SutProvider<SecretsController> sutProvider, SecretUpdateRequestModel data, Guid secretId, Guid organizationId, Guid userId, Project mockProject)
{ {
// We currently only allow a secret to be in one project at a time // We currently only allow a secret to be in one project at a time
if (data.ProjectIds != null && data.ProjectIds.Length > 1) if (data.ProjectIds != null && data.ProjectIds.Length > 1)
@ -169,26 +160,87 @@ public class SecretsControllerTests
data.ProjectIds = new Guid[] { data.ProjectIds.ElementAt(0) }; data.ProjectIds = new Guid[] { data.ProjectIds.ElementAt(0) };
} }
sutProvider.GetDependency<IAuthorizationService>()
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), data.ToSecret(organizationId),
Arg.Any<IEnumerable<IAuthorizationRequirement>>()).ReturnsForAnyArgs(AuthorizationResult.Success());
var resultSecret = data.ToSecret(organizationId);
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId); sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
if (permissionType == PermissionType.RunAsAdmin) sutProvider.GetDependency<ICreateSecretCommand>().CreateAsync(default).ReturnsForAnyArgs(resultSecret);
await sutProvider.Sut.CreateAsync(organizationId, data);
await sutProvider.GetDependency<ICreateSecretCommand>().Received(1)
.CreateAsync(Arg.Any<Secret>());
}
[Theory]
[BitAutoData]
public async void UpdateSecret_NoAccess_Throws(SutProvider<SecretsController> sutProvider, SecretUpdateRequestModel data, Guid secretId, Guid organizationId, Secret mockSecret)
{
// We currently only allow a secret to be in one project at a time
if (data.ProjectIds != null && data.ProjectIds.Length > 1)
{ {
sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(organizationId).Returns(true); data.ProjectIds = new Guid[] { data.ProjectIds.ElementAt(0) };
}
else
{
data.ProjectIds = new Guid[] { mockProject.Id };
sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(organizationId).Returns(false);
sutProvider.GetDependency<IProjectRepository>().AccessToProjectAsync(default, default, default)
.Returns((true, true));
} }
var resultSecret = data.ToSecret(secretId); sutProvider.GetDependency<IAuthorizationService>()
sutProvider.GetDependency<IUpdateSecretCommand>().UpdateAsync(default, userId).ReturnsForAnyArgs(resultSecret); .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), data.ToSecret(secretId, organizationId),
Arg.Any<IEnumerable<IAuthorizationRequirement>>()).ReturnsForAnyArgs(AuthorizationResult.Failed());
sutProvider.GetDependency<ISecretRepository>().GetByIdAsync(secretId).ReturnsForAnyArgs(mockSecret);
var result = await sutProvider.Sut.UpdateSecretAsync(secretId, data); var resultSecret = data.ToSecret(secretId, organizationId);
sutProvider.GetDependency<IUpdateSecretCommand>().UpdateAsync(default).ReturnsForAnyArgs(resultSecret);
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.UpdateSecretAsync(secretId, data));
await sutProvider.GetDependency<IUpdateSecretCommand>().DidNotReceiveWithAnyArgs()
.UpdateAsync(Arg.Any<Secret>());
}
[Theory]
[BitAutoData]
public async void UpdateSecret_SecretDoesNotExist_Throws(SutProvider<SecretsController> sutProvider, SecretUpdateRequestModel data, Guid secretId, Guid organizationId)
{
// We currently only allow a secret to be in one project at a time
if (data.ProjectIds != null && data.ProjectIds.Length > 1)
{
data.ProjectIds = new Guid[] { data.ProjectIds.ElementAt(0) };
}
sutProvider.GetDependency<IAuthorizationService>()
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), data.ToSecret(secretId, organizationId),
Arg.Any<IEnumerable<IAuthorizationRequirement>>()).ReturnsForAnyArgs(AuthorizationResult.Failed());
var resultSecret = data.ToSecret(secretId, organizationId);
sutProvider.GetDependency<IUpdateSecretCommand>().UpdateAsync(default).ReturnsForAnyArgs(resultSecret);
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.UpdateSecretAsync(secretId, data));
await sutProvider.GetDependency<IUpdateSecretCommand>().DidNotReceiveWithAnyArgs()
.UpdateAsync(Arg.Any<Secret>());
}
[Theory]
[BitAutoData]
public async void UpdateSecret_Success(SutProvider<SecretsController> sutProvider, SecretUpdateRequestModel data, Guid secretId, Guid organizationId, Secret mockSecret)
{
// We currently only allow a secret to be in one project at a time
if (data.ProjectIds != null && data.ProjectIds.Length > 1)
{
data.ProjectIds = new Guid[] { data.ProjectIds.ElementAt(0) };
}
sutProvider.GetDependency<IAuthorizationService>()
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), data.ToSecret(secretId, organizationId),
Arg.Any<IEnumerable<IAuthorizationRequirement>>()).ReturnsForAnyArgs(AuthorizationResult.Success());
sutProvider.GetDependency<ISecretRepository>().GetByIdAsync(secretId).ReturnsForAnyArgs(mockSecret);
var resultSecret = data.ToSecret(secretId, organizationId);
sutProvider.GetDependency<IUpdateSecretCommand>().UpdateAsync(default).ReturnsForAnyArgs(resultSecret);
await sutProvider.Sut.UpdateSecretAsync(secretId, data);
await sutProvider.GetDependency<IUpdateSecretCommand>().Received(1) await sutProvider.GetDependency<IUpdateSecretCommand>().Received(1)
.UpdateAsync(Arg.Any<Secret>(), userId); .UpdateAsync(Arg.Any<Secret>());
} }
[Theory] [Theory]