1
0
mirror of https://github.com/bitwarden/server.git synced 2025-02-22 02:51:33 +01: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(
IAccessPolicyRepository accessPolicyRepository,
IOrganizationUserRepository organizationUserRepository,
IProjectRepository projectRepository
)
IProjectRepository projectRepository)
{
_accessPolicyRepository = accessPolicyRepository;
_organizationUserRepository = organizationUserRepository;

View File

@ -1,6 +1,4 @@
using Bit.Core.Context;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Exceptions;
using Bit.Core.SecretsManager.Commands.Projects.Interfaces;
using Bit.Core.SecretsManager.Entities;
using Bit.Core.SecretsManager.Repositories;
@ -10,15 +8,13 @@ namespace Bit.Commercial.Core.SecretsManager.Commands.Projects;
public class UpdateProjectCommand : IUpdateProjectCommand
{
private readonly IProjectRepository _projectRepository;
private readonly ICurrentContext _currentContext;
public UpdateProjectCommand(IProjectRepository projectRepository, ICurrentContext currentContext)
public UpdateProjectCommand(IProjectRepository 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);
if (project == null)
@ -26,20 +22,6 @@ public class UpdateProjectCommand : IUpdateProjectCommand
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.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.Porting;
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.ServiceAccounts.Interfaces;
using Bit.Core.SecretsManager.Commands.Trash.Interfaces;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.DependencyInjection;
namespace Bit.Commercial.Core.SecretsManager;
@ -20,6 +22,7 @@ public static class SecretsManagerCollectionExtensions
{
public static void AddSecretsManagerServices(this IServiceCollection services)
{
services.AddScoped<IAuthorizationHandler, ProjectAuthorizationHandler>();
services.AddScoped<ICreateSecretCommand, CreateSecretCommand>();
services.AddScoped<IUpdateSecretCommand, UpdateSecretCommand>();
services.AddScoped<IDeleteSecretCommand, DeleteSecretCommand>();

View File

@ -125,7 +125,7 @@ public class ProjectRepository : Repository<Core.SecretsManager.Entities.Project
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)

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 Xunit;
namespace Bit.Commercial.Core.Test.SecretsManager.AccessPolicies;
namespace Bit.Commercial.Core.Test.SecretsManager.Commands.AccessPolicies;
[SutProviderCustomize]
[ProjectCustomize]

View File

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

View File

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

View File

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

View File

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

View File

@ -10,7 +10,7 @@ using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
namespace Bit.Commercial.Core.Test.SecretsManager.Projects;
namespace Bit.Commercial.Core.Test.SecretsManager.Commands.Projects;
[SutProviderCustomize]
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 Xunit;
namespace Bit.Commercial.Core.Test.SecretsManager.Secrets;
namespace Bit.Commercial.Core.Test.SecretsManager.Commands.Secrets;
[SutProviderCustomize]
[SecretCustomize]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -8,7 +8,7 @@ using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
namespace Bit.Commercial.Core.Test.SecretsManager.Trash;
namespace Bit.Commercial.Core.Test.SecretsManager.Commands.Trash;
[SutProviderCustomize]
[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.Enums;
using Bit.Core.Exceptions;
using Bit.Core.SecretsManager.AuthorizationRequirements;
using Bit.Core.SecretsManager.Commands.Projects.Interfaces;
using Bit.Core.SecretsManager.Repositories;
using Bit.Core.Services;
@ -22,6 +23,7 @@ public class ProjectsController : Controller
private readonly ICreateProjectCommand _createProjectCommand;
private readonly IUpdateProjectCommand _updateProjectCommand;
private readonly IDeleteProjectCommand _deleteProjectCommand;
private readonly IAuthorizationService _authorizationService;
public ProjectsController(
ICurrentContext currentContext,
@ -29,7 +31,8 @@ public class ProjectsController : Controller
IProjectRepository projectRepository,
ICreateProjectCommand createProjectCommand,
IUpdateProjectCommand updateProjectCommand,
IDeleteProjectCommand deleteProjectCommand)
IDeleteProjectCommand deleteProjectCommand,
IAuthorizationService authorizationService)
{
_currentContext = currentContext;
_userService = userService;
@ -37,6 +40,7 @@ public class ProjectsController : Controller
_createProjectCommand = createProjectCommand;
_updateProjectCommand = updateProjectCommand;
_deleteProjectCommand = deleteProjectCommand;
_authorizationService = authorizationService;
}
[HttpGet("organizations/{organizationId}/projects")]
@ -58,26 +62,37 @@ public class ProjectsController : Controller
}
[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();
}
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.
return new ProjectResponseModel(result, true, true);
}
[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.
return new ProjectResponseModel(result, true, true);

View File

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

View File

@ -12,11 +12,10 @@ public class ProjectUpdateRequestModel
public Project ToProject(Guid id)
{
return new Project()
return new Project
{
Id = id,
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
{
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.Test.SecretsManager.Enums;
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.Attributes;
using Bit.Test.Common.Helpers;
using Microsoft.AspNetCore.Authorization;
using NSubstitute;
using Xunit;
@ -102,10 +104,18 @@ public class ProjectsControllerTests
[Theory]
[BitAutoData]
public async void Create_SmNotEnabled_Throws(SutProvider<ProjectsController> sutProvider, Guid orgId,
ProjectCreateRequestModel data)
public async void Create_NoAccess_Throws(SutProvider<ProjectsController> sutProvider,
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 sutProvider.GetDependency<ICreateProjectCommand>().DidNotReceiveWithAnyArgs()
@ -113,22 +123,17 @@ public class ProjectsControllerTests
}
[Theory]
[BitAutoData(PermissionType.RunAsAdmin)]
[BitAutoData(PermissionType.RunAsUserWithPermission)]
public async void Create_Success(PermissionType permissionType, SutProvider<ProjectsController> sutProvider,
[BitAutoData]
public async void Create_Success(SutProvider<ProjectsController> sutProvider,
Guid orgId, ProjectCreateRequestModel data)
{
switch (permissionType)
{
case PermissionType.RunAsAdmin:
SetupAdmin(sutProvider, orgId);
break;
case PermissionType.RunAsUserWithPermission:
SetupUserWithPermission(sutProvider, orgId);
break;
}
sutProvider.GetDependency<IAuthorizationService>()
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), data.ToProject(orgId),
Arg.Any<IEnumerable<IAuthorizationRequirement>>()).ReturnsForAnyArgs(AuthorizationResult.Success());
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(Guid.NewGuid());
var resultProject = data.ToProject(orgId);
sutProvider.GetDependency<ICreateProjectCommand>().CreateAsync(default, default)
.ReturnsForAnyArgs(resultProject);
@ -139,29 +144,44 @@ public class ProjectsControllerTests
}
[Theory]
[BitAutoData(PermissionType.RunAsAdmin)]
[BitAutoData(PermissionType.RunAsUserWithPermission)]
public async void Update_Success(PermissionType permissionType, SutProvider<ProjectsController> sutProvider,
Guid orgId, ProjectUpdateRequestModel data)
[BitAutoData]
public async void Update_NoAccess_Throws(SutProvider<ProjectsController> sutProvider,
Guid userId, ProjectUpdateRequestModel data, Project existingProject)
{
switch (permissionType)
{
case PermissionType.RunAsAdmin:
SetupAdmin(sutProvider, orgId);
break;
case PermissionType.RunAsUserWithPermission:
SetupUserWithPermission(sutProvider, orgId);
break;
}
sutProvider.GetDependency<IAuthorizationService>()
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), data.ToProject(existingProject.Id),
Arg.Any<IEnumerable<IAuthorizationRequirement>>()).ReturnsForAnyArgs(AuthorizationResult.Failed());
sutProvider.GetDependency<IProjectRepository>().GetByIdAsync(existingProject.Id).ReturnsForAnyArgs(existingProject);
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
var resultProject = data.ToProject(orgId);
sutProvider.GetDependency<IUpdateProjectCommand>().UpdateAsync(default, default)
var resultProject = data.ToProject(existingProject.Id);
sutProvider.GetDependency<IUpdateProjectCommand>().UpdateAsync(default)
.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)
.UpdateAsync(Arg.Any<Project>(), Arg.Any<Guid>());
.UpdateAsync(Arg.Any<Project>());
}
[Theory]