1
0
mirror of https://github.com/bitwarden/server.git synced 2024-11-21 12:05:42 +01: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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
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>();

View File

@ -101,6 +101,35 @@ public class ServiceAccountRepository : Repository<Core.SecretsManager.Entities.
await dbContext.SaveChangesAsync();
}
public async Task<(bool Read, bool Write)> AccessToServiceAccountAsync(Guid id, Guid userId,
AccessClientType accessType)
{
using var scope = ServiceScopeFactory.CreateScope();
var dbContext = GetDatabaseContext(scope);
var serviceAccount = dbContext.ServiceAccount.Where(sa => sa.Id == id);
var query = accessType switch
{
AccessClientType.NoAccessCheck => serviceAccount.Select(_ => new { Read = true, Write = true }),
AccessClientType.User => serviceAccount.Select(sa => new
{
Read = sa.UserAccessPolicies.Any(ap => ap.OrganizationUser.User.Id == userId && ap.Read) ||
sa.GroupAccessPolicies.Any(ap =>
ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.User.Id == userId && ap.Read)),
Write = sa.UserAccessPolicies.Any(ap => ap.OrganizationUser.User.Id == userId && ap.Write) ||
sa.GroupAccessPolicies.Any(ap =>
ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.User.Id == userId && ap.Write)),
}),
AccessClientType.ServiceAccount => serviceAccount.Select(_ => new { Read = false, Write = false }),
_ => serviceAccount.Select(_ => new { Read = false, Write = false }),
};
var policy = await query.FirstOrDefaultAsync();
return policy == null ? (false, false) : (policy.Read, policy.Write);
}
private static Expression<Func<ServiceAccount, bool>> UserHasReadAccessToServiceAccount(Guid userId) => sa =>
sa.UserAccessPolicies.Any(ap => ap.OrganizationUser.User.Id == userId && ap.Read) ||
sa.GroupAccessPolicies.Any(ap => ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.User.Id == userId && ap.Read));

View File

@ -4,11 +4,10 @@ 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.Queries.Interfaces;
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;
@ -23,21 +22,22 @@ namespace Bit.Commercial.Core.Test.SecretsManager.AuthorizationHandlers.Projects
public class ProjectAuthorizationHandlerTests
{
private static void SetupPermission(SutProvider<ProjectAuthorizationHandler> sutProvider,
PermissionType permissionType, Guid organizationId)
PermissionType permissionType, Guid organizationId, Guid userId = new())
{
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);
sutProvider.GetDependency<IAccessClientQuery>().GetAccessClientAsync(default, organizationId)
.ReturnsForAnyArgs(
(AccessClientType.NoAccessCheck, userId));
break;
case PermissionType.RunAsUserWithPermission:
sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(organizationId).Returns(false);
sutProvider.GetDependency<IAccessClientQuery>().GetAccessClientAsync(default, organizationId)
.ReturnsForAnyArgs(
(AccessClientType.User, userId));
break;
default:
throw new ArgumentOutOfRangeException(nameof(permissionType), permissionType, null);
@ -63,7 +63,6 @@ public class ProjectAuthorizationHandlerTests
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, project);
await Assert.ThrowsAsync<ArgumentException>(() => sutProvider.Sut.HandleAsync(authzContext));
}
@ -74,7 +73,6 @@ public class ProjectAuthorizationHandlerTests
{
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));
@ -105,17 +103,16 @@ public class ProjectAuthorizationHandlerTests
}
[Theory]
[BitAutoData(ClientType.ServiceAccount)]
[BitAutoData(ClientType.Organization)]
public async Task CanCreateProject_NotSupportedClientTypes_DoesNotSucceed(ClientType clientType,
[BitAutoData(AccessClientType.ServiceAccount)]
[BitAutoData(AccessClientType.Organization)]
public async Task CanCreateProject_NotSupportedClientTypes_DoesNotSucceed(AccessClientType 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);
sutProvider.GetDependency<IAccessClientQuery>().GetAccessClientAsync(default, project.OrganizationId)
.ReturnsForAnyArgs(
(clientType, new Guid()));
var requirement = ProjectOperations.Create;
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, project);
@ -167,7 +164,6 @@ public class ProjectAuthorizationHandlerTests
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));
@ -188,8 +184,9 @@ public class ProjectAuthorizationHandlerTests
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(project.OrganizationId)
.Returns(true);
sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(project.OrganizationId).Returns(false);
sutProvider.GetDependency<ICurrentContext>().ClientType
.Returns(ClientType.ServiceAccount);
sutProvider.GetDependency<IAccessClientQuery>().GetAccessClientAsync(default, project.OrganizationId)
.ReturnsForAnyArgs(
(AccessClientType.ServiceAccount, new Guid()));
var requirement = ProjectOperations.Update;
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, project);
@ -206,8 +203,7 @@ public class ProjectAuthorizationHandlerTests
SutProvider<ProjectAuthorizationHandler> sutProvider, Project project, ClaimsPrincipal claimsPrincipal,
Guid userId)
{
SetupPermission(sutProvider, permissionType, project.OrganizationId);
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
SetupPermission(sutProvider, permissionType, project.OrganizationId, userId);
sutProvider.GetDependency<IProjectRepository>()
.AccessToProjectAsync(project.Id, userId, Arg.Any<AccessClientType>())
.Returns((read, write));
@ -229,8 +225,7 @@ public class ProjectAuthorizationHandlerTests
SutProvider<ProjectAuthorizationHandler> sutProvider, Project project, ClaimsPrincipal claimsPrincipal,
Guid userId)
{
SetupPermission(sutProvider, permissionType, project.OrganizationId);
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
SetupPermission(sutProvider, permissionType, project.OrganizationId, userId);
sutProvider.GetDependency<IProjectRepository>()
.AccessToProjectAsync(project.Id, userId, Arg.Any<AccessClientType>())
.Returns((read, write));

View File

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

View File

@ -1,5 +1,4 @@
using Bit.Commercial.Core.SecretsManager.Commands.ServiceAccounts;
using Bit.Core.Context;
using Bit.Core.Exceptions;
using Bit.Core.SecretsManager.Entities;
using Bit.Core.SecretsManager.Repositories;
@ -16,49 +15,20 @@ public class UpdateServiceAccountCommandTests
{
[Theory]
[BitAutoData]
public async Task UpdateAsync_ServiceAccountDoesNotExist_ThrowsNotFound(ServiceAccount data, Guid userId, SutProvider<UpdateServiceAccountCommand> sutProvider)
public async Task UpdateAsync_ServiceAccountDoesNotExist_ThrowsNotFound(ServiceAccount data, SutProvider<UpdateServiceAccountCommand> sutProvider)
{
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.UpdateAsync(data, userId));
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.UpdateAsync(data));
await sutProvider.GetDependency<IServiceAccountRepository>().DidNotReceiveWithAnyArgs().ReplaceAsync(default);
}
[Theory]
[BitAutoData]
public async Task UpdateAsync_User_NoAccess(ServiceAccount data, Guid userId, SutProvider<UpdateServiceAccountCommand> sutProvider)
public async Task UpdateAsync_Success(ServiceAccount data, SutProvider<UpdateServiceAccountCommand> sutProvider)
{
sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(data.Id).Returns(data);
sutProvider.GetDependency<IServiceAccountRepository>().UserHasWriteAccessToServiceAccount(data.Id, userId).Returns(false);
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.UpdateAsync(data, userId));
await sutProvider.GetDependency<IServiceAccountRepository>().DidNotReceiveWithAnyArgs().ReplaceAsync(default);
}
[Theory]
[BitAutoData]
public async Task UpdateAsync_User_Success(ServiceAccount data, Guid userId, SutProvider<UpdateServiceAccountCommand> sutProvider)
{
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(data.OrganizationId).Returns(true);
sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(data.Id).Returns(data);
sutProvider.GetDependency<IServiceAccountRepository>().UserHasWriteAccessToServiceAccount(data.Id, userId).Returns(true);
await sutProvider.Sut.UpdateAsync(data, userId);
await sutProvider.GetDependency<IServiceAccountRepository>().Received(1)
.ReplaceAsync(Arg.Is(AssertHelper.AssertPropertyEqual(data)));
}
[Theory]
[BitAutoData]
public async Task UpdateAsync_Admin_Success(ServiceAccount data, Guid userId, SutProvider<UpdateServiceAccountCommand> sutProvider)
{
sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(data.Id).Returns(data);
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(data.OrganizationId).Returns(true);
sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(data.OrganizationId).Returns(true);
await sutProvider.Sut.UpdateAsync(data, userId);
await sutProvider.Sut.UpdateAsync(data);
await sutProvider.GetDependency<IServiceAccountRepository>().Received(1)
.ReplaceAsync(Arg.Is(AssertHelper.AssertPropertyEqual(data)));
@ -66,11 +36,9 @@ public class UpdateServiceAccountCommandTests
[Theory]
[BitAutoData]
public async Task UpdateAsync_DoesNotModifyOrganizationId(ServiceAccount existingServiceAccount, Guid userId, SutProvider<UpdateServiceAccountCommand> sutProvider)
public async Task UpdateAsync_DoesNotModifyOrganizationId(ServiceAccount existingServiceAccount, SutProvider<UpdateServiceAccountCommand> sutProvider)
{
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(existingServiceAccount.OrganizationId).Returns(true);
sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(existingServiceAccount.Id).Returns(existingServiceAccount);
sutProvider.GetDependency<IServiceAccountRepository>().UserHasWriteAccessToServiceAccount(existingServiceAccount.Id, userId).Returns(true);
var updatedOrgId = Guid.NewGuid();
var serviceAccountUpdate = new ServiceAccount()
@ -80,7 +48,7 @@ public class UpdateServiceAccountCommandTests
Name = existingServiceAccount.Name,
};
var result = await sutProvider.Sut.UpdateAsync(serviceAccountUpdate, userId);
var result = await sutProvider.Sut.UpdateAsync(serviceAccountUpdate);
Assert.Equal(existingServiceAccount.OrganizationId, result.OrganizationId);
Assert.NotEqual(existingServiceAccount.OrganizationId, updatedOrgId);
@ -88,11 +56,9 @@ public class UpdateServiceAccountCommandTests
[Theory]
[BitAutoData]
public async Task UpdateAsync_DoesNotModifyCreationDate(ServiceAccount existingServiceAccount, Guid userId, SutProvider<UpdateServiceAccountCommand> sutProvider)
public async Task UpdateAsync_DoesNotModifyCreationDate(ServiceAccount existingServiceAccount, SutProvider<UpdateServiceAccountCommand> sutProvider)
{
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(existingServiceAccount.OrganizationId).Returns(true);
sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(existingServiceAccount.Id).Returns(existingServiceAccount);
sutProvider.GetDependency<IServiceAccountRepository>().UserHasWriteAccessToServiceAccount(existingServiceAccount.Id, userId).Returns(true);
var updatedCreationDate = DateTime.UtcNow;
var serviceAccountUpdate = new ServiceAccount()
@ -102,7 +68,7 @@ public class UpdateServiceAccountCommandTests
Name = existingServiceAccount.Name,
};
var result = await sutProvider.Sut.UpdateAsync(serviceAccountUpdate, userId);
var result = await sutProvider.Sut.UpdateAsync(serviceAccountUpdate);
Assert.Equal(existingServiceAccount.CreationDate, result.CreationDate);
Assert.NotEqual(existingServiceAccount.CreationDate, updatedCreationDate);
@ -110,11 +76,9 @@ public class UpdateServiceAccountCommandTests
[Theory]
[BitAutoData]
public async Task UpdateAsync_RevisionDateIsUpdatedToUtcNow(ServiceAccount existingServiceAccount, Guid userId, SutProvider<UpdateServiceAccountCommand> sutProvider)
public async Task UpdateAsync_RevisionDateIsUpdatedToUtcNow(ServiceAccount existingServiceAccount, SutProvider<UpdateServiceAccountCommand> sutProvider)
{
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(existingServiceAccount.OrganizationId).Returns(true);
sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(existingServiceAccount.Id).Returns(existingServiceAccount);
sutProvider.GetDependency<IServiceAccountRepository>().UserHasWriteAccessToServiceAccount(existingServiceAccount.Id, userId).Returns(true);
var updatedRevisionDate = DateTime.UtcNow.AddDays(10);
var serviceAccountUpdate = new ServiceAccount()
@ -124,7 +88,7 @@ public class UpdateServiceAccountCommandTests
Name = existingServiceAccount.Name,
};
var result = await sutProvider.Sut.UpdateAsync(serviceAccountUpdate, userId);
var result = await sutProvider.Sut.UpdateAsync(serviceAccountUpdate);
Assert.NotEqual(serviceAccountUpdate.RevisionDate, result.RevisionDate);
AssertHelper.AssertRecent(result.RevisionDate);

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.AccessTokens.Interfaces;
using Bit.Core.SecretsManager.Commands.ServiceAccounts.Interfaces;
using Bit.Core.SecretsManager.Repositories;
@ -21,6 +22,7 @@ public class ServiceAccountsController : Controller
{
private readonly ICurrentContext _currentContext;
private readonly IUserService _userService;
private readonly IAuthorizationService _authorizationService;
private readonly IServiceAccountRepository _serviceAccountRepository;
private readonly IApiKeyRepository _apiKeyRepository;
private readonly ICreateAccessTokenCommand _createAccessTokenCommand;
@ -32,6 +34,7 @@ public class ServiceAccountsController : Controller
public ServiceAccountsController(
ICurrentContext currentContext,
IUserService userService,
IAuthorizationService authorizationService,
IServiceAccountRepository serviceAccountRepository,
IApiKeyRepository apiKeyRepository,
ICreateAccessTokenCommand createAccessTokenCommand,
@ -42,6 +45,7 @@ public class ServiceAccountsController : Controller
{
_currentContext = currentContext;
_userService = userService;
_authorizationService = authorizationService;
_serviceAccountRepository = serviceAccountRepository;
_apiKeyRepository = apiKeyRepository;
_createServiceAccountCommand = createServiceAccountCommand;
@ -73,32 +77,13 @@ public class ServiceAccountsController : Controller
[HttpGet("{id}")]
public async Task<ServiceAccountResponseModel> GetByServiceAccountIdAsync(
[FromRoute] Guid id)
[FromRoute] Guid id)
{
var userId = _userService.GetProperUserId(User).Value;
var serviceAccount = await _serviceAccountRepository.GetByIdAsync(id);
var authorizationResult =
await _authorizationService.AuthorizeAsync(User, serviceAccount, ServiceAccountOperations.Read);
if (serviceAccount == null)
{
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(id, userId),
_ => false,
};
if (!hasAccess)
if (!authorizationResult.Succeeded)
{
throw new NotFoundException();
}
@ -110,12 +95,18 @@ public class ServiceAccountsController : Controller
public async Task<ServiceAccountResponseModel> CreateAsync([FromRoute] Guid organizationId,
[FromBody] ServiceAccountCreateRequestModel createRequest)
{
if (!_currentContext.AccessSecretsManager(organizationId))
var serviceAccount = createRequest.ToServiceAccount(organizationId);
var authorizationResult =
await _authorizationService.AuthorizeAsync(User, serviceAccount, ServiceAccountOperations.Create);
if (!authorizationResult.Succeeded)
{
throw new NotFoundException();
}
var userId = _userService.GetProperUserId(User).Value;
var result = await _createServiceAccountCommand.CreateAsync(createRequest.ToServiceAccount(organizationId), userId);
var result =
await _createServiceAccountCommand.CreateAsync(createRequest.ToServiceAccount(organizationId), userId);
return new ServiceAccountResponseModel(result);
}
@ -123,9 +114,16 @@ public class ServiceAccountsController : Controller
public async Task<ServiceAccountResponseModel> UpdateAsync([FromRoute] Guid id,
[FromBody] ServiceAccountUpdateRequestModel updateRequest)
{
var userId = _userService.GetProperUserId(User).Value;
var serviceAccount = await _serviceAccountRepository.GetByIdAsync(id);
var authorizationResult =
await _authorizationService.AuthorizeAsync(User, serviceAccount, ServiceAccountOperations.Update);
var result = await _updateServiceAccountCommand.UpdateAsync(updateRequest.ToServiceAccount(id), userId);
if (!authorizationResult.Succeeded)
{
throw new NotFoundException();
}
var result = await _updateServiceAccountCommand.UpdateAsync(updateRequest.ToServiceAccount(id));
return new ServiceAccountResponseModel(result);
}

View File

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

View File

@ -4,5 +4,5 @@ namespace Bit.Core.SecretsManager.Commands.ServiceAccounts.Interfaces;
public interface IUpdateServiceAccountCommand
{
Task<ServiceAccount> UpdateAsync(ServiceAccount serviceAccount, Guid userId);
Task<ServiceAccount> UpdateAsync(ServiceAccount serviceAccount);
}

View File

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

View File

@ -14,4 +14,5 @@ public interface IServiceAccountRepository
Task<bool> UserHasReadAccessToServiceAccount(Guid id, Guid userId);
Task<bool> UserHasWriteAccessToServiceAccount(Guid id, Guid userId);
Task<IEnumerable<ServiceAccount>> GetManyByOrganizationIdWriteAccessAsync(Guid organizationId, Guid userId, AccessClientType accessType);
Task<(bool Read, bool Write)> AccessToServiceAccountAsync(Guid id, Guid userId, AccessClientType accessType);
}

View File

@ -139,11 +139,21 @@ public class ServiceAccountsControllerTests : IClassFixture<ApiApplicationFactor
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Fact]
public async Task GetByServiceAccountId_ServiceAccountDoesNotExist_NotFound()
{
var (org, _) = await _organizationHelper.Initialize(true, true);
await LoginAsync(_email);
var response = await _client.GetAsync($"/service-accounts/{new Guid()}");
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Fact]
public async Task GetByServiceAccountId_UserWithoutPermission_NotFound()
{
var (org, _) = await _organizationHelper.Initialize(true, true);
var (email, orgUser) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);
var (email, _) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);
await LoginAsync(email);
var serviceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount
@ -161,30 +171,7 @@ public class ServiceAccountsControllerTests : IClassFixture<ApiApplicationFactor
[InlineData(PermissionType.RunAsUserWithPermission)]
public async Task GetByServiceAccountId_Success(PermissionType permissionType)
{
var (org, _) = await _organizationHelper.Initialize(true, true);
await LoginAsync(_email);
var serviceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount
{
OrganizationId = org.Id,
Name = _mockEncryptedString,
});
if (permissionType == PermissionType.RunAsUserWithPermission)
{
var (email, orgUser) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);
await LoginAsync(email);
await _accessPolicyRepository.CreateManyAsync(new List<BaseAccessPolicy> {
new UserServiceAccountAccessPolicy
{
GrantedServiceAccountId = serviceAccount.Id,
OrganizationUserId = orgUser.Id,
Write = true,
Read = true,
},
});
}
var serviceAccount = await SetupServiceAccountWithAccessAsync(permissionType);
var response = await _client.GetAsync($"/service-accounts/{serviceAccount.Id}");
response.EnsureSuccessStatusCode();
@ -212,12 +199,25 @@ public class ServiceAccountsControllerTests : IClassFixture<ApiApplicationFactor
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Fact]
public async Task Create_Admin_Success()
[Theory]
[InlineData(PermissionType.RunAsAdmin)]
[InlineData(PermissionType.RunAsUserWithPermission)]
public async Task Create_Success(PermissionType permissionType)
{
var (org, orgUser) = await _organizationHelper.Initialize(true, true);
var (org, adminOrgUser) = await _organizationHelper.Initialize(true, true);
await LoginAsync(_email);
var orgUserId = adminOrgUser.Id;
var currentUserId = adminOrgUser.UserId!.Value;
if (permissionType == PermissionType.RunAsUserWithPermission)
{
var (email, orgUser) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);
await LoginAsync(email);
orgUserId = orgUser.Id;
currentUserId = orgUser.UserId!.Value;
}
var request = new ServiceAccountCreateRequestModel { Name = _mockEncryptedString };
var response = await _client.PostAsJsonAsync($"/organizations/{org.Id}/service-accounts", request);
@ -236,9 +236,11 @@ public class ServiceAccountsControllerTests : IClassFixture<ApiApplicationFactor
AssertHelper.AssertRecent(createdServiceAccount.CreationDate);
// Check permissions have been bootstrapped.
var accessPolicies = await _accessPolicyRepository.GetManyByGrantedServiceAccountIdAsync(createdServiceAccount.Id, orgUser.UserId!.Value);
var accessPolicies = await _accessPolicyRepository.GetManyByGrantedServiceAccountIdAsync(createdServiceAccount.Id, currentUserId);
Assert.NotNull(accessPolicies);
var ap = accessPolicies!.First();
var ap = (UserServiceAccountAccessPolicy)accessPolicies.First();
Assert.Equal(createdServiceAccount.Id, ap.GrantedServiceAccountId);
Assert.Equal(orgUserId, ap.OrganizationUserId);
Assert.True(ap.Read);
Assert.True(ap.Write);
AssertHelper.AssertRecent(ap.CreationDate);
@ -266,73 +268,6 @@ public class ServiceAccountsControllerTests : IClassFixture<ApiApplicationFactor
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Fact]
public async Task Update_Admin()
{
var (org, _) = await _organizationHelper.Initialize(true, true);
await LoginAsync(_email);
var initialServiceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount
{
OrganizationId = org.Id,
Name = _mockEncryptedString,
});
var request = new ServiceAccountUpdateRequestModel { Name = _mockNewName };
var response = await _client.PutAsJsonAsync($"/service-accounts/{initialServiceAccount.Id}", request);
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<ServiceAccountResponseModel>();
Assert.NotNull(result);
Assert.Equal(request.Name, result!.Name);
Assert.NotEqual(initialServiceAccount.Name, result.Name);
AssertHelper.AssertRecent(result.RevisionDate);
Assert.NotEqual(initialServiceAccount.RevisionDate, result.RevisionDate);
var updatedServiceAccount = await _serviceAccountRepository.GetByIdAsync(initialServiceAccount.Id);
Assert.NotNull(result);
Assert.Equal(request.Name, updatedServiceAccount.Name);
AssertHelper.AssertRecent(updatedServiceAccount.RevisionDate);
AssertHelper.AssertRecent(updatedServiceAccount.CreationDate);
Assert.NotEqual(initialServiceAccount.Name, updatedServiceAccount.Name);
Assert.NotEqual(initialServiceAccount.RevisionDate, updatedServiceAccount.RevisionDate);
}
[Fact]
public async Task Update_User_WithPermission()
{
var (org, _) = await _organizationHelper.Initialize(true, true);
var (email, orgUser) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);
await LoginAsync(email);
var initialServiceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount
{
OrganizationId = org.Id,
Name = _mockEncryptedString,
});
await CreateUserPolicyAsync(orgUser.Id, initialServiceAccount.Id, true, true);
var request = new ServiceAccountUpdateRequestModel { Name = _mockNewName };
var response = await _client.PutAsJsonAsync($"/service-accounts/{initialServiceAccount.Id}", request);
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<ServiceAccountResponseModel>();
Assert.NotNull(result);
Assert.Equal(request.Name, result!.Name);
Assert.NotEqual(initialServiceAccount.Name, result.Name);
AssertHelper.AssertRecent(result.RevisionDate);
Assert.NotEqual(initialServiceAccount.RevisionDate, result.RevisionDate);
var updatedServiceAccount = await _serviceAccountRepository.GetByIdAsync(initialServiceAccount.Id);
Assert.NotNull(result);
Assert.Equal(request.Name, updatedServiceAccount.Name);
AssertHelper.AssertRecent(updatedServiceAccount.RevisionDate);
AssertHelper.AssertRecent(updatedServiceAccount.CreationDate);
Assert.NotEqual(initialServiceAccount.Name, updatedServiceAccount.Name);
Assert.NotEqual(initialServiceAccount.RevisionDate, updatedServiceAccount.RevisionDate);
}
[Fact]
public async Task Update_User_NoPermissions()
{
@ -352,6 +287,45 @@ public class ServiceAccountsControllerTests : IClassFixture<ApiApplicationFactor
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Fact]
public async Task Update_NonExistingServiceAccount_NotFound()
{
await _organizationHelper.Initialize(true, true);
await LoginAsync(_email);
var request = new ServiceAccountUpdateRequestModel { Name = _mockNewName };
var response = await _client.PutAsJsonAsync("/service-accounts/c53de509-4581-402c-8cbd-f26d2c516fba", request);
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Theory]
[InlineData(PermissionType.RunAsAdmin)]
[InlineData(PermissionType.RunAsUserWithPermission)]
public async Task Update_Success(PermissionType permissionType)
{
var initialServiceAccount = await SetupServiceAccountWithAccessAsync(permissionType);
var request = new ServiceAccountUpdateRequestModel { Name = _mockNewName };
var response = await _client.PutAsJsonAsync($"/service-accounts/{initialServiceAccount.Id}", request);
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<ServiceAccountResponseModel>();
Assert.NotNull(result);
Assert.Equal(request.Name, result!.Name);
Assert.NotEqual(initialServiceAccount.Name, result.Name);
AssertHelper.AssertRecent(result.RevisionDate);
Assert.NotEqual(initialServiceAccount.RevisionDate, result.RevisionDate);
var updatedServiceAccount = await _serviceAccountRepository.GetByIdAsync(initialServiceAccount.Id);
Assert.NotNull(result);
Assert.Equal(request.Name, updatedServiceAccount.Name);
AssertHelper.AssertRecent(updatedServiceAccount.RevisionDate);
AssertHelper.AssertRecent(updatedServiceAccount.CreationDate);
Assert.NotEqual(initialServiceAccount.Name, updatedServiceAccount.Name);
Assert.NotEqual(initialServiceAccount.RevisionDate, updatedServiceAccount.RevisionDate);
}
[Theory]
[InlineData(false, false)]
[InlineData(true, false)]
@ -837,4 +811,35 @@ public class ServiceAccountsControllerTests : IClassFixture<ApiApplicationFactor
return serviceAccountIds;
}
private async Task<ServiceAccount> SetupServiceAccountWithAccessAsync(PermissionType permissionType)
{
var (org, _) = await _organizationHelper.Initialize(true, true);
await LoginAsync(_email);
var initialServiceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount
{
OrganizationId = org.Id,
Name = _mockEncryptedString,
});
if (permissionType == PermissionType.RunAsAdmin)
{
return initialServiceAccount;
}
var (email, orgUser) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);
await LoginAsync(email);
var accessPolicies = new List<BaseAccessPolicy>
{
new UserServiceAccountAccessPolicy
{
GrantedServiceAccountId = initialServiceAccount.Id, OrganizationUserId = orgUser.Id, Read = true, Write = true,
},
};
await _accessPolicyRepository.CreateManyAsync(accessPolicies);
return initialServiceAccount;
}
}

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.Core.Context;
using Bit.Core.Enums;
@ -11,6 +12,7 @@ using Bit.Core.Services;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Bit.Test.Common.Helpers;
using Microsoft.AspNetCore.Authorization;
using NSubstitute;
using NSubstitute.ReturnsExtensions;
using Xunit;
@ -62,13 +64,30 @@ public class ServiceAccountsControllerTests
sutProvider.Sut.ListByOrganizationAsync(orgId));
}
[Theory]
[BitAutoData]
public async void CreateServiceAccount_NoAccess_Throws(SutProvider<ServiceAccountsController> sutProvider, ServiceAccountCreateRequestModel data, Guid organizationId)
{
sutProvider.GetDependency<IAuthorizationService>()
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), data.ToServiceAccount(organizationId),
Arg.Any<IEnumerable<IAuthorizationRequirement>>()).ReturnsForAnyArgs(AuthorizationResult.Failed());
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(Guid.NewGuid());
var resultServiceAccount = data.ToServiceAccount(organizationId);
sutProvider.GetDependency<ICreateServiceAccountCommand>().CreateAsync(default, default).ReturnsForAnyArgs(resultServiceAccount);
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.CreateAsync(organizationId, data));
await sutProvider.GetDependency<ICreateServiceAccountCommand>().DidNotReceiveWithAnyArgs()
.CreateAsync(Arg.Any<ServiceAccount>(), Arg.Any<Guid>());
}
[Theory]
[BitAutoData]
public async void CreateServiceAccount_Success(SutProvider<ServiceAccountsController> sutProvider, ServiceAccountCreateRequestModel data, Guid organizationId)
{
sutProvider.GetDependency<IAuthorizationService>()
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), data.ToServiceAccount(organizationId),
Arg.Any<IEnumerable<IAuthorizationRequirement>>()).ReturnsForAnyArgs(AuthorizationResult.Success());
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(Guid.NewGuid());
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(organizationId).Returns(true);
sutProvider.GetDependency<ICurrentContext>().OrganizationUser(default).ReturnsForAnyArgs(true);
var resultServiceAccount = data.ToServiceAccount(organizationId);
sutProvider.GetDependency<ICreateServiceAccountCommand>().CreateAsync(default, default).ReturnsForAnyArgs(resultServiceAccount);
@ -79,30 +98,33 @@ public class ServiceAccountsControllerTests
[Theory]
[BitAutoData]
public async void CreateServiceAccount_NotOrgUser_Throws(SutProvider<ServiceAccountsController> sutProvider, ServiceAccountCreateRequestModel data, Guid organizationId)
public async void UpdateServiceAccount_NoAccess_Throws(SutProvider<ServiceAccountsController> sutProvider, ServiceAccountUpdateRequestModel data, ServiceAccount existingServiceAccount)
{
sutProvider.GetDependency<ICurrentContext>().OrganizationUser(default).ReturnsForAnyArgs(false);
var resultServiceAccount = data.ToServiceAccount(organizationId);
sutProvider.GetDependency<ICreateServiceAccountCommand>().CreateAsync(default, default).ReturnsForAnyArgs(resultServiceAccount);
sutProvider.GetDependency<IAuthorizationService>()
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), data.ToServiceAccount(existingServiceAccount.Id),
Arg.Any<IEnumerable<IAuthorizationRequirement>>()).ReturnsForAnyArgs(AuthorizationResult.Failed());
sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(existingServiceAccount.Id).ReturnsForAnyArgs(existingServiceAccount);
var resultServiceAccount = data.ToServiceAccount(existingServiceAccount.Id);
sutProvider.GetDependency<IUpdateServiceAccountCommand>().UpdateAsync(default).ReturnsForAnyArgs(resultServiceAccount);
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.CreateAsync(organizationId, data));
await sutProvider.GetDependency<ICreateServiceAccountCommand>()
.DidNotReceiveWithAnyArgs()
.CreateAsync(Arg.Any<ServiceAccount>(), Arg.Any<Guid>());
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.UpdateAsync(existingServiceAccount.Id, data));
await sutProvider.GetDependency<IUpdateServiceAccountCommand>().DidNotReceiveWithAnyArgs()
.UpdateAsync(Arg.Any<ServiceAccount>());
}
[Theory]
[BitAutoData]
public async void UpdateServiceAccount_Success(SutProvider<ServiceAccountsController> sutProvider, ServiceAccountUpdateRequestModel data, Guid serviceAccountId)
public async void UpdateServiceAccount_Success(SutProvider<ServiceAccountsController> sutProvider, ServiceAccountUpdateRequestModel data, ServiceAccount existingServiceAccount)
{
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(Guid.NewGuid());
var resultServiceAccount = data.ToServiceAccount(serviceAccountId);
sutProvider.GetDependency<IUpdateServiceAccountCommand>().UpdateAsync(default, default).ReturnsForAnyArgs(resultServiceAccount);
sutProvider.GetDependency<IAuthorizationService>()
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), data.ToServiceAccount(existingServiceAccount.Id),
Arg.Any<IEnumerable<IAuthorizationRequirement>>()).ReturnsForAnyArgs(AuthorizationResult.Success());
var resultServiceAccount = data.ToServiceAccount(existingServiceAccount.Id);
sutProvider.GetDependency<IUpdateServiceAccountCommand>().UpdateAsync(default).ReturnsForAnyArgs(resultServiceAccount);
var result = await sutProvider.Sut.UpdateAsync(serviceAccountId, data);
var result = await sutProvider.Sut.UpdateAsync(existingServiceAccount.Id, data);
await sutProvider.GetDependency<IUpdateServiceAccountCommand>().Received(1)
.UpdateAsync(Arg.Any<ServiceAccount>(), Arg.Any<Guid>());
.UpdateAsync(Arg.Any<ServiceAccount>());
}
[Theory]