mirror of
https://github.com/bitwarden/server.git
synced 2025-04-05 05:00:19 -05:00
[SM-923] Add project service accounts access policies management endpoints (#3993)
* Add new models * Update repositories * Add new authz handler * Add new query * Add new command * Add authz, command, and query to DI * Add new endpoint to controller * Add query unit tests * Add api unit tests * Add api integration tests
This commit is contained in:
parent
e302ee1520
commit
7f8cea58d0
@ -0,0 +1,107 @@
|
|||||||
|
#nullable enable
|
||||||
|
using Bit.Core.Context;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.SecretsManager.AuthorizationRequirements;
|
||||||
|
using Bit.Core.SecretsManager.Enums.AccessPolicies;
|
||||||
|
using Bit.Core.SecretsManager.Models.Data.AccessPolicyUpdates;
|
||||||
|
using Bit.Core.SecretsManager.Queries.Interfaces;
|
||||||
|
using Bit.Core.SecretsManager.Repositories;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
|
||||||
|
namespace Bit.Commercial.Core.SecretsManager.AuthorizationHandlers.AccessPolicies;
|
||||||
|
|
||||||
|
public class ProjectServiceAccountsAccessPoliciesAuthorizationHandler : AuthorizationHandler<
|
||||||
|
ProjectServiceAccountsAccessPoliciesOperationRequirement,
|
||||||
|
ProjectServiceAccountsAccessPoliciesUpdates>
|
||||||
|
{
|
||||||
|
private readonly IAccessClientQuery _accessClientQuery;
|
||||||
|
private readonly ICurrentContext _currentContext;
|
||||||
|
private readonly IProjectRepository _projectRepository;
|
||||||
|
private readonly IServiceAccountRepository _serviceAccountRepository;
|
||||||
|
|
||||||
|
public ProjectServiceAccountsAccessPoliciesAuthorizationHandler(ICurrentContext currentContext,
|
||||||
|
IAccessClientQuery accessClientQuery,
|
||||||
|
IProjectRepository projectRepository,
|
||||||
|
IServiceAccountRepository serviceAccountRepository)
|
||||||
|
{
|
||||||
|
_currentContext = currentContext;
|
||||||
|
_accessClientQuery = accessClientQuery;
|
||||||
|
_serviceAccountRepository = serviceAccountRepository;
|
||||||
|
_projectRepository = projectRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context,
|
||||||
|
ProjectServiceAccountsAccessPoliciesOperationRequirement requirement,
|
||||||
|
ProjectServiceAccountsAccessPoliciesUpdates 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 == ProjectServiceAccountsAccessPoliciesOperations.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,
|
||||||
|
ProjectServiceAccountsAccessPoliciesOperationRequirement requirement,
|
||||||
|
ProjectServiceAccountsAccessPoliciesUpdates resource,
|
||||||
|
AccessClientType accessClient, Guid userId)
|
||||||
|
{
|
||||||
|
var access =
|
||||||
|
await _projectRepository.AccessToProjectAsync(resource.ProjectId, userId,
|
||||||
|
accessClient);
|
||||||
|
if (!access.Write)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var serviceAccountIds = resource.ServiceAccountAccessPolicyUpdates.Select(update =>
|
||||||
|
update.AccessPolicy.ServiceAccountId!.Value).ToList();
|
||||||
|
|
||||||
|
var inSameOrganization =
|
||||||
|
await _serviceAccountRepository.ServiceAccountsAreInOrganizationAsync(serviceAccountIds,
|
||||||
|
resource.OrganizationId);
|
||||||
|
if (!inSameOrganization)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Users can only create access policies for service accounts they have access to.
|
||||||
|
// User can delete and update any service account access policy if they have write access to the project.
|
||||||
|
var serviceAccountIdsToCheck = resource.ServiceAccountAccessPolicyUpdates
|
||||||
|
.Where(update => update.Operation == AccessPolicyOperation.Create).Select(update =>
|
||||||
|
update.AccessPolicy.ServiceAccountId!.Value).ToList();
|
||||||
|
|
||||||
|
if (serviceAccountIdsToCheck.Count == 0)
|
||||||
|
{
|
||||||
|
context.Succeed(requirement);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var serviceAccountsAccess =
|
||||||
|
await _serviceAccountRepository.AccessToServiceAccountsAsync(serviceAccountIdsToCheck, userId,
|
||||||
|
accessClient);
|
||||||
|
if (serviceAccountsAccess.Count == serviceAccountIdsToCheck.Count &&
|
||||||
|
serviceAccountsAccess.All(a => a.Value.Write))
|
||||||
|
{
|
||||||
|
context.Succeed(requirement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
#nullable enable
|
||||||
|
using Bit.Core.SecretsManager.Commands.AccessPolicies.Interfaces;
|
||||||
|
using Bit.Core.SecretsManager.Models.Data.AccessPolicyUpdates;
|
||||||
|
using Bit.Core.SecretsManager.Repositories;
|
||||||
|
|
||||||
|
namespace Bit.Commercial.Core.SecretsManager.Commands.AccessPolicies;
|
||||||
|
|
||||||
|
public class UpdateProjectServiceAccountsAccessPoliciesCommand : IUpdateProjectServiceAccountsAccessPoliciesCommand
|
||||||
|
{
|
||||||
|
private readonly IAccessPolicyRepository _accessPolicyRepository;
|
||||||
|
|
||||||
|
public UpdateProjectServiceAccountsAccessPoliciesCommand(IAccessPolicyRepository accessPolicyRepository)
|
||||||
|
{
|
||||||
|
_accessPolicyRepository = accessPolicyRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateAsync(ProjectServiceAccountsAccessPoliciesUpdates accessPoliciesUpdates)
|
||||||
|
{
|
||||||
|
if (!accessPoliciesUpdates.ServiceAccountAccessPolicyUpdates.Any())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _accessPolicyRepository.UpdateProjectServiceAccountsAccessPoliciesAsync(accessPoliciesUpdates);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,44 @@
|
|||||||
|
#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 ProjectServiceAccountsAccessPoliciesUpdatesQuery : IProjectServiceAccountsAccessPoliciesUpdatesQuery
|
||||||
|
{
|
||||||
|
private readonly IAccessPolicyRepository _accessPolicyRepository;
|
||||||
|
|
||||||
|
public ProjectServiceAccountsAccessPoliciesUpdatesQuery(IAccessPolicyRepository accessPolicyRepository)
|
||||||
|
{
|
||||||
|
_accessPolicyRepository = accessPolicyRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ProjectServiceAccountsAccessPoliciesUpdates> GetAsync(
|
||||||
|
ProjectServiceAccountsAccessPolicies projectServiceAccountsAccessPolicies)
|
||||||
|
{
|
||||||
|
var currentPolicies =
|
||||||
|
await _accessPolicyRepository.GetProjectServiceAccountsAccessPoliciesAsync(
|
||||||
|
projectServiceAccountsAccessPolicies.ProjectId);
|
||||||
|
|
||||||
|
if (currentPolicies == null)
|
||||||
|
{
|
||||||
|
return new ProjectServiceAccountsAccessPoliciesUpdates
|
||||||
|
{
|
||||||
|
ProjectId = projectServiceAccountsAccessPolicies.ProjectId,
|
||||||
|
OrganizationId = projectServiceAccountsAccessPolicies.OrganizationId,
|
||||||
|
ServiceAccountAccessPolicyUpdates =
|
||||||
|
projectServiceAccountsAccessPolicies.ServiceAccountAccessPolicies.Select(p =>
|
||||||
|
new ServiceAccountProjectAccessPolicyUpdate
|
||||||
|
{
|
||||||
|
Operation = AccessPolicyOperation.Create,
|
||||||
|
AccessPolicy = p
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentPolicies.GetPolicyUpdates(projectServiceAccountsAccessPolicies);
|
||||||
|
}
|
||||||
|
}
|
@ -42,12 +42,14 @@ public static class SecretsManagerCollectionExtensions
|
|||||||
services.AddScoped<IAuthorizationHandler, ProjectPeopleAccessPoliciesAuthorizationHandler>();
|
services.AddScoped<IAuthorizationHandler, ProjectPeopleAccessPoliciesAuthorizationHandler>();
|
||||||
services.AddScoped<IAuthorizationHandler, ServiceAccountPeopleAccessPoliciesAuthorizationHandler>();
|
services.AddScoped<IAuthorizationHandler, ServiceAccountPeopleAccessPoliciesAuthorizationHandler>();
|
||||||
services.AddScoped<IAuthorizationHandler, ServiceAccountGrantedPoliciesAuthorizationHandler>();
|
services.AddScoped<IAuthorizationHandler, ServiceAccountGrantedPoliciesAuthorizationHandler>();
|
||||||
|
services.AddScoped<IAuthorizationHandler, ProjectServiceAccountsAccessPoliciesAuthorizationHandler>();
|
||||||
services.AddScoped<IAccessClientQuery, AccessClientQuery>();
|
services.AddScoped<IAccessClientQuery, AccessClientQuery>();
|
||||||
services.AddScoped<IMaxProjectsQuery, MaxProjectsQuery>();
|
services.AddScoped<IMaxProjectsQuery, MaxProjectsQuery>();
|
||||||
services.AddScoped<ISameOrganizationQuery, SameOrganizationQuery>();
|
services.AddScoped<ISameOrganizationQuery, SameOrganizationQuery>();
|
||||||
services.AddScoped<IServiceAccountSecretsDetailsQuery, ServiceAccountSecretsDetailsQuery>();
|
services.AddScoped<IServiceAccountSecretsDetailsQuery, ServiceAccountSecretsDetailsQuery>();
|
||||||
services.AddScoped<IServiceAccountGrantedPolicyUpdatesQuery, ServiceAccountGrantedPolicyUpdatesQuery>();
|
services.AddScoped<IServiceAccountGrantedPolicyUpdatesQuery, ServiceAccountGrantedPolicyUpdatesQuery>();
|
||||||
services.AddScoped<ISecretsSyncQuery, SecretsSyncQuery>();
|
services.AddScoped<ISecretsSyncQuery, SecretsSyncQuery>();
|
||||||
|
services.AddScoped<IProjectServiceAccountsAccessPoliciesUpdatesQuery, ProjectServiceAccountsAccessPoliciesUpdatesQuery>();
|
||||||
services.AddScoped<ICreateSecretCommand, CreateSecretCommand>();
|
services.AddScoped<ICreateSecretCommand, CreateSecretCommand>();
|
||||||
services.AddScoped<IUpdateSecretCommand, UpdateSecretCommand>();
|
services.AddScoped<IUpdateSecretCommand, UpdateSecretCommand>();
|
||||||
services.AddScoped<IDeleteSecretCommand, DeleteSecretCommand>();
|
services.AddScoped<IDeleteSecretCommand, DeleteSecretCommand>();
|
||||||
@ -67,5 +69,6 @@ public static class SecretsManagerCollectionExtensions
|
|||||||
services.AddScoped<IEmptyTrashCommand, EmptyTrashCommand>();
|
services.AddScoped<IEmptyTrashCommand, EmptyTrashCommand>();
|
||||||
services.AddScoped<IRestoreTrashCommand, RestoreTrashCommand>();
|
services.AddScoped<IRestoreTrashCommand, RestoreTrashCommand>();
|
||||||
services.AddScoped<IUpdateServiceAccountGrantedPoliciesCommand, UpdateServiceAccountGrantedPoliciesCommand>();
|
services.AddScoped<IUpdateServiceAccountGrantedPoliciesCommand, UpdateServiceAccountGrantedPoliciesCommand>();
|
||||||
|
services.AddScoped<IUpdateProjectServiceAccountsAccessPoliciesCommand, UpdateProjectServiceAccountsAccessPoliciesCommand>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -465,12 +465,68 @@ public class AccessPolicyRepository : BaseEntityFrameworkRepository, IAccessPoli
|
|||||||
dbContext.RemoveRange(policiesToDelete);
|
dbContext.RemoveRange(policiesToDelete);
|
||||||
}
|
}
|
||||||
|
|
||||||
await UpsertServiceAccountGrantedPoliciesAsync(dbContext, currentAccessPolicies,
|
await UpsertServiceAccountProjectPoliciesAsync(dbContext, currentAccessPolicies,
|
||||||
updates.ProjectGrantedPolicyUpdates.Where(pu => pu.Operation != AccessPolicyOperation.Delete).ToList());
|
updates.ProjectGrantedPolicyUpdates.Where(pu => pu.Operation != AccessPolicyOperation.Delete).ToList());
|
||||||
await UpdateServiceAccountRevisionAsync(dbContext, updates.ServiceAccountId);
|
await UpdateServiceAccountRevisionAsync(dbContext, updates.ServiceAccountId);
|
||||||
await dbContext.SaveChangesAsync();
|
await dbContext.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<ProjectServiceAccountsAccessPolicies?> GetProjectServiceAccountsAccessPoliciesAsync(Guid projectId)
|
||||||
|
{
|
||||||
|
await using var scope = ServiceScopeFactory.CreateAsyncScope();
|
||||||
|
var dbContext = GetDatabaseContext(scope);
|
||||||
|
var entities = await dbContext.ServiceAccountProjectAccessPolicy
|
||||||
|
.Where(ap => ap.GrantedProjectId == projectId)
|
||||||
|
.Include(ap => ap.ServiceAccount)
|
||||||
|
.Include(ap => ap.GrantedProject)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
if (entities.Count == 0)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ProjectServiceAccountsAccessPolicies(projectId, entities.Select(MapToCore).ToList());
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateProjectServiceAccountsAccessPoliciesAsync(
|
||||||
|
ProjectServiceAccountsAccessPoliciesUpdates updates)
|
||||||
|
{
|
||||||
|
await using var scope = ServiceScopeFactory.CreateAsyncScope();
|
||||||
|
var dbContext = GetDatabaseContext(scope);
|
||||||
|
await using var transaction = await dbContext.Database.BeginTransactionAsync();
|
||||||
|
|
||||||
|
var currentAccessPolicies = await dbContext.ServiceAccountProjectAccessPolicy
|
||||||
|
.Where(ap => ap.GrantedProjectId == updates.ProjectId)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
if (currentAccessPolicies.Count != 0)
|
||||||
|
{
|
||||||
|
var serviceAccountIdsToDelete = updates.ServiceAccountAccessPolicyUpdates
|
||||||
|
.Where(pu => pu.Operation == AccessPolicyOperation.Delete)
|
||||||
|
.Select(pu => pu.AccessPolicy.ServiceAccountId!.Value)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var accessPolicyIdsToDelete = currentAccessPolicies
|
||||||
|
.Where(entity => serviceAccountIdsToDelete.Contains(entity.ServiceAccountId!.Value))
|
||||||
|
.Select(ap => ap.Id)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
await dbContext.ServiceAccountProjectAccessPolicy
|
||||||
|
.Where(ap => accessPolicyIdsToDelete.Contains(ap.Id))
|
||||||
|
.ExecuteDeleteAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
await UpsertServiceAccountProjectPoliciesAsync(dbContext, currentAccessPolicies,
|
||||||
|
updates.ServiceAccountAccessPolicyUpdates.Where(update => update.Operation != AccessPolicyOperation.Delete)
|
||||||
|
.ToList());
|
||||||
|
var effectedServiceAccountIds = updates.ServiceAccountAccessPolicyUpdates
|
||||||
|
.Select(sa => sa.AccessPolicy.ServiceAccountId!.Value).ToList();
|
||||||
|
await UpdateServiceAccountsRevisionAsync(dbContext, effectedServiceAccountIds);
|
||||||
|
await dbContext.SaveChangesAsync();
|
||||||
|
await transaction.CommitAsync();
|
||||||
|
}
|
||||||
|
|
||||||
private static async Task UpsertPeoplePoliciesAsync(DatabaseContext dbContext,
|
private static async Task UpsertPeoplePoliciesAsync(DatabaseContext dbContext,
|
||||||
List<BaseAccessPolicy> policies, IReadOnlyCollection<AccessPolicy> userPolicyEntities,
|
List<BaseAccessPolicy> policies, IReadOnlyCollection<AccessPolicy> userPolicyEntities,
|
||||||
IReadOnlyCollection<AccessPolicy> groupPolicyEntities)
|
IReadOnlyCollection<AccessPolicy> groupPolicyEntities)
|
||||||
@ -506,7 +562,7 @@ public class AccessPolicyRepository : BaseEntityFrameworkRepository, IAccessPoli
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task UpsertServiceAccountGrantedPoliciesAsync(DatabaseContext dbContext,
|
private async Task UpsertServiceAccountProjectPoliciesAsync(DatabaseContext dbContext,
|
||||||
IReadOnlyCollection<ServiceAccountProjectAccessPolicy> currentPolices,
|
IReadOnlyCollection<ServiceAccountProjectAccessPolicy> currentPolices,
|
||||||
List<ServiceAccountProjectAccessPolicyUpdate> policyUpdates)
|
List<ServiceAccountProjectAccessPolicyUpdate> policyUpdates)
|
||||||
{
|
{
|
||||||
@ -515,7 +571,8 @@ public class AccessPolicyRepository : BaseEntityFrameworkRepository, IAccessPoli
|
|||||||
{
|
{
|
||||||
var updatedEntity = MapToEntity(policyUpdate.AccessPolicy);
|
var updatedEntity = MapToEntity(policyUpdate.AccessPolicy);
|
||||||
var currentEntity = currentPolices.FirstOrDefault(e =>
|
var currentEntity = currentPolices.FirstOrDefault(e =>
|
||||||
e.GrantedProjectId == policyUpdate.AccessPolicy.GrantedProjectId!.Value);
|
e.GrantedProjectId == policyUpdate.AccessPolicy.GrantedProjectId!.Value &&
|
||||||
|
e.ServiceAccountId == policyUpdate.AccessPolicy.ServiceAccountId!.Value);
|
||||||
|
|
||||||
switch (policyUpdate.Operation)
|
switch (policyUpdate.Operation)
|
||||||
{
|
{
|
||||||
@ -628,4 +685,13 @@ public class AccessPolicyRepository : BaseEntityFrameworkRepository, IAccessPoli
|
|||||||
entity.RevisionDate = DateTime.UtcNow;
|
entity.RevisionDate = DateTime.UtcNow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static async Task UpdateServiceAccountsRevisionAsync(DatabaseContext dbContext, List<Guid> serviceAccountIds)
|
||||||
|
{
|
||||||
|
var utcNow = DateTime.UtcNow;
|
||||||
|
await dbContext.ServiceAccount
|
||||||
|
.Where(sa => serviceAccountIds.Contains(sa.Id))
|
||||||
|
.ExecuteUpdateAsync(setters =>
|
||||||
|
setters.SetProperty(sa => sa.RevisionDate, utcNow));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -112,30 +112,29 @@ public class ServiceAccountRepository : Repository<Core.SecretsManager.Entities.
|
|||||||
public async Task<(bool Read, bool Write)> AccessToServiceAccountAsync(Guid id, Guid userId,
|
public async Task<(bool Read, bool Write)> AccessToServiceAccountAsync(Guid id, Guid userId,
|
||||||
AccessClientType accessType)
|
AccessClientType accessType)
|
||||||
{
|
{
|
||||||
using var scope = ServiceScopeFactory.CreateScope();
|
await using var scope = ServiceScopeFactory.CreateAsyncScope();
|
||||||
var dbContext = GetDatabaseContext(scope);
|
var dbContext = GetDatabaseContext(scope);
|
||||||
|
|
||||||
var serviceAccount = dbContext.ServiceAccount.Where(sa => sa.Id == id);
|
var serviceAccountQuery = dbContext.ServiceAccount.Where(sa => sa.Id == id);
|
||||||
|
|
||||||
var query = accessType switch
|
var accessQuery = BuildServiceAccountAccessQuery(serviceAccountQuery, userId, accessType);
|
||||||
{
|
var access = await accessQuery.FirstOrDefaultAsync();
|
||||||
AccessClientType.NoAccessCheck => serviceAccount.Select(_ => new { Read = true, Write = true }),
|
|
||||||
AccessClientType.User => serviceAccount.Select(sa => new
|
|
||||||
{
|
|
||||||
Read = sa.UserAccessPolicies.Any(ap => ap.OrganizationUser.User.Id == userId && ap.Read) ||
|
|
||||||
sa.GroupAccessPolicies.Any(ap =>
|
|
||||||
ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.User.Id == userId && ap.Read)),
|
|
||||||
Write = sa.UserAccessPolicies.Any(ap => ap.OrganizationUser.User.Id == userId && ap.Write) ||
|
|
||||||
sa.GroupAccessPolicies.Any(ap =>
|
|
||||||
ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.User.Id == userId && ap.Write)),
|
|
||||||
}),
|
|
||||||
AccessClientType.ServiceAccount => serviceAccount.Select(_ => new { Read = false, Write = false }),
|
|
||||||
_ => serviceAccount.Select(_ => new { Read = false, Write = false }),
|
|
||||||
};
|
|
||||||
|
|
||||||
var policy = await query.FirstOrDefaultAsync();
|
return access == null ? (false, false) : (access.Read, access.Write);
|
||||||
|
}
|
||||||
|
|
||||||
return policy == null ? (false, false) : (policy.Read, policy.Write);
|
public async Task<Dictionary<Guid, (bool Read, bool Write)>> AccessToServiceAccountsAsync(
|
||||||
|
IEnumerable<Guid> ids,
|
||||||
|
Guid userId,
|
||||||
|
AccessClientType accessType)
|
||||||
|
{
|
||||||
|
await using var scope = ServiceScopeFactory.CreateAsyncScope();
|
||||||
|
var dbContext = GetDatabaseContext(scope);
|
||||||
|
|
||||||
|
var serviceAccountsQuery = dbContext.ServiceAccount.Where(p => ids.Contains(p.Id));
|
||||||
|
var accessQuery = BuildServiceAccountAccessQuery(serviceAccountsQuery, userId, accessType);
|
||||||
|
|
||||||
|
return await accessQuery.ToDictionaryAsync(access => access.Id, access => (access.Read, access.Write));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<int> GetServiceAccountCountByOrganizationIdAsync(Guid organizationId)
|
public async Task<int> GetServiceAccountCountByOrganizationIdAsync(Guid organizationId)
|
||||||
@ -148,6 +147,15 @@ public class ServiceAccountRepository : Repository<Core.SecretsManager.Entities.
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<bool> ServiceAccountsAreInOrganizationAsync(List<Guid> serviceAccountIds, Guid organizationId)
|
||||||
|
{
|
||||||
|
await using var scope = ServiceScopeFactory.CreateAsyncScope();
|
||||||
|
var dbContext = GetDatabaseContext(scope);
|
||||||
|
var result = await dbContext.ServiceAccount.CountAsync(sa =>
|
||||||
|
sa.OrganizationId == organizationId && serviceAccountIds.Contains(sa.Id));
|
||||||
|
return serviceAccountIds.Count == result;
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<ServiceAccountSecretsDetails>> GetManyByOrganizationIdWithSecretsDetailsAsync(
|
public async Task<IEnumerable<ServiceAccountSecretsDetails>> GetManyByOrganizationIdWithSecretsDetailsAsync(
|
||||||
Guid organizationId, Guid userId, AccessClientType accessType)
|
Guid organizationId, Guid userId, AccessClientType accessType)
|
||||||
{
|
{
|
||||||
@ -186,6 +194,27 @@ public class ServiceAccountRepository : Repository<Core.SecretsManager.Entities.
|
|||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private record ServiceAccountAccess(Guid Id, bool Read, bool Write);
|
||||||
|
|
||||||
|
private static IQueryable<ServiceAccountAccess> BuildServiceAccountAccessQuery(IQueryable<ServiceAccount> serviceAccountQuery, Guid userId,
|
||||||
|
AccessClientType accessType) =>
|
||||||
|
accessType switch
|
||||||
|
{
|
||||||
|
AccessClientType.NoAccessCheck => serviceAccountQuery.Select(sa => new ServiceAccountAccess(sa.Id, true, true)),
|
||||||
|
AccessClientType.User => serviceAccountQuery.Select(sa => new ServiceAccountAccess
|
||||||
|
(
|
||||||
|
sa.Id,
|
||||||
|
sa.UserAccessPolicies.Any(ap => ap.OrganizationUser.User.Id == userId && ap.Read) ||
|
||||||
|
sa.GroupAccessPolicies.Any(ap =>
|
||||||
|
ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.User.Id == userId && ap.Read)),
|
||||||
|
sa.UserAccessPolicies.Any(ap => ap.OrganizationUser.User.Id == userId && ap.Write) ||
|
||||||
|
sa.GroupAccessPolicies.Any(ap =>
|
||||||
|
ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.User.Id == userId && ap.Write))
|
||||||
|
)),
|
||||||
|
AccessClientType.ServiceAccount => serviceAccountQuery.Select(sa => new ServiceAccountAccess(sa.Id, false, false)),
|
||||||
|
_ => serviceAccountQuery.Select(sa => new ServiceAccountAccess(sa.Id, false, false))
|
||||||
|
};
|
||||||
|
|
||||||
private static Expression<Func<ServiceAccount, bool>> UserHasReadAccessToServiceAccount(Guid userId) => sa =>
|
private static Expression<Func<ServiceAccount, bool>> UserHasReadAccessToServiceAccount(Guid userId) => sa =>
|
||||||
sa.UserAccessPolicies.Any(ap => ap.OrganizationUser.User.Id == userId && ap.Read) ||
|
sa.UserAccessPolicies.Any(ap => ap.OrganizationUser.User.Id == userId && ap.Read) ||
|
||||||
sa.GroupAccessPolicies.Any(ap => ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.User.Id == userId && ap.Read));
|
sa.GroupAccessPolicies.Any(ap => ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.User.Id == userId && ap.Read));
|
||||||
|
@ -0,0 +1,342 @@
|
|||||||
|
#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.Entities;
|
||||||
|
using Bit.Core.SecretsManager.Enums.AccessPolicies;
|
||||||
|
using Bit.Core.SecretsManager.Models.Data.AccessPolicyUpdates;
|
||||||
|
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 ProjectServiceAccountsAccessPoliciesAuthorizationHandlerTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void ServiceAccountGrantedPoliciesOperations_OnlyPublicStatic()
|
||||||
|
{
|
||||||
|
var publicStaticFields =
|
||||||
|
typeof(ProjectServiceAccountsAccessPoliciesOperations).GetFields(BindingFlags.Public | BindingFlags.Static);
|
||||||
|
var allFields = typeof(ProjectServiceAccountsAccessPoliciesOperations).GetFields();
|
||||||
|
Assert.Equal(publicStaticFields.Length, allFields.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task Handler_AccessSecretsManagerFalse_DoesNotSucceed(
|
||||||
|
SutProvider<ProjectServiceAccountsAccessPoliciesAuthorizationHandler> sutProvider,
|
||||||
|
ProjectServiceAccountsAccessPoliciesUpdates resource,
|
||||||
|
ClaimsPrincipal claimsPrincipal)
|
||||||
|
{
|
||||||
|
var requirement = ProjectServiceAccountsAccessPoliciesOperations.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<ProjectServiceAccountsAccessPoliciesAuthorizationHandler> sutProvider,
|
||||||
|
ProjectServiceAccountsAccessPoliciesUpdates resource,
|
||||||
|
ClaimsPrincipal claimsPrincipal)
|
||||||
|
{
|
||||||
|
var requirement = ProjectServiceAccountsAccessPoliciesOperations.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_UnsupportedProjectServiceAccountsPoliciesOperationRequirement_Throws(
|
||||||
|
SutProvider<ProjectServiceAccountsAccessPoliciesAuthorizationHandler> sutProvider,
|
||||||
|
ProjectServiceAccountsAccessPoliciesUpdates resource,
|
||||||
|
ClaimsPrincipal claimsPrincipal)
|
||||||
|
{
|
||||||
|
var requirement = new ProjectServiceAccountsAccessPoliciesOperationRequirement();
|
||||||
|
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_UserHasNoWriteAccessToProject_DoesNotSucceed(
|
||||||
|
AccessClientType accessClientType,
|
||||||
|
bool projectReadAccess,
|
||||||
|
bool projectWriteAccess,
|
||||||
|
SutProvider<ProjectServiceAccountsAccessPoliciesAuthorizationHandler> sutProvider,
|
||||||
|
ProjectServiceAccountsAccessPoliciesUpdates resource,
|
||||||
|
Guid userId,
|
||||||
|
ClaimsPrincipal claimsPrincipal)
|
||||||
|
{
|
||||||
|
var requirement = ProjectServiceAccountsAccessPoliciesOperations.Updates;
|
||||||
|
SetupUserSubstitutes(sutProvider, accessClientType, resource, userId);
|
||||||
|
sutProvider.GetDependency<IProjectRepository>()
|
||||||
|
.AccessToProjectAsync(resource.ProjectId, userId, accessClientType)
|
||||||
|
.Returns((projectReadAccess, projectWriteAccess));
|
||||||
|
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_ServiceAccountsInDifferentOrganization_DoesNotSucceed(
|
||||||
|
SutProvider<ProjectServiceAccountsAccessPoliciesAuthorizationHandler> sutProvider,
|
||||||
|
ProjectServiceAccountsAccessPoliciesUpdates resource,
|
||||||
|
Guid userId,
|
||||||
|
ClaimsPrincipal claimsPrincipal)
|
||||||
|
{
|
||||||
|
var requirement = ProjectServiceAccountsAccessPoliciesOperations.Updates;
|
||||||
|
SetupUserSubstitutes(sutProvider, AccessClientType.NoAccessCheck, resource, userId);
|
||||||
|
sutProvider.GetDependency<IProjectRepository>()
|
||||||
|
.AccessToProjectAsync(resource.ProjectId, userId, AccessClientType.NoAccessCheck)
|
||||||
|
.Returns((true, true));
|
||||||
|
sutProvider.GetDependency<IServiceAccountRepository>()
|
||||||
|
.ServiceAccountsAreInOrganizationAsync(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_UserHasAccessToProject_NoCreatesRequested_Success(
|
||||||
|
AccessClientType accessClientType,
|
||||||
|
SutProvider<ProjectServiceAccountsAccessPoliciesAuthorizationHandler> sutProvider,
|
||||||
|
ProjectServiceAccountsAccessPoliciesUpdates resource,
|
||||||
|
Guid userId,
|
||||||
|
ClaimsPrincipal claimsPrincipal)
|
||||||
|
{
|
||||||
|
var requirement = ProjectServiceAccountsAccessPoliciesOperations.Updates;
|
||||||
|
resource = RemoveAllCreates(resource);
|
||||||
|
SetupServiceAccountsAccessTest(sutProvider, accessClientType, resource, userId);
|
||||||
|
|
||||||
|
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
|
||||||
|
claimsPrincipal, resource);
|
||||||
|
|
||||||
|
await sutProvider.Sut.HandleAsync(authzContext);
|
||||||
|
|
||||||
|
Assert.True(authzContext.HasSucceeded);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData(AccessClientType.NoAccessCheck)]
|
||||||
|
[BitAutoData(AccessClientType.User)]
|
||||||
|
public async Task Handler_UserHasNoAccessToCreateServiceAccounts_DoesNotSucceed(
|
||||||
|
AccessClientType accessClientType,
|
||||||
|
SutProvider<ProjectServiceAccountsAccessPoliciesAuthorizationHandler> sutProvider,
|
||||||
|
ProjectServiceAccountsAccessPoliciesUpdates resource,
|
||||||
|
Guid userId,
|
||||||
|
ClaimsPrincipal claimsPrincipal)
|
||||||
|
{
|
||||||
|
var requirement = ProjectServiceAccountsAccessPoliciesOperations.Updates;
|
||||||
|
resource = AddServiceAccountCreateUpdate(resource);
|
||||||
|
SetupServiceAccountsAccessTest(sutProvider, accessClientType, resource, userId);
|
||||||
|
var accessResult = resource.ServiceAccountAccessPolicyUpdates
|
||||||
|
.Where(x => x.Operation == AccessPolicyOperation.Create)
|
||||||
|
.Select(x => x.AccessPolicy.ServiceAccountId!.Value)
|
||||||
|
.ToDictionary(id => id, _ => (false, false));
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IServiceAccountRepository>()
|
||||||
|
.AccessToServiceAccountsAsync(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<ProjectServiceAccountsAccessPoliciesAuthorizationHandler> sutProvider,
|
||||||
|
ProjectServiceAccountsAccessPoliciesUpdates resource,
|
||||||
|
Guid userId,
|
||||||
|
ClaimsPrincipal claimsPrincipal)
|
||||||
|
{
|
||||||
|
var requirement = ProjectServiceAccountsAccessPoliciesOperations.Updates;
|
||||||
|
resource = AddServiceAccountCreateUpdate(resource);
|
||||||
|
SetupServiceAccountsAccessTest(sutProvider, accessClientType, resource, userId);
|
||||||
|
|
||||||
|
var accessResult = resource.ServiceAccountAccessPolicyUpdates
|
||||||
|
.Where(x => x.Operation == AccessPolicyOperation.Create)
|
||||||
|
.Select(x => x.AccessPolicy.ServiceAccountId!.Value)
|
||||||
|
.ToDictionary(id => id, _ => (false, false));
|
||||||
|
|
||||||
|
accessResult[accessResult.First().Key] = (true, true);
|
||||||
|
accessResult.Remove(accessResult.Last().Key);
|
||||||
|
sutProvider.GetDependency<IServiceAccountRepository>()
|
||||||
|
.AccessToServiceAccountsAsync(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_UserHasAccessToSomeCreateServiceAccounts_DoesNotSucceed(
|
||||||
|
AccessClientType accessClientType,
|
||||||
|
SutProvider<ProjectServiceAccountsAccessPoliciesAuthorizationHandler> sutProvider,
|
||||||
|
ProjectServiceAccountsAccessPoliciesUpdates resource,
|
||||||
|
Guid userId,
|
||||||
|
ClaimsPrincipal claimsPrincipal)
|
||||||
|
{
|
||||||
|
var requirement = ProjectServiceAccountsAccessPoliciesOperations.Updates;
|
||||||
|
resource = AddServiceAccountCreateUpdate(resource);
|
||||||
|
SetupServiceAccountsAccessTest(sutProvider, accessClientType, resource, userId);
|
||||||
|
|
||||||
|
var accessResult = resource.ServiceAccountAccessPolicyUpdates
|
||||||
|
.Where(x => x.Operation == AccessPolicyOperation.Create)
|
||||||
|
.Select(x => x.AccessPolicy.ServiceAccountId!.Value)
|
||||||
|
.ToDictionary(id => id, _ => (false, false));
|
||||||
|
|
||||||
|
accessResult[accessResult.First().Key] = (true, true);
|
||||||
|
sutProvider.GetDependency<IServiceAccountRepository>()
|
||||||
|
.AccessToServiceAccountsAsync(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_UserHasAccessToAllCreateServiceAccounts_Success(
|
||||||
|
AccessClientType accessClientType,
|
||||||
|
SutProvider<ProjectServiceAccountsAccessPoliciesAuthorizationHandler> sutProvider,
|
||||||
|
ProjectServiceAccountsAccessPoliciesUpdates resource,
|
||||||
|
Guid userId,
|
||||||
|
ClaimsPrincipal claimsPrincipal)
|
||||||
|
{
|
||||||
|
var requirement = ProjectServiceAccountsAccessPoliciesOperations.Updates;
|
||||||
|
resource = AddServiceAccountCreateUpdate(resource);
|
||||||
|
SetupServiceAccountsAccessTest(sutProvider, accessClientType, resource, userId);
|
||||||
|
|
||||||
|
var accessResult = resource.ServiceAccountAccessPolicyUpdates
|
||||||
|
.Where(x => x.Operation == AccessPolicyOperation.Create)
|
||||||
|
.Select(x => x.AccessPolicy.ServiceAccountId!.Value)
|
||||||
|
.ToDictionary(id => id, _ => (true, true));
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IServiceAccountRepository>()
|
||||||
|
.AccessToServiceAccountsAsync(Arg.Any<List<Guid>>(), userId, accessClientType)
|
||||||
|
.Returns(accessResult);
|
||||||
|
|
||||||
|
|
||||||
|
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
|
||||||
|
claimsPrincipal, resource);
|
||||||
|
|
||||||
|
await sutProvider.Sut.HandleAsync(authzContext);
|
||||||
|
|
||||||
|
Assert.True(authzContext.HasSucceeded);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SetupUserSubstitutes(
|
||||||
|
SutProvider<ProjectServiceAccountsAccessPoliciesAuthorizationHandler> sutProvider,
|
||||||
|
AccessClientType accessClientType,
|
||||||
|
ProjectServiceAccountsAccessPoliciesUpdates resource,
|
||||||
|
Guid userId = new())
|
||||||
|
{
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(resource.OrganizationId)
|
||||||
|
.Returns(true);
|
||||||
|
sutProvider.GetDependency<IAccessClientQuery>().GetAccessClientAsync(default, resource.OrganizationId)
|
||||||
|
.ReturnsForAnyArgs((accessClientType, userId));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SetupServiceAccountsAccessTest(
|
||||||
|
SutProvider<ProjectServiceAccountsAccessPoliciesAuthorizationHandler> sutProvider,
|
||||||
|
AccessClientType accessClientType,
|
||||||
|
ProjectServiceAccountsAccessPoliciesUpdates resource,
|
||||||
|
Guid userId = new())
|
||||||
|
{
|
||||||
|
SetupUserSubstitutes(sutProvider, accessClientType, resource, userId);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IProjectRepository>()
|
||||||
|
.AccessToProjectAsync(resource.ProjectId, userId, accessClientType)
|
||||||
|
.Returns((true, true));
|
||||||
|
sutProvider.GetDependency<IServiceAccountRepository>()
|
||||||
|
.ServiceAccountsAreInOrganizationAsync(Arg.Any<List<Guid>>(), resource.OrganizationId)
|
||||||
|
.Returns(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ProjectServiceAccountsAccessPoliciesUpdates AddServiceAccountCreateUpdate(
|
||||||
|
ProjectServiceAccountsAccessPoliciesUpdates resource)
|
||||||
|
{
|
||||||
|
resource.ServiceAccountAccessPolicyUpdates = resource.ServiceAccountAccessPolicyUpdates.Append(
|
||||||
|
new ServiceAccountProjectAccessPolicyUpdate
|
||||||
|
{
|
||||||
|
AccessPolicy = new ServiceAccountProjectAccessPolicy
|
||||||
|
{
|
||||||
|
ServiceAccountId = Guid.NewGuid(),
|
||||||
|
GrantedProjectId = resource.ProjectId,
|
||||||
|
Read = true,
|
||||||
|
Write = true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return resource;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ProjectServiceAccountsAccessPoliciesUpdates RemoveAllCreates(
|
||||||
|
ProjectServiceAccountsAccessPoliciesUpdates resource)
|
||||||
|
{
|
||||||
|
resource.ServiceAccountAccessPolicyUpdates =
|
||||||
|
resource.ServiceAccountAccessPolicyUpdates.Where(x => x.Operation != AccessPolicyOperation.Create);
|
||||||
|
return resource;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,42 @@
|
|||||||
|
using Bit.Commercial.Core.SecretsManager.Commands.AccessPolicies;
|
||||||
|
using Bit.Core.SecretsManager.Models.Data.AccessPolicyUpdates;
|
||||||
|
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 UpdateProjectServiceAccountsAccessPoliciesCommandTests
|
||||||
|
{
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task UpdateAsync_NoUpdates_DoesNotCallRepository(
|
||||||
|
SutProvider<UpdateProjectServiceAccountsAccessPoliciesCommand> sutProvider,
|
||||||
|
ProjectServiceAccountsAccessPoliciesUpdates data)
|
||||||
|
{
|
||||||
|
data.ServiceAccountAccessPolicyUpdates = [];
|
||||||
|
await sutProvider.Sut.UpdateAsync(data);
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IAccessPolicyRepository>()
|
||||||
|
.DidNotReceiveWithAnyArgs()
|
||||||
|
.UpdateProjectServiceAccountsAccessPoliciesAsync(Arg.Any<ProjectServiceAccountsAccessPoliciesUpdates>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task UpdateAsync_HasUpdates_CallsRepository(
|
||||||
|
SutProvider<UpdateProjectServiceAccountsAccessPoliciesCommand> sutProvider,
|
||||||
|
ProjectServiceAccountsAccessPoliciesUpdates data)
|
||||||
|
{
|
||||||
|
await sutProvider.Sut.UpdateAsync(data);
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IAccessPolicyRepository>()
|
||||||
|
.Received(1)
|
||||||
|
.UpdateProjectServiceAccountsAccessPoliciesAsync(Arg.Any<ProjectServiceAccountsAccessPoliciesUpdates>());
|
||||||
|
}
|
||||||
|
}
|
@ -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 ProjectServiceAccountsAccessPoliciesUpdatesQueryTests
|
||||||
|
{
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task GetAsync_NoCurrentAccessPolicies_ReturnsAllCreates(
|
||||||
|
SutProvider<ProjectServiceAccountsAccessPoliciesUpdatesQuery> sutProvider,
|
||||||
|
ProjectServiceAccountsAccessPolicies data)
|
||||||
|
{
|
||||||
|
sutProvider.GetDependency<IAccessPolicyRepository>()
|
||||||
|
.GetProjectServiceAccountsAccessPoliciesAsync(data.ProjectId)
|
||||||
|
.ReturnsNullForAnyArgs();
|
||||||
|
|
||||||
|
var result = await sutProvider.Sut.GetAsync(data);
|
||||||
|
|
||||||
|
Assert.Equal(data.ProjectId, result.ProjectId);
|
||||||
|
Assert.Equal(data.OrganizationId, result.OrganizationId);
|
||||||
|
Assert.Equal(data.ServiceAccountAccessPolicies.Count(), result.ServiceAccountAccessPolicyUpdates.Count());
|
||||||
|
Assert.All(result.ServiceAccountAccessPolicyUpdates, p =>
|
||||||
|
{
|
||||||
|
Assert.Equal(AccessPolicyOperation.Create, p.Operation);
|
||||||
|
Assert.Contains(data.ServiceAccountAccessPolicies, x => x == p.AccessPolicy);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task GetAsync_CurrentAccessPolicies_ReturnsChanges(
|
||||||
|
SutProvider<ProjectServiceAccountsAccessPoliciesUpdatesQuery> sutProvider,
|
||||||
|
ProjectServiceAccountsAccessPolicies data, ServiceAccountProjectAccessPolicy currentPolicyToDelete)
|
||||||
|
{
|
||||||
|
foreach (var policy in data.ServiceAccountAccessPolicies)
|
||||||
|
{
|
||||||
|
policy.GrantedProjectId = data.ProjectId;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentPolicyToDelete.GrantedProjectId = data.ProjectId;
|
||||||
|
|
||||||
|
var updatePolicy = new ServiceAccountProjectAccessPolicy
|
||||||
|
{
|
||||||
|
ServiceAccountId = data.ServiceAccountAccessPolicies.First().ServiceAccountId,
|
||||||
|
GrantedProjectId = data.ProjectId,
|
||||||
|
Read = !data.ServiceAccountAccessPolicies.First().Read,
|
||||||
|
Write = !data.ServiceAccountAccessPolicies.First().Write
|
||||||
|
};
|
||||||
|
|
||||||
|
var currentPolicies = new ProjectServiceAccountsAccessPolicies
|
||||||
|
{
|
||||||
|
ProjectId = data.ProjectId,
|
||||||
|
OrganizationId = data.OrganizationId,
|
||||||
|
ServiceAccountAccessPolicies = [updatePolicy, currentPolicyToDelete]
|
||||||
|
};
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IAccessPolicyRepository>()
|
||||||
|
.GetProjectServiceAccountsAccessPoliciesAsync(data.ProjectId)
|
||||||
|
.ReturnsForAnyArgs(currentPolicies);
|
||||||
|
|
||||||
|
var result = await sutProvider.Sut.GetAsync(data);
|
||||||
|
|
||||||
|
Assert.Equal(data.ProjectId, result.ProjectId);
|
||||||
|
Assert.Equal(data.OrganizationId, result.OrganizationId);
|
||||||
|
Assert.Single(result.ServiceAccountAccessPolicyUpdates.Where(x =>
|
||||||
|
x.Operation == AccessPolicyOperation.Delete && x.AccessPolicy == currentPolicyToDelete));
|
||||||
|
Assert.Single(result.ServiceAccountAccessPolicyUpdates.Where(x =>
|
||||||
|
x.Operation == AccessPolicyOperation.Update &&
|
||||||
|
x.AccessPolicy.GrantedProjectId == updatePolicy.GrantedProjectId));
|
||||||
|
Assert.Equal(result.ServiceAccountAccessPolicyUpdates.Count() - 2,
|
||||||
|
result.ServiceAccountAccessPolicyUpdates.Count(x => x.Operation == AccessPolicyOperation.Create));
|
||||||
|
}
|
||||||
|
}
|
@ -29,8 +29,10 @@ public class AccessPoliciesController : Controller
|
|||||||
private readonly IServiceAccountRepository _serviceAccountRepository;
|
private readonly IServiceAccountRepository _serviceAccountRepository;
|
||||||
private readonly IUpdateAccessPolicyCommand _updateAccessPolicyCommand;
|
private readonly IUpdateAccessPolicyCommand _updateAccessPolicyCommand;
|
||||||
private readonly IUpdateServiceAccountGrantedPoliciesCommand _updateServiceAccountGrantedPoliciesCommand;
|
private readonly IUpdateServiceAccountGrantedPoliciesCommand _updateServiceAccountGrantedPoliciesCommand;
|
||||||
|
private readonly IUpdateProjectServiceAccountsAccessPoliciesCommand _updateProjectServiceAccountsAccessPoliciesCommand;
|
||||||
private readonly IAccessClientQuery _accessClientQuery;
|
private readonly IAccessClientQuery _accessClientQuery;
|
||||||
private readonly IServiceAccountGrantedPolicyUpdatesQuery _serviceAccountGrantedPolicyUpdatesQuery;
|
private readonly IServiceAccountGrantedPolicyUpdatesQuery _serviceAccountGrantedPolicyUpdatesQuery;
|
||||||
|
private readonly IProjectServiceAccountsAccessPoliciesUpdatesQuery _projectServiceAccountsAccessPoliciesUpdatesQuery;
|
||||||
private readonly IUserService _userService;
|
private readonly IUserService _userService;
|
||||||
private readonly IAuthorizationService _authorizationService;
|
private readonly IAuthorizationService _authorizationService;
|
||||||
|
|
||||||
@ -43,9 +45,11 @@ public class AccessPoliciesController : Controller
|
|||||||
IProjectRepository projectRepository,
|
IProjectRepository projectRepository,
|
||||||
IAccessClientQuery accessClientQuery,
|
IAccessClientQuery accessClientQuery,
|
||||||
IServiceAccountGrantedPolicyUpdatesQuery serviceAccountGrantedPolicyUpdatesQuery,
|
IServiceAccountGrantedPolicyUpdatesQuery serviceAccountGrantedPolicyUpdatesQuery,
|
||||||
|
IProjectServiceAccountsAccessPoliciesUpdatesQuery projectServiceAccountsAccessPoliciesUpdatesQuery,
|
||||||
IUpdateServiceAccountGrantedPoliciesCommand updateServiceAccountGrantedPoliciesCommand,
|
IUpdateServiceAccountGrantedPoliciesCommand updateServiceAccountGrantedPoliciesCommand,
|
||||||
ICreateAccessPoliciesCommand createAccessPoliciesCommand,
|
ICreateAccessPoliciesCommand createAccessPoliciesCommand,
|
||||||
IDeleteAccessPolicyCommand deleteAccessPolicyCommand,
|
IDeleteAccessPolicyCommand deleteAccessPolicyCommand,
|
||||||
|
IUpdateProjectServiceAccountsAccessPoliciesCommand updateProjectServiceAccountsAccessPoliciesCommand,
|
||||||
IUpdateAccessPolicyCommand updateAccessPolicyCommand)
|
IUpdateAccessPolicyCommand updateAccessPolicyCommand)
|
||||||
{
|
{
|
||||||
_authorizationService = authorizationService;
|
_authorizationService = authorizationService;
|
||||||
@ -60,6 +64,8 @@ public class AccessPoliciesController : Controller
|
|||||||
_updateServiceAccountGrantedPoliciesCommand = updateServiceAccountGrantedPoliciesCommand;
|
_updateServiceAccountGrantedPoliciesCommand = updateServiceAccountGrantedPoliciesCommand;
|
||||||
_accessClientQuery = accessClientQuery;
|
_accessClientQuery = accessClientQuery;
|
||||||
_serviceAccountGrantedPolicyUpdatesQuery = serviceAccountGrantedPolicyUpdatesQuery;
|
_serviceAccountGrantedPolicyUpdatesQuery = serviceAccountGrantedPolicyUpdatesQuery;
|
||||||
|
_projectServiceAccountsAccessPoliciesUpdatesQuery = projectServiceAccountsAccessPoliciesUpdatesQuery;
|
||||||
|
_updateProjectServiceAccountsAccessPoliciesCommand = updateProjectServiceAccountsAccessPoliciesCommand;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("/projects/{id}/access-policies")]
|
[HttpPost("/projects/{id}/access-policies")]
|
||||||
@ -296,6 +302,41 @@ public class AccessPoliciesController : Controller
|
|||||||
return await GetServiceAccountGrantedPoliciesAsync(serviceAccount);
|
return await GetServiceAccountGrantedPoliciesAsync(serviceAccount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("/projects/{id}/access-policies/service-accounts")]
|
||||||
|
public async Task<ProjectServiceAccountsAccessPoliciesResponseModel>
|
||||||
|
GetProjectServiceAccountsAccessPoliciesAsync(
|
||||||
|
[FromRoute] Guid id)
|
||||||
|
{
|
||||||
|
var project = await _projectRepository.GetByIdAsync(id);
|
||||||
|
await CheckUserHasWriteAccessToProjectAsync(project);
|
||||||
|
var results =
|
||||||
|
await _accessPolicyRepository.GetProjectServiceAccountsAccessPoliciesAsync(id);
|
||||||
|
return new ProjectServiceAccountsAccessPoliciesResponseModel(results);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPut("/projects/{id}/access-policies/service-accounts")]
|
||||||
|
public async Task<ProjectServiceAccountsAccessPoliciesResponseModel>
|
||||||
|
PutProjectServiceAccountsAccessPoliciesAsync([FromRoute] Guid id,
|
||||||
|
[FromBody] ProjectServiceAccountsAccessPoliciesRequestModel request)
|
||||||
|
{
|
||||||
|
var project = await _projectRepository.GetByIdAsync(id) ?? throw new NotFoundException();
|
||||||
|
var accessPoliciesUpdates =
|
||||||
|
await _projectServiceAccountsAccessPoliciesUpdatesQuery.GetAsync(
|
||||||
|
request.ToProjectServiceAccountsAccessPolicies(project));
|
||||||
|
|
||||||
|
var authorizationResult = await _authorizationService.AuthorizeAsync(User, accessPoliciesUpdates,
|
||||||
|
ProjectServiceAccountsAccessPoliciesOperations.Updates);
|
||||||
|
if (!authorizationResult.Succeeded)
|
||||||
|
{
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
await _updateProjectServiceAccountsAccessPoliciesCommand.UpdateAsync(accessPoliciesUpdates);
|
||||||
|
|
||||||
|
var results = await _accessPolicyRepository.GetProjectServiceAccountsAccessPoliciesAsync(id);
|
||||||
|
return new ProjectServiceAccountsAccessPoliciesResponseModel(results);
|
||||||
|
}
|
||||||
|
|
||||||
private async Task<(AccessClientType AccessClientType, Guid UserId)> CheckUserHasWriteAccessToProjectAsync(Project project)
|
private async Task<(AccessClientType AccessClientType, Guid UserId)> CheckUserHasWriteAccessToProjectAsync(Project project)
|
||||||
{
|
{
|
||||||
if (project == null)
|
if (project == null)
|
||||||
|
@ -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 ProjectServiceAccountsAccessPoliciesRequestModel
|
||||||
|
{
|
||||||
|
public required IEnumerable<AccessPolicyRequest> ServiceAccountAccessPolicyRequests { get; set; }
|
||||||
|
|
||||||
|
public ProjectServiceAccountsAccessPolicies ToProjectServiceAccountsAccessPolicies(Project project)
|
||||||
|
{
|
||||||
|
var serviceAccountAccessPolicies = ServiceAccountAccessPolicyRequests
|
||||||
|
.Select(x => x.ToServiceAccountProjectAccessPolicy(project.Id, project.OrganizationId))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
AccessPolicyHelpers.CheckForDistinctAccessPolicies(serviceAccountAccessPolicies);
|
||||||
|
AccessPolicyHelpers.CheckAccessPoliciesHaveReadPermission(serviceAccountAccessPolicies);
|
||||||
|
|
||||||
|
return new ProjectServiceAccountsAccessPolicies
|
||||||
|
{
|
||||||
|
ProjectId = project.Id,
|
||||||
|
OrganizationId = project.OrganizationId,
|
||||||
|
ServiceAccountAccessPolicies = serviceAccountAccessPolicies
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,29 @@
|
|||||||
|
#nullable enable
|
||||||
|
using Bit.Core.Models.Api;
|
||||||
|
using Bit.Core.SecretsManager.Models.Data;
|
||||||
|
|
||||||
|
namespace Bit.Api.SecretsManager.Models.Response;
|
||||||
|
|
||||||
|
public class ProjectServiceAccountsAccessPoliciesResponseModel : ResponseModel
|
||||||
|
{
|
||||||
|
private const string _objectName = "ProjectServiceAccountsAccessPolicies";
|
||||||
|
|
||||||
|
public ProjectServiceAccountsAccessPoliciesResponseModel(
|
||||||
|
ProjectServiceAccountsAccessPolicies? projectServiceAccountsAccessPolicies)
|
||||||
|
: base(_objectName)
|
||||||
|
{
|
||||||
|
if (projectServiceAccountsAccessPolicies == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ServiceAccountAccessPolicies = projectServiceAccountsAccessPolicies.ServiceAccountAccessPolicies
|
||||||
|
.Select(x => new ServiceAccountProjectAccessPolicyResponseModel(x)).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ProjectServiceAccountsAccessPoliciesResponseModel() : base(_objectName)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ServiceAccountProjectAccessPolicyResponseModel> ServiceAccountAccessPolicies { get; set; } = [];
|
||||||
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
#nullable enable
|
||||||
|
using Microsoft.AspNetCore.Authorization.Infrastructure;
|
||||||
|
|
||||||
|
namespace Bit.Core.SecretsManager.AuthorizationRequirements;
|
||||||
|
|
||||||
|
public class ProjectServiceAccountsAccessPoliciesOperationRequirement : OperationAuthorizationRequirement
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class ProjectServiceAccountsAccessPoliciesOperations
|
||||||
|
{
|
||||||
|
public static readonly ProjectServiceAccountsAccessPoliciesOperationRequirement Updates = new() { Name = nameof(Updates) };
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
#nullable enable
|
||||||
|
using Bit.Core.SecretsManager.Models.Data.AccessPolicyUpdates;
|
||||||
|
|
||||||
|
namespace Bit.Core.SecretsManager.Commands.AccessPolicies.Interfaces;
|
||||||
|
|
||||||
|
public interface IUpdateProjectServiceAccountsAccessPoliciesCommand
|
||||||
|
{
|
||||||
|
Task UpdateAsync(ProjectServiceAccountsAccessPoliciesUpdates accessPoliciesUpdates);
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
#nullable enable
|
||||||
|
namespace Bit.Core.SecretsManager.Models.Data.AccessPolicyUpdates;
|
||||||
|
|
||||||
|
public class ProjectServiceAccountsAccessPoliciesUpdates
|
||||||
|
{
|
||||||
|
public Guid ProjectId { get; set; }
|
||||||
|
public Guid OrganizationId { get; set; }
|
||||||
|
public IEnumerable<ServiceAccountProjectAccessPolicyUpdate> ServiceAccountAccessPolicyUpdates { get; set; } = [];
|
||||||
|
}
|
@ -0,0 +1,80 @@
|
|||||||
|
#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 ProjectServiceAccountsAccessPolicies
|
||||||
|
{
|
||||||
|
public ProjectServiceAccountsAccessPolicies()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public ProjectServiceAccountsAccessPolicies(Guid projectId,
|
||||||
|
IEnumerable<BaseAccessPolicy> policies)
|
||||||
|
{
|
||||||
|
ProjectId = projectId;
|
||||||
|
ServiceAccountAccessPolicies = policies
|
||||||
|
.OfType<ServiceAccountProjectAccessPolicy>()
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var project = ServiceAccountAccessPolicies.FirstOrDefault()?.GrantedProject;
|
||||||
|
if (project != null)
|
||||||
|
{
|
||||||
|
OrganizationId = project.OrganizationId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Guid ProjectId { get; set; }
|
||||||
|
public Guid OrganizationId { get; set; }
|
||||||
|
public IEnumerable<ServiceAccountProjectAccessPolicy> ServiceAccountAccessPolicies { get; set; } = [];
|
||||||
|
|
||||||
|
public ProjectServiceAccountsAccessPoliciesUpdates GetPolicyUpdates(ProjectServiceAccountsAccessPolicies requested)
|
||||||
|
{
|
||||||
|
var currentServiceAccountIds = GetServiceAccountIds(ServiceAccountAccessPolicies);
|
||||||
|
var requestedServiceAccountIds = GetServiceAccountIds(requested.ServiceAccountAccessPolicies);
|
||||||
|
|
||||||
|
var serviceAccountIdsToBeDeleted = currentServiceAccountIds.Except(requestedServiceAccountIds).ToList();
|
||||||
|
var serviceAccountIdsToBeCreated = requestedServiceAccountIds.Except(currentServiceAccountIds).ToList();
|
||||||
|
var serviceAccountIdsToBeUpdated = GetServiceAccountIdsToBeUpdated(requested);
|
||||||
|
|
||||||
|
var policiesToBeDeleted =
|
||||||
|
CreatePolicyUpdates(ServiceAccountAccessPolicies, serviceAccountIdsToBeDeleted,
|
||||||
|
AccessPolicyOperation.Delete);
|
||||||
|
var policiesToBeCreated = CreatePolicyUpdates(requested.ServiceAccountAccessPolicies,
|
||||||
|
serviceAccountIdsToBeCreated,
|
||||||
|
AccessPolicyOperation.Create);
|
||||||
|
var policiesToBeUpdated = CreatePolicyUpdates(requested.ServiceAccountAccessPolicies,
|
||||||
|
serviceAccountIdsToBeUpdated,
|
||||||
|
AccessPolicyOperation.Update);
|
||||||
|
|
||||||
|
return new ProjectServiceAccountsAccessPoliciesUpdates
|
||||||
|
{
|
||||||
|
OrganizationId = OrganizationId,
|
||||||
|
ProjectId = ProjectId,
|
||||||
|
ServiceAccountAccessPolicyUpdates =
|
||||||
|
policiesToBeDeleted.Concat(policiesToBeCreated).Concat(policiesToBeUpdated)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<ServiceAccountProjectAccessPolicyUpdate> CreatePolicyUpdates(
|
||||||
|
IEnumerable<ServiceAccountProjectAccessPolicy> policies, List<Guid> serviceAccountIds,
|
||||||
|
AccessPolicyOperation operation) =>
|
||||||
|
policies
|
||||||
|
.Where(ap => serviceAccountIds.Contains(ap.ServiceAccountId!.Value))
|
||||||
|
.Select(ap => new ServiceAccountProjectAccessPolicyUpdate { Operation = operation, AccessPolicy = ap })
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
private List<Guid> GetServiceAccountIdsToBeUpdated(ProjectServiceAccountsAccessPolicies requested) =>
|
||||||
|
ServiceAccountAccessPolicies
|
||||||
|
.Where(currentAp => requested.ServiceAccountAccessPolicies.Any(requestedAp =>
|
||||||
|
requestedAp.GrantedProjectId == currentAp.GrantedProjectId &&
|
||||||
|
requestedAp.ServiceAccountId == currentAp.ServiceAccountId &&
|
||||||
|
(requestedAp.Write != currentAp.Write || requestedAp.Read != currentAp.Read)))
|
||||||
|
.Select(ap => ap.ServiceAccountId!.Value)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
private static List<Guid> GetServiceAccountIds(IEnumerable<ServiceAccountProjectAccessPolicy> policies) =>
|
||||||
|
policies.Select(ap => ap.ServiceAccountId!.Value).ToList();
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
#nullable enable
|
||||||
|
using Bit.Core.SecretsManager.Models.Data;
|
||||||
|
using Bit.Core.SecretsManager.Models.Data.AccessPolicyUpdates;
|
||||||
|
|
||||||
|
namespace Bit.Core.SecretsManager.Queries.AccessPolicies.Interfaces;
|
||||||
|
|
||||||
|
public interface IProjectServiceAccountsAccessPoliciesUpdatesQuery
|
||||||
|
{
|
||||||
|
Task<ProjectServiceAccountsAccessPoliciesUpdates> GetAsync(ProjectServiceAccountsAccessPolicies grantedPolicies);
|
||||||
|
}
|
@ -2,6 +2,7 @@
|
|||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.SecretsManager.Entities;
|
using Bit.Core.SecretsManager.Entities;
|
||||||
using Bit.Core.SecretsManager.Models.Data;
|
using Bit.Core.SecretsManager.Models.Data;
|
||||||
|
using Bit.Core.SecretsManager.Models.Data.AccessPolicyUpdates;
|
||||||
|
|
||||||
namespace Bit.Core.SecretsManager.Repositories;
|
namespace Bit.Core.SecretsManager.Repositories;
|
||||||
|
|
||||||
@ -22,4 +23,6 @@ public interface IAccessPolicyRepository
|
|||||||
Task<ServiceAccountGrantedPoliciesPermissionDetails?> GetServiceAccountGrantedPoliciesPermissionDetailsAsync(
|
Task<ServiceAccountGrantedPoliciesPermissionDetails?> GetServiceAccountGrantedPoliciesPermissionDetailsAsync(
|
||||||
Guid serviceAccountId, Guid userId, AccessClientType accessClientType);
|
Guid serviceAccountId, Guid userId, AccessClientType accessClientType);
|
||||||
Task UpdateServiceAccountGrantedPoliciesAsync(ServiceAccountGrantedPoliciesUpdates policyUpdates);
|
Task UpdateServiceAccountGrantedPoliciesAsync(ServiceAccountGrantedPoliciesUpdates policyUpdates);
|
||||||
|
Task<ProjectServiceAccountsAccessPolicies?> GetProjectServiceAccountsAccessPoliciesAsync(Guid projectId);
|
||||||
|
Task UpdateProjectServiceAccountsAccessPoliciesAsync(ProjectServiceAccountsAccessPoliciesUpdates updates);
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,9 @@ public interface IServiceAccountRepository
|
|||||||
Task<bool> UserHasWriteAccessToServiceAccount(Guid id, Guid userId);
|
Task<bool> UserHasWriteAccessToServiceAccount(Guid id, Guid userId);
|
||||||
Task<IEnumerable<ServiceAccount>> GetManyByOrganizationIdWriteAccessAsync(Guid organizationId, Guid userId, AccessClientType accessType);
|
Task<IEnumerable<ServiceAccount>> GetManyByOrganizationIdWriteAccessAsync(Guid organizationId, Guid userId, AccessClientType accessType);
|
||||||
Task<(bool Read, bool Write)> AccessToServiceAccountAsync(Guid id, Guid userId, AccessClientType accessType);
|
Task<(bool Read, bool Write)> AccessToServiceAccountAsync(Guid id, Guid userId, AccessClientType accessType);
|
||||||
|
Task<Dictionary<Guid, (bool Read, bool Write)>> AccessToServiceAccountsAsync(IEnumerable<Guid> ids, Guid userId,
|
||||||
|
AccessClientType accessType);
|
||||||
Task<int> GetServiceAccountCountByOrganizationIdAsync(Guid organizationId);
|
Task<int> GetServiceAccountCountByOrganizationIdAsync(Guid organizationId);
|
||||||
Task<IEnumerable<ServiceAccountSecretsDetails>> GetManyByOrganizationIdWithSecretsDetailsAsync(Guid organizationId, Guid userId, AccessClientType accessType);
|
Task<IEnumerable<ServiceAccountSecretsDetails>> GetManyByOrganizationIdWithSecretsDetailsAsync(Guid organizationId, Guid userId, AccessClientType accessType);
|
||||||
|
Task<bool> ServiceAccountsAreInOrganizationAsync(List<Guid> serviceAccountIds, Guid organizationId);
|
||||||
}
|
}
|
||||||
|
@ -53,10 +53,25 @@ public class NoopServiceAccountRepository : IServiceAccountRepository
|
|||||||
return Task.FromResult((false, false));
|
return Task.FromResult((false, false));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Task<Dictionary<Guid, (bool Read, bool Write)>> AccessToServiceAccountsAsync(IEnumerable<Guid> ids,
|
||||||
|
Guid userId, AccessClientType accessType)
|
||||||
|
{
|
||||||
|
return Task.FromResult(null as Dictionary<Guid, (bool Read, bool Write)>);
|
||||||
|
}
|
||||||
|
|
||||||
public Task<int> GetServiceAccountCountByOrganizationIdAsync(Guid organizationId)
|
public Task<int> GetServiceAccountCountByOrganizationIdAsync(Guid organizationId)
|
||||||
{
|
{
|
||||||
return Task.FromResult(0);
|
return Task.FromResult(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<IEnumerable<ServiceAccountSecretsDetails>> GetManyByOrganizationIdWithSecretsDetailsAsync(Guid organizationId, Guid userId, AccessClientType accessType) => throw new NotImplementedException();
|
public Task<IEnumerable<ServiceAccountSecretsDetails>> GetManyByOrganizationIdWithSecretsDetailsAsync(
|
||||||
|
Guid organizationId, Guid userId, AccessClientType accessType)
|
||||||
|
{
|
||||||
|
return Task.FromResult(null as IEnumerable<ServiceAccountSecretsDetails>);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<bool> ServiceAccountsAreInOrganizationAsync(List<Guid> serviceAccountIds, Guid organizationId)
|
||||||
|
{
|
||||||
|
return Task.FromResult(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1169,6 +1169,209 @@ public class AccessPoliciesControllerTests : IClassFixture<ApiApplicationFactory
|
|||||||
Assert.Single(result.GrantedProjectPolicies);
|
Assert.Single(result.GrantedProjectPolicies);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[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 GetProjectServiceAccountsAccessPoliciesAsync_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($"/projects/{initData.ProjectId}/access-policies/service-accounts");
|
||||||
|
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetProjectServiceAccountsAccessPoliciesAsync_NoAccessPolicies_ReturnsEmpty()
|
||||||
|
{
|
||||||
|
var (org, _) = await _organizationHelper.Initialize(true, true, true);
|
||||||
|
await _loginHelper.LoginAsync(_email);
|
||||||
|
|
||||||
|
var project = await _projectRepository.CreateAsync(new Project
|
||||||
|
{
|
||||||
|
OrganizationId = org.Id,
|
||||||
|
Name = _mockEncryptedString,
|
||||||
|
});
|
||||||
|
|
||||||
|
var response = await _client.GetAsync($"/projects/{project.Id}/access-policies/service-accounts");
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
var result = await response.Content.ReadFromJsonAsync<ProjectServiceAccountsAccessPoliciesResponseModel>();
|
||||||
|
|
||||||
|
Assert.NotNull(result);
|
||||||
|
Assert.Empty(result.ServiceAccountAccessPolicies);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetProjectServiceAccountsAccessPoliciesAsync_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($"/projects/{initData.ProjectId}/access-policies/service-accounts");
|
||||||
|
|
||||||
|
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(PermissionType.RunAsAdmin)]
|
||||||
|
[InlineData(PermissionType.RunAsUserWithPermission)]
|
||||||
|
public async Task GetProjectServiceAccountsAccessPoliciesAsync_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 UserProjectAccessPolicy
|
||||||
|
{
|
||||||
|
GrantedProjectId = initData.ProjectId, OrganizationUserId = orgUser.Id, Read = true, Write = true,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
await _accessPolicyRepository.CreateManyAsync(accessPolicies);
|
||||||
|
}
|
||||||
|
|
||||||
|
var response = await _client.GetAsync($"/projects/{initData.ProjectId}/access-policies/service-accounts");
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
var result = await response.Content
|
||||||
|
.ReadFromJsonAsync<ProjectServiceAccountsAccessPoliciesResponseModel>();
|
||||||
|
|
||||||
|
Assert.NotNull(result);
|
||||||
|
Assert.NotEmpty(result.ServiceAccountAccessPolicies);
|
||||||
|
Assert.Equal(initData.ServiceAccountId, result.ServiceAccountAccessPolicies.First().ServiceAccountId);
|
||||||
|
Assert.NotNull(result.ServiceAccountAccessPolicies.First().ServiceAccountName);
|
||||||
|
Assert.NotNull(result.ServiceAccountAccessPolicies.First().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 PutProjectServiceAccountsAccessPoliciesAsync_SmNotEnabled_NotFound(bool useSecrets,
|
||||||
|
bool accessSecrets, bool organizationEnabled)
|
||||||
|
{
|
||||||
|
var (_, organizationUser) =
|
||||||
|
await _organizationHelper.Initialize(useSecrets, accessSecrets, organizationEnabled);
|
||||||
|
await _loginHelper.LoginAsync(_email);
|
||||||
|
|
||||||
|
var (projectId, serviceAccountId) = await CreateProjectAndServiceAccountAsync(organizationUser.OrganizationId);
|
||||||
|
|
||||||
|
var request = new ProjectServiceAccountsAccessPoliciesRequestModel
|
||||||
|
{
|
||||||
|
ServiceAccountAccessPolicyRequests =
|
||||||
|
[
|
||||||
|
new AccessPolicyRequest { GranteeId = serviceAccountId, Read = true, Write = true }
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
var response = await _client.PutAsJsonAsync($"/projects/{projectId}/access-policies/service-accounts", request);
|
||||||
|
|
||||||
|
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task PutProjectServiceAccountsAccessPoliciesAsync_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 ProjectServiceAccountsAccessPoliciesRequestModel
|
||||||
|
{
|
||||||
|
ServiceAccountAccessPolicyRequests =
|
||||||
|
[
|
||||||
|
new AccessPolicyRequest { GranteeId = serviceAccountId, Read = true, Write = true }
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
var response = await _client.PutAsJsonAsync($"/projects/{projectId}/access-policies/service-accounts", request);
|
||||||
|
|
||||||
|
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(PermissionType.RunAsAdmin)]
|
||||||
|
[InlineData(PermissionType.RunAsUserWithPermission)]
|
||||||
|
public async Task PutProjectServiceAccountsAccessPoliciesAsync_MismatchedOrgIds_ReturnsNotFound(
|
||||||
|
PermissionType permissionType)
|
||||||
|
{
|
||||||
|
var (_, organizationUser) = await _organizationHelper.Initialize(true, true, true);
|
||||||
|
await _loginHelper.LoginAsync(_email);
|
||||||
|
|
||||||
|
var (project, request) =
|
||||||
|
await SetupProjectServiceAccountsAccessPoliciesRequestAsync(permissionType, organizationUser,
|
||||||
|
false);
|
||||||
|
|
||||||
|
var newOrg = await _organizationHelper.CreateSmOrganizationAsync();
|
||||||
|
|
||||||
|
var serviceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount
|
||||||
|
{
|
||||||
|
Name = _mockEncryptedString,
|
||||||
|
OrganizationId = newOrg.Id
|
||||||
|
});
|
||||||
|
request.ServiceAccountAccessPolicyRequests =
|
||||||
|
[
|
||||||
|
new AccessPolicyRequest { GranteeId = serviceAccount.Id, Read = true, Write = true }
|
||||||
|
];
|
||||||
|
|
||||||
|
var response =
|
||||||
|
await _client.PutAsJsonAsync($"/projects/{project.Id}/access-policies/service-accounts", 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 PutProjectServiceAccountsAccessPoliciesAsync_Success(PermissionType permissionType,
|
||||||
|
bool createPreviousAccessPolicy)
|
||||||
|
{
|
||||||
|
var (_, organizationUser) = await _organizationHelper.Initialize(true, true, true);
|
||||||
|
await _loginHelper.LoginAsync(_email);
|
||||||
|
|
||||||
|
var (project, request) =
|
||||||
|
await SetupProjectServiceAccountsAccessPoliciesRequestAsync(permissionType, organizationUser,
|
||||||
|
createPreviousAccessPolicy);
|
||||||
|
|
||||||
|
var response =
|
||||||
|
await _client.PutAsJsonAsync($"/projects/{project.Id}/access-policies/service-accounts", request);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
var result = await response.Content
|
||||||
|
.ReadFromJsonAsync<ProjectServiceAccountsAccessPoliciesResponseModel>();
|
||||||
|
|
||||||
|
Assert.NotNull(result);
|
||||||
|
Assert.Equal(request.ServiceAccountAccessPolicyRequests.First().GranteeId,
|
||||||
|
result.ServiceAccountAccessPolicies.First().ServiceAccountId);
|
||||||
|
Assert.True(result.ServiceAccountAccessPolicies.First().Read);
|
||||||
|
Assert.True(result.ServiceAccountAccessPolicies.First().Write);
|
||||||
|
Assert.Single(result.ServiceAccountAccessPolicies);
|
||||||
|
}
|
||||||
|
|
||||||
private async Task<RequestSetupData> SetupAccessPolicyRequest(Guid organizationId)
|
private async Task<RequestSetupData> SetupAccessPolicyRequest(Guid organizationId)
|
||||||
{
|
{
|
||||||
var project = await _projectRepository.CreateAsync(new Project
|
var project = await _projectRepository.CreateAsync(new Project
|
||||||
@ -1184,13 +1387,15 @@ public class AccessPoliciesControllerTests : IClassFixture<ApiApplicationFactory
|
|||||||
});
|
});
|
||||||
|
|
||||||
var accessPolicy = await _accessPolicyRepository.CreateManyAsync(
|
var accessPolicy = await _accessPolicyRepository.CreateManyAsync(
|
||||||
new List<BaseAccessPolicy>
|
[
|
||||||
|
new ServiceAccountProjectAccessPolicy
|
||||||
{
|
{
|
||||||
new ServiceAccountProjectAccessPolicy
|
Read = true,
|
||||||
{
|
Write = true,
|
||||||
Read = true, Write = true, ServiceAccountId = serviceAccount.Id, GrantedProjectId = project.Id,
|
ServiceAccountId = serviceAccount.Id,
|
||||||
},
|
GrantedProjectId = project.Id,
|
||||||
});
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
return new RequestSetupData
|
return new RequestSetupData
|
||||||
{
|
{
|
||||||
@ -1395,6 +1600,65 @@ public class AccessPoliciesControllerTests : IClassFixture<ApiApplicationFactory
|
|||||||
return (serviceAccount, request);
|
return (serviceAccount, request);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<(Project project, ProjectServiceAccountsAccessPoliciesRequestModel request)>
|
||||||
|
SetupProjectServiceAccountsAccessPoliciesRequestAsync(
|
||||||
|
PermissionType permissionType, OrganizationUser organizationUser, bool createPreviousAccessPolicy)
|
||||||
|
{
|
||||||
|
var (project, currentUser) = await SetupProjectPeoplePermissionAsync(permissionType, organizationUser);
|
||||||
|
var serviceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount
|
||||||
|
{
|
||||||
|
Name = _mockEncryptedString,
|
||||||
|
OrganizationId = currentUser.OrganizationId
|
||||||
|
});
|
||||||
|
|
||||||
|
var accessPolicies = new List<BaseAccessPolicy>
|
||||||
|
{
|
||||||
|
new UserServiceAccountAccessPolicy
|
||||||
|
{
|
||||||
|
GrantedServiceAccountId = serviceAccount.Id,
|
||||||
|
OrganizationUserId = currentUser.Id,
|
||||||
|
Read = true,
|
||||||
|
Write = true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var request = new ProjectServiceAccountsAccessPoliciesRequestModel
|
||||||
|
{
|
||||||
|
ServiceAccountAccessPolicyRequests =
|
||||||
|
[
|
||||||
|
new() { GranteeId = serviceAccount.Id, Read = true, Write = true }
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
if (createPreviousAccessPolicy)
|
||||||
|
{
|
||||||
|
var anotherServiceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount
|
||||||
|
{
|
||||||
|
Name = _mockEncryptedString,
|
||||||
|
OrganizationId = currentUser.OrganizationId
|
||||||
|
});
|
||||||
|
|
||||||
|
accessPolicies.Add(new UserServiceAccountAccessPolicy
|
||||||
|
{
|
||||||
|
GrantedServiceAccountId = anotherServiceAccount.Id,
|
||||||
|
OrganizationUserId = currentUser.Id,
|
||||||
|
Read = true,
|
||||||
|
Write = true
|
||||||
|
});
|
||||||
|
accessPolicies.Add(new ServiceAccountProjectAccessPolicy
|
||||||
|
{
|
||||||
|
GrantedProjectId = project.Id,
|
||||||
|
ServiceAccountId = anotherServiceAccount.Id,
|
||||||
|
Read = true,
|
||||||
|
Write = true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await _accessPolicyRepository.CreateManyAsync(accessPolicies);
|
||||||
|
|
||||||
|
return (project, request);
|
||||||
|
}
|
||||||
|
|
||||||
private class RequestSetupData
|
private class RequestSetupData
|
||||||
{
|
{
|
||||||
public Guid ProjectId { get; set; }
|
public Guid ProjectId { get; set; }
|
||||||
|
@ -6,9 +6,11 @@ using Bit.Api.Test.SecretsManager.Enums;
|
|||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
|
using Bit.Core.Identity;
|
||||||
using Bit.Core.SecretsManager.Commands.AccessPolicies.Interfaces;
|
using Bit.Core.SecretsManager.Commands.AccessPolicies.Interfaces;
|
||||||
using Bit.Core.SecretsManager.Entities;
|
using Bit.Core.SecretsManager.Entities;
|
||||||
using Bit.Core.SecretsManager.Models.Data;
|
using Bit.Core.SecretsManager.Models.Data;
|
||||||
|
using Bit.Core.SecretsManager.Models.Data.AccessPolicyUpdates;
|
||||||
using Bit.Core.SecretsManager.Queries.Interfaces;
|
using Bit.Core.SecretsManager.Queries.Interfaces;
|
||||||
using Bit.Core.SecretsManager.Repositories;
|
using Bit.Core.SecretsManager.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
@ -1082,6 +1084,195 @@ public class AccessPoliciesControllerTests
|
|||||||
.UpdateAsync(Arg.Any<ServiceAccountGrantedPoliciesUpdates>());
|
.UpdateAsync(Arg.Any<ServiceAccountGrantedPoliciesUpdates>());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task GetProjectServiceAccountsAccessPoliciesAsync_ProjectDoesntExist_ThrowsNotFound(
|
||||||
|
SutProvider<AccessPoliciesController> sutProvider,
|
||||||
|
ServiceAccount data)
|
||||||
|
{
|
||||||
|
sutProvider.GetDependency<IProjectRepository>().GetByIdAsync(data.Id).ReturnsNull();
|
||||||
|
|
||||||
|
await Assert.ThrowsAsync<NotFoundException>(() =>
|
||||||
|
sutProvider.Sut.GetProjectServiceAccountsAccessPoliciesAsync(data.Id));
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IAccessPolicyRepository>().Received(0)
|
||||||
|
.GetProjectServiceAccountsAccessPoliciesAsync(Arg.Any<Guid>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task GetProjectServiceAccountsAccessPoliciesAsync_NoAccess_ThrowsNotFound(
|
||||||
|
SutProvider<AccessPoliciesController> sutProvider,
|
||||||
|
Project data)
|
||||||
|
{
|
||||||
|
SetupUserWithoutPermission(sutProvider, data.OrganizationId);
|
||||||
|
sutProvider.GetDependency<IProjectRepository>().GetByIdAsync(data.Id).Returns(data);
|
||||||
|
sutProvider.GetDependency<IProjectRepository>().AccessToProjectAsync(default, default, default)
|
||||||
|
.ReturnsForAnyArgs((false, false));
|
||||||
|
|
||||||
|
await Assert.ThrowsAsync<NotFoundException>(() =>
|
||||||
|
sutProvider.Sut.GetProjectServiceAccountsAccessPoliciesAsync(data.Id));
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IAccessPolicyRepository>().Received(0)
|
||||||
|
.GetProjectServiceAccountsAccessPoliciesAsync(Arg.Any<Guid>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task GetProjectServiceAccountsAccessPoliciesAsync_ClientIsServiceAccount_ThrowsNotFound(
|
||||||
|
SutProvider<AccessPoliciesController> sutProvider,
|
||||||
|
Project data)
|
||||||
|
{
|
||||||
|
SetupUserWithoutPermission(sutProvider, data.OrganizationId);
|
||||||
|
sutProvider.GetDependency<IProjectRepository>().GetByIdAsync(data.Id).Returns(data);
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().ClientType = ClientType.ServiceAccount;
|
||||||
|
sutProvider.GetDependency<IProjectRepository>().AccessToProjectAsync(default, default, default)
|
||||||
|
.ReturnsForAnyArgs((true, true));
|
||||||
|
|
||||||
|
await Assert.ThrowsAsync<NotFoundException>(() =>
|
||||||
|
sutProvider.Sut.GetProjectServiceAccountsAccessPoliciesAsync(data.Id));
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IAccessPolicyRepository>().Received(0)
|
||||||
|
.GetProjectServiceAccountsAccessPoliciesAsync(Arg.Any<Guid>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task GetProjectServiceAccountsAccessPoliciesAsync_HasAccessNoPolicies_ReturnsEmptyList(
|
||||||
|
SutProvider<AccessPoliciesController> sutProvider,
|
||||||
|
Project data)
|
||||||
|
{
|
||||||
|
SetupUserWithoutPermission(sutProvider, data.OrganizationId);
|
||||||
|
sutProvider.GetDependency<IProjectRepository>().GetByIdAsync(data.Id).Returns(data);
|
||||||
|
sutProvider.GetDependency<IProjectRepository>().AccessToProjectAsync(default, default, default)
|
||||||
|
.ReturnsForAnyArgs((true, true));
|
||||||
|
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IAccessPolicyRepository>()
|
||||||
|
.GetProjectServiceAccountsAccessPoliciesAsync(Arg.Any<Guid>())
|
||||||
|
.ReturnsNullForAnyArgs();
|
||||||
|
|
||||||
|
var result = await sutProvider.Sut.GetProjectServiceAccountsAccessPoliciesAsync(data.Id);
|
||||||
|
|
||||||
|
Assert.Empty(result.ServiceAccountAccessPolicies);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task GetProjectServiceAccountsAccessPoliciesAsync_HasAccess_Success(
|
||||||
|
SutProvider<AccessPoliciesController> sutProvider,
|
||||||
|
ProjectServiceAccountsAccessPolicies policies,
|
||||||
|
Project data)
|
||||||
|
{
|
||||||
|
SetupUserWithoutPermission(sutProvider, data.OrganizationId);
|
||||||
|
sutProvider.GetDependency<IProjectRepository>().GetByIdAsync(data.Id).Returns(data);
|
||||||
|
sutProvider.GetDependency<IProjectRepository>().AccessToProjectAsync(default, default, default)
|
||||||
|
.ReturnsForAnyArgs((true, true));
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IAccessPolicyRepository>()
|
||||||
|
.GetProjectServiceAccountsAccessPoliciesAsync(Arg.Any<Guid>())
|
||||||
|
.ReturnsForAnyArgs(policies);
|
||||||
|
|
||||||
|
var result = await sutProvider.Sut.GetProjectServiceAccountsAccessPoliciesAsync(data.Id);
|
||||||
|
|
||||||
|
Assert.NotEmpty(result.ServiceAccountAccessPolicies);
|
||||||
|
Assert.Equal(policies.ServiceAccountAccessPolicies.Count(), result.ServiceAccountAccessPolicies.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task PutProjectServiceAccountsAccessPoliciesAsync_ProjectDoesNotExist_Throws(
|
||||||
|
SutProvider<AccessPoliciesController> sutProvider,
|
||||||
|
Project data,
|
||||||
|
ProjectServiceAccountsAccessPoliciesRequestModel request)
|
||||||
|
{
|
||||||
|
await Assert.ThrowsAsync<NotFoundException>(() =>
|
||||||
|
sutProvider.Sut.PutProjectServiceAccountsAccessPoliciesAsync(data.Id, request));
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IUpdateProjectServiceAccountsAccessPoliciesCommand>().DidNotReceiveWithAnyArgs()
|
||||||
|
.UpdateAsync(Arg.Any<ProjectServiceAccountsAccessPoliciesUpdates>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task PutProjectServiceAccountsAccessPoliciesAsync_DuplicatePolicyRequest_ThrowsBadRequestException(
|
||||||
|
SutProvider<AccessPoliciesController> sutProvider,
|
||||||
|
Project data,
|
||||||
|
ProjectServiceAccountsAccessPoliciesRequestModel request)
|
||||||
|
{
|
||||||
|
var dup = new AccessPolicyRequest { GranteeId = Guid.NewGuid(), Read = true, Write = true };
|
||||||
|
request.ServiceAccountAccessPolicyRequests = [dup, dup];
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IProjectRepository>().GetByIdAsync(data.Id).ReturnsForAnyArgs(data);
|
||||||
|
|
||||||
|
await Assert.ThrowsAsync<BadRequestException>(() =>
|
||||||
|
sutProvider.Sut.PutProjectServiceAccountsAccessPoliciesAsync(data.Id, request));
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IUpdateProjectServiceAccountsAccessPoliciesCommand>().DidNotReceiveWithAnyArgs()
|
||||||
|
.UpdateAsync(Arg.Any<ProjectServiceAccountsAccessPoliciesUpdates>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task PutProjectServiceAccountsAccessPoliciesAsync_InvalidPolicyRequest_ThrowsBadRequestException(
|
||||||
|
SutProvider<AccessPoliciesController> sutProvider,
|
||||||
|
Project data,
|
||||||
|
ProjectServiceAccountsAccessPoliciesRequestModel request)
|
||||||
|
{
|
||||||
|
var policyRequest = new AccessPolicyRequest { GranteeId = Guid.NewGuid(), Read = false, Write = true };
|
||||||
|
request.ServiceAccountAccessPolicyRequests = [policyRequest];
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IProjectRepository>().GetByIdAsync(data.Id).ReturnsForAnyArgs(data);
|
||||||
|
|
||||||
|
await Assert.ThrowsAsync<BadRequestException>(() =>
|
||||||
|
sutProvider.Sut.PutProjectServiceAccountsAccessPoliciesAsync(data.Id, request));
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IUpdateProjectServiceAccountsAccessPoliciesCommand>().DidNotReceiveWithAnyArgs()
|
||||||
|
.UpdateAsync(Arg.Any<ProjectServiceAccountsAccessPoliciesUpdates>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task PutProjectServiceAccountsAccessPoliciesAsync_UserHasNoAccess_ThrowsNotFoundException(
|
||||||
|
SutProvider<AccessPoliciesController> sutProvider,
|
||||||
|
Project data,
|
||||||
|
ProjectServiceAccountsAccessPoliciesRequestModel request)
|
||||||
|
{
|
||||||
|
request = SetupValidRequest(request);
|
||||||
|
sutProvider.GetDependency<IProjectRepository>().GetByIdAsync(data.Id).ReturnsForAnyArgs(data);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IAuthorizationService>()
|
||||||
|
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), Arg.Any<ProjectServiceAccountsAccessPoliciesUpdates>(),
|
||||||
|
Arg.Any<IEnumerable<IAuthorizationRequirement>>()).Returns(AuthorizationResult.Failed());
|
||||||
|
|
||||||
|
await Assert.ThrowsAsync<NotFoundException>(() =>
|
||||||
|
sutProvider.Sut.PutProjectServiceAccountsAccessPoliciesAsync(data.Id, request));
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IUpdateProjectServiceAccountsAccessPoliciesCommand>().DidNotReceiveWithAnyArgs()
|
||||||
|
.UpdateAsync(Arg.Any<ProjectServiceAccountsAccessPoliciesUpdates>());
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task PutProjectServiceAccountsAccessPoliciesAsync_Success(
|
||||||
|
SutProvider<AccessPoliciesController> sutProvider,
|
||||||
|
Project data,
|
||||||
|
ProjectServiceAccountsAccessPoliciesRequestModel request)
|
||||||
|
{
|
||||||
|
request = SetupValidRequest(request);
|
||||||
|
sutProvider.GetDependency<IProjectRepository>().GetByIdAsync(data.Id).ReturnsForAnyArgs(data);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IAuthorizationService>()
|
||||||
|
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), Arg.Any<ProjectServiceAccountsAccessPoliciesUpdates>(),
|
||||||
|
Arg.Any<IEnumerable<IAuthorizationRequirement>>()).Returns(AuthorizationResult.Success());
|
||||||
|
|
||||||
|
await sutProvider.Sut.PutProjectServiceAccountsAccessPoliciesAsync(data.Id, request);
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IUpdateProjectServiceAccountsAccessPoliciesCommand>().Received(1)
|
||||||
|
.UpdateAsync(Arg.Any<ProjectServiceAccountsAccessPoliciesUpdates>());
|
||||||
|
}
|
||||||
|
|
||||||
private static AccessPoliciesCreateRequest AddRequestsOverMax(AccessPoliciesCreateRequest request)
|
private static AccessPoliciesCreateRequest AddRequestsOverMax(AccessPoliciesCreateRequest request)
|
||||||
{
|
{
|
||||||
var newRequests = new List<AccessPolicyRequest>();
|
var newRequests = new List<AccessPolicyRequest>();
|
||||||
@ -1168,4 +1359,14 @@ public class AccessPoliciesControllerTests
|
|||||||
|
|
||||||
return request;
|
return request;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static ProjectServiceAccountsAccessPoliciesRequestModel SetupValidRequest(ProjectServiceAccountsAccessPoliciesRequestModel request)
|
||||||
|
{
|
||||||
|
foreach (var policyRequest in request.ServiceAccountAccessPolicyRequests)
|
||||||
|
{
|
||||||
|
policyRequest.Read = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return request;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,79 @@
|
|||||||
|
#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 ProjectServiceAccountsAccessPoliciesTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void GetPolicyUpdates_NoChanges_ReturnsEmptyList()
|
||||||
|
{
|
||||||
|
var serviceAccountId1 = Guid.NewGuid();
|
||||||
|
var serviceAccountId2 = Guid.NewGuid();
|
||||||
|
var projectId = Guid.NewGuid();
|
||||||
|
|
||||||
|
var existing = new ProjectServiceAccountsAccessPolicies
|
||||||
|
{
|
||||||
|
ServiceAccountAccessPolicies = new List<ServiceAccountProjectAccessPolicy>
|
||||||
|
{
|
||||||
|
new() { ServiceAccountId = serviceAccountId1, GrantedProjectId = projectId, Read = true, Write = true },
|
||||||
|
new() { ServiceAccountId = serviceAccountId2, GrantedProjectId = projectId, Read = false, Write = true }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = existing.GetPolicyUpdates(existing);
|
||||||
|
|
||||||
|
Assert.Empty(result.ServiceAccountAccessPolicyUpdates);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetPolicyUpdates_ReturnsCorrectPolicyChanges()
|
||||||
|
{
|
||||||
|
var serviceAccountId1 = Guid.NewGuid();
|
||||||
|
var serviceAccountId2 = Guid.NewGuid();
|
||||||
|
var serviceAccountId3 = Guid.NewGuid();
|
||||||
|
var serviceAccountId4 = Guid.NewGuid();
|
||||||
|
var projectId = Guid.NewGuid();
|
||||||
|
|
||||||
|
var existing = new ProjectServiceAccountsAccessPolicies
|
||||||
|
{
|
||||||
|
ServiceAccountAccessPolicies = new List<ServiceAccountProjectAccessPolicy>
|
||||||
|
{
|
||||||
|
new() { ServiceAccountId = serviceAccountId1, GrantedProjectId = projectId, Read = true, Write = true },
|
||||||
|
new() { ServiceAccountId = serviceAccountId3, GrantedProjectId = projectId, Read = true, Write = true },
|
||||||
|
new() { ServiceAccountId = serviceAccountId4, GrantedProjectId = projectId, Read = true, Write = true }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var requested = new ProjectServiceAccountsAccessPolicies
|
||||||
|
{
|
||||||
|
ServiceAccountAccessPolicies = new List<ServiceAccountProjectAccessPolicy>
|
||||||
|
{
|
||||||
|
new() { ServiceAccountId = serviceAccountId1, GrantedProjectId = projectId, Read = true, Write = false },
|
||||||
|
new() { ServiceAccountId = serviceAccountId2, GrantedProjectId = projectId, Read = false, Write = true },
|
||||||
|
new() { ServiceAccountId = serviceAccountId3, GrantedProjectId = projectId, Read = true, Write = true }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
var result = existing.GetPolicyUpdates(requested);
|
||||||
|
|
||||||
|
Assert.Contains(serviceAccountId2, result.ServiceAccountAccessPolicyUpdates
|
||||||
|
.Where(pu => pu.Operation == AccessPolicyOperation.Create)
|
||||||
|
.Select(pu => pu.AccessPolicy.ServiceAccountId!.Value));
|
||||||
|
|
||||||
|
Assert.Contains(serviceAccountId4, result.ServiceAccountAccessPolicyUpdates
|
||||||
|
.Where(pu => pu.Operation == AccessPolicyOperation.Delete)
|
||||||
|
.Select(pu => pu.AccessPolicy.ServiceAccountId!.Value));
|
||||||
|
|
||||||
|
Assert.Contains(serviceAccountId1, result.ServiceAccountAccessPolicyUpdates
|
||||||
|
.Where(pu => pu.Operation == AccessPolicyOperation.Update)
|
||||||
|
.Select(pu => pu.AccessPolicy.ServiceAccountId!.Value));
|
||||||
|
|
||||||
|
Assert.DoesNotContain(serviceAccountId3, result.ServiceAccountAccessPolicyUpdates
|
||||||
|
.Select(pu => pu.AccessPolicy.ServiceAccountId!.Value));
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user