1
0
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:
Thomas Avery 2024-05-02 11:06:20 -05:00 committed by GitHub
parent e302ee1520
commit 7f8cea58d0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 1559 additions and 29 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,86 @@
#nullable enable
using Bit.Commercial.Core.SecretsManager.Queries.AccessPolicies;
using Bit.Core.SecretsManager.Entities;
using Bit.Core.SecretsManager.Enums.AccessPolicies;
using Bit.Core.SecretsManager.Models.Data;
using Bit.Core.SecretsManager.Repositories;
using Bit.Core.Test.SecretsManager.AutoFixture.ProjectsFixture;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using NSubstitute.ReturnsExtensions;
using Xunit;
namespace Bit.Commercial.Core.Test.SecretsManager.Queries.AccessPolicies;
[SutProviderCustomize]
[ProjectCustomize]
public class 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));
}
}

View File

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

View File

@ -0,0 +1,28 @@
#nullable enable
using Bit.Api.SecretsManager.Utilities;
using Bit.Core.SecretsManager.Entities;
using Bit.Core.SecretsManager.Models.Data;
namespace Bit.Api.SecretsManager.Models.Request;
public class 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
};
}
}

View File

@ -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; } = [];
}

View File

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

View File

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

View File

@ -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; } = [];
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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