1
0
mirror of https://github.com/bitwarden/server.git synced 2025-04-05 13:08:17 -05:00

[SM-702] Extract access policy checks from create/update project commands (#2842)

* Move to access query for project commands

* Swap to hasAccess method per action

* Swap to authorization handler pattern

* Move ProjectOperationRequirement to Core

* Add default throw + tests

* Swap to reflection for testing switch
This commit is contained in:
Thomas Avery 2023-05-03 09:40:24 -05:00 committed by GitHub
parent 7be19b53f4
commit 5474d3da18
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 490 additions and 166 deletions

View File

@ -0,0 +1,88 @@
using Bit.Core.Context;
using Bit.Core.Enums;
using Bit.Core.SecretsManager.AuthorizationRequirements;
using Bit.Core.SecretsManager.Entities;
using Bit.Core.SecretsManager.Repositories;
using Bit.Core.Services;
using Microsoft.AspNetCore.Authorization;
namespace Bit.Commercial.Core.SecretsManager.AuthorizationHandlers.Projects;
public class ProjectAuthorizationHandler : AuthorizationHandler<ProjectOperationRequirement, Project>
{
private readonly ICurrentContext _currentContext;
private readonly IProjectRepository _projectRepository;
private readonly IUserService _userService;
public ProjectAuthorizationHandler(ICurrentContext currentContext, IUserService userService,
IProjectRepository projectRepository)
{
_currentContext = currentContext;
_userService = userService;
_projectRepository = projectRepository;
}
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context,
ProjectOperationRequirement requirement,
Project resource)
{
if (!_currentContext.AccessSecretsManager(resource.OrganizationId))
{
return;
}
switch (requirement)
{
case not null when requirement == ProjectOperations.Create:
await CanCreateProjectAsync(context, requirement, resource);
break;
case not null when requirement == ProjectOperations.Update:
await CanUpdateProjectAsync(context, requirement, resource);
break;
default:
throw new ArgumentException("Unsupported project operation requirement type provided.", nameof(requirement));
}
}
private async Task CanCreateProjectAsync(AuthorizationHandlerContext context,
ProjectOperationRequirement requirement, Project resource)
{
var accessClient = await GetAccessClientAsync(resource.OrganizationId);
var hasAccess = accessClient switch
{
AccessClientType.NoAccessCheck => true,
AccessClientType.User => true,
AccessClientType.ServiceAccount => false,
_ => false,
};
if (hasAccess)
{
context.Succeed(requirement);
}
}
private async Task CanUpdateProjectAsync(AuthorizationHandlerContext context,
ProjectOperationRequirement requirement, Project resource)
{
var accessClient = await GetAccessClientAsync(resource.OrganizationId);
if (accessClient == AccessClientType.ServiceAccount)
{
return;
}
var userId = _userService.GetProperUserId(context.User).Value;
var access = await _projectRepository.AccessToProjectAsync(resource.Id, userId, accessClient);
if (access.Write)
{
context.Succeed(requirement);
}
}
private async Task<AccessClientType> GetAccessClientAsync(Guid organizationId)
{
var orgAdmin = await _currentContext.OrganizationAdmin(organizationId);
return AccessClientHelper.ToAccessClient(_currentContext.ClientType, orgAdmin);
}
}

View File

@ -14,8 +14,7 @@ public class CreateProjectCommand : ICreateProjectCommand
public CreateProjectCommand( public CreateProjectCommand(
IAccessPolicyRepository accessPolicyRepository, IAccessPolicyRepository accessPolicyRepository,
IOrganizationUserRepository organizationUserRepository, IOrganizationUserRepository organizationUserRepository,
IProjectRepository projectRepository IProjectRepository projectRepository)
)
{ {
_accessPolicyRepository = accessPolicyRepository; _accessPolicyRepository = accessPolicyRepository;
_organizationUserRepository = organizationUserRepository; _organizationUserRepository = organizationUserRepository;

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.Projects.Interfaces; using Bit.Core.SecretsManager.Commands.Projects.Interfaces;
using Bit.Core.SecretsManager.Entities; using Bit.Core.SecretsManager.Entities;
using Bit.Core.SecretsManager.Repositories; using Bit.Core.SecretsManager.Repositories;
@ -10,15 +8,13 @@ namespace Bit.Commercial.Core.SecretsManager.Commands.Projects;
public class UpdateProjectCommand : IUpdateProjectCommand public class UpdateProjectCommand : IUpdateProjectCommand
{ {
private readonly IProjectRepository _projectRepository; private readonly IProjectRepository _projectRepository;
private readonly ICurrentContext _currentContext;
public UpdateProjectCommand(IProjectRepository projectRepository, ICurrentContext currentContext) public UpdateProjectCommand(IProjectRepository projectRepository)
{ {
_projectRepository = projectRepository; _projectRepository = projectRepository;
_currentContext = currentContext;
} }
public async Task<Project> UpdateAsync(Project updatedProject, Guid userId) public async Task<Project> UpdateAsync(Project updatedProject)
{ {
var project = await _projectRepository.GetByIdAsync(updatedProject.Id); var project = await _projectRepository.GetByIdAsync(updatedProject.Id);
if (project == null) if (project == null)
@ -26,20 +22,6 @@ public class UpdateProjectCommand : IUpdateProjectCommand
throw new NotFoundException(); throw new NotFoundException();
} }
if (!_currentContext.AccessSecretsManager(project.OrganizationId))
{
throw new NotFoundException();
}
var orgAdmin = await _currentContext.OrganizationAdmin(project.OrganizationId);
var accessClient = AccessClientHelper.ToAccessClient(_currentContext.ClientType, orgAdmin);
var access = await _projectRepository.AccessToProjectAsync(updatedProject.Id, userId, accessClient);
if (!access.Write || accessClient == AccessClientType.ServiceAccount)
{
throw new NotFoundException();
}
project.Name = updatedProject.Name; project.Name = updatedProject.Name;
project.RevisionDate = DateTime.UtcNow; project.RevisionDate = DateTime.UtcNow;

View File

@ -1,4 +1,5 @@
using Bit.Commercial.Core.SecretsManager.Commands.AccessPolicies; using Bit.Commercial.Core.SecretsManager.AuthorizationHandlers.Projects;
using Bit.Commercial.Core.SecretsManager.Commands.AccessPolicies;
using Bit.Commercial.Core.SecretsManager.Commands.AccessTokens; using Bit.Commercial.Core.SecretsManager.Commands.AccessTokens;
using Bit.Commercial.Core.SecretsManager.Commands.Porting; using Bit.Commercial.Core.SecretsManager.Commands.Porting;
using Bit.Commercial.Core.SecretsManager.Commands.Projects; using Bit.Commercial.Core.SecretsManager.Commands.Projects;
@ -12,6 +13,7 @@ using Bit.Core.SecretsManager.Commands.Projects.Interfaces;
using Bit.Core.SecretsManager.Commands.Secrets.Interfaces; using Bit.Core.SecretsManager.Commands.Secrets.Interfaces;
using Bit.Core.SecretsManager.Commands.ServiceAccounts.Interfaces; using Bit.Core.SecretsManager.Commands.ServiceAccounts.Interfaces;
using Bit.Core.SecretsManager.Commands.Trash.Interfaces; using Bit.Core.SecretsManager.Commands.Trash.Interfaces;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
namespace Bit.Commercial.Core.SecretsManager; namespace Bit.Commercial.Core.SecretsManager;
@ -20,6 +22,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<ICreateSecretCommand, CreateSecretCommand>(); services.AddScoped<ICreateSecretCommand, CreateSecretCommand>();
services.AddScoped<IUpdateSecretCommand, UpdateSecretCommand>(); services.AddScoped<IUpdateSecretCommand, UpdateSecretCommand>();
services.AddScoped<IDeleteSecretCommand, DeleteSecretCommand>(); services.AddScoped<IDeleteSecretCommand, DeleteSecretCommand>();

View File

@ -125,7 +125,7 @@ public class ProjectRepository : Repository<Core.SecretsManager.Entities.Project
var policy = await query.FirstOrDefaultAsync(); var policy = await query.FirstOrDefaultAsync();
return (policy.Read, policy.Write); return policy == null ? (false, false) : (policy.Read, policy.Write);
} }
public async Task<bool> ProjectsAreInOrganization(List<Guid> projectIds, Guid organizationId) public async Task<bool> ProjectsAreInOrganization(List<Guid> projectIds, Guid organizationId)

View File

@ -0,0 +1,245 @@
using System.Reflection;
using System.Security.Claims;
using Bit.Commercial.Core.SecretsManager.AuthorizationHandlers.Projects;
using Bit.Commercial.Core.Test.SecretsManager.Enums;
using Bit.Core.Context;
using Bit.Core.Enums;
using Bit.Core.Identity;
using Bit.Core.SecretsManager.AuthorizationRequirements;
using Bit.Core.SecretsManager.Entities;
using Bit.Core.SecretsManager.Repositories;
using Bit.Core.Services;
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.Projects;
[SutProviderCustomize]
[ProjectCustomize]
public class ProjectAuthorizationHandlerTests
{
private static void SetupPermission(SutProvider<ProjectAuthorizationHandler> sutProvider,
PermissionType permissionType, Guid organizationId)
{
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(organizationId)
.Returns(true);
sutProvider.GetDependency<ICurrentContext>().ClientType
.Returns(ClientType.User);
switch (permissionType)
{
case PermissionType.RunAsAdmin:
sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(organizationId).Returns(true);
break;
case PermissionType.RunAsUserWithPermission:
sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(organizationId).Returns(false);
break;
default:
throw new ArgumentOutOfRangeException(nameof(permissionType), permissionType, null);
}
}
[Fact]
public void ProjectOperations_OnlyPublicStatic()
{
var publicStaticFields = typeof(ProjectOperations).GetFields(BindingFlags.Public | BindingFlags.Static);
var allFields = typeof(ProjectOperations).GetFields();
Assert.Equal(publicStaticFields.Length, allFields.Length);
}
[Theory]
[BitAutoData]
public async Task Handler_UnsupportedProjectOperationRequirement_Throws(
SutProvider<ProjectAuthorizationHandler> sutProvider, Project project, ClaimsPrincipal claimsPrincipal)
{
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(project.OrganizationId)
.Returns(true);
var requirement = new ProjectOperationRequirement();
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, project);
await Assert.ThrowsAsync<ArgumentException>(() => sutProvider.Sut.HandleAsync(authzContext));
}
[Theory]
[BitAutoData]
public async Task Handler_SupportedProjectOperationRequirement_DoesNotThrow(
SutProvider<ProjectAuthorizationHandler> sutProvider, Project project, ClaimsPrincipal claimsPrincipal)
{
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(project.OrganizationId)
.Returns(true);
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(new Guid());
var requirements = typeof(ProjectOperations).GetFields(BindingFlags.Public | BindingFlags.Static)
.Select(i => (ProjectOperationRequirement)i.GetValue(null));
foreach (var req in requirements)
{
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { req },
claimsPrincipal, project);
await sutProvider.Sut.HandleAsync(authzContext);
}
}
[Theory]
[BitAutoData]
public async Task CanCreateProject_AccessToSecretsManagerFalse_DoesNotSucceed(
SutProvider<ProjectAuthorizationHandler> sutProvider, Project project, ClaimsPrincipal claimsPrincipal)
{
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(project.OrganizationId)
.Returns(false);
var requirement = ProjectOperations.Create;
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, project);
await sutProvider.Sut.HandleAsync(authzContext);
Assert.False(authzContext.HasSucceeded);
}
[Theory]
[BitAutoData(ClientType.ServiceAccount)]
[BitAutoData(ClientType.Organization)]
public async Task CanCreateProject_NotSupportedClientTypes_DoesNotSucceed(ClientType clientType,
SutProvider<ProjectAuthorizationHandler> sutProvider, Project project, ClaimsPrincipal claimsPrincipal)
{
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(project.OrganizationId)
.Returns(true);
sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(project.OrganizationId)
.Returns(false);
sutProvider.GetDependency<ICurrentContext>().ClientType
.Returns(clientType);
var requirement = ProjectOperations.Create;
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, project);
await sutProvider.Sut.HandleAsync(authzContext);
Assert.False(authzContext.HasSucceeded);
}
[Theory]
[BitAutoData(PermissionType.RunAsAdmin)]
[BitAutoData(PermissionType.RunAsUserWithPermission)]
public async Task CanCreateProject_Success(PermissionType permissionType,
SutProvider<ProjectAuthorizationHandler> sutProvider, Project project, ClaimsPrincipal claimsPrincipal)
{
SetupPermission(sutProvider, permissionType, project.OrganizationId);
var requirement = ProjectOperations.Create;
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, project);
await sutProvider.Sut.HandleAsync(authzContext);
Assert.True(authzContext.HasSucceeded);
}
[Theory]
[BitAutoData]
public async Task CanUpdateProject_AccessToSecretsManagerFalse_DoesNotSucceed(
SutProvider<ProjectAuthorizationHandler> sutProvider, Project project, ClaimsPrincipal claimsPrincipal)
{
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(project.OrganizationId)
.Returns(false);
var requirement = ProjectOperations.Update;
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, project);
await sutProvider.Sut.HandleAsync(authzContext);
Assert.False(authzContext.HasSucceeded);
}
[Theory]
[BitAutoData]
public async Task CanUpdateProject_NullResource_DoesNotSucceed(
SutProvider<ProjectAuthorizationHandler> sutProvider, Project project, ClaimsPrincipal claimsPrincipal,
Guid userId)
{
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(project.OrganizationId)
.Returns(true);
SetupPermission(sutProvider, PermissionType.RunAsAdmin, project.OrganizationId);
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
sutProvider.GetDependency<IProjectRepository>()
.AccessToProjectAsync(project.Id, userId, Arg.Any<AccessClientType>())
.Returns((true, true));
var requirement = ProjectOperations.Update;
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, null);
await sutProvider.Sut.HandleAsync(authzContext);
Assert.False(authzContext.HasSucceeded);
}
[Theory]
[BitAutoData]
public async Task CanUpdateProject_NotSupportedClientType_DoesNotSucceed(
SutProvider<ProjectAuthorizationHandler> sutProvider, Project project, ClaimsPrincipal claimsPrincipal)
{
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(project.OrganizationId)
.Returns(true);
sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(project.OrganizationId).Returns(false);
sutProvider.GetDependency<ICurrentContext>().ClientType
.Returns(ClientType.ServiceAccount);
var requirement = ProjectOperations.Update;
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, project);
await sutProvider.Sut.HandleAsync(authzContext);
Assert.False(authzContext.HasSucceeded);
}
[Theory]
[BitAutoData(PermissionType.RunAsUserWithPermission, true, false)]
[BitAutoData(PermissionType.RunAsUserWithPermission, false, false)]
public async Task CanUpdateProject_ShouldNotSucceed(PermissionType permissionType, bool read, bool write,
SutProvider<ProjectAuthorizationHandler> sutProvider, Project project, ClaimsPrincipal claimsPrincipal,
Guid userId)
{
SetupPermission(sutProvider, permissionType, project.OrganizationId);
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
sutProvider.GetDependency<IProjectRepository>()
.AccessToProjectAsync(project.Id, userId, Arg.Any<AccessClientType>())
.Returns((read, write));
var requirement = ProjectOperations.Update;
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, project);
await sutProvider.Sut.HandleAsync(authzContext);
Assert.False(authzContext.HasSucceeded);
}
[Theory]
[BitAutoData(PermissionType.RunAsAdmin, true, true)]
[BitAutoData(PermissionType.RunAsAdmin, false, true)]
[BitAutoData(PermissionType.RunAsUserWithPermission, true, true)]
[BitAutoData(PermissionType.RunAsUserWithPermission, false, true)]
public async Task CanUpdateProject_Success(PermissionType permissionType, bool read, bool write,
SutProvider<ProjectAuthorizationHandler> sutProvider, Project project, ClaimsPrincipal claimsPrincipal,
Guid userId)
{
SetupPermission(sutProvider, permissionType, project.OrganizationId);
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
sutProvider.GetDependency<IProjectRepository>()
.AccessToProjectAsync(project.Id, userId, Arg.Any<AccessClientType>())
.Returns((read, write));
var requirement = ProjectOperations.Update;
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, project);
await sutProvider.Sut.HandleAsync(authzContext);
Assert.True(authzContext.HasSucceeded);
}
}

View File

@ -11,7 +11,7 @@ using Bit.Test.Common.Helpers;
using NSubstitute; using NSubstitute;
using Xunit; using Xunit;
namespace Bit.Commercial.Core.Test.SecretsManager.AccessPolicies; namespace Bit.Commercial.Core.Test.SecretsManager.Commands.AccessPolicies;
[SutProviderCustomize] [SutProviderCustomize]
[ProjectCustomize] [ProjectCustomize]

View File

@ -13,7 +13,7 @@ using NSubstitute;
using NSubstitute.ReturnsExtensions; using NSubstitute.ReturnsExtensions;
using Xunit; using Xunit;
namespace Bit.Commercial.Core.Test.SecretsManager.AccessPolicies; namespace Bit.Commercial.Core.Test.SecretsManager.Commands.AccessPolicies;
[SutProviderCustomize] [SutProviderCustomize]
[ProjectCustomize] [ProjectCustomize]

View File

@ -13,7 +13,7 @@ using Bit.Test.Common.Helpers;
using NSubstitute; using NSubstitute;
using Xunit; using Xunit;
namespace Bit.Commercial.Core.Test.SecretsManager.AccessPolicies; namespace Bit.Commercial.Core.Test.SecretsManager.Commands.AccessPolicies;
[SutProviderCustomize] [SutProviderCustomize]
[ProjectCustomize] [ProjectCustomize]

View File

@ -9,7 +9,7 @@ using Bit.Test.Common.Helpers;
using NSubstitute; using NSubstitute;
using Xunit; using Xunit;
namespace Bit.Commercial.Core.Test.SecretsManager.AccessTokens; namespace Bit.Commercial.Core.Test.SecretsManager.Commands.AccessTokens;
[SutProviderCustomize] [SutProviderCustomize]
public class CreateServiceAccountCommandTests public class CreateServiceAccountCommandTests

View File

@ -9,7 +9,7 @@ using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute; using NSubstitute;
using Xunit; using Xunit;
namespace Bit.Commercial.Core.Test.SecretsManager.Projects; namespace Bit.Commercial.Core.Test.SecretsManager.Commands.Projects;
[SutProviderCustomize] [SutProviderCustomize]
[ProjectCustomize] [ProjectCustomize]

View File

@ -10,7 +10,7 @@ using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute; using NSubstitute;
using Xunit; using Xunit;
namespace Bit.Commercial.Core.Test.SecretsManager.Projects; namespace Bit.Commercial.Core.Test.SecretsManager.Commands.Projects;
[SutProviderCustomize] [SutProviderCustomize]
public class DeleteProjectCommandTests public class DeleteProjectCommandTests

View File

@ -0,0 +1,43 @@
using Bit.Commercial.Core.SecretsManager.Commands.Projects;
using Bit.Core.Exceptions;
using Bit.Core.SecretsManager.Entities;
using Bit.Core.SecretsManager.Repositories;
using Bit.Core.Test.SecretsManager.AutoFixture.ProjectsFixture;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using NSubstitute.ReturnsExtensions;
using Xunit;
namespace Bit.Commercial.Core.Test.SecretsManager.Commands.Projects;
[SutProviderCustomize]
[ProjectCustomize]
public class UpdateProjectCommandTests
{
[Theory]
[BitAutoData]
public async Task UpdateAsync_Throws_NotFoundException(Project project, SutProvider<UpdateProjectCommand> sutProvider)
{
sutProvider.GetDependency<IProjectRepository>().GetByIdAsync(project.Id).ReturnsNull();
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.UpdateAsync(project));
await sutProvider.GetDependency<IProjectRepository>().DidNotReceiveWithAnyArgs().ReplaceAsync(default);
}
[Theory]
[BitAutoData]
public async Task UpdateAsync_Success(Project project, SutProvider<UpdateProjectCommand> sutProvider)
{
sutProvider.GetDependency<IProjectRepository>().GetByIdAsync(project.Id).Returns(project);
var updatedProject = new Project { Id = project.Id, Name = "newName" };
var result = await sutProvider.Sut.UpdateAsync(updatedProject);
Assert.NotNull(result);
Assert.Equal("newName", result.Name);
await sutProvider.GetDependency<IProjectRepository>().ReceivedWithAnyArgs(1).ReplaceAsync(default);
}
}

View File

@ -11,7 +11,7 @@ using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute; using NSubstitute;
using Xunit; using Xunit;
namespace Bit.Commercial.Core.Test.SecretsManager.Secrets; namespace Bit.Commercial.Core.Test.SecretsManager.Commands.Secrets;
[SutProviderCustomize] [SutProviderCustomize]
[SecretCustomize] [SecretCustomize]

View File

@ -12,7 +12,7 @@ using Bit.Test.Common.Helpers;
using NSubstitute; using NSubstitute;
using Xunit; using Xunit;
namespace Bit.Commercial.Core.Test.SecretsManager.Secrets; namespace Bit.Commercial.Core.Test.SecretsManager.Commands.Secrets;
[SutProviderCustomize] [SutProviderCustomize]
[ProjectCustomize] [ProjectCustomize]

View File

@ -13,7 +13,7 @@ using Bit.Test.Common.Helpers;
using NSubstitute; using NSubstitute;
using Xunit; using Xunit;
namespace Bit.Commercial.Core.Test.SecretsManager.Secrets; namespace Bit.Commercial.Core.Test.SecretsManager.Commands.Secrets;
[SutProviderCustomize] [SutProviderCustomize]
[SecretCustomize] [SecretCustomize]

View File

@ -9,7 +9,7 @@ using Bit.Test.Common.Helpers;
using NSubstitute; using NSubstitute;
using Xunit; using Xunit;
namespace Bit.Commercial.Core.Test.SecretsManager.ServiceAccounts; namespace Bit.Commercial.Core.Test.SecretsManager.Commands.ServiceAccounts;
[SutProviderCustomize] [SutProviderCustomize]
public class CreateServiceAccountCommandTests public class CreateServiceAccountCommandTests

View File

@ -6,7 +6,7 @@ using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute; using NSubstitute;
using Xunit; using Xunit;
namespace Bit.Commercial.Core.Test.SecretsManager.ServiceAccounts; namespace Bit.Commercial.Core.Test.SecretsManager.Commands.ServiceAccounts;
[SutProviderCustomize] [SutProviderCustomize]
public class RevokeAccessTokenCommandTests public class RevokeAccessTokenCommandTests

View File

@ -9,7 +9,7 @@ using Bit.Test.Common.Helpers;
using NSubstitute; using NSubstitute;
using Xunit; using Xunit;
namespace Bit.Commercial.Core.Test.SecretsManager.ServiceAccounts; namespace Bit.Commercial.Core.Test.SecretsManager.Commands.ServiceAccounts;
[SutProviderCustomize] [SutProviderCustomize]
public class UpdateServiceAccountCommandTests public class UpdateServiceAccountCommandTests

View File

@ -8,7 +8,7 @@ using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute; using NSubstitute;
using Xunit; using Xunit;
namespace Bit.Commercial.Core.Test.SecretsManager.Trash; namespace Bit.Commercial.Core.Test.SecretsManager.Commands.Trash;
[SutProviderCustomize] [SutProviderCustomize]
[ProjectCustomize] [ProjectCustomize]

View File

@ -8,7 +8,7 @@ using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute; using NSubstitute;
using Xunit; using Xunit;
namespace Bit.Commercial.Core.Test.SecretsManager.Trash; namespace Bit.Commercial.Core.Test.SecretsManager.Commands.Trash;
[SutProviderCustomize] [SutProviderCustomize]
[ProjectCustomize] [ProjectCustomize]

View File

@ -1,83 +0,0 @@
using Bit.Commercial.Core.SecretsManager.Commands.Projects;
using Bit.Core.Context;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.SecretsManager.Entities;
using Bit.Core.SecretsManager.Repositories;
using Bit.Core.Test.SecretsManager.AutoFixture.ProjectsFixture;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Bit.Test.Common.Helpers;
using NSubstitute;
using NSubstitute.ReturnsExtensions;
using Xunit;
namespace Bit.Commercial.Core.Test.SecretsManager.Projects;
[SutProviderCustomize]
[ProjectCustomize]
public class UpdateProjectCommandTests
{
[Theory]
[BitAutoData]
public async Task UpdateAsync_Throws_NotFoundException(Project project, Guid userId, SutProvider<UpdateProjectCommand> sutProvider)
{
sutProvider.GetDependency<IProjectRepository>().GetByIdAsync(project.Id).ReturnsNull();
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.UpdateAsync(project, userId));
await sutProvider.GetDependency<IProjectRepository>().DidNotReceiveWithAnyArgs().ReplaceAsync(default);
}
[Theory]
[BitAutoData]
public async Task UpdateAsync_Admin_Succeeds(Project project, Guid userId, SutProvider<UpdateProjectCommand> sutProvider)
{
sutProvider.GetDependency<IProjectRepository>().GetByIdAsync(project.Id).Returns(project);
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(project.OrganizationId).Returns(true);
sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(project.OrganizationId).Returns(true);
sutProvider.GetDependency<IProjectRepository>().AccessToProjectAsync(project.Id, userId, AccessClientType.NoAccessCheck)
.Returns((true, true));
var project2 = new Project { Id = project.Id, Name = "newName" };
var result = await sutProvider.Sut.UpdateAsync(project2, userId);
Assert.NotNull(result);
Assert.Equal("newName", result.Name);
AssertHelper.AssertRecent(result.RevisionDate);
await sutProvider.GetDependency<IProjectRepository>().ReceivedWithAnyArgs(1).ReplaceAsync(default);
}
[Theory]
[BitAutoData]
public async Task UpdateAsync_User_NoAccess(Project project, Guid userId, SutProvider<UpdateProjectCommand> sutProvider)
{
sutProvider.GetDependency<IProjectRepository>().GetByIdAsync(project.Id).Returns(project);
sutProvider.GetDependency<IProjectRepository>().AccessToProjectAsync(project.Id, userId, AccessClientType.User)
.Returns((false, false));
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(project.OrganizationId).Returns(true);
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.UpdateAsync(project, userId));
await sutProvider.GetDependency<IProjectRepository>().DidNotReceiveWithAnyArgs().ReplaceAsync(default);
}
[Theory]
[BitAutoData]
public async Task UpdateAsync_User_Success(Project project, Guid userId, SutProvider<UpdateProjectCommand> sutProvider)
{
sutProvider.GetDependency<IProjectRepository>().GetByIdAsync(project.Id).Returns(project);
sutProvider.GetDependency<IProjectRepository>().AccessToProjectAsync(project.Id, userId, AccessClientType.User)
.Returns((true, true));
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(project.OrganizationId).Returns(true);
var project2 = new Project { Id = project.Id, Name = "newName" };
var result = await sutProvider.Sut.UpdateAsync(project2, userId);
Assert.NotNull(result);
Assert.Equal("newName", result.Name);
await sutProvider.GetDependency<IProjectRepository>().ReceivedWithAnyArgs(1).ReplaceAsync(default);
}
}

View File

@ -4,6 +4,7 @@ using Bit.Api.SecretsManager.Models.Response;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.SecretsManager.AuthorizationRequirements;
using Bit.Core.SecretsManager.Commands.Projects.Interfaces; using Bit.Core.SecretsManager.Commands.Projects.Interfaces;
using Bit.Core.SecretsManager.Repositories; using Bit.Core.SecretsManager.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
@ -22,6 +23,7 @@ public class ProjectsController : Controller
private readonly ICreateProjectCommand _createProjectCommand; private readonly ICreateProjectCommand _createProjectCommand;
private readonly IUpdateProjectCommand _updateProjectCommand; private readonly IUpdateProjectCommand _updateProjectCommand;
private readonly IDeleteProjectCommand _deleteProjectCommand; private readonly IDeleteProjectCommand _deleteProjectCommand;
private readonly IAuthorizationService _authorizationService;
public ProjectsController( public ProjectsController(
ICurrentContext currentContext, ICurrentContext currentContext,
@ -29,7 +31,8 @@ public class ProjectsController : Controller
IProjectRepository projectRepository, IProjectRepository projectRepository,
ICreateProjectCommand createProjectCommand, ICreateProjectCommand createProjectCommand,
IUpdateProjectCommand updateProjectCommand, IUpdateProjectCommand updateProjectCommand,
IDeleteProjectCommand deleteProjectCommand) IDeleteProjectCommand deleteProjectCommand,
IAuthorizationService authorizationService)
{ {
_currentContext = currentContext; _currentContext = currentContext;
_userService = userService; _userService = userService;
@ -37,6 +40,7 @@ public class ProjectsController : Controller
_createProjectCommand = createProjectCommand; _createProjectCommand = createProjectCommand;
_updateProjectCommand = updateProjectCommand; _updateProjectCommand = updateProjectCommand;
_deleteProjectCommand = deleteProjectCommand; _deleteProjectCommand = deleteProjectCommand;
_authorizationService = authorizationService;
} }
[HttpGet("organizations/{organizationId}/projects")] [HttpGet("organizations/{organizationId}/projects")]
@ -58,26 +62,37 @@ public class ProjectsController : Controller
} }
[HttpPost("organizations/{organizationId}/projects")] [HttpPost("organizations/{organizationId}/projects")]
public async Task<ProjectResponseModel> CreateAsync([FromRoute] Guid organizationId, [FromBody] ProjectCreateRequestModel createRequest) public async Task<ProjectResponseModel> CreateAsync([FromRoute] Guid organizationId,
[FromBody] ProjectCreateRequestModel createRequest)
{ {
if (!_currentContext.AccessSecretsManager(organizationId)) var project = createRequest.ToProject(organizationId);
var authorizationResult =
await _authorizationService.AuthorizeAsync(User, project, ProjectOperations.Create);
if (!authorizationResult.Succeeded)
{ {
throw new NotFoundException(); throw new NotFoundException();
} }
var userId = _userService.GetProperUserId(User).Value; var userId = _userService.GetProperUserId(User).Value;
var result = await _createProjectCommand.CreateAsync(createRequest.ToProject(organizationId), userId); var result = await _createProjectCommand.CreateAsync(project, userId);
// Creating a project means you have read & write permission. // Creating a project means you have read & write permission.
return new ProjectResponseModel(result, true, true); return new ProjectResponseModel(result, true, true);
} }
[HttpPut("projects/{id}")] [HttpPut("projects/{id}")]
public async Task<ProjectResponseModel> UpdateAsync([FromRoute] Guid id, [FromBody] ProjectUpdateRequestModel updateRequest) public async Task<ProjectResponseModel> UpdateAsync([FromRoute] Guid id,
[FromBody] ProjectUpdateRequestModel updateRequest)
{ {
var userId = _userService.GetProperUserId(User).Value; var project = await _projectRepository.GetByIdAsync(id);
var authorizationResult =
await _authorizationService.AuthorizeAsync(User, project, ProjectOperations.Update);
if (!authorizationResult.Succeeded)
{
throw new NotFoundException();
}
var result = await _updateProjectCommand.UpdateAsync(updateRequest.ToProject(id), userId); var result = await _updateProjectCommand.UpdateAsync(updateRequest.ToProject(id));
// Updating a project means you have read & write permission. // Updating a project means you have read & write permission.
return new ProjectResponseModel(result, true, true); return new ProjectResponseModel(result, true, true);

View File

@ -12,7 +12,7 @@ public class ProjectCreateRequestModel
public Project ToProject(Guid organizationId) public Project ToProject(Guid organizationId)
{ {
return new Project() return new Project
{ {
OrganizationId = organizationId, OrganizationId = organizationId,
Name = Name, Name = Name,

View File

@ -12,11 +12,10 @@ public class ProjectUpdateRequestModel
public Project ToProject(Guid id) public Project ToProject(Guid id)
{ {
return new Project() return new Project
{ {
Id = id, Id = id,
Name = Name, Name = Name,
}; };
} }
} }

View File

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

View File

@ -4,5 +4,5 @@ namespace Bit.Core.SecretsManager.Commands.Projects.Interfaces;
public interface IUpdateProjectCommand public interface IUpdateProjectCommand
{ {
Task<Project> UpdateAsync(Project updatedProject, Guid userId); Task<Project> UpdateAsync(Project updatedProject);
} }

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.ProjectsFixture;
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;
@ -102,10 +104,18 @@ public class ProjectsControllerTests
[Theory] [Theory]
[BitAutoData] [BitAutoData]
public async void Create_SmNotEnabled_Throws(SutProvider<ProjectsController> sutProvider, Guid orgId, public async void Create_NoAccess_Throws(SutProvider<ProjectsController> sutProvider,
ProjectCreateRequestModel data) Guid orgId, ProjectCreateRequestModel data)
{ {
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(orgId).Returns(false); sutProvider.GetDependency<IAuthorizationService>()
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), data.ToProject(orgId),
Arg.Any<IEnumerable<IAuthorizationRequirement>>()).ReturnsForAnyArgs(AuthorizationResult.Failed());
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(Guid.NewGuid());
var resultProject = data.ToProject(orgId);
sutProvider.GetDependency<ICreateProjectCommand>().CreateAsync(default, default)
.ReturnsForAnyArgs(resultProject);
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.CreateAsync(orgId, data)); await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.CreateAsync(orgId, data));
await sutProvider.GetDependency<ICreateProjectCommand>().DidNotReceiveWithAnyArgs() await sutProvider.GetDependency<ICreateProjectCommand>().DidNotReceiveWithAnyArgs()
@ -113,22 +123,17 @@ public class ProjectsControllerTests
} }
[Theory] [Theory]
[BitAutoData(PermissionType.RunAsAdmin)] [BitAutoData]
[BitAutoData(PermissionType.RunAsUserWithPermission)] public async void Create_Success(SutProvider<ProjectsController> sutProvider,
public async void Create_Success(PermissionType permissionType, SutProvider<ProjectsController> sutProvider,
Guid orgId, ProjectCreateRequestModel data) Guid orgId, ProjectCreateRequestModel data)
{ {
switch (permissionType) sutProvider.GetDependency<IAuthorizationService>()
{ .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), data.ToProject(orgId),
case PermissionType.RunAsAdmin: Arg.Any<IEnumerable<IAuthorizationRequirement>>()).ReturnsForAnyArgs(AuthorizationResult.Success());
SetupAdmin(sutProvider, orgId); sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(Guid.NewGuid());
break;
case PermissionType.RunAsUserWithPermission:
SetupUserWithPermission(sutProvider, orgId);
break;
}
var resultProject = data.ToProject(orgId); var resultProject = data.ToProject(orgId);
sutProvider.GetDependency<ICreateProjectCommand>().CreateAsync(default, default) sutProvider.GetDependency<ICreateProjectCommand>().CreateAsync(default, default)
.ReturnsForAnyArgs(resultProject); .ReturnsForAnyArgs(resultProject);
@ -139,29 +144,44 @@ public class ProjectsControllerTests
} }
[Theory] [Theory]
[BitAutoData(PermissionType.RunAsAdmin)] [BitAutoData]
[BitAutoData(PermissionType.RunAsUserWithPermission)] public async void Update_NoAccess_Throws(SutProvider<ProjectsController> sutProvider,
public async void Update_Success(PermissionType permissionType, SutProvider<ProjectsController> sutProvider, Guid userId, ProjectUpdateRequestModel data, Project existingProject)
Guid orgId, ProjectUpdateRequestModel data)
{ {
switch (permissionType) sutProvider.GetDependency<IAuthorizationService>()
{ .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), data.ToProject(existingProject.Id),
case PermissionType.RunAsAdmin: Arg.Any<IEnumerable<IAuthorizationRequirement>>()).ReturnsForAnyArgs(AuthorizationResult.Failed());
SetupAdmin(sutProvider, orgId); sutProvider.GetDependency<IProjectRepository>().GetByIdAsync(existingProject.Id).ReturnsForAnyArgs(existingProject);
break; sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
case PermissionType.RunAsUserWithPermission:
SetupUserWithPermission(sutProvider, orgId);
break;
}
var resultProject = data.ToProject(orgId); var resultProject = data.ToProject(existingProject.Id);
sutProvider.GetDependency<IUpdateProjectCommand>().UpdateAsync(default, default) sutProvider.GetDependency<IUpdateProjectCommand>().UpdateAsync(default)
.ReturnsForAnyArgs(resultProject); .ReturnsForAnyArgs(resultProject);
await sutProvider.Sut.UpdateAsync(orgId, data); await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.UpdateAsync(existingProject.Id, data));
await sutProvider.GetDependency<IUpdateProjectCommand>().DidNotReceiveWithAnyArgs()
.UpdateAsync(Arg.Any<Project>());
}
[Theory]
[BitAutoData]
public async void Update_Success(SutProvider<ProjectsController> sutProvider,
Guid userId, ProjectUpdateRequestModel data, Project existingProject)
{
sutProvider.GetDependency<IAuthorizationService>()
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), data.ToProject(existingProject.Id),
Arg.Any<IEnumerable<IAuthorizationRequirement>>()).ReturnsForAnyArgs(AuthorizationResult.Success());
sutProvider.GetDependency<IProjectRepository>().GetByIdAsync(existingProject.Id).ReturnsForAnyArgs(existingProject);
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
var resultProject = data.ToProject(existingProject.Id);
sutProvider.GetDependency<IUpdateProjectCommand>().UpdateAsync(default)
.ReturnsForAnyArgs(resultProject);
await sutProvider.Sut.UpdateAsync(existingProject.Id, data);
await sutProvider.GetDependency<IUpdateProjectCommand>().Received(1) await sutProvider.GetDependency<IUpdateProjectCommand>().Received(1)
.UpdateAsync(Arg.Any<Project>(), Arg.Any<Guid>()); .UpdateAsync(Arg.Any<Project>());
} }
[Theory] [Theory]