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

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

* Add the ability to get multi projects access

* Add access policy helper + tests

* Add new data/request models

* Add access policy operations to repo

* Add authz handler for new operations

* Add new controller endpoints

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

View File

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

View File

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

View File

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

View File

@ -41,10 +41,12 @@ public static class SecretsManagerCollectionExtensions
services.AddScoped<IAuthorizationHandler, AccessPolicyAuthorizationHandler>(); services.AddScoped<IAuthorizationHandler, AccessPolicyAuthorizationHandler>();
services.AddScoped<IAuthorizationHandler, ProjectPeopleAccessPoliciesAuthorizationHandler>(); services.AddScoped<IAuthorizationHandler, ProjectPeopleAccessPoliciesAuthorizationHandler>();
services.AddScoped<IAuthorizationHandler, ServiceAccountPeopleAccessPoliciesAuthorizationHandler>(); services.AddScoped<IAuthorizationHandler, ServiceAccountPeopleAccessPoliciesAuthorizationHandler>();
services.AddScoped<IAuthorizationHandler, ServiceAccountGrantedPoliciesAuthorizationHandler>();
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<ISecretsSyncQuery, SecretsSyncQuery>(); services.AddScoped<ISecretsSyncQuery, SecretsSyncQuery>();
services.AddScoped<ICreateSecretCommand, CreateSecretCommand>(); services.AddScoped<ICreateSecretCommand, CreateSecretCommand>();
services.AddScoped<IUpdateSecretCommand, UpdateSecretCommand>(); services.AddScoped<IUpdateSecretCommand, UpdateSecretCommand>();
@ -64,5 +66,6 @@ public static class SecretsManagerCollectionExtensions
services.AddScoped<IImportCommand, ImportCommand>(); services.AddScoped<IImportCommand, ImportCommand>();
services.AddScoped<IEmptyTrashCommand, EmptyTrashCommand>(); services.AddScoped<IEmptyTrashCommand, EmptyTrashCommand>();
services.AddScoped<IRestoreTrashCommand, RestoreTrashCommand>(); services.AddScoped<IRestoreTrashCommand, RestoreTrashCommand>();
services.AddScoped<IUpdateServiceAccountGrantedPoliciesCommand, UpdateServiceAccountGrantedPoliciesCommand>();
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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