mirror of
https://github.com/bitwarden/server.git
synced 2025-07-03 00:52:49 -05:00
[SM-704] Extract Authorization For ServiceAccounts (#2869)
* 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 handler
This commit is contained in:
@ -2,23 +2,23 @@
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.SecretsManager.AuthorizationRequirements;
|
||||
using Bit.Core.SecretsManager.Entities;
|
||||
using Bit.Core.SecretsManager.Queries.Interfaces;
|
||||
using Bit.Core.SecretsManager.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
namespace Bit.Commercial.Core.SecretsManager.AuthorizationHandlers.Projects;
|
||||
|
||||
public class ProjectAuthorizationHandler : AuthorizationHandler<ProjectOperationRequirement, Project>
|
||||
{
|
||||
private readonly IAccessClientQuery _accessClientQuery;
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly IProjectRepository _projectRepository;
|
||||
private readonly IUserService _userService;
|
||||
|
||||
public ProjectAuthorizationHandler(ICurrentContext currentContext, IUserService userService,
|
||||
public ProjectAuthorizationHandler(ICurrentContext currentContext, IAccessClientQuery accessClientQuery,
|
||||
IProjectRepository projectRepository)
|
||||
{
|
||||
_currentContext = currentContext;
|
||||
_userService = userService;
|
||||
_accessClientQuery = accessClientQuery;
|
||||
_projectRepository = projectRepository;
|
||||
}
|
||||
|
||||
@ -40,14 +40,14 @@ public class ProjectAuthorizationHandler : AuthorizationHandler<ProjectOperation
|
||||
await CanUpdateProjectAsync(context, requirement, resource);
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentException("Unsupported project operation requirement type provided.", nameof(requirement));
|
||||
throw new ArgumentException("Unsupported operation requirement type provided.", nameof(requirement));
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CanCreateProjectAsync(AuthorizationHandlerContext context,
|
||||
ProjectOperationRequirement requirement, Project resource)
|
||||
{
|
||||
var accessClient = await GetAccessClientAsync(resource.OrganizationId);
|
||||
var (accessClient, _) = await _accessClientQuery.GetAccessClientAsync(context.User, resource.OrganizationId);
|
||||
var hasAccess = accessClient switch
|
||||
{
|
||||
AccessClientType.NoAccessCheck => true,
|
||||
@ -65,13 +65,13 @@ public class ProjectAuthorizationHandler : AuthorizationHandler<ProjectOperation
|
||||
private async Task CanUpdateProjectAsync(AuthorizationHandlerContext context,
|
||||
ProjectOperationRequirement requirement, Project resource)
|
||||
{
|
||||
var accessClient = await GetAccessClientAsync(resource.OrganizationId);
|
||||
var (accessClient, userId) =
|
||||
await _accessClientQuery.GetAccessClientAsync(context.User, 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)
|
||||
@ -79,10 +79,4 @@ public class ProjectAuthorizationHandler : AuthorizationHandler<ProjectOperation
|
||||
context.Succeed(requirement);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<AccessClientType> GetAccessClientAsync(Guid organizationId)
|
||||
{
|
||||
var orgAdmin = await _currentContext.OrganizationAdmin(organizationId);
|
||||
return AccessClientHelper.ToAccessClient(_currentContext.ClientType, orgAdmin);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +1,4 @@
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.SecretsManager.Commands.ServiceAccounts.Interfaces;
|
||||
using Bit.Core.SecretsManager.Entities;
|
||||
@ -18,7 +17,7 @@ public class UpdateServiceAccountCommand : IUpdateServiceAccountCommand
|
||||
_currentContext = currentContext;
|
||||
}
|
||||
|
||||
public async Task<ServiceAccount> UpdateAsync(ServiceAccount updatedServiceAccount, Guid userId)
|
||||
public async Task<ServiceAccount> UpdateAsync(ServiceAccount updatedServiceAccount)
|
||||
{
|
||||
var serviceAccount = await _serviceAccountRepository.GetByIdAsync(updatedServiceAccount.Id);
|
||||
if (serviceAccount == null)
|
||||
@ -26,26 +25,6 @@ public class UpdateServiceAccountCommand : IUpdateServiceAccountCommand
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
if (!_currentContext.AccessSecretsManager(serviceAccount.OrganizationId))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var orgAdmin = await _currentContext.OrganizationAdmin(serviceAccount.OrganizationId);
|
||||
var accessClient = AccessClientHelper.ToAccessClient(_currentContext.ClientType, orgAdmin);
|
||||
|
||||
var hasAccess = accessClient switch
|
||||
{
|
||||
AccessClientType.NoAccessCheck => true,
|
||||
AccessClientType.User => await _serviceAccountRepository.UserHasWriteAccessToServiceAccount(updatedServiceAccount.Id, userId),
|
||||
_ => false,
|
||||
};
|
||||
|
||||
if (!hasAccess)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
serviceAccount.Name = updatedServiceAccount.Name;
|
||||
serviceAccount.RevisionDate = DateTime.UtcNow;
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
using Bit.Commercial.Core.SecretsManager.AuthorizationHandlers.Projects;
|
||||
using Bit.Commercial.Core.SecretsManager.AuthorizationHandlers.ServiceAccounts;
|
||||
using Bit.Commercial.Core.SecretsManager.Commands.AccessPolicies;
|
||||
using Bit.Commercial.Core.SecretsManager.Commands.AccessTokens;
|
||||
using Bit.Commercial.Core.SecretsManager.Commands.Porting;
|
||||
@ -6,6 +7,7 @@ using Bit.Commercial.Core.SecretsManager.Commands.Projects;
|
||||
using Bit.Commercial.Core.SecretsManager.Commands.Secrets;
|
||||
using Bit.Commercial.Core.SecretsManager.Commands.ServiceAccounts;
|
||||
using Bit.Commercial.Core.SecretsManager.Commands.Trash;
|
||||
using Bit.Commercial.Core.SecretsManager.Queries;
|
||||
using Bit.Core.SecretsManager.Commands.AccessPolicies.Interfaces;
|
||||
using Bit.Core.SecretsManager.Commands.AccessTokens.Interfaces;
|
||||
using Bit.Core.SecretsManager.Commands.Porting.Interfaces;
|
||||
@ -13,6 +15,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 Bit.Core.SecretsManager.Queries.Interfaces;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
@ -23,6 +26,8 @@ public static class SecretsManagerCollectionExtensions
|
||||
public static void AddSecretsManagerServices(this IServiceCollection services)
|
||||
{
|
||||
services.AddScoped<IAuthorizationHandler, ProjectAuthorizationHandler>();
|
||||
services.AddScoped<IAuthorizationHandler, ServiceAccountAuthorizationHandler>();
|
||||
services.AddScoped<IAccessClientQuery, AccessClientQuery>();
|
||||
services.AddScoped<ICreateSecretCommand, CreateSecretCommand>();
|
||||
services.AddScoped<IUpdateSecretCommand, UpdateSecretCommand>();
|
||||
services.AddScoped<IDeleteSecretCommand, DeleteSecretCommand>();
|
||||
|
Reference in New Issue
Block a user