1
0
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:
Thomas Avery
2023-05-31 13:49:58 -05:00
committed by GitHub
parent c08e2a7473
commit d1155ee376
16 changed files with 694 additions and 249 deletions

View File

@ -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);
}
}

View File

@ -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);
}
}
}

View File

@ -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;

View File

@ -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);
}
}

View File

@ -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>();