Browse Source
* 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 * Extract authorization out of commands * Unit tests for authorization handler * Formatting * Swap to reflection for testing switch * Swap to check read & reflections in test * fix wording on exception * Refactor GetAccessClient into its own query * Use accessClientQuery in project handlerpull/2991/head
16 changed files with 674 additions and 229 deletions
@ -0,0 +1,100 @@
@@ -0,0 +1,100 @@
|
||||
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.ServiceAccounts; |
||||
|
||||
public class
|
||||
ServiceAccountAuthorizationHandler : AuthorizationHandler<ServiceAccountOperationRequirement, ServiceAccount> |
||||
{ |
||||
private readonly IAccessClientQuery _accessClientQuery; |
||||
private readonly ICurrentContext _currentContext; |
||||
private readonly IServiceAccountRepository _serviceAccountRepository; |
||||
|
||||
public ServiceAccountAuthorizationHandler(ICurrentContext currentContext, |
||||
IAccessClientQuery accessClientQuery, |
||||
IServiceAccountRepository serviceAccountRepository) |
||||
{ |
||||
_currentContext = currentContext; |
||||
_accessClientQuery = accessClientQuery; |
||||
_serviceAccountRepository = serviceAccountRepository; |
||||
} |
||||
|
||||
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, |
||||
ServiceAccountOperationRequirement requirement, |
||||
ServiceAccount resource) |
||||
{ |
||||
if (!_currentContext.AccessSecretsManager(resource.OrganizationId)) |
||||
{ |
||||
return; |
||||
} |
||||
|
||||
switch (requirement) |
||||
{ |
||||
case not null when requirement == ServiceAccountOperations.Create: |
||||
await CanCreateServiceAccountAsync(context, requirement, resource); |
||||
break; |
||||
case not null when requirement == ServiceAccountOperations.Read: |
||||
await CanReadServiceAccountAsync(context, requirement, resource); |
||||
break; |
||||
case not null when requirement == ServiceAccountOperations.Update: |
||||
await CanUpdateServiceAccountAsync(context, requirement, resource); |
||||
break; |
||||
default: |
||||
throw new ArgumentException("Unsupported operation requirement type provided.", |
||||
nameof(requirement)); |
||||
} |
||||
} |
||||
|
||||
private async Task CanCreateServiceAccountAsync(AuthorizationHandlerContext context, |
||||
ServiceAccountOperationRequirement requirement, ServiceAccount resource) |
||||
{ |
||||
var (accessClient, _) = await _accessClientQuery.GetAccessClientAsync(context.User, resource.OrganizationId); |
||||
var hasAccess = accessClient switch |
||||
{ |
||||
AccessClientType.NoAccessCheck => true, |
||||
AccessClientType.User => true, |
||||
AccessClientType.ServiceAccount => false, |
||||
_ => false, |
||||
}; |
||||
|
||||
if (hasAccess) |
||||
{ |
||||
context.Succeed(requirement); |
||||
} |
||||
} |
||||
|
||||
private async Task CanReadServiceAccountAsync(AuthorizationHandlerContext context, |
||||
ServiceAccountOperationRequirement requirement, ServiceAccount resource) |
||||
{ |
||||
var (accessClient, userId) = |
||||
await _accessClientQuery.GetAccessClientAsync(context.User, resource.OrganizationId); |
||||
var access = |
||||
await _serviceAccountRepository.AccessToServiceAccountAsync(resource.Id, userId, |
||||
accessClient); |
||||
|
||||
if (access.Read) |
||||
{ |
||||
context.Succeed(requirement); |
||||
} |
||||
} |
||||
|
||||
private async Task CanUpdateServiceAccountAsync(AuthorizationHandlerContext context, |
||||
ServiceAccountOperationRequirement requirement, ServiceAccount resource) |
||||
{ |
||||
var (accessClient, userId) = |
||||
await _accessClientQuery.GetAccessClientAsync(context.User, resource.OrganizationId); |
||||
var access = |
||||
await _serviceAccountRepository.AccessToServiceAccountAsync(resource.Id, userId, |
||||
accessClient); |
||||
|
||||
if (access.Write) |
||||
{ |
||||
context.Succeed(requirement); |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,28 @@
@@ -0,0 +1,28 @@
|
||||
using System.Security.Claims; |
||||
using Bit.Core.Context; |
||||
using Bit.Core.Enums; |
||||
using Bit.Core.SecretsManager.Queries.Interfaces; |
||||
using Bit.Core.Services; |
||||
|
||||
namespace Bit.Commercial.Core.SecretsManager.Queries; |
||||
|
||||
public class AccessClientQuery : IAccessClientQuery |
||||
{ |
||||
private readonly ICurrentContext _currentContext; |
||||
private readonly IUserService _userService; |
||||
|
||||
public AccessClientQuery(ICurrentContext currentContext, IUserService userService) |
||||
{ |
||||
_currentContext = currentContext; |
||||
_userService = userService; |
||||
} |
||||
|
||||
public async Task<(AccessClientType AccessClientType, Guid UserId)> GetAccessClientAsync( |
||||
ClaimsPrincipal claimsPrincipal, Guid organizationId) |
||||
{ |
||||
var orgAdmin = await _currentContext.OrganizationAdmin(organizationId); |
||||
var accessClient = AccessClientHelper.ToAccessClient(_currentContext.ClientType, orgAdmin); |
||||
var userId = _userService.GetProperUserId(claimsPrincipal).Value; |
||||
return (accessClient, userId); |
||||
} |
||||
} |
||||
@ -0,0 +1,302 @@
@@ -0,0 +1,302 @@
|
||||
using System.Reflection; |
||||
using System.Security.Claims; |
||||
using Bit.Commercial.Core.SecretsManager.AuthorizationHandlers.ServiceAccounts; |
||||
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.Test.Common.AutoFixture; |
||||
using Bit.Test.Common.AutoFixture.Attributes; |
||||
using Microsoft.AspNetCore.Authorization; |
||||
using NSubstitute; |
||||
using Xunit; |
||||
|
||||
namespace Bit.Commercial.Core.Test.SecretsManager.AuthorizationHandlers.ServiceAccounts; |
||||
|
||||
[SutProviderCustomize] |
||||
public class ServiceAccountAuthorizationHandlerTests |
||||
{ |
||||
private static void SetupPermission(SutProvider<ServiceAccountAuthorizationHandler> sutProvider, |
||||
PermissionType permissionType, Guid organizationId, Guid userId = new()) |
||||
{ |
||||
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(organizationId) |
||||
.Returns(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( |
||||
(AccessClientType.User, userId)); |
||||
break; |
||||
default: |
||||
throw new ArgumentOutOfRangeException(nameof(permissionType), permissionType, null); |
||||
} |
||||
} |
||||
|
||||
[Fact] |
||||
public void ServiceAccountOperations_OnlyPublicStatic() |
||||
{ |
||||
var publicStaticFields = typeof(ServiceAccountOperations).GetFields(BindingFlags.Public | BindingFlags.Static); |
||||
var allFields = typeof(ServiceAccountOperations).GetFields(); |
||||
Assert.Equal(publicStaticFields.Length, allFields.Length); |
||||
} |
||||
|
||||
[Theory] |
||||
[BitAutoData] |
||||
public async Task Handler_UnsupportedServiceAccountOperationRequirement_Throws( |
||||
SutProvider<ServiceAccountAuthorizationHandler> sutProvider, ServiceAccount serviceAccount, |
||||
ClaimsPrincipal claimsPrincipal) |
||||
{ |
||||
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(serviceAccount.OrganizationId) |
||||
.Returns(true); |
||||
var requirement = new ServiceAccountOperationRequirement(); |
||||
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement }, |
||||
claimsPrincipal, serviceAccount); |
||||
|
||||
await Assert.ThrowsAsync<ArgumentException>(() => sutProvider.Sut.HandleAsync(authzContext)); |
||||
} |
||||
|
||||
[Theory] |
||||
[BitAutoData] |
||||
public async Task Handler_SupportedServiceAccountOperationRequirement_DoesNotThrow( |
||||
SutProvider<ServiceAccountAuthorizationHandler> sutProvider, ServiceAccount serviceAccount, |
||||
ClaimsPrincipal claimsPrincipal) |
||||
{ |
||||
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(serviceAccount.OrganizationId) |
||||
.Returns(true); |
||||
|
||||
var requirements = typeof(ServiceAccountOperations).GetFields(BindingFlags.Public | BindingFlags.Static) |
||||
.Select(i => (ServiceAccountOperationRequirement)i.GetValue(null)); |
||||
|
||||
foreach (var req in requirements) |
||||
{ |
||||
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { req }, |
||||
claimsPrincipal, serviceAccount); |
||||
|
||||
await sutProvider.Sut.HandleAsync(authzContext); |
||||
} |
||||
} |
||||
|
||||
[Theory] |
||||
[BitAutoData] |
||||
public async Task CanCreateServiceAccount_AccessToSecretsManagerFalse_DoesNotSucceed( |
||||
SutProvider<ServiceAccountAuthorizationHandler> sutProvider, ServiceAccount serviceAccount, |
||||
ClaimsPrincipal claimsPrincipal) |
||||
{ |
||||
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(serviceAccount.OrganizationId) |
||||
.Returns(false); |
||||
var requirement = ServiceAccountOperations.Create; |
||||
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement }, |
||||
claimsPrincipal, serviceAccount); |
||||
|
||||
await sutProvider.Sut.HandleAsync(authzContext); |
||||
|
||||
Assert.False(authzContext.HasSucceeded); |
||||
} |
||||
|
||||
[Theory] |
||||
[BitAutoData(AccessClientType.ServiceAccount)] |
||||
[BitAutoData(AccessClientType.Organization)] |
||||
public async Task CanCreateServiceAccount_NotSupportedClientTypes_DoesNotSucceed(AccessClientType clientType, |
||||
SutProvider<ServiceAccountAuthorizationHandler> sutProvider, ServiceAccount serviceAccount, |
||||
ClaimsPrincipal claimsPrincipal) |
||||
{ |
||||
var requirement = ServiceAccountOperations.Create; |
||||
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(serviceAccount.OrganizationId) |
||||
.Returns(true); |
||||
sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(serviceAccount.OrganizationId) |
||||
.Returns(false); |
||||
sutProvider.GetDependency<IAccessClientQuery>().GetAccessClientAsync(default, serviceAccount.OrganizationId) |
||||
.ReturnsForAnyArgs( |
||||
(clientType, new Guid())); |
||||
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement }, |
||||
claimsPrincipal, serviceAccount); |
||||
|
||||
await sutProvider.Sut.HandleAsync(authzContext); |
||||
|
||||
Assert.False(authzContext.HasSucceeded); |
||||
} |
||||
|
||||
[Theory] |
||||
[BitAutoData(PermissionType.RunAsAdmin)] |
||||
[BitAutoData(PermissionType.RunAsUserWithPermission)] |
||||
public async Task CanCreateServiceAccount_Success(PermissionType permissionType, |
||||
SutProvider<ServiceAccountAuthorizationHandler> sutProvider, ServiceAccount serviceAccount, |
||||
ClaimsPrincipal claimsPrincipal) |
||||
{ |
||||
var requirement = ServiceAccountOperations.Create; |
||||
SetupPermission(sutProvider, permissionType, serviceAccount.OrganizationId); |
||||
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement }, |
||||
claimsPrincipal, serviceAccount); |
||||
|
||||
await sutProvider.Sut.HandleAsync(authzContext); |
||||
|
||||
Assert.True(authzContext.HasSucceeded); |
||||
} |
||||
|
||||
[Theory] |
||||
[BitAutoData] |
||||
public async Task CanUpdateServiceAccount_AccessToSecretsManagerFalse_DoesNotSucceed( |
||||
SutProvider<ServiceAccountAuthorizationHandler> sutProvider, ServiceAccount serviceAccount, |
||||
ClaimsPrincipal claimsPrincipal) |
||||
{ |
||||
var requirement = ServiceAccountOperations.Update; |
||||
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(serviceAccount.OrganizationId) |
||||
.Returns(false); |
||||
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement }, |
||||
claimsPrincipal, serviceAccount); |
||||
|
||||
await sutProvider.Sut.HandleAsync(authzContext); |
||||
|
||||
Assert.False(authzContext.HasSucceeded); |
||||
} |
||||
|
||||
[Theory] |
||||
[BitAutoData] |
||||
public async Task CanUpdateServiceAccount_NullResource_DoesNotSucceed( |
||||
SutProvider<ServiceAccountAuthorizationHandler> sutProvider, ServiceAccount serviceAccount, |
||||
ClaimsPrincipal claimsPrincipal, |
||||
Guid userId) |
||||
{ |
||||
var requirement = ServiceAccountOperations.Update; |
||||
SetupPermission(sutProvider, PermissionType.RunAsAdmin, serviceAccount.OrganizationId, userId); |
||||
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement }, |
||||
claimsPrincipal, null); |
||||
|
||||
await sutProvider.Sut.HandleAsync(authzContext); |
||||
|
||||
Assert.False(authzContext.HasSucceeded); |
||||
} |
||||
|
||||
[Theory] |
||||
[BitAutoData(PermissionType.RunAsUserWithPermission, true, false)] |
||||
[BitAutoData(PermissionType.RunAsUserWithPermission, false, false)] |
||||
public async Task CanUpdateServiceAccount_ShouldNotSucceed(PermissionType permissionType, bool read, bool write, |
||||
SutProvider<ServiceAccountAuthorizationHandler> sutProvider, ServiceAccount serviceAccount, |
||||
ClaimsPrincipal claimsPrincipal, |
||||
Guid userId) |
||||
{ |
||||
var requirement = ServiceAccountOperations.Update; |
||||
SetupPermission(sutProvider, permissionType, serviceAccount.OrganizationId, userId); |
||||
sutProvider.GetDependency<IServiceAccountRepository>() |
||||
.AccessToServiceAccountAsync(serviceAccount.Id, userId, Arg.Any<AccessClientType>()) |
||||
.Returns((read, write)); |
||||
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement }, |
||||
claimsPrincipal, serviceAccount); |
||||
|
||||
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 CanUpdateServiceAccount_Success(PermissionType permissionType, bool read, bool write, |
||||
SutProvider<ServiceAccountAuthorizationHandler> sutProvider, ServiceAccount serviceAccount, |
||||
ClaimsPrincipal claimsPrincipal, |
||||
Guid userId) |
||||
{ |
||||
var requirement = ServiceAccountOperations.Update; |
||||
SetupPermission(sutProvider, permissionType, serviceAccount.OrganizationId, userId); |
||||
sutProvider.GetDependency<IServiceAccountRepository>() |
||||
.AccessToServiceAccountAsync(serviceAccount.Id, userId, Arg.Any<AccessClientType>()) |
||||
.Returns((read, write)); |
||||
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement }, |
||||
claimsPrincipal, serviceAccount); |
||||
|
||||
await sutProvider.Sut.HandleAsync(authzContext); |
||||
|
||||
Assert.True(authzContext.HasSucceeded); |
||||
} |
||||
|
||||
|
||||
[Theory] |
||||
[BitAutoData] |
||||
public async Task CanReadServiceAccount_AccessToSecretsManagerFalse_DoesNotSucceed( |
||||
SutProvider<ServiceAccountAuthorizationHandler> sutProvider, ServiceAccount serviceAccount, |
||||
ClaimsPrincipal claimsPrincipal) |
||||
{ |
||||
var requirement = ServiceAccountOperations.Read; |
||||
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(serviceAccount.OrganizationId) |
||||
.Returns(false); |
||||
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement }, |
||||
claimsPrincipal, serviceAccount); |
||||
|
||||
await sutProvider.Sut.HandleAsync(authzContext); |
||||
|
||||
Assert.False(authzContext.HasSucceeded); |
||||
} |
||||
|
||||
[Theory] |
||||
[BitAutoData] |
||||
public async Task CanReadServiceAccount_NullResource_DoesNotSucceed( |
||||
SutProvider<ServiceAccountAuthorizationHandler> sutProvider, ServiceAccount serviceAccount, |
||||
ClaimsPrincipal claimsPrincipal, |
||||
Guid userId) |
||||
{ |
||||
var requirement = ServiceAccountOperations.Read; |
||||
SetupPermission(sutProvider, PermissionType.RunAsAdmin, serviceAccount.OrganizationId, userId); |
||||
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement }, |
||||
claimsPrincipal, null); |
||||
|
||||
await sutProvider.Sut.HandleAsync(authzContext); |
||||
|
||||
Assert.False(authzContext.HasSucceeded); |
||||
} |
||||
|
||||
[Theory] |
||||
[BitAutoData(PermissionType.RunAsUserWithPermission, false, false)] |
||||
[BitAutoData(PermissionType.RunAsUserWithPermission, false, true)] |
||||
public async Task CanReadServiceAccount_ShouldNotSucceed(PermissionType permissionType, bool read, bool write, |
||||
SutProvider<ServiceAccountAuthorizationHandler> sutProvider, ServiceAccount serviceAccount, |
||||
ClaimsPrincipal claimsPrincipal, |
||||
Guid userId) |
||||
{ |
||||
var requirement = ServiceAccountOperations.Read; |
||||
SetupPermission(sutProvider, permissionType, serviceAccount.OrganizationId, userId); |
||||
sutProvider.GetDependency<IServiceAccountRepository>() |
||||
.AccessToServiceAccountAsync(serviceAccount.Id, userId, Arg.Any<AccessClientType>()) |
||||
.Returns((read, write)); |
||||
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement }, |
||||
claimsPrincipal, serviceAccount); |
||||
|
||||
await sutProvider.Sut.HandleAsync(authzContext); |
||||
|
||||
Assert.False(authzContext.HasSucceeded); |
||||
} |
||||
|
||||
[Theory] |
||||
[BitAutoData(PermissionType.RunAsAdmin, true, true)] |
||||
[BitAutoData(PermissionType.RunAsAdmin, true, false)] |
||||
[BitAutoData(PermissionType.RunAsUserWithPermission, true, true)] |
||||
[BitAutoData(PermissionType.RunAsUserWithPermission, true, false)] |
||||
public async Task CanReadServiceAccount_Success(PermissionType permissionType, bool read, bool write, |
||||
SutProvider<ServiceAccountAuthorizationHandler> sutProvider, ServiceAccount serviceAccount, |
||||
ClaimsPrincipal claimsPrincipal, |
||||
Guid userId) |
||||
{ |
||||
var requirement = ServiceAccountOperations.Read; |
||||
SetupPermission(sutProvider, permissionType, serviceAccount.OrganizationId, userId); |
||||
sutProvider.GetDependency<IServiceAccountRepository>() |
||||
.AccessToServiceAccountAsync(serviceAccount.Id, userId, Arg.Any<AccessClientType>()) |
||||
.Returns((read, write)); |
||||
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement }, |
||||
claimsPrincipal, serviceAccount); |
||||
|
||||
await sutProvider.Sut.HandleAsync(authzContext); |
||||
|
||||
Assert.True(authzContext.HasSucceeded); |
||||
} |
||||
} |
||||
@ -0,0 +1,14 @@
@@ -0,0 +1,14 @@
|
||||
using Microsoft.AspNetCore.Authorization.Infrastructure; |
||||
|
||||
namespace Bit.Core.SecretsManager.AuthorizationRequirements; |
||||
|
||||
public class ServiceAccountOperationRequirement : OperationAuthorizationRequirement |
||||
{ |
||||
} |
||||
|
||||
public static class ServiceAccountOperations |
||||
{ |
||||
public static readonly ServiceAccountOperationRequirement Create = new() { Name = nameof(Create) }; |
||||
public static readonly ServiceAccountOperationRequirement Read = new() { Name = nameof(Read) }; |
||||
public static readonly ServiceAccountOperationRequirement Update = new() { Name = nameof(Update) }; |
||||
} |
||||
@ -0,0 +1,9 @@
@@ -0,0 +1,9 @@
|
||||
using System.Security.Claims; |
||||
using Bit.Core.Enums; |
||||
|
||||
namespace Bit.Core.SecretsManager.Queries.Interfaces; |
||||
|
||||
public interface IAccessClientQuery |
||||
{ |
||||
Task<(AccessClientType AccessClientType, Guid UserId)> GetAccessClientAsync(ClaimsPrincipal claimsPrincipal, Guid organizationId); |
||||
} |
||||
Loading…
Reference in new issue