1
0
mirror of https://github.com/bitwarden/server.git synced 2025-04-04 20:50:21 -05:00

[SM-910] Add service account granted policies management endpoints (#3736)

* Add the ability to get multi projects access

* Add access policy helper + tests

* Add new data/request models

* Add access policy operations to repo

* Add authz handler for new operations

* Add new controller endpoints

* add updating service account revision
This commit is contained in:
Thomas Avery 2024-05-01 11:47:11 -05:00 committed by GitHub
parent a14646eaad
commit ebd88393c8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 1772 additions and 578 deletions

View File

@ -0,0 +1,88 @@
#nullable enable
using Bit.Core.Context;
using Bit.Core.Enums;
using Bit.Core.SecretsManager.AuthorizationRequirements;
using Bit.Core.SecretsManager.Models.Data;
using Bit.Core.SecretsManager.Queries.Interfaces;
using Bit.Core.SecretsManager.Repositories;
using Microsoft.AspNetCore.Authorization;
namespace Bit.Commercial.Core.SecretsManager.AuthorizationHandlers.AccessPolicies;
public class ServiceAccountGrantedPoliciesAuthorizationHandler : AuthorizationHandler<
ServiceAccountGrantedPoliciesOperationRequirement,
ServiceAccountGrantedPoliciesUpdates>
{
private readonly IAccessClientQuery _accessClientQuery;
private readonly ICurrentContext _currentContext;
private readonly IProjectRepository _projectRepository;
private readonly IServiceAccountRepository _serviceAccountRepository;
public ServiceAccountGrantedPoliciesAuthorizationHandler(ICurrentContext currentContext,
IAccessClientQuery accessClientQuery,
IProjectRepository projectRepository,
IServiceAccountRepository serviceAccountRepository)
{
_currentContext = currentContext;
_accessClientQuery = accessClientQuery;
_serviceAccountRepository = serviceAccountRepository;
_projectRepository = projectRepository;
}
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context,
ServiceAccountGrantedPoliciesOperationRequirement requirement,
ServiceAccountGrantedPoliciesUpdates resource)
{
if (!_currentContext.AccessSecretsManager(resource.OrganizationId))
{
return;
}
// Only users and admins should be able to manipulate access policies
var (accessClient, userId) =
await _accessClientQuery.GetAccessClientAsync(context.User, resource.OrganizationId);
if (accessClient != AccessClientType.User && accessClient != AccessClientType.NoAccessCheck)
{
return;
}
switch (requirement)
{
case not null when requirement == ServiceAccountGrantedPoliciesOperations.Updates:
await CanUpdateAsync(context, requirement, resource, accessClient,
userId);
break;
default:
throw new ArgumentException("Unsupported operation requirement type provided.",
nameof(requirement));
}
}
private async Task CanUpdateAsync(AuthorizationHandlerContext context,
ServiceAccountGrantedPoliciesOperationRequirement requirement, ServiceAccountGrantedPoliciesUpdates resource,
AccessClientType accessClient, Guid userId)
{
var access =
await _serviceAccountRepository.AccessToServiceAccountAsync(resource.ServiceAccountId, userId,
accessClient);
if (access.Write)
{
var projectIdsToCheck = resource.ProjectGrantedPolicyUpdates.Select(update =>
update.AccessPolicy.GrantedProjectId!.Value).ToList();
var sameOrganization =
await _projectRepository.ProjectsAreInOrganization(projectIdsToCheck, resource.OrganizationId);
if (!sameOrganization)
{
return;
}
var projectsAccess =
await _projectRepository.AccessToProjectsAsync(projectIdsToCheck, userId, accessClient);
if (projectsAccess.Count == projectIdsToCheck.Count && projectsAccess.All(a => a.Value.Write))
{
context.Succeed(requirement);
}
}
}
}

View File

@ -0,0 +1,26 @@
#nullable enable
using Bit.Core.SecretsManager.Commands.AccessPolicies.Interfaces;
using Bit.Core.SecretsManager.Models.Data;
using Bit.Core.SecretsManager.Repositories;
namespace Bit.Commercial.Core.SecretsManager.Commands.AccessPolicies;
public class UpdateServiceAccountGrantedPoliciesCommand : IUpdateServiceAccountGrantedPoliciesCommand
{
private readonly IAccessPolicyRepository _accessPolicyRepository;
public UpdateServiceAccountGrantedPoliciesCommand(IAccessPolicyRepository accessPolicyRepository)
{
_accessPolicyRepository = accessPolicyRepository;
}
public async Task UpdateAsync(ServiceAccountGrantedPoliciesUpdates grantedPoliciesUpdates)
{
if (!grantedPoliciesUpdates.ProjectGrantedPolicyUpdates.Any())
{
return;
}
await _accessPolicyRepository.UpdateServiceAccountGrantedPoliciesAsync(grantedPoliciesUpdates);
}
}

View File

@ -0,0 +1,41 @@
#nullable enable
using Bit.Core.SecretsManager.Enums.AccessPolicies;
using Bit.Core.SecretsManager.Models.Data;
using Bit.Core.SecretsManager.Models.Data.AccessPolicyUpdates;
using Bit.Core.SecretsManager.Queries.AccessPolicies.Interfaces;
using Bit.Core.SecretsManager.Repositories;
namespace Bit.Commercial.Core.SecretsManager.Queries.AccessPolicies;
public class ServiceAccountGrantedPolicyUpdatesQuery : IServiceAccountGrantedPolicyUpdatesQuery
{
private readonly IAccessPolicyRepository _accessPolicyRepository;
public ServiceAccountGrantedPolicyUpdatesQuery(IAccessPolicyRepository accessPolicyRepository)
{
_accessPolicyRepository = accessPolicyRepository;
}
public async Task<ServiceAccountGrantedPoliciesUpdates> GetAsync(
ServiceAccountGrantedPolicies grantedPolicies)
{
var currentPolicies =
await _accessPolicyRepository.GetServiceAccountGrantedPoliciesAsync(grantedPolicies.ServiceAccountId);
if (currentPolicies == null)
{
return new ServiceAccountGrantedPoliciesUpdates
{
ServiceAccountId = grantedPolicies.ServiceAccountId,
OrganizationId = grantedPolicies.OrganizationId,
ProjectGrantedPolicyUpdates = grantedPolicies.ProjectGrantedPolicies.Select(p =>
new ServiceAccountProjectAccessPolicyUpdate
{
Operation = AccessPolicyOperation.Create,
AccessPolicy = p
})
};
}
return currentPolicies.GetPolicyUpdates(grantedPolicies);
}
}

View File

@ -41,10 +41,12 @@ public static class SecretsManagerCollectionExtensions
services.AddScoped<IAuthorizationHandler, AccessPolicyAuthorizationHandler>();
services.AddScoped<IAuthorizationHandler, ProjectPeopleAccessPoliciesAuthorizationHandler>();
services.AddScoped<IAuthorizationHandler, ServiceAccountPeopleAccessPoliciesAuthorizationHandler>();
services.AddScoped<IAuthorizationHandler, ServiceAccountGrantedPoliciesAuthorizationHandler>();
services.AddScoped<IAccessClientQuery, AccessClientQuery>();
services.AddScoped<IMaxProjectsQuery, MaxProjectsQuery>();
services.AddScoped<ISameOrganizationQuery, SameOrganizationQuery>();
services.AddScoped<IServiceAccountSecretsDetailsQuery, ServiceAccountSecretsDetailsQuery>();
services.AddScoped<IServiceAccountGrantedPolicyUpdatesQuery, ServiceAccountGrantedPolicyUpdatesQuery>();
services.AddScoped<ISecretsSyncQuery, SecretsSyncQuery>();
services.AddScoped<ICreateSecretCommand, CreateSecretCommand>();
services.AddScoped<IUpdateSecretCommand, UpdateSecretCommand>();
@ -64,5 +66,6 @@ public static class SecretsManagerCollectionExtensions
services.AddScoped<IImportCommand, ImportCommand>();
services.AddScoped<IEmptyTrashCommand, EmptyTrashCommand>();
services.AddScoped<IRestoreTrashCommand, RestoreTrashCommand>();
services.AddScoped<IUpdateServiceAccountGrantedPoliciesCommand, UpdateServiceAccountGrantedPoliciesCommand>();
}
}

View File

@ -1,7 +1,8 @@
using System.Linq.Expressions;
using AutoMapper;
using AutoMapper;
using Bit.Core.Enums;
using Bit.Core.SecretsManager.Enums.AccessPolicies;
using Bit.Core.SecretsManager.Models.Data;
using Bit.Core.SecretsManager.Models.Data.AccessPolicyUpdates;
using Bit.Core.SecretsManager.Repositories;
using Bit.Infrastructure.EntityFramework.Repositories;
using Bit.Infrastructure.EntityFramework.SecretsManager.Discriminators;
@ -19,14 +20,7 @@ public class AccessPolicyRepository : BaseEntityFrameworkRepository, IAccessPoli
{
}
private static Expression<Func<ServiceAccountProjectAccessPolicy, bool>> UserHasWriteAccessToProject(Guid userId) =>
policy =>
policy.GrantedProject.UserAccessPolicies.Any(ap => ap.OrganizationUser.User.Id == userId && ap.Write) ||
policy.GrantedProject.GroupAccessPolicies.Any(ap =>
ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.User.Id == userId && ap.Write));
public async Task<List<Core.SecretsManager.Entities.BaseAccessPolicy>> CreateManyAsync(
List<Core.SecretsManager.Entities.BaseAccessPolicy> baseAccessPolicies)
public async Task<List<Core.SecretsManager.Entities.BaseAccessPolicy>> CreateManyAsync(List<Core.SecretsManager.Entities.BaseAccessPolicy> baseAccessPolicies)
{
await using var scope = ServiceScopeFactory.CreateAsyncScope();
var dbContext = GetDatabaseContext(scope);
@ -219,29 +213,6 @@ public class AccessPolicyRepository : BaseEntityFrameworkRepository, IAccessPoli
}
}
public async Task<IEnumerable<Core.SecretsManager.Entities.BaseAccessPolicy>> GetManyByServiceAccountIdAsync(Guid id, Guid userId,
AccessClientType accessType)
{
using var scope = ServiceScopeFactory.CreateScope();
var dbContext = GetDatabaseContext(scope);
var query = dbContext.ServiceAccountProjectAccessPolicy.Where(ap =>
ap.ServiceAccountId == id);
query = accessType switch
{
AccessClientType.NoAccessCheck => query,
AccessClientType.User => query.Where(UserHasWriteAccessToProject(userId)),
_ => throw new ArgumentOutOfRangeException(nameof(accessType), accessType, null),
};
var entities = await query
.Include(ap => ap.ServiceAccount)
.Include(ap => ap.GrantedProject)
.ToListAsync();
return entities.Select(MapToCore);
}
public async Task<PeopleGrantees> GetPeopleGranteesAsync(Guid organizationId, Guid currentUserId)
{
using var scope = ServiceScopeFactory.CreateScope();
@ -429,6 +400,77 @@ public class AccessPolicyRepository : BaseEntityFrameworkRepository, IAccessPoli
return await GetPeoplePoliciesByGrantedServiceAccountIdAsync(peopleAccessPolicies.Id, userId);
}
public async Task<ServiceAccountGrantedPolicies?> GetServiceAccountGrantedPoliciesAsync(Guid serviceAccountId)
{
await using var scope = ServiceScopeFactory.CreateAsyncScope();
var dbContext = GetDatabaseContext(scope);
var entities = await dbContext.ServiceAccountProjectAccessPolicy
.Where(ap => ap.ServiceAccountId == serviceAccountId)
.Include(ap => ap.ServiceAccount)
.Include(ap => ap.GrantedProject)
.ToListAsync();
if (entities.Count == 0)
{
return null;
}
return new ServiceAccountGrantedPolicies(serviceAccountId, entities.Select(MapToCore).ToList());
}
public async Task<ServiceAccountGrantedPoliciesPermissionDetails?>
GetServiceAccountGrantedPoliciesPermissionDetailsAsync(Guid serviceAccountId, Guid userId,
AccessClientType accessClientType)
{
await using var scope = ServiceScopeFactory.CreateAsyncScope();
var dbContext = GetDatabaseContext(scope);
var accessPolicyQuery = dbContext.ServiceAccountProjectAccessPolicy
.Where(ap => ap.ServiceAccountId == serviceAccountId)
.Include(ap => ap.ServiceAccount)
.Include(ap => ap.GrantedProject);
var accessPoliciesPermissionDetails =
await ToPermissionDetails(accessPolicyQuery, userId, accessClientType).ToListAsync();
if (accessPoliciesPermissionDetails.Count == 0)
{
return null;
}
return new ServiceAccountGrantedPoliciesPermissionDetails
{
ServiceAccountId = serviceAccountId,
OrganizationId = accessPoliciesPermissionDetails.First().AccessPolicy.GrantedProject!.OrganizationId,
ProjectGrantedPolicies = accessPoliciesPermissionDetails
};
}
public async Task UpdateServiceAccountGrantedPoliciesAsync(ServiceAccountGrantedPoliciesUpdates updates)
{
await using var scope = ServiceScopeFactory.CreateAsyncScope();
var dbContext = GetDatabaseContext(scope);
var currentAccessPolicies = await dbContext.ServiceAccountProjectAccessPolicy
.Where(ap => ap.ServiceAccountId == updates.ServiceAccountId)
.ToListAsync();
if (currentAccessPolicies.Count != 0)
{
var projectIdsToDelete = updates.ProjectGrantedPolicyUpdates
.Where(pu => pu.Operation == AccessPolicyOperation.Delete)
.Select(pu => pu.AccessPolicy.GrantedProjectId!.Value)
.ToList();
var policiesToDelete = currentAccessPolicies
.Where(entity => projectIdsToDelete.Contains(entity.GrantedProjectId!.Value))
.ToList();
dbContext.RemoveRange(policiesToDelete);
}
await UpsertServiceAccountGrantedPoliciesAsync(dbContext, currentAccessPolicies,
updates.ProjectGrantedPolicyUpdates.Where(pu => pu.Operation != AccessPolicyOperation.Delete).ToList());
await UpdateServiceAccountRevisionAsync(dbContext, updates.ServiceAccountId);
await dbContext.SaveChangesAsync();
}
private static async Task UpsertPeoplePoliciesAsync(DatabaseContext dbContext,
List<BaseAccessPolicy> policies, IReadOnlyCollection<AccessPolicy> userPolicyEntities,
IReadOnlyCollection<AccessPolicy> groupPolicyEntities)
@ -464,6 +506,36 @@ public class AccessPolicyRepository : BaseEntityFrameworkRepository, IAccessPoli
}
}
private async Task UpsertServiceAccountGrantedPoliciesAsync(DatabaseContext dbContext,
IReadOnlyCollection<ServiceAccountProjectAccessPolicy> currentPolices,
List<ServiceAccountProjectAccessPolicyUpdate> policyUpdates)
{
var currentDate = DateTime.UtcNow;
foreach (var policyUpdate in policyUpdates)
{
var updatedEntity = MapToEntity(policyUpdate.AccessPolicy);
var currentEntity = currentPolices.FirstOrDefault(e =>
e.GrantedProjectId == policyUpdate.AccessPolicy.GrantedProjectId!.Value);
switch (policyUpdate.Operation)
{
case AccessPolicyOperation.Create when currentEntity == null:
updatedEntity.SetNewId();
await dbContext.AddAsync(updatedEntity);
break;
case AccessPolicyOperation.Update when currentEntity != null:
dbContext.AccessPolicies.Attach(currentEntity);
currentEntity.Read = updatedEntity.Read;
currentEntity.Write = updatedEntity.Write;
currentEntity.RevisionDate = currentDate;
break;
default:
throw new InvalidOperationException("Policy updates failed due to unexpected state.");
}
}
}
private Core.SecretsManager.Entities.BaseAccessPolicy MapToCore(
BaseAccessPolicy baseAccessPolicyEntity) =>
baseAccessPolicyEntity switch
@ -518,4 +590,42 @@ public class AccessPolicyRepository : BaseEntityFrameworkRepository, IAccessPoli
return MapToCore(baseAccessPolicyEntity);
}
}
private IQueryable<ServiceAccountProjectAccessPolicyPermissionDetails> ToPermissionDetails(
IQueryable<ServiceAccountProjectAccessPolicy>
query, Guid userId, AccessClientType accessClientType)
{
var permissionDetails = accessClientType switch
{
AccessClientType.NoAccessCheck => query.Select(ap => new ServiceAccountProjectAccessPolicyPermissionDetails
{
AccessPolicy =
Mapper.Map<Core.SecretsManager.Entities.ServiceAccountProjectAccessPolicy>(ap),
HasPermission = true
}),
AccessClientType.User => query.Select(ap => new ServiceAccountProjectAccessPolicyPermissionDetails
{
AccessPolicy =
Mapper.Map<Core.SecretsManager.Entities.ServiceAccountProjectAccessPolicy>(ap),
HasPermission =
(ap.GrantedProject.UserAccessPolicies.Any(p => p.OrganizationUser.UserId == userId && p.Write) ||
ap.GrantedProject.GroupAccessPolicies.Any(p =>
p.Group.GroupUsers.Any(gu => gu.OrganizationUser.UserId == userId && p.Write))) &&
(ap.ServiceAccount.UserAccessPolicies.Any(p => p.OrganizationUser.UserId == userId && p.Write) ||
ap.ServiceAccount.GroupAccessPolicies.Any(p =>
p.Group.GroupUsers.Any(gu => gu.OrganizationUser.UserId == userId && p.Write)))
}),
_ => throw new ArgumentOutOfRangeException(nameof(accessClientType), accessClientType, null)
};
return permissionDetails;
}
private static async Task UpdateServiceAccountRevisionAsync(DatabaseContext dbContext, Guid serviceAccountId)
{
var entity = await dbContext.ServiceAccount.FindAsync(serviceAccountId);
if (entity != null)
{
entity.RevisionDate = DateTime.UtcNow;
}
}
}

View File

@ -140,27 +140,8 @@ public class ProjectRepository : Repository<Core.SecretsManager.Entities.Project
var projectQuery = dbContext.Project
.Where(s => s.Id == id);
var query = accessType switch
{
AccessClientType.NoAccessCheck => projectQuery.Select(_ => new { Read = true, Write = true }),
AccessClientType.User => projectQuery.Select(p => new
{
Read = p.UserAccessPolicies.Any(ap => ap.OrganizationUser.User.Id == userId && ap.Read)
|| p.GroupAccessPolicies.Any(ap =>
ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.User.Id == userId && ap.Read)),
Write = p.UserAccessPolicies.Any(ap => ap.OrganizationUser.User.Id == userId && ap.Write) ||
p.GroupAccessPolicies.Any(ap =>
ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.User.Id == userId && ap.Write)),
}),
AccessClientType.ServiceAccount => projectQuery.Select(p => new
{
Read = p.ServiceAccountAccessPolicies.Any(ap => ap.ServiceAccountId == userId && ap.Read),
Write = p.ServiceAccountAccessPolicies.Any(ap => ap.ServiceAccountId == userId && ap.Write),
}),
_ => projectQuery.Select(_ => new { Read = false, Write = false }),
};
var policy = await query.FirstOrDefaultAsync();
var accessQuery = BuildProjectAccessQuery(projectQuery, userId, accessType);
var policy = await accessQuery.FirstOrDefaultAsync();
return policy == null ? (false, false) : (policy.Read, policy.Write);
}
@ -174,6 +155,46 @@ public class ProjectRepository : Repository<Core.SecretsManager.Entities.Project
return projectIds.Count == results.Count;
}
public async Task<Dictionary<Guid, (bool Read, bool Write)>> AccessToProjectsAsync(
IEnumerable<Guid> projectIds,
Guid userId,
AccessClientType accessType)
{
await using var scope = ServiceScopeFactory.CreateAsyncScope();
var dbContext = GetDatabaseContext(scope);
var projectsQuery = dbContext.Project.Where(p => projectIds.Contains(p.Id));
var accessQuery = BuildProjectAccessQuery(projectsQuery, userId, accessType);
return await accessQuery.ToDictionaryAsync(pa => pa.Id, pa => (pa.Read, pa.Write));
}
private record ProjectAccess(Guid Id, bool Read, bool Write);
private static IQueryable<ProjectAccess> BuildProjectAccessQuery(IQueryable<Project> projectQuery, Guid userId,
AccessClientType accessType) =>
accessType switch
{
AccessClientType.NoAccessCheck => projectQuery.Select(p => new ProjectAccess(p.Id, true, true)),
AccessClientType.User => projectQuery.Select(p => new ProjectAccess
(
p.Id,
p.UserAccessPolicies.Any(ap => ap.OrganizationUser.User.Id == userId && ap.Read) ||
p.GroupAccessPolicies.Any(ap =>
ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.User.Id == userId && ap.Read)),
p.UserAccessPolicies.Any(ap => ap.OrganizationUser.User.Id == userId && ap.Write) ||
p.GroupAccessPolicies.Any(ap =>
ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.User.Id == userId && ap.Write))
)),
AccessClientType.ServiceAccount => projectQuery.Select(p => new ProjectAccess
(
p.Id,
p.ServiceAccountAccessPolicies.Any(ap => ap.ServiceAccountId == userId && ap.Read),
p.ServiceAccountAccessPolicies.Any(ap => ap.ServiceAccountId == userId && ap.Write)
)),
_ => projectQuery.Select(p => new ProjectAccess(p.Id, false, false))
};
private IQueryable<ProjectPermissionDetails> ProjectToPermissionDetails(IQueryable<Project> query, Guid userId, AccessClientType accessType)
{
var projects = accessType switch

View File

@ -0,0 +1,273 @@
#nullable enable
using System.Reflection;
using System.Security.Claims;
using Bit.Commercial.Core.SecretsManager.AuthorizationHandlers.AccessPolicies;
using Bit.Core.Context;
using Bit.Core.Enums;
using Bit.Core.SecretsManager.AuthorizationRequirements;
using Bit.Core.SecretsManager.Models.Data;
using Bit.Core.SecretsManager.Queries.Interfaces;
using Bit.Core.SecretsManager.Repositories;
using Bit.Core.Test.SecretsManager.AutoFixture.ProjectsFixture;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Microsoft.AspNetCore.Authorization;
using NSubstitute;
using Xunit;
namespace Bit.Commercial.Core.Test.SecretsManager.AuthorizationHandlers.AccessPolicies;
[SutProviderCustomize]
[ProjectCustomize]
public class ServiceAccountGrantedPoliciesAuthorizationHandlerTests
{
[Fact]
public void ServiceAccountGrantedPoliciesOperations_OnlyPublicStatic()
{
var publicStaticFields =
typeof(ServiceAccountGrantedPoliciesOperations).GetFields(BindingFlags.Public | BindingFlags.Static);
var allFields = typeof(ServiceAccountGrantedPoliciesOperations).GetFields();
Assert.Equal(publicStaticFields.Length, allFields.Length);
}
[Theory]
[BitAutoData]
public async Task Handler_AccessSecretsManagerFalse_DoesNotSucceed(
SutProvider<ServiceAccountGrantedPoliciesAuthorizationHandler> sutProvider,
ServiceAccountGrantedPoliciesUpdates resource,
ClaimsPrincipal claimsPrincipal)
{
var requirement = ServiceAccountGrantedPoliciesOperations.Updates;
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(resource.OrganizationId)
.Returns(false);
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, resource);
await sutProvider.Sut.HandleAsync(authzContext);
Assert.False(authzContext.HasSucceeded);
}
[Theory]
[BitAutoData(AccessClientType.ServiceAccount)]
[BitAutoData(AccessClientType.Organization)]
public async Task Handler_UnsupportedClientTypes_DoesNotSucceed(
AccessClientType accessClientType,
SutProvider<ServiceAccountGrantedPoliciesAuthorizationHandler> sutProvider,
ServiceAccountGrantedPoliciesUpdates resource,
ClaimsPrincipal claimsPrincipal)
{
var requirement = ServiceAccountGrantedPoliciesOperations.Updates;
SetupUserSubstitutes(sutProvider, accessClientType, resource);
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, resource);
await sutProvider.Sut.HandleAsync(authzContext);
Assert.False(authzContext.HasSucceeded);
}
[Theory]
[BitAutoData]
public async Task Handler_UnsupportedServiceAccountGrantedPoliciesOperationRequirement_Throws(
SutProvider<ServiceAccountGrantedPoliciesAuthorizationHandler> sutProvider,
ServiceAccountGrantedPoliciesUpdates resource,
ClaimsPrincipal claimsPrincipal)
{
var requirement = new ServiceAccountGrantedPoliciesOperationRequirement();
SetupUserSubstitutes(sutProvider, AccessClientType.NoAccessCheck, resource);
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, resource);
await Assert.ThrowsAsync<ArgumentException>(() => sutProvider.Sut.HandleAsync(authzContext));
}
[Theory]
[BitAutoData(AccessClientType.NoAccessCheck, false, false)]
[BitAutoData(AccessClientType.NoAccessCheck, true, false)]
[BitAutoData(AccessClientType.User, false, false)]
[BitAutoData(AccessClientType.User, true, false)]
public async Task Handler_UserHasNoWriteAccessToServiceAccount_DoesNotSucceed(
AccessClientType accessClientType,
bool saReadAccess,
bool saWriteAccess,
SutProvider<ServiceAccountGrantedPoliciesAuthorizationHandler> sutProvider,
ServiceAccountGrantedPoliciesUpdates resource,
Guid userId,
ClaimsPrincipal claimsPrincipal)
{
var requirement = ServiceAccountGrantedPoliciesOperations.Updates;
SetupUserSubstitutes(sutProvider, accessClientType, resource, userId);
sutProvider.GetDependency<IServiceAccountRepository>()
.AccessToServiceAccountAsync(resource.ServiceAccountId, userId, accessClientType)
.Returns((saReadAccess, saWriteAccess));
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, resource);
await sutProvider.Sut.HandleAsync(authzContext);
Assert.False(authzContext.HasSucceeded);
}
[Theory]
[BitAutoData]
public async Task Handler_GrantedProjectsInDifferentOrganization_DoesNotSucceed(
SutProvider<ServiceAccountGrantedPoliciesAuthorizationHandler> sutProvider,
ServiceAccountGrantedPoliciesUpdates resource,
Guid userId,
ClaimsPrincipal claimsPrincipal)
{
var requirement = ServiceAccountGrantedPoliciesOperations.Updates;
SetupUserSubstitutes(sutProvider, AccessClientType.NoAccessCheck, resource, userId);
sutProvider.GetDependency<IServiceAccountRepository>()
.AccessToServiceAccountAsync(resource.ServiceAccountId, userId, AccessClientType.NoAccessCheck)
.Returns((true, true));
sutProvider.GetDependency<IProjectRepository>()
.ProjectsAreInOrganization(Arg.Any<List<Guid>>(), resource.OrganizationId)
.Returns(false);
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, resource);
await sutProvider.Sut.HandleAsync(authzContext);
Assert.False(authzContext.HasSucceeded);
}
[Theory]
[BitAutoData(AccessClientType.NoAccessCheck)]
[BitAutoData(AccessClientType.User)]
public async Task Handler_UserHasNoAccessToGrantedProjects_DoesNotSucceed(
AccessClientType accessClientType,
SutProvider<ServiceAccountGrantedPoliciesAuthorizationHandler> sutProvider,
ServiceAccountGrantedPoliciesUpdates resource,
Guid userId,
ClaimsPrincipal claimsPrincipal)
{
var requirement = ServiceAccountGrantedPoliciesOperations.Updates;
var projectIds = SetupProjectAccessTest(sutProvider, accessClientType, resource, userId);
sutProvider.GetDependency<IProjectRepository>()
.AccessToProjectsAsync(Arg.Any<List<Guid>>(), userId, accessClientType)
.Returns(projectIds.ToDictionary(projectId => projectId, _ => (false, false)));
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, resource);
await sutProvider.Sut.HandleAsync(authzContext);
Assert.False(authzContext.HasSucceeded);
}
[Theory]
[BitAutoData(AccessClientType.NoAccessCheck)]
[BitAutoData(AccessClientType.User)]
public async Task Handler_UserHasAccessToSomeGrantedProjects_DoesNotSucceed(
AccessClientType accessClientType,
SutProvider<ServiceAccountGrantedPoliciesAuthorizationHandler> sutProvider,
ServiceAccountGrantedPoliciesUpdates resource,
Guid userId,
ClaimsPrincipal claimsPrincipal)
{
var requirement = ServiceAccountGrantedPoliciesOperations.Updates;
var projectIds = SetupProjectAccessTest(sutProvider, accessClientType, resource, userId);
var accessResult = projectIds.ToDictionary(projectId => projectId, _ => (false, false));
accessResult[projectIds.First()] = (true, true);
sutProvider.GetDependency<IProjectRepository>()
.AccessToProjectsAsync(Arg.Any<List<Guid>>(), userId, accessClientType)
.Returns(accessResult);
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, resource);
await sutProvider.Sut.HandleAsync(authzContext);
Assert.False(authzContext.HasSucceeded);
}
[Theory]
[BitAutoData(AccessClientType.NoAccessCheck)]
[BitAutoData(AccessClientType.User)]
public async Task Handler_AccessResultsPartial_DoesNotSucceed(
AccessClientType accessClientType,
SutProvider<ServiceAccountGrantedPoliciesAuthorizationHandler> sutProvider,
ServiceAccountGrantedPoliciesUpdates resource,
Guid userId,
ClaimsPrincipal claimsPrincipal)
{
var requirement = ServiceAccountGrantedPoliciesOperations.Updates;
var projectIds = SetupProjectAccessTest(sutProvider, accessClientType, resource, userId);
var accessResult = projectIds.ToDictionary(projectId => projectId, _ => (false, false));
accessResult.Remove(projectIds.First());
sutProvider.GetDependency<IProjectRepository>()
.AccessToProjectsAsync(Arg.Any<List<Guid>>(), userId, accessClientType)
.Returns(accessResult);
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, resource);
await sutProvider.Sut.HandleAsync(authzContext);
Assert.False(authzContext.HasSucceeded);
}
[Theory]
[BitAutoData(AccessClientType.NoAccessCheck)]
[BitAutoData(AccessClientType.User)]
public async Task Handler_UserHasAccessToAllGrantedProjects_Success(
AccessClientType accessClientType,
SutProvider<ServiceAccountGrantedPoliciesAuthorizationHandler> sutProvider,
ServiceAccountGrantedPoliciesUpdates resource,
Guid userId,
ClaimsPrincipal claimsPrincipal)
{
var requirement = ServiceAccountGrantedPoliciesOperations.Updates;
var projectIds = SetupProjectAccessTest(sutProvider, accessClientType, resource, userId);
sutProvider.GetDependency<IProjectRepository>()
.AccessToProjectsAsync(Arg.Any<List<Guid>>(), userId, accessClientType)
.Returns(projectIds.ToDictionary(projectId => projectId, _ => (true, true)));
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, resource);
await sutProvider.Sut.HandleAsync(authzContext);
Assert.True(authzContext.HasSucceeded);
}
private static void SetupUserSubstitutes(
SutProvider<ServiceAccountGrantedPoliciesAuthorizationHandler> sutProvider,
AccessClientType accessClientType,
ServiceAccountGrantedPoliciesUpdates resource,
Guid userId = new())
{
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(resource.OrganizationId)
.Returns(true);
sutProvider.GetDependency<IAccessClientQuery>().GetAccessClientAsync(default, resource.OrganizationId)
.ReturnsForAnyArgs((accessClientType, userId));
}
private static List<Guid> SetupProjectAccessTest(
SutProvider<ServiceAccountGrantedPoliciesAuthorizationHandler> sutProvider,
AccessClientType accessClientType,
ServiceAccountGrantedPoliciesUpdates resource,
Guid userId = new())
{
SetupUserSubstitutes(sutProvider, accessClientType, resource, userId);
sutProvider.GetDependency<IServiceAccountRepository>()
.AccessToServiceAccountAsync(resource.ServiceAccountId, userId, accessClientType)
.Returns((true, true));
sutProvider.GetDependency<IProjectRepository>()
.ProjectsAreInOrganization(Arg.Any<List<Guid>>(), resource.OrganizationId)
.Returns(true);
return resource.ProjectGrantedPolicyUpdates
.Select(pu => pu.AccessPolicy.GrantedProjectId!.Value)
.ToList();
}
}

View File

@ -0,0 +1,43 @@
#nullable enable
using Bit.Commercial.Core.SecretsManager.Commands.AccessPolicies;
using Bit.Core.SecretsManager.Models.Data;
using Bit.Core.SecretsManager.Repositories;
using Bit.Core.Test.SecretsManager.AutoFixture.ProjectsFixture;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
namespace Bit.Commercial.Core.Test.SecretsManager.Commands.AccessPolicies;
[SutProviderCustomize]
[ProjectCustomize]
public class UpdateServiceAccountGrantedPoliciesCommandTests
{
[Theory]
[BitAutoData]
public async Task UpdateAsync_NoUpdates_DoesNotCallRepository(
SutProvider<UpdateServiceAccountGrantedPoliciesCommand> sutProvider,
ServiceAccountGrantedPoliciesUpdates data)
{
data.ProjectGrantedPolicyUpdates = [];
await sutProvider.Sut.UpdateAsync(data);
await sutProvider.GetDependency<IAccessPolicyRepository>()
.DidNotReceiveWithAnyArgs()
.UpdateServiceAccountGrantedPoliciesAsync(Arg.Any<ServiceAccountGrantedPoliciesUpdates>());
}
[Theory]
[BitAutoData]
public async Task UpdateAsync_HasUpdates_CallsRepository(
SutProvider<UpdateServiceAccountGrantedPoliciesCommand> sutProvider,
ServiceAccountGrantedPoliciesUpdates data)
{
await sutProvider.Sut.UpdateAsync(data);
await sutProvider.GetDependency<IAccessPolicyRepository>()
.Received(1)
.UpdateServiceAccountGrantedPoliciesAsync(Arg.Any<ServiceAccountGrantedPoliciesUpdates>());
}
}

View File

@ -0,0 +1,86 @@
#nullable enable
using Bit.Commercial.Core.SecretsManager.Queries.AccessPolicies;
using Bit.Core.SecretsManager.Entities;
using Bit.Core.SecretsManager.Enums.AccessPolicies;
using Bit.Core.SecretsManager.Models.Data;
using Bit.Core.SecretsManager.Repositories;
using Bit.Core.Test.SecretsManager.AutoFixture.ProjectsFixture;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using NSubstitute.ReturnsExtensions;
using Xunit;
namespace Bit.Commercial.Core.Test.SecretsManager.Queries.AccessPolicies;
[SutProviderCustomize]
[ProjectCustomize]
public class ServiceAccountGrantedPolicyUpdatesQueryTests
{
[Theory]
[BitAutoData]
public async Task GetAsync_NoCurrentGrantedPolicies_ReturnsAllCreates(
SutProvider<ServiceAccountGrantedPolicyUpdatesQuery> sutProvider,
ServiceAccountGrantedPolicies data)
{
sutProvider.GetDependency<IAccessPolicyRepository>()
.GetServiceAccountGrantedPoliciesAsync(data.ServiceAccountId)
.ReturnsNullForAnyArgs();
var result = await sutProvider.Sut.GetAsync(data);
Assert.Equal(data.ServiceAccountId, result.ServiceAccountId);
Assert.Equal(data.OrganizationId, result.OrganizationId);
Assert.Equal(data.ProjectGrantedPolicies.Count(), result.ProjectGrantedPolicyUpdates.Count());
Assert.All(result.ProjectGrantedPolicyUpdates, p =>
{
Assert.Equal(AccessPolicyOperation.Create, p.Operation);
Assert.Contains(data.ProjectGrantedPolicies, x => x == p.AccessPolicy);
});
}
[Theory]
[BitAutoData]
public async Task GetAsync_CurrentGrantedPolicies_ReturnsChanges(
SutProvider<ServiceAccountGrantedPolicyUpdatesQuery> sutProvider,
ServiceAccountGrantedPolicies data, ServiceAccountProjectAccessPolicy currentPolicyToDelete)
{
foreach (var grantedPolicy in data.ProjectGrantedPolicies)
{
grantedPolicy.ServiceAccountId = data.ServiceAccountId;
}
currentPolicyToDelete.ServiceAccountId = data.ServiceAccountId;
var updatePolicy = new ServiceAccountProjectAccessPolicy
{
ServiceAccountId = data.ServiceAccountId,
GrantedProjectId = data.ProjectGrantedPolicies.First().GrantedProjectId,
Read = !data.ProjectGrantedPolicies.First().Read,
Write = !data.ProjectGrantedPolicies.First().Write
};
var currentPolicies = new ServiceAccountGrantedPolicies
{
ServiceAccountId = data.ServiceAccountId,
OrganizationId = data.OrganizationId,
ProjectGrantedPolicies = [updatePolicy, currentPolicyToDelete]
};
sutProvider.GetDependency<IAccessPolicyRepository>()
.GetServiceAccountGrantedPoliciesAsync(data.ServiceAccountId)
.ReturnsForAnyArgs(currentPolicies);
var result = await sutProvider.Sut.GetAsync(data);
Assert.Equal(data.ServiceAccountId, result.ServiceAccountId);
Assert.Equal(data.OrganizationId, result.OrganizationId);
Assert.Single(result.ProjectGrantedPolicyUpdates.Where(x =>
x.Operation == AccessPolicyOperation.Delete && x.AccessPolicy == currentPolicyToDelete));
Assert.Single(result.ProjectGrantedPolicyUpdates.Where(x =>
x.Operation == AccessPolicyOperation.Update &&
x.AccessPolicy.GrantedProjectId == updatePolicy.GrantedProjectId));
Assert.Equal(result.ProjectGrantedPolicyUpdates.Count() - 2,
result.ProjectGrantedPolicyUpdates.Count(x => x.Operation == AccessPolicyOperation.Create));
}
}

View File

@ -7,6 +7,8 @@ using Bit.Core.Exceptions;
using Bit.Core.SecretsManager.AuthorizationRequirements;
using Bit.Core.SecretsManager.Commands.AccessPolicies.Interfaces;
using Bit.Core.SecretsManager.Entities;
using Bit.Core.SecretsManager.Queries.AccessPolicies.Interfaces;
using Bit.Core.SecretsManager.Queries.Interfaces;
using Bit.Core.SecretsManager.Repositories;
using Bit.Core.Services;
using Microsoft.AspNetCore.Authorization;
@ -26,6 +28,9 @@ public class AccessPoliciesController : Controller
private readonly IProjectRepository _projectRepository;
private readonly IServiceAccountRepository _serviceAccountRepository;
private readonly IUpdateAccessPolicyCommand _updateAccessPolicyCommand;
private readonly IUpdateServiceAccountGrantedPoliciesCommand _updateServiceAccountGrantedPoliciesCommand;
private readonly IAccessClientQuery _accessClientQuery;
private readonly IServiceAccountGrantedPolicyUpdatesQuery _serviceAccountGrantedPolicyUpdatesQuery;
private readonly IUserService _userService;
private readonly IAuthorizationService _authorizationService;
@ -36,6 +41,9 @@ public class AccessPoliciesController : Controller
IAccessPolicyRepository accessPolicyRepository,
IServiceAccountRepository serviceAccountRepository,
IProjectRepository projectRepository,
IAccessClientQuery accessClientQuery,
IServiceAccountGrantedPolicyUpdatesQuery serviceAccountGrantedPolicyUpdatesQuery,
IUpdateServiceAccountGrantedPoliciesCommand updateServiceAccountGrantedPoliciesCommand,
ICreateAccessPoliciesCommand createAccessPoliciesCommand,
IDeleteAccessPolicyCommand deleteAccessPolicyCommand,
IUpdateAccessPolicyCommand updateAccessPolicyCommand)
@ -49,6 +57,9 @@ public class AccessPoliciesController : Controller
_createAccessPoliciesCommand = createAccessPoliciesCommand;
_deleteAccessPolicyCommand = deleteAccessPolicyCommand;
_updateAccessPolicyCommand = updateAccessPolicyCommand;
_updateServiceAccountGrantedPoliciesCommand = updateServiceAccountGrantedPoliciesCommand;
_accessClientQuery = accessClientQuery;
_serviceAccountGrantedPolicyUpdatesQuery = serviceAccountGrantedPolicyUpdatesQuery;
}
[HttpPost("/projects/{id}/access-policies")]
@ -89,61 +100,6 @@ public class AccessPoliciesController : Controller
return new ProjectAccessPoliciesResponseModel(results);
}
[HttpPost("/service-accounts/{id}/granted-policies")]
public async Task<ListResponseModel<ServiceAccountProjectAccessPolicyResponseModel>>
CreateServiceAccountGrantedPoliciesAsync([FromRoute] Guid id,
[FromBody] List<GrantedAccessPolicyRequest> requests)
{
if (requests.Count > _maxBulkCreation)
{
throw new BadRequestException($"Can process no more than {_maxBulkCreation} creation requests at once.");
}
if (requests.Count != requests.DistinctBy(request => request.GrantedId).Count())
{
throw new BadRequestException("Resources must be unique");
}
var serviceAccount = await _serviceAccountRepository.GetByIdAsync(id);
if (serviceAccount == null)
{
throw new NotFoundException();
}
var policies = requests.Select(request => request.ToServiceAccountProjectAccessPolicy(id, serviceAccount.OrganizationId)).ToList();
foreach (var policy in policies)
{
var authorizationResult = await _authorizationService.AuthorizeAsync(User, policy, AccessPolicyOperations.Create);
if (!authorizationResult.Succeeded)
{
throw new NotFoundException();
}
}
var results =
await _createAccessPoliciesCommand.CreateManyAsync(new List<BaseAccessPolicy>(policies));
var responses = results.Select(ap =>
new ServiceAccountProjectAccessPolicyResponseModel((ServiceAccountProjectAccessPolicy)ap));
return new ListResponseModel<ServiceAccountProjectAccessPolicyResponseModel>(responses);
}
[HttpGet("/service-accounts/{id}/granted-policies")]
public async Task<ListResponseModel<ServiceAccountProjectAccessPolicyResponseModel>>
GetServiceAccountGrantedPoliciesAsync([FromRoute] Guid id)
{
var serviceAccount = await _serviceAccountRepository.GetByIdAsync(id);
if (serviceAccount == null)
{
throw new NotFoundException();
}
var (accessClient, userId) = await GetAccessClientTypeAsync(serviceAccount.OrganizationId);
var results = await _accessPolicyRepository.GetManyByServiceAccountIdAsync(id, userId, accessClient);
var responses = results.Select(ap =>
new ServiceAccountProjectAccessPolicyResponseModel((ServiceAccountProjectAccessPolicy)ap));
return new ListResponseModel<ServiceAccountProjectAccessPolicyResponseModel>(responses);
}
[HttpPut("{id}")]
public async Task<BaseAccessPolicyResponseModel> UpdateAccessPolicyAsync([FromRoute] Guid id,
[FromBody] AccessPolicyUpdateRequest request)
@ -303,6 +259,43 @@ public class AccessPoliciesController : Controller
return new ServiceAccountPeopleAccessPoliciesResponseModel(results, userId);
}
[HttpGet("/service-accounts/{id}/granted-policies")]
public async Task<ServiceAccountGrantedPoliciesPermissionDetailsResponseModel>
GetServiceAccountGrantedPoliciesAsync([FromRoute] Guid id)
{
var serviceAccount = await _serviceAccountRepository.GetByIdAsync(id);
var authorizationResult =
await _authorizationService.AuthorizeAsync(User, serviceAccount, ServiceAccountOperations.Update);
if (!authorizationResult.Succeeded)
{
throw new NotFoundException();
}
return await GetServiceAccountGrantedPoliciesAsync(serviceAccount);
}
[HttpPut("/service-accounts/{id}/granted-policies")]
public async Task<ServiceAccountGrantedPoliciesPermissionDetailsResponseModel>
PutServiceAccountGrantedPoliciesAsync([FromRoute] Guid id,
[FromBody] ServiceAccountGrantedPoliciesRequestModel request)
{
var serviceAccount = await _serviceAccountRepository.GetByIdAsync(id) ?? throw new NotFoundException();
var grantedPoliciesUpdates =
await _serviceAccountGrantedPolicyUpdatesQuery.GetAsync(request.ToGrantedPolicies(serviceAccount));
var authorizationResult = await _authorizationService.AuthorizeAsync(User, grantedPoliciesUpdates,
ServiceAccountGrantedPoliciesOperations.Updates);
if (!authorizationResult.Succeeded)
{
throw new NotFoundException();
}
await _updateServiceAccountGrantedPoliciesCommand.UpdateAsync(grantedPoliciesUpdates);
return await GetServiceAccountGrantedPoliciesAsync(serviceAccount);
}
private async Task<(AccessClientType AccessClientType, Guid UserId)> CheckUserHasWriteAccessToProjectAsync(Project project)
{
if (project == null)
@ -355,4 +348,11 @@ public class AccessPoliciesController : Controller
var accessClient = AccessClientHelper.ToAccessClient(_currentContext.ClientType, orgAdmin);
return (accessClient, userId);
}
private async Task<ServiceAccountGrantedPoliciesPermissionDetailsResponseModel> GetServiceAccountGrantedPoliciesAsync(ServiceAccount serviceAccount)
{
var (accessClient, userId) = await _accessClientQuery.GetAccessClientAsync(User, serviceAccount.OrganizationId);
var results = await _accessPolicyRepository.GetServiceAccountGrantedPoliciesPermissionDetailsAsync(serviceAccount.Id, userId, accessClient);
return new ServiceAccountGrantedPoliciesPermissionDetailsResponseModel(results);
}
}

View File

@ -0,0 +1,28 @@
#nullable enable
using Bit.Api.SecretsManager.Utilities;
using Bit.Core.SecretsManager.Entities;
using Bit.Core.SecretsManager.Models.Data;
namespace Bit.Api.SecretsManager.Models.Request;
public class ServiceAccountGrantedPoliciesRequestModel
{
public required IEnumerable<GrantedAccessPolicyRequest> ProjectGrantedPolicyRequests { get; set; }
public ServiceAccountGrantedPolicies ToGrantedPolicies(ServiceAccount serviceAccount)
{
var projectGrantedPolicies = ProjectGrantedPolicyRequests
.Select(x => x.ToServiceAccountProjectAccessPolicy(serviceAccount.Id, serviceAccount.OrganizationId))
.ToList();
AccessPolicyHelpers.CheckForDistinctAccessPolicies(projectGrantedPolicies);
AccessPolicyHelpers.CheckAccessPoliciesHaveReadPermission(projectGrantedPolicies);
return new ServiceAccountGrantedPolicies
{
ServiceAccountId = serviceAccount.Id,
OrganizationId = serviceAccount.OrganizationId,
ProjectGrantedPolicies = projectGrantedPolicies
};
}
}

View File

@ -0,0 +1,30 @@
#nullable enable
using Bit.Core.Models.Api;
using Bit.Core.SecretsManager.Models.Data;
namespace Bit.Api.SecretsManager.Models.Response;
public class ServiceAccountGrantedPoliciesPermissionDetailsResponseModel : ResponseModel
{
private const string _objectName = "ServiceAccountGrantedPoliciesPermissionDetails";
public ServiceAccountGrantedPoliciesPermissionDetailsResponseModel(
ServiceAccountGrantedPoliciesPermissionDetails? grantedPoliciesPermissionDetails)
: base(_objectName)
{
if (grantedPoliciesPermissionDetails == null)
{
return;
}
GrantedProjectPolicies = grantedPoliciesPermissionDetails.ProjectGrantedPolicies
.Select(x => new ServiceAccountProjectAccessPolicyPermissionDetailsResponseModel(x)).ToList();
}
public ServiceAccountGrantedPoliciesPermissionDetailsResponseModel() : base(_objectName)
{
}
public List<ServiceAccountProjectAccessPolicyPermissionDetailsResponseModel> GrantedProjectPolicies { get; set; } =
[];
}

View File

@ -0,0 +1,25 @@
#nullable enable
using Bit.Core.Models.Api;
using Bit.Core.SecretsManager.Models.Data;
namespace Bit.Api.SecretsManager.Models.Response;
public class ServiceAccountProjectAccessPolicyPermissionDetailsResponseModel : ResponseModel
{
private const string _objectName = "serviceAccountProjectAccessPolicyPermissionDetails";
public ServiceAccountProjectAccessPolicyPermissionDetailsResponseModel(
ServiceAccountProjectAccessPolicyPermissionDetails apPermissionDetails, string obj = _objectName) : base(obj)
{
AccessPolicy = new ServiceAccountProjectAccessPolicyResponseModel(apPermissionDetails.AccessPolicy);
HasPermission = apPermissionDetails.HasPermission;
}
public ServiceAccountProjectAccessPolicyPermissionDetailsResponseModel()
: base(_objectName)
{
}
public ServiceAccountProjectAccessPolicyResponseModel AccessPolicy { get; set; } = new();
public bool HasPermission { get; set; }
}

View File

@ -0,0 +1,40 @@
#nullable enable
using Bit.Core.Exceptions;
using Bit.Core.SecretsManager.Entities;
namespace Bit.Api.SecretsManager.Utilities;
public static class AccessPolicyHelpers
{
public static void CheckForDistinctAccessPolicies(IReadOnlyCollection<BaseAccessPolicy> accessPolicies)
{
var distinctAccessPolicies = accessPolicies.DistinctBy(baseAccessPolicy =>
{
return baseAccessPolicy switch
{
UserProjectAccessPolicy ap => new Tuple<Guid?, Guid?>(ap.OrganizationUserId, ap.GrantedProjectId),
GroupProjectAccessPolicy ap => new Tuple<Guid?, Guid?>(ap.GroupId, ap.GrantedProjectId),
ServiceAccountProjectAccessPolicy ap => new Tuple<Guid?, Guid?>(ap.ServiceAccountId,
ap.GrantedProjectId),
UserServiceAccountAccessPolicy ap => new Tuple<Guid?, Guid?>(ap.OrganizationUserId,
ap.GrantedServiceAccountId),
GroupServiceAccountAccessPolicy ap => new Tuple<Guid?, Guid?>(ap.GroupId, ap.GrantedServiceAccountId),
_ => throw new ArgumentException("Unsupported access policy type provided.", nameof(baseAccessPolicy)),
};
}).ToList();
if (accessPolicies.Count != distinctAccessPolicies.Count)
{
throw new BadRequestException("Resources must be unique");
}
}
public static void CheckAccessPoliciesHaveReadPermission(IEnumerable<BaseAccessPolicy> accessPolicies)
{
var accessPoliciesPermission = accessPolicies.All(policy => policy.Read);
if (!accessPoliciesPermission)
{
throw new BadRequestException("Resources must be Read = true");
}
}
}

View File

@ -0,0 +1,14 @@
#nullable enable
using Microsoft.AspNetCore.Authorization.Infrastructure;
namespace Bit.Core.SecretsManager.AuthorizationRequirements;
public class ServiceAccountGrantedPoliciesOperationRequirement : OperationAuthorizationRequirement
{
}
public static class ServiceAccountGrantedPoliciesOperations
{
public static readonly ServiceAccountGrantedPoliciesOperationRequirement Updates = new() { Name = nameof(Updates) };
}

View File

@ -0,0 +1,9 @@
#nullable enable
using Bit.Core.SecretsManager.Models.Data;
namespace Bit.Core.SecretsManager.Commands.AccessPolicies.Interfaces;
public interface IUpdateServiceAccountGrantedPoliciesCommand
{
Task UpdateAsync(ServiceAccountGrantedPoliciesUpdates grantedPoliciesUpdates);
}

View File

@ -0,0 +1,8 @@
namespace Bit.Core.SecretsManager.Enums.AccessPolicies;
public enum AccessPolicyOperation
{
Create,
Update,
Delete
}

View File

@ -0,0 +1,11 @@
#nullable enable
using Bit.Core.SecretsManager.Entities;
using Bit.Core.SecretsManager.Enums.AccessPolicies;
namespace Bit.Core.SecretsManager.Models.Data.AccessPolicyUpdates;
public class ServiceAccountProjectAccessPolicyUpdate
{
public AccessPolicyOperation Operation { get; set; }
public required ServiceAccountProjectAccessPolicy AccessPolicy { get; set; }
}

View File

@ -0,0 +1,83 @@
#nullable enable
using Bit.Core.SecretsManager.Entities;
using Bit.Core.SecretsManager.Enums.AccessPolicies;
using Bit.Core.SecretsManager.Models.Data.AccessPolicyUpdates;
namespace Bit.Core.SecretsManager.Models.Data;
public class ServiceAccountGrantedPolicies
{
public ServiceAccountGrantedPolicies(Guid serviceAccountId, IEnumerable<BaseAccessPolicy> policies)
{
ServiceAccountId = serviceAccountId;
ProjectGrantedPolicies = policies.Where(x => x is ServiceAccountProjectAccessPolicy)
.Cast<ServiceAccountProjectAccessPolicy>().ToList();
var serviceAccount = ProjectGrantedPolicies.FirstOrDefault()?.ServiceAccount;
if (serviceAccount != null)
{
OrganizationId = serviceAccount.OrganizationId;
}
}
public ServiceAccountGrantedPolicies()
{
}
public Guid ServiceAccountId { get; set; }
public Guid OrganizationId { get; set; }
public IEnumerable<ServiceAccountProjectAccessPolicy> ProjectGrantedPolicies { get; set; } =
new List<ServiceAccountProjectAccessPolicy>();
public ServiceAccountGrantedPoliciesUpdates GetPolicyUpdates(ServiceAccountGrantedPolicies requested)
{
var currentProjectIds = ProjectGrantedPolicies.Select(p => p.GrantedProjectId!.Value).ToList();
var requestedProjectIds = requested.ProjectGrantedPolicies.Select(p => p.GrantedProjectId!.Value).ToList();
var projectIdsToBeDeleted = currentProjectIds.Except(requestedProjectIds).ToList();
var projectIdsToBeCreated = requestedProjectIds.Except(currentProjectIds).ToList();
var projectIdsToBeUpdated = GetProjectIdsToBeUpdated(requested);
var policiesToBeDeleted =
CreatePolicyUpdates(ProjectGrantedPolicies, projectIdsToBeDeleted, AccessPolicyOperation.Delete);
var policiesToBeCreated = CreatePolicyUpdates(requested.ProjectGrantedPolicies, projectIdsToBeCreated,
AccessPolicyOperation.Create);
var policiesToBeUpdated = CreatePolicyUpdates(requested.ProjectGrantedPolicies, projectIdsToBeUpdated,
AccessPolicyOperation.Update);
return new ServiceAccountGrantedPoliciesUpdates
{
OrganizationId = OrganizationId,
ServiceAccountId = ServiceAccountId,
ProjectGrantedPolicyUpdates =
policiesToBeDeleted.Concat(policiesToBeCreated).Concat(policiesToBeUpdated)
};
}
private static List<ServiceAccountProjectAccessPolicyUpdate> CreatePolicyUpdates(
IEnumerable<ServiceAccountProjectAccessPolicy> policies, List<Guid> projectIds,
AccessPolicyOperation operation) =>
policies
.Where(ap => projectIds.Contains(ap.GrantedProjectId!.Value))
.Select(ap => new ServiceAccountProjectAccessPolicyUpdate { Operation = operation, AccessPolicy = ap })
.ToList();
private List<Guid> GetProjectIdsToBeUpdated(ServiceAccountGrantedPolicies requested) =>
ProjectGrantedPolicies
.Where(currentAp => requested.ProjectGrantedPolicies.Any(requestedAp =>
requestedAp.GrantedProjectId == currentAp.GrantedProjectId &&
requestedAp.ServiceAccountId == currentAp.ServiceAccountId &&
(requestedAp.Write != currentAp.Write || requestedAp.Read != currentAp.Read)))
.Select(ap => ap.GrantedProjectId!.Value)
.ToList();
}
public class ServiceAccountGrantedPoliciesUpdates
{
public Guid ServiceAccountId { get; set; }
public Guid OrganizationId { get; set; }
public IEnumerable<ServiceAccountProjectAccessPolicyUpdate> ProjectGrantedPolicyUpdates { get; set; } =
new List<ServiceAccountProjectAccessPolicyUpdate>();
}

View File

@ -0,0 +1,17 @@
#nullable enable
using Bit.Core.SecretsManager.Entities;
namespace Bit.Core.SecretsManager.Models.Data;
public class ServiceAccountGrantedPoliciesPermissionDetails
{
public Guid ServiceAccountId { get; set; }
public Guid OrganizationId { get; set; }
public required IEnumerable<ServiceAccountProjectAccessPolicyPermissionDetails> ProjectGrantedPolicies { get; set; }
}
public class ServiceAccountProjectAccessPolicyPermissionDetails
{
public required ServiceAccountProjectAccessPolicy AccessPolicy { get; set; }
public bool HasPermission { get; set; }
}

View File

@ -0,0 +1,9 @@
#nullable enable
using Bit.Core.SecretsManager.Models.Data;
namespace Bit.Core.SecretsManager.Queries.AccessPolicies.Interfaces;
public interface IServiceAccountGrantedPolicyUpdatesQuery
{
Task<ServiceAccountGrantedPoliciesUpdates> GetAsync(ServiceAccountGrantedPolicies grantedPolicies);
}

View File

@ -11,8 +11,6 @@ public interface IAccessPolicyRepository
Task<bool> AccessPolicyExists(BaseAccessPolicy baseAccessPolicy);
Task<BaseAccessPolicy?> GetByIdAsync(Guid id);
Task<IEnumerable<BaseAccessPolicy>> GetManyByGrantedProjectIdAsync(Guid id, Guid userId);
Task<IEnumerable<BaseAccessPolicy>> GetManyByServiceAccountIdAsync(Guid id, Guid userId,
AccessClientType accessType);
Task ReplaceAsync(BaseAccessPolicy baseAccessPolicy);
Task DeleteAsync(Guid id);
Task<IEnumerable<BaseAccessPolicy>> GetPeoplePoliciesByGrantedProjectIdAsync(Guid id, Guid userId);
@ -20,4 +18,8 @@ public interface IAccessPolicyRepository
Task<PeopleGrantees> GetPeopleGranteesAsync(Guid organizationId, Guid currentUserId);
Task<IEnumerable<BaseAccessPolicy>> GetPeoplePoliciesByGrantedServiceAccountIdAsync(Guid id, Guid userId);
Task<IEnumerable<BaseAccessPolicy>> ReplaceServiceAccountPeopleAsync(ServiceAccountPeopleAccessPolicies peopleAccessPolicies, Guid userId);
Task<ServiceAccountGrantedPolicies?> GetServiceAccountGrantedPoliciesAsync(Guid serviceAccountId);
Task<ServiceAccountGrantedPoliciesPermissionDetails?> GetServiceAccountGrantedPoliciesPermissionDetailsAsync(
Guid serviceAccountId, Guid userId, AccessClientType accessClientType);
Task UpdateServiceAccountGrantedPoliciesAsync(ServiceAccountGrantedPoliciesUpdates policyUpdates);
}

View File

@ -17,4 +17,6 @@ public interface IProjectRepository
Task<(bool Read, bool Write)> AccessToProjectAsync(Guid id, Guid userId, AccessClientType accessType);
Task<bool> ProjectsAreInOrganization(List<Guid> projectIds, Guid organizationId);
Task<int> GetProjectCountByOrganizationIdAsync(Guid organizationId);
Task<Dictionary<Guid, (bool Read, bool Write)>> AccessToProjectsAsync(IEnumerable<Guid> projectIds, Guid userId,
AccessClientType accessType);
}

View File

@ -62,4 +62,10 @@ public class NoopProjectRepository : IProjectRepository
{
return Task.FromResult(0);
}
public Task<Dictionary<Guid, (bool Read, bool Write)>> AccessToProjectsAsync(IEnumerable<Guid> projectIds,
Guid userId, AccessClientType accessType)
{
return Task.FromResult(null as Dictionary<Guid, (bool Read, bool Write)>);
}
}

View File

@ -623,210 +623,6 @@ public class AccessPoliciesControllerTests : IClassFixture<ApiApplicationFactory
Assert.Equal(project.Id, result.Data.First(x => x.Id == project.Id).Id);
}
[Theory]
[InlineData(false, false, false)]
[InlineData(false, false, true)]
[InlineData(false, true, false)]
[InlineData(false, true, true)]
[InlineData(true, false, false)]
[InlineData(true, false, true)]
[InlineData(true, true, false)]
public async Task CreateServiceAccountGrantedPolicies_SmAccessDenied_NotFound(bool useSecrets, bool accessSecrets, bool organizationEnabled)
{
var (org, _) = await _organizationHelper.Initialize(useSecrets, accessSecrets, organizationEnabled);
await _loginHelper.LoginAsync(_email);
var serviceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount
{
OrganizationId = org.Id,
Name = _mockEncryptedString,
});
var request = new List<GrantedAccessPolicyRequest> { new() { GrantedId = new Guid() } };
var response =
await _client.PostAsJsonAsync($"/service-accounts/{serviceAccount.Id}/granted-policies", request);
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Fact]
public async Task CreateServiceAccountGrantedPolicies_NoPermission()
{
// Create a new account as a user
var (org, _) = await _organizationHelper.Initialize(true, true, true);
var (email, _) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);
await _loginHelper.LoginAsync(email);
var serviceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount
{
OrganizationId = org.Id,
Name = _mockEncryptedString,
});
var project = await _projectRepository.CreateAsync(new Project
{
OrganizationId = org.Id,
Name = _mockEncryptedString,
});
var request =
new List<GrantedAccessPolicyRequest> { new() { GrantedId = project.Id, Read = true, Write = true } };
var response =
await _client.PostAsJsonAsync($"/service-accounts/{serviceAccount.Id}/granted-policies", request);
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Theory]
[InlineData(PermissionType.RunAsAdmin)]
[InlineData(PermissionType.RunAsUserWithPermission)]
public async Task CreateServiceAccountGrantedPolicies_MismatchedOrgId_NotFound(PermissionType permissionType)
{
var (org, _) = await _organizationHelper.Initialize(true, true, true);
await _loginHelper.LoginAsync(_email);
var (projectId, serviceAccountId) = await CreateProjectAndServiceAccountAsync(org.Id, true);
await SetupProjectAndServiceAccountPermissionAsync(permissionType, projectId, serviceAccountId);
var request =
new List<GrantedAccessPolicyRequest> { new() { GrantedId = projectId, Read = true, Write = true } };
var response =
await _client.PostAsJsonAsync($"/service-accounts/{serviceAccountId}/granted-policies", request);
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Theory]
[InlineData(PermissionType.RunAsAdmin)]
[InlineData(PermissionType.RunAsUserWithPermission)]
public async Task CreateServiceAccountGrantedPolicies_Success(PermissionType permissionType)
{
var (org, _) = await _organizationHelper.Initialize(true, true, true);
await _loginHelper.LoginAsync(_email);
var (projectId, serviceAccountId) = await CreateProjectAndServiceAccountAsync(org.Id);
await SetupProjectAndServiceAccountPermissionAsync(permissionType, projectId, serviceAccountId);
var request =
new List<GrantedAccessPolicyRequest> { new() { GrantedId = projectId, Read = true, Write = true } };
var response =
await _client.PostAsJsonAsync($"/service-accounts/{serviceAccountId}/granted-policies", request);
response.EnsureSuccessStatusCode();
var result = await response.Content
.ReadFromJsonAsync<ListResponseModel<ServiceAccountProjectAccessPolicyResponseModel>>();
Assert.NotNull(result);
Assert.NotEmpty(result.Data);
Assert.Equal(projectId, result.Data.First().GrantedProjectId);
var createdAccessPolicy =
await _accessPolicyRepository.GetByIdAsync(result.Data.First().Id);
Assert.NotNull(createdAccessPolicy);
Assert.Equal(result.Data.First().Read, createdAccessPolicy.Read);
Assert.Equal(result.Data.First().Write, createdAccessPolicy.Write);
Assert.Equal(result.Data.First().Id, createdAccessPolicy.Id);
AssertHelper.AssertRecent(createdAccessPolicy.CreationDate);
AssertHelper.AssertRecent(createdAccessPolicy.RevisionDate);
}
[Theory]
[InlineData(false, false, false)]
[InlineData(false, false, true)]
[InlineData(false, true, false)]
[InlineData(false, true, true)]
[InlineData(true, false, false)]
[InlineData(true, false, true)]
[InlineData(true, true, false)]
public async Task GetServiceAccountGrantedPolicies_SmAccessDenied_NotFound(bool useSecrets, bool accessSecrets, bool organizationEnabled)
{
var (org, _) = await _organizationHelper.Initialize(useSecrets, accessSecrets, organizationEnabled);
await _loginHelper.LoginAsync(_email);
var initData = await SetupAccessPolicyRequest(org.Id);
var response = await _client.GetAsync($"/service-accounts/{initData.ServiceAccountId}/granted-policies");
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Fact]
public async Task GetServiceAccountGrantedPolicies_ReturnsEmpty()
{
var (org, _) = await _organizationHelper.Initialize(true, true, true);
await _loginHelper.LoginAsync(_email);
var serviceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount
{
OrganizationId = org.Id,
Name = _mockEncryptedString,
});
var response = await _client.GetAsync($"/service-accounts/{serviceAccount.Id}/granted-policies");
response.EnsureSuccessStatusCode();
var result = await response.Content
.ReadFromJsonAsync<ListResponseModel<ServiceAccountProjectAccessPolicyResponseModel>>();
Assert.NotNull(result);
Assert.Empty(result.Data);
}
[Fact]
public async Task GetServiceAccountGrantedPolicies_NoPermission_ReturnsEmpty()
{
// Create a new account as a user
await _organizationHelper.Initialize(true, true, true);
var (email, orgUser) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);
await _loginHelper.LoginAsync(email);
var initData = await SetupAccessPolicyRequest(orgUser.OrganizationId);
var response = await _client.GetAsync($"/service-accounts/{initData.ServiceAccountId}/granted-policies");
var result = await response.Content
.ReadFromJsonAsync<ListResponseModel<ServiceAccountProjectAccessPolicyResponseModel>>();
Assert.NotNull(result);
Assert.Empty(result.Data);
}
[Theory]
[InlineData(PermissionType.RunAsAdmin)]
[InlineData(PermissionType.RunAsUserWithPermission)]
public async Task GetServiceAccountGrantedPolicies(PermissionType permissionType)
{
var (org, _) = await _organizationHelper.Initialize(true, true, true);
await _loginHelper.LoginAsync(_email);
var initData = await SetupAccessPolicyRequest(org.Id);
if (permissionType == PermissionType.RunAsUserWithPermission)
{
var (email, orgUser) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);
await _loginHelper.LoginAsync(email);
var accessPolicies = new List<BaseAccessPolicy>
{
new UserProjectAccessPolicy
{
GrantedProjectId = initData.ProjectId, OrganizationUserId = orgUser.Id, Read = true, Write = true,
},
};
await _accessPolicyRepository.CreateManyAsync(accessPolicies);
}
var response = await _client.GetAsync($"/service-accounts/{initData.ServiceAccountId}/granted-policies");
response.EnsureSuccessStatusCode();
var result = await response.Content
.ReadFromJsonAsync<ListResponseModel<ServiceAccountProjectAccessPolicyResponseModel>>();
Assert.NotNull(result?.Data);
Assert.NotEmpty(result.Data);
Assert.Equal(initData.ServiceAccountId, result.Data.First().ServiceAccountId);
Assert.NotNull(result.Data.First().ServiceAccountName);
Assert.NotNull(result.Data.First().GrantedProjectName);
}
[Theory]
[InlineData(false, false, false)]
[InlineData(false, false, true)]
@ -1090,12 +886,16 @@ public class AccessPoliciesControllerTests : IClassFixture<ApiApplicationFactory
}
[Theory]
[InlineData(false, false)]
[InlineData(true, false)]
[InlineData(false, true)]
public async Task PutServiceAccountPeopleAccessPolicies_SmNotEnabled_NotFound(bool useSecrets, bool accessSecrets)
[InlineData(false, false, false)]
[InlineData(false, false, true)]
[InlineData(false, true, false)]
[InlineData(false, true, true)]
[InlineData(true, false, false)]
[InlineData(true, false, true)]
[InlineData(true, true, false)]
public async Task PutServiceAccountPeopleAccessPolicies_SmNotEnabled_NotFound(bool useSecrets, bool accessSecrets, bool organizationEnabled)
{
var (_, organizationUser) = await _organizationHelper.Initialize(useSecrets, accessSecrets, true);
var (_, organizationUser) = await _organizationHelper.Initialize(useSecrets, accessSecrets, organizationEnabled);
await _loginHelper.LoginAsync(_email);
var (serviceAccount, request) = await SetupServiceAccountPeopleRequestAsync(PermissionType.RunAsAdmin, organizationUser);
@ -1185,6 +985,190 @@ public class AccessPoliciesControllerTests : IClassFixture<ApiApplicationFactory
Assert.Equal(result.UserAccessPolicies.First().Id, createdAccessPolicy.Id);
}
[Theory]
[InlineData(false, false, false)]
[InlineData(false, false, true)]
[InlineData(false, true, false)]
[InlineData(false, true, true)]
[InlineData(true, false, false)]
[InlineData(true, false, true)]
[InlineData(true, true, false)]
public async Task GetServiceAccountGrantedPoliciesAsync_SmAccessDenied_ReturnsNotFound(bool useSecrets, bool accessSecrets, bool organizationEnabled)
{
var (org, _) = await _organizationHelper.Initialize(useSecrets, accessSecrets, organizationEnabled);
await _loginHelper.LoginAsync(_email);
var initData = await SetupAccessPolicyRequest(org.Id);
var response = await _client.GetAsync($"/service-accounts/{initData.ServiceAccountId}/granted-policies");
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Fact]
public async Task GetServiceAccountGrantedPoliciesAsync_NoAccessPolicies_ReturnsEmpty()
{
var (org, _) = await _organizationHelper.Initialize(true, true, true);
await _loginHelper.LoginAsync(_email);
var serviceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount
{
OrganizationId = org.Id,
Name = _mockEncryptedString,
});
var response = await _client.GetAsync($"/service-accounts/{serviceAccount.Id}/granted-policies");
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<ServiceAccountGrantedPoliciesPermissionDetailsResponseModel>();
Assert.NotNull(result);
Assert.Empty(result.GrantedProjectPolicies);
}
[Fact]
public async Task GetServiceAccountGrantedPoliciesAsync_UserDoesntHavePermission_ReturnsNotFound()
{
// Create a new account as a user
await _organizationHelper.Initialize(true, true, true);
var (email, orgUser) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);
await _loginHelper.LoginAsync(email);
var initData = await SetupAccessPolicyRequest(orgUser.OrganizationId);
var response = await _client.GetAsync($"/service-accounts/{initData.ServiceAccountId}/granted-policies");
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Theory]
[InlineData(PermissionType.RunAsAdmin)]
[InlineData(PermissionType.RunAsUserWithPermission)]
public async Task GetServiceAccountGrantedPoliciesAsync_Success(PermissionType permissionType)
{
var (org, _) = await _organizationHelper.Initialize(true, true, true);
await _loginHelper.LoginAsync(_email);
var initData = await SetupAccessPolicyRequest(org.Id);
if (permissionType == PermissionType.RunAsUserWithPermission)
{
var (email, orgUser) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);
await _loginHelper.LoginAsync(email);
var accessPolicies = new List<BaseAccessPolicy>
{
new UserServiceAccountAccessPolicy
{
GrantedServiceAccountId = initData.ServiceAccountId, OrganizationUserId = orgUser.Id, Read = true, Write = true,
}
};
await _accessPolicyRepository.CreateManyAsync(accessPolicies);
}
var response = await _client.GetAsync($"/service-accounts/{initData.ServiceAccountId}/granted-policies");
response.EnsureSuccessStatusCode();
var result = await response.Content
.ReadFromJsonAsync<ServiceAccountGrantedPoliciesPermissionDetailsResponseModel>();
Assert.NotNull(result);
Assert.NotEmpty(result.GrantedProjectPolicies);
Assert.Equal(initData.ServiceAccountId, result.GrantedProjectPolicies.First().AccessPolicy.ServiceAccountId);
Assert.NotNull(result.GrantedProjectPolicies.First().AccessPolicy.ServiceAccountName);
Assert.NotNull(result.GrantedProjectPolicies.First().AccessPolicy.GrantedProjectName);
}
[Theory]
[InlineData(false, false, false)]
[InlineData(false, false, true)]
[InlineData(false, true, false)]
[InlineData(false, true, true)]
[InlineData(true, false, false)]
[InlineData(true, false, true)]
[InlineData(true, true, false)]
public async Task PutServiceAccountGrantedPoliciesAsync_SmNotEnabled_NotFound(bool useSecrets, bool accessSecrets, bool organizationEnabled)
{
var (_, organizationUser) = await _organizationHelper.Initialize(useSecrets, accessSecrets, organizationEnabled);
await _loginHelper.LoginAsync(_email);
var (serviceAccount, request) = await SetupServiceAccountGrantedPoliciesRequestAsync(PermissionType.RunAsAdmin, organizationUser, false);
var response = await _client.PutAsJsonAsync($"/service-accounts/{serviceAccount.Id}/granted-policies", request);
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Fact]
public async Task PutServiceAccountGrantedPoliciesAsync_UserHasNoPermission_ReturnsNotFound()
{
var (org, _) = await _organizationHelper.Initialize(true, true, true);
var (email, _) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);
await _loginHelper.LoginAsync(email);
var (projectId, serviceAccountId) = await CreateProjectAndServiceAccountAsync(org.Id);
var request = new ServiceAccountGrantedPoliciesRequestModel
{
ProjectGrantedPolicyRequests = new List<GrantedAccessPolicyRequest>
{
new() { GrantedId = projectId, Read = true, Write = true }
}
};
var response = await _client.PutAsJsonAsync($"/service-accounts/{serviceAccountId}/granted-policies", request);
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Theory]
[InlineData(PermissionType.RunAsAdmin)]
[InlineData(PermissionType.RunAsUserWithPermission)]
public async Task PutServiceAccountGrantedPoliciesAsync_MismatchedOrgIds_ReturnsNotFound(PermissionType permissionType)
{
var (_, organizationUser) = await _organizationHelper.Initialize(true, true, true);
await _loginHelper.LoginAsync(_email);
var (serviceAccount, request) = await SetupServiceAccountGrantedPoliciesRequestAsync(permissionType, organizationUser, false);
var newOrg = await _organizationHelper.CreateSmOrganizationAsync();
var project = await _projectRepository.CreateAsync(new Project
{
Name = _mockEncryptedString,
OrganizationId = newOrg.Id
});
request.ProjectGrantedPolicyRequests = new List<GrantedAccessPolicyRequest>
{
new() { GrantedId = project.Id, Read = true, Write = true }
};
var response = await _client.PutAsJsonAsync($"/service-accounts/{serviceAccount.Id}/granted-policies", request);
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Theory]
[InlineData(PermissionType.RunAsAdmin, false)]
[InlineData(PermissionType.RunAsAdmin, true)]
[InlineData(PermissionType.RunAsUserWithPermission, false)]
[InlineData(PermissionType.RunAsUserWithPermission, true)]
public async Task PutServiceAccountGrantedPoliciesAsync_Success(PermissionType permissionType, bool createPreviousAccessPolicy)
{
var (_, organizationUser) = await _organizationHelper.Initialize(true, true, true);
await _loginHelper.LoginAsync(_email);
var (serviceAccount, request) = await SetupServiceAccountGrantedPoliciesRequestAsync(permissionType, organizationUser, createPreviousAccessPolicy);
var response = await _client.PutAsJsonAsync($"/service-accounts/{serviceAccount.Id}/granted-policies", request);
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<ServiceAccountGrantedPoliciesPermissionDetailsResponseModel>();
Assert.NotNull(result);
Assert.Equal(request.ProjectGrantedPolicyRequests.First().GrantedId,
result.GrantedProjectPolicies.First().AccessPolicy.GrantedProjectId);
Assert.True(result.GrantedProjectPolicies.First().AccessPolicy.Read);
Assert.True(result.GrantedProjectPolicies.First().AccessPolicy.Write);
Assert.True(result.GrantedProjectPolicies.First().HasPermission);
Assert.Single(result.GrantedProjectPolicies);
}
private async Task<RequestSetupData> SetupAccessPolicyRequest(Guid organizationId)
{
var project = await _projectRepository.CreateAsync(new Project
@ -1275,6 +1259,7 @@ public class AccessPoliciesControllerTests : IClassFixture<ApiApplicationFactory
Write = true
}
};
await _accessPolicyRepository.CreateManyAsync(accessPolicies);
return (serviceAccount, organizationUser);
@ -1357,6 +1342,59 @@ public class AccessPoliciesControllerTests : IClassFixture<ApiApplicationFactory
}
}
private async Task<(ServiceAccount serviceAccount, ServiceAccountGrantedPoliciesRequestModel request)> SetupServiceAccountGrantedPoliciesRequestAsync(
PermissionType permissionType, OrganizationUser organizationUser, bool createPreviousAccessPolicy)
{
var (serviceAccount, currentUser) = await SetupServiceAccountPeoplePermissionAsync(permissionType, organizationUser);
var project = await _projectRepository.CreateAsync(new Project
{
Name = _mockEncryptedString,
OrganizationId = organizationUser.OrganizationId
});
var accessPolicies = new List<BaseAccessPolicy>
{
new UserProjectAccessPolicy
{
GrantedProjectId = project.Id, OrganizationUserId = currentUser.Id, Read = true, Write = true,
},
};
if (createPreviousAccessPolicy)
{
var anotherProject = await _projectRepository.CreateAsync(new Project
{
Name = _mockEncryptedString,
OrganizationId = organizationUser.OrganizationId
});
accessPolicies.Add(new UserProjectAccessPolicy
{
GrantedProjectId = anotherProject.Id,
OrganizationUserId = currentUser.Id,
Read = true,
Write = true,
});
accessPolicies.Add(new ServiceAccountProjectAccessPolicy
{
GrantedProjectId = anotherProject.Id,
ServiceAccountId = serviceAccount.Id,
Read = true,
Write = true,
});
}
await _accessPolicyRepository.CreateManyAsync(accessPolicies);
var request = new ServiceAccountGrantedPoliciesRequestModel
{
ProjectGrantedPolicyRequests = new List<GrantedAccessPolicyRequest>
{
new() { GrantedId = project.Id, Read = true, Write = true }
}
};
return (serviceAccount, request);
}
private class RequestSetupData
{
public Guid ProjectId { get; set; }

View File

@ -1,4 +1,5 @@
using System.Security.Claims;
#nullable enable
using System.Security.Claims;
using Bit.Api.SecretsManager.Controllers;
using Bit.Api.SecretsManager.Models.Request;
using Bit.Api.Test.SecretsManager.Enums;
@ -8,6 +9,7 @@ using Bit.Core.Exceptions;
using Bit.Core.SecretsManager.Commands.AccessPolicies.Interfaces;
using Bit.Core.SecretsManager.Entities;
using Bit.Core.SecretsManager.Models.Data;
using Bit.Core.SecretsManager.Queries.Interfaces;
using Bit.Core.SecretsManager.Repositories;
using Bit.Core.Services;
using Bit.Core.Test.SecretsManager.AutoFixture.ProjectsFixture;
@ -29,83 +31,6 @@ public class AccessPoliciesControllerTests
{
private const int _overMax = 16;
private static AccessPoliciesCreateRequest AddRequestsOverMax(AccessPoliciesCreateRequest request)
{
var newRequests = new List<AccessPolicyRequest>();
for (var i = 0; i < _overMax; i++)
{
newRequests.Add(new AccessPolicyRequest { GranteeId = new Guid(), Read = true, Write = true });
}
request.UserAccessPolicyRequests = newRequests;
return request;
}
private static List<GrantedAccessPolicyRequest> AddRequestsOverMax(List<GrantedAccessPolicyRequest> request)
{
for (var i = 0; i < _overMax; i++)
{
request.Add(new GrantedAccessPolicyRequest { GrantedId = new Guid() });
}
return request;
}
private static PeopleAccessPoliciesRequestModel SetRequestToCanReadWrite(PeopleAccessPoliciesRequestModel request)
{
foreach (var ap in request.UserAccessPolicyRequests)
{
ap.Read = true;
ap.Write = true;
}
foreach (var ap in request.GroupAccessPolicyRequests)
{
ap.Read = true;
ap.Write = true;
}
return request;
}
private static void SetupAdmin(SutProvider<AccessPoliciesController> sutProvider, Guid organizationId)
{
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(default).ReturnsForAnyArgs(true);
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(Guid.NewGuid());
sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(organizationId).Returns(true);
}
private static void SetupUserWithPermission(SutProvider<AccessPoliciesController> sutProvider, Guid organizationId)
{
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(default).ReturnsForAnyArgs(true);
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(Guid.NewGuid());
sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(organizationId).Returns(false);
sutProvider.GetDependency<ICurrentContext>().OrganizationUser(default).ReturnsForAnyArgs(true);
}
private static void SetupUserWithoutPermission(SutProvider<AccessPoliciesController> sutProvider,
Guid organizationId)
{
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(default).ReturnsForAnyArgs(true);
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(Guid.NewGuid());
sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(organizationId).Returns(false);
sutProvider.GetDependency<ICurrentContext>().OrganizationUser(default).ReturnsForAnyArgs(true);
}
private static void SetupPermission(SutProvider<AccessPoliciesController> sutProvider,
PermissionType permissionType, Guid orgId)
{
switch (permissionType)
{
case PermissionType.RunAsAdmin:
SetupAdmin(sutProvider, orgId);
break;
case PermissionType.RunAsUserWithPermission:
SetupUserWithPermission(sutProvider, orgId);
break;
}
}
[Theory]
[BitAutoData(PermissionType.RunAsAdmin)]
[BitAutoData(PermissionType.RunAsUserWithPermission)]
@ -222,71 +147,6 @@ public class AccessPoliciesControllerTests
.GetManyByGrantedProjectIdAsync(Arg.Any<Guid>(), Arg.Any<Guid>());
}
[Theory]
[BitAutoData(PermissionType.RunAsAdmin)]
[BitAutoData(PermissionType.RunAsUserWithPermission)]
public async Task GetServiceAccountGrantedPolicies_ReturnsEmptyList(
PermissionType permissionType,
SutProvider<AccessPoliciesController> sutProvider,
Guid id, ServiceAccount data)
{
sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(data.Id).ReturnsForAnyArgs(data);
switch (permissionType)
{
case PermissionType.RunAsAdmin:
SetupAdmin(sutProvider, data.OrganizationId);
break;
case PermissionType.RunAsUserWithPermission:
SetupUserWithPermission(sutProvider, data.OrganizationId);
sutProvider.GetDependency<IServiceAccountRepository>()
.UserHasWriteAccessToServiceAccount(default, default)
.ReturnsForAnyArgs(true);
break;
}
var result = await sutProvider.Sut.GetServiceAccountGrantedPoliciesAsync(id);
await sutProvider.GetDependency<IAccessPolicyRepository>().Received(1)
.GetManyByServiceAccountIdAsync(Arg.Is(AssertHelper.AssertPropertyEqual(id)), Arg.Any<Guid>(),
Arg.Any<AccessClientType>());
Assert.Empty(result.Data);
}
[Theory]
[BitAutoData(PermissionType.RunAsAdmin)]
[BitAutoData(PermissionType.RunAsUserWithPermission)]
public async Task GetServiceAccountGrantedPolicies_Success(
PermissionType permissionType,
SutProvider<AccessPoliciesController> sutProvider,
Guid id,
ServiceAccount data,
ServiceAccountProjectAccessPolicy resultAccessPolicy)
{
sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(default).ReturnsForAnyArgs(data);
switch (permissionType)
{
case PermissionType.RunAsAdmin:
SetupAdmin(sutProvider, data.OrganizationId);
break;
case PermissionType.RunAsUserWithPermission:
SetupUserWithPermission(sutProvider, data.OrganizationId);
break;
}
sutProvider.GetDependency<IAccessPolicyRepository>().GetManyByServiceAccountIdAsync(default, default, default)
.ReturnsForAnyArgs(new List<BaseAccessPolicy> { resultAccessPolicy });
var result = await sutProvider.Sut.GetServiceAccountGrantedPoliciesAsync(id);
await sutProvider.GetDependency<IAccessPolicyRepository>().Received(1)
.GetManyByServiceAccountIdAsync(Arg.Is(AssertHelper.AssertPropertyEqual(id)), Arg.Any<Guid>(),
Arg.Any<AccessClientType>());
Assert.NotEmpty(result.Data);
}
[Theory]
[BitAutoData]
public async Task CreateProjectAccessPolicies_RequestMoreThanMax_Throws(
@ -403,121 +263,6 @@ public class AccessPoliciesControllerTests
.CreateManyAsync(Arg.Any<List<BaseAccessPolicy>>());
}
[Theory]
[BitAutoData]
public async Task CreateServiceAccountGrantedPolicies_RequestMoreThanMax_Throws(
SutProvider<AccessPoliciesController> sutProvider,
Guid id,
ServiceAccount serviceAccount,
ServiceAccountProjectAccessPolicy data,
List<GrantedAccessPolicyRequest> request)
{
sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(default).ReturnsForAnyArgs(serviceAccount);
sutProvider.GetDependency<ICreateAccessPoliciesCommand>()
.CreateManyAsync(default)
.ReturnsForAnyArgs(new List<BaseAccessPolicy> { data });
request = AddRequestsOverMax(request);
await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.CreateServiceAccountGrantedPoliciesAsync(id, request));
await sutProvider.GetDependency<ICreateAccessPoliciesCommand>().DidNotReceiveWithAnyArgs()
.CreateManyAsync(Arg.Any<List<BaseAccessPolicy>>());
}
[Theory]
[BitAutoData]
public async Task CreateServiceAccountGrantedPolicies_ServiceAccountDoesNotExist_Throws(
SutProvider<AccessPoliciesController> sutProvider,
Guid id,
List<GrantedAccessPolicyRequest> request)
{
await Assert.ThrowsAsync<NotFoundException>(() =>
sutProvider.Sut.CreateServiceAccountGrantedPoliciesAsync(id, request));
await sutProvider.GetDependency<ICreateAccessPoliciesCommand>().DidNotReceiveWithAnyArgs()
.CreateManyAsync(Arg.Any<List<BaseAccessPolicy>>());
}
[Theory]
[BitAutoData]
public async Task CreateServiceAccountGrantedPolicies_DuplicatePolicy_Throws(
SutProvider<AccessPoliciesController> sutProvider,
Guid id,
ServiceAccount serviceAccount,
ServiceAccountProjectAccessPolicy data,
List<GrantedAccessPolicyRequest> request)
{
var dup = new GrantedAccessPolicyRequest { GrantedId = Guid.NewGuid(), Read = true, Write = true };
request.Add(dup);
request.Add(dup);
sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(default).ReturnsForAnyArgs(serviceAccount);
sutProvider.GetDependency<ICreateAccessPoliciesCommand>()
.CreateManyAsync(default)
.ReturnsForAnyArgs(new List<BaseAccessPolicy> { data });
await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.CreateServiceAccountGrantedPoliciesAsync(id, request));
await sutProvider.GetDependency<ICreateAccessPoliciesCommand>().DidNotReceiveWithAnyArgs()
.CreateManyAsync(Arg.Any<List<BaseAccessPolicy>>());
}
[Theory]
[BitAutoData]
public async Task CreateServiceAccountGrantedPolicies_NoAccess_Throws(
SutProvider<AccessPoliciesController> sutProvider,
Guid id,
ServiceAccount serviceAccount,
ServiceAccountProjectAccessPolicy data,
List<GrantedAccessPolicyRequest> request)
{
sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(default).ReturnsForAnyArgs(serviceAccount);
sutProvider.GetDependency<ICreateAccessPoliciesCommand>()
.CreateManyAsync(default)
.ReturnsForAnyArgs(new List<BaseAccessPolicy> { data });
foreach (var policy in request)
{
sutProvider.GetDependency<IAuthorizationService>()
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), policy,
Arg.Any<IEnumerable<IAuthorizationRequirement>>()).ReturnsForAnyArgs(AuthorizationResult.Failed());
}
await Assert.ThrowsAsync<NotFoundException>(() =>
sutProvider.Sut.CreateServiceAccountGrantedPoliciesAsync(id, request));
await sutProvider.GetDependency<ICreateAccessPoliciesCommand>().DidNotReceiveWithAnyArgs()
.CreateManyAsync(Arg.Any<List<BaseAccessPolicy>>());
}
[Theory]
[BitAutoData]
public async Task CreateServiceAccountGrantedPolicies_Success(
SutProvider<AccessPoliciesController> sutProvider,
Guid id,
ServiceAccount serviceAccount,
ServiceAccountProjectAccessPolicy data,
List<GrantedAccessPolicyRequest> request)
{
sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(default).ReturnsForAnyArgs(serviceAccount);
sutProvider.GetDependency<ICreateAccessPoliciesCommand>()
.CreateManyAsync(default)
.ReturnsForAnyArgs(new List<BaseAccessPolicy> { data });
foreach (var policy in request)
{
sutProvider.GetDependency<IAuthorizationService>()
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), policy,
Arg.Any<IEnumerable<IAuthorizationRequirement>>()).ReturnsForAnyArgs(AuthorizationResult.Success());
}
await sutProvider.Sut.CreateServiceAccountGrantedPoliciesAsync(id, request);
await sutProvider.GetDependency<ICreateAccessPoliciesCommand>().Received(1)
.CreateManyAsync(Arg.Any<List<BaseAccessPolicy>>());
}
[Theory]
[BitAutoData]
public async Task UpdateAccessPolicies_NoAccess_Throws(
@ -1165,4 +910,262 @@ public class AccessPoliciesControllerTests
await sutProvider.GetDependency<IAccessPolicyRepository>().Received(1)
.ReplaceServiceAccountPeopleAsync(Arg.Any<ServiceAccountPeopleAccessPolicies>(), Arg.Any<Guid>());
}
[Theory]
[BitAutoData]
public async Task GetServiceAccountGrantedPoliciesAsync_NoAccess_ThrowsNotFound(
SutProvider<AccessPoliciesController> sutProvider,
ServiceAccount data)
{
sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(data.Id).Returns(data);
sutProvider.GetDependency<IAuthorizationService>()
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), data,
Arg.Any<IEnumerable<IAuthorizationRequirement>>()).ReturnsForAnyArgs(AuthorizationResult.Failed());
await Assert.ThrowsAsync<NotFoundException>(() =>
sutProvider.Sut.GetServiceAccountGrantedPoliciesAsync(data.Id));
await sutProvider.GetDependency<IAccessPolicyRepository>().Received(0)
.GetServiceAccountGrantedPoliciesPermissionDetailsAsync(Arg.Any<Guid>(), Arg.Any<Guid>(),
Arg.Any<AccessClientType>());
}
[Theory]
[BitAutoData(AccessClientType.NoAccessCheck)]
[BitAutoData(AccessClientType.User)]
public async Task GetServiceAccountGrantedPoliciesAsync_HasAccessNoPolicies_ReturnsEmptyList(
AccessClientType accessClientType,
SutProvider<AccessPoliciesController> sutProvider,
Guid userId,
ServiceAccount data)
{
sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(data.Id).Returns(data);
sutProvider.GetDependency<IAuthorizationService>()
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), data,
Arg.Any<IEnumerable<IAuthorizationRequirement>>()).ReturnsForAnyArgs(AuthorizationResult.Success());
sutProvider.GetDependency<IAccessClientQuery>()
.GetAccessClientAsync(Arg.Any<ClaimsPrincipal>(), data.OrganizationId).Returns((accessClientType, userId));
sutProvider.GetDependency<IAccessPolicyRepository>()
.GetServiceAccountGrantedPoliciesPermissionDetailsAsync(Arg.Any<Guid>(), Arg.Any<Guid>(),
Arg.Any<AccessClientType>())
.ReturnsNull();
var result = await sutProvider.Sut.GetServiceAccountGrantedPoliciesAsync(data.Id);
Assert.Empty(result.GrantedProjectPolicies);
}
[Theory]
[BitAutoData(AccessClientType.NoAccessCheck)]
[BitAutoData(AccessClientType.User)]
public async Task GetServiceAccountGrantedPoliciesAsync_HasAccess_Success(
AccessClientType accessClientType,
SutProvider<AccessPoliciesController> sutProvider,
Guid userId,
ServiceAccountGrantedPoliciesPermissionDetails policies,
ServiceAccount data)
{
sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(data.Id).Returns(data);
sutProvider.GetDependency<IAuthorizationService>()
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), data,
Arg.Any<IEnumerable<IAuthorizationRequirement>>()).ReturnsForAnyArgs(AuthorizationResult.Success());
sutProvider.GetDependency<IAccessClientQuery>()
.GetAccessClientAsync(Arg.Any<ClaimsPrincipal>(), data.OrganizationId).Returns((accessClientType, userId));
sutProvider.GetDependency<IAccessPolicyRepository>()
.GetServiceAccountGrantedPoliciesPermissionDetailsAsync(Arg.Any<Guid>(), Arg.Any<Guid>(),
Arg.Any<AccessClientType>())
.Returns(policies);
var result = await sutProvider.Sut.GetServiceAccountGrantedPoliciesAsync(data.Id);
Assert.NotEmpty(result.GrantedProjectPolicies);
Assert.Equal(policies.ProjectGrantedPolicies.Count(), result.GrantedProjectPolicies.Count);
}
[Theory]
[BitAutoData]
public async Task PutServiceAccountGrantedPoliciesAsync_ServiceAccountDoesNotExist_Throws(
SutProvider<AccessPoliciesController> sutProvider,
ServiceAccount data,
ServiceAccountGrantedPoliciesRequestModel request)
{
await Assert.ThrowsAsync<NotFoundException>(() =>
sutProvider.Sut.PutServiceAccountGrantedPoliciesAsync(data.Id, request));
await sutProvider.GetDependency<IUpdateServiceAccountGrantedPoliciesCommand>().DidNotReceiveWithAnyArgs()
.UpdateAsync(Arg.Any<ServiceAccountGrantedPoliciesUpdates>());
}
[Theory]
[BitAutoData]
public async Task PutServiceAccountGrantedPoliciesAsync_DuplicatePolicyRequest_ThrowsBadRequestException(
SutProvider<AccessPoliciesController> sutProvider,
ServiceAccount data,
ServiceAccountGrantedPoliciesRequestModel request)
{
var dup = new GrantedAccessPolicyRequest { GrantedId = Guid.NewGuid(), Read = true, Write = true };
request.ProjectGrantedPolicyRequests = new[] { dup, dup };
sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(data.Id).ReturnsForAnyArgs(data);
await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.PutServiceAccountGrantedPoliciesAsync(data.Id, request));
await sutProvider.GetDependency<IUpdateServiceAccountGrantedPoliciesCommand>().DidNotReceiveWithAnyArgs()
.UpdateAsync(Arg.Any<ServiceAccountGrantedPoliciesUpdates>());
}
[Theory]
[BitAutoData]
public async Task PutServiceAccountGrantedPoliciesAsync_InvalidPolicyRequest_ThrowsBadRequestException(
SutProvider<AccessPoliciesController> sutProvider,
ServiceAccount data,
ServiceAccountGrantedPoliciesRequestModel request)
{
var policyRequest = new GrantedAccessPolicyRequest { GrantedId = Guid.NewGuid(), Read = false, Write = true };
request.ProjectGrantedPolicyRequests = new[] { policyRequest };
sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(data.Id).ReturnsForAnyArgs(data);
await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.PutServiceAccountGrantedPoliciesAsync(data.Id, request));
await sutProvider.GetDependency<IUpdateServiceAccountGrantedPoliciesCommand>().DidNotReceiveWithAnyArgs()
.UpdateAsync(Arg.Any<ServiceAccountGrantedPoliciesUpdates>());
}
[Theory]
[BitAutoData]
public async Task PutServiceAccountGrantedPoliciesAsync_UserHasNoAccess_ThrowsNotFoundException(
SutProvider<AccessPoliciesController> sutProvider,
ServiceAccount data,
ServiceAccountGrantedPoliciesRequestModel request)
{
request = SetupValidRequest(request);
sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(data.Id).ReturnsForAnyArgs(data);
sutProvider.GetDependency<IAuthorizationService>()
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), Arg.Any<ServiceAccountGrantedPoliciesUpdates>(),
Arg.Any<IEnumerable<IAuthorizationRequirement>>()).Returns(AuthorizationResult.Failed());
await Assert.ThrowsAsync<NotFoundException>(() =>
sutProvider.Sut.PutServiceAccountGrantedPoliciesAsync(data.Id, request));
await sutProvider.GetDependency<IUpdateServiceAccountGrantedPoliciesCommand>().DidNotReceiveWithAnyArgs()
.UpdateAsync(Arg.Any<ServiceAccountGrantedPoliciesUpdates>());
}
[Theory]
[BitAutoData]
public async Task PutServiceAccountGrantedPoliciesAsync_Success(
SutProvider<AccessPoliciesController> sutProvider,
ServiceAccount data,
ServiceAccountGrantedPoliciesRequestModel request)
{
request = SetupValidRequest(request);
sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(data.Id).ReturnsForAnyArgs(data);
sutProvider.GetDependency<IAuthorizationService>()
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), Arg.Any<ServiceAccountGrantedPoliciesUpdates>(),
Arg.Any<IEnumerable<IAuthorizationRequirement>>()).Returns(AuthorizationResult.Success());
await sutProvider.Sut.PutServiceAccountGrantedPoliciesAsync(data.Id, request);
await sutProvider.GetDependency<IUpdateServiceAccountGrantedPoliciesCommand>().Received(1)
.UpdateAsync(Arg.Any<ServiceAccountGrantedPoliciesUpdates>());
}
private static AccessPoliciesCreateRequest AddRequestsOverMax(AccessPoliciesCreateRequest request)
{
var newRequests = new List<AccessPolicyRequest>();
for (var i = 0; i < _overMax; i++)
{
newRequests.Add(new AccessPolicyRequest { GranteeId = new Guid(), Read = true, Write = true });
}
request.UserAccessPolicyRequests = newRequests;
return request;
}
private static List<GrantedAccessPolicyRequest> AddRequestsOverMax(List<GrantedAccessPolicyRequest> request)
{
for (var i = 0; i < _overMax; i++)
{
request.Add(new GrantedAccessPolicyRequest { GrantedId = new Guid() });
}
return request;
}
private static PeopleAccessPoliciesRequestModel SetRequestToCanReadWrite(PeopleAccessPoliciesRequestModel request)
{
foreach (var ap in request.UserAccessPolicyRequests)
{
ap.Read = true;
ap.Write = true;
}
foreach (var ap in request.GroupAccessPolicyRequests)
{
ap.Read = true;
ap.Write = true;
}
return request;
}
private static void SetupAdmin(SutProvider<AccessPoliciesController> sutProvider, Guid organizationId)
{
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(default).ReturnsForAnyArgs(true);
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(Guid.NewGuid());
sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(organizationId).Returns(true);
}
private static void SetupUserWithPermission(SutProvider<AccessPoliciesController> sutProvider, Guid organizationId)
{
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(default).ReturnsForAnyArgs(true);
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(Guid.NewGuid());
sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(organizationId).Returns(false);
sutProvider.GetDependency<ICurrentContext>().OrganizationUser(default).ReturnsForAnyArgs(true);
}
private static void SetupUserWithoutPermission(SutProvider<AccessPoliciesController> sutProvider,
Guid organizationId)
{
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(default).ReturnsForAnyArgs(true);
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(Guid.NewGuid());
sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(organizationId).Returns(false);
sutProvider.GetDependency<ICurrentContext>().OrganizationUser(default).ReturnsForAnyArgs(true);
}
private static void SetupPermission(SutProvider<AccessPoliciesController> sutProvider,
PermissionType permissionType, Guid orgId)
{
switch (permissionType)
{
case PermissionType.RunAsAdmin:
SetupAdmin(sutProvider, orgId);
break;
case PermissionType.RunAsUserWithPermission:
SetupUserWithPermission(sutProvider, orgId);
break;
}
}
private static ServiceAccountGrantedPoliciesRequestModel SetupValidRequest(ServiceAccountGrantedPoliciesRequestModel request)
{
foreach (var policyRequest in request.ProjectGrantedPolicyRequests)
{
policyRequest.Read = true;
}
return request;
}
}

View File

@ -0,0 +1,101 @@
#nullable enable
using Bit.Api.SecretsManager.Utilities;
using Bit.Core.Exceptions;
using Bit.Core.SecretsManager.Entities;
using Bit.Core.Test.SecretsManager.AutoFixture.ProjectsFixture;
using Bit.Core.Test.SecretsManager.AutoFixture.SecretsFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Xunit;
namespace Bit.Api.Test.SecretsManager.Utilities;
[ProjectCustomize]
[SecretCustomize]
public class AccessPolicyHelpersTests
{
[Theory]
[BitAutoData]
public void CheckForDistinctAccessPolicies_DuplicateAccessPolicies_ThrowsBadRequestException(
UserProjectAccessPolicy userProjectAccessPolicy, UserServiceAccountAccessPolicy userServiceAccountAccessPolicy,
GroupProjectAccessPolicy groupProjectAccessPolicy,
GroupServiceAccountAccessPolicy groupServiceAccountAccessPolicy,
ServiceAccountProjectAccessPolicy serviceAccountProjectAccessPolicy)
{
var accessPolicies = new List<BaseAccessPolicy>
{
userProjectAccessPolicy,
userProjectAccessPolicy,
userServiceAccountAccessPolicy,
userServiceAccountAccessPolicy,
groupProjectAccessPolicy,
groupProjectAccessPolicy,
groupServiceAccountAccessPolicy,
groupServiceAccountAccessPolicy,
serviceAccountProjectAccessPolicy,
serviceAccountProjectAccessPolicy
};
Assert.Throws<BadRequestException>(() =>
{
AccessPolicyHelpers.CheckForDistinctAccessPolicies(accessPolicies);
});
}
[Fact]
public void CheckForDistinctAccessPolicies_UnsupportedAccessPolicy_ThrowsArgumentException()
{
var accessPolicies = new List<BaseAccessPolicy> { new UnsupportedAccessPolicy() };
Assert.Throws<ArgumentException>(() => { AccessPolicyHelpers.CheckForDistinctAccessPolicies(accessPolicies); });
}
[Theory]
[BitAutoData]
public void CheckForDistinctAccessPolicies_DistinctPolicies_Success(UserProjectAccessPolicy userProjectAccessPolicy,
UserServiceAccountAccessPolicy userServiceAccountAccessPolicy,
GroupProjectAccessPolicy groupProjectAccessPolicy,
GroupServiceAccountAccessPolicy groupServiceAccountAccessPolicy,
ServiceAccountProjectAccessPolicy serviceAccountProjectAccessPolicy)
{
var accessPolicies = new List<BaseAccessPolicy>
{
userProjectAccessPolicy,
userServiceAccountAccessPolicy,
groupProjectAccessPolicy,
groupServiceAccountAccessPolicy,
serviceAccountProjectAccessPolicy
};
AccessPolicyHelpers.CheckForDistinctAccessPolicies(accessPolicies);
}
[Fact]
public void CheckAccessPoliciesHaveReadPermission_ReadPermissionFalse_ThrowsBadRequestException()
{
var accessPolicies = new List<BaseAccessPolicy>
{
new UserProjectAccessPolicy { Read = false, Write = true },
new GroupProjectAccessPolicy { Read = true, Write = false }
};
Assert.Throws<BadRequestException>(() =>
{
AccessPolicyHelpers.CheckAccessPoliciesHaveReadPermission(accessPolicies);
});
}
[Fact]
public void CheckAccessPoliciesHaveReadPermission_AllReadIsTrue_Success()
{
var accessPolicies = new List<BaseAccessPolicy>
{
new UserProjectAccessPolicy { Read = true, Write = true },
new GroupProjectAccessPolicy { Read = true, Write = false }
};
AccessPolicyHelpers.CheckAccessPoliciesHaveReadPermission(accessPolicies);
}
private class UnsupportedAccessPolicy : BaseAccessPolicy;
}

View File

@ -0,0 +1,77 @@
#nullable enable
using Bit.Core.SecretsManager.Entities;
using Bit.Core.SecretsManager.Enums.AccessPolicies;
using Bit.Core.SecretsManager.Models.Data;
using Xunit;
namespace Bit.Core.Test.SecretsManager.Models;
public class ServiceAccountGrantedPoliciesTests
{
[Fact]
public void GetPolicyUpdates_NoChanges_ReturnsEmptyLists()
{
var projectId1 = Guid.NewGuid();
var projectId2 = Guid.NewGuid();
var existing = new ServiceAccountGrantedPolicies
{
ProjectGrantedPolicies = new List<ServiceAccountProjectAccessPolicy>
{
new() { GrantedProjectId = projectId1, Read = true, Write = true },
new() { GrantedProjectId = projectId2, Read = false, Write = true }
}
};
var result = existing.GetPolicyUpdates(existing);
Assert.Empty(result.ProjectGrantedPolicyUpdates);
}
[Fact]
public void GetPolicyUpdates_ReturnsCorrectPolicyChanges()
{
var projectId1 = Guid.NewGuid();
var projectId2 = Guid.NewGuid();
var projectId3 = Guid.NewGuid();
var projectId4 = Guid.NewGuid();
var existing = new ServiceAccountGrantedPolicies
{
ProjectGrantedPolicies = new List<ServiceAccountProjectAccessPolicy>
{
new() { GrantedProjectId = projectId1, Read = true, Write = true },
new() { GrantedProjectId = projectId3, Read = true, Write = true },
new() { GrantedProjectId = projectId4, Read = true, Write = true }
}
};
var requested = new ServiceAccountGrantedPolicies
{
ProjectGrantedPolicies = new List<ServiceAccountProjectAccessPolicy>
{
new() { GrantedProjectId = projectId1, Read = true, Write = false },
new() { GrantedProjectId = projectId2, Read = false, Write = true },
new() { GrantedProjectId = projectId3, Read = true, Write = true }
}
};
var result = existing.GetPolicyUpdates(requested);
Assert.Contains(projectId2, result.ProjectGrantedPolicyUpdates
.Where(pu => pu.Operation == AccessPolicyOperation.Create)
.Select(pu => pu.AccessPolicy.GrantedProjectId!.Value));
Assert.Contains(projectId4, result.ProjectGrantedPolicyUpdates
.Where(pu => pu.Operation == AccessPolicyOperation.Delete)
.Select(pu => pu.AccessPolicy.GrantedProjectId!.Value));
Assert.Contains(projectId1, result.ProjectGrantedPolicyUpdates
.Where(pu => pu.Operation == AccessPolicyOperation.Update)
.Select(pu => pu.AccessPolicy.GrantedProjectId!.Value));
Assert.DoesNotContain(projectId3, result.ProjectGrantedPolicyUpdates
.Select(pu => pu.AccessPolicy.GrantedProjectId!.Value));
}
}