Browse Source

Merge branch 'sm-923' into SM-923-Issues

SM-923-Issues
cd-bitwarden 2 years ago
parent
commit
d7922351cc
  1. 89
      bitwarden_license/src/Commercial.Core/SecretsManager/AuthorizationHandlers/AccessPolicies/ProjectServiceAccountsAccessPoliciesAuthorizationHandler.cs
  2. 12
      bitwarden_license/src/Commercial.Core/SecretsManager/Queries/AccessPolicies/SameOrganizationQuery.cs
  3. 1
      bitwarden_license/src/Commercial.Core/SecretsManager/SecretsManagerCollectionExtensions.cs
  4. 76
      bitwarden_license/src/Commercial.Infrastructure.EntityFramework/SecretsManager/Repositories/AccessPolicyRepository.cs
  5. 66
      bitwarden_license/src/Commercial.Infrastructure.EntityFramework/SecretsManager/Repositories/ServiceAccountRepository.cs
  6. 222
      bitwarden_license/test/Commercial.Core.Test/SecretsManager/AuthorizationHandlers/AccessPolicies/ProjectServiceAccountsAccessPoliciesAuthorizationHandlerTests.cs
  7. 35
      src/Api/SecretsManager/Controllers/AccessPoliciesController.cs
  8. 28
      src/Api/SecretsManager/Models/Request/AccessPoliciesCreateRequest.cs
  9. 5
      src/Api/SecretsManager/Models/Request/PeopleAccessPoliciesRequestModel.cs
  10. 32
      src/Api/SecretsManager/Models/Request/ServiceAccountsAccessPoliciesRequestModel.cs
  11. 35
      src/Api/SecretsManager/Models/Response/ProjectServiceAccountsAccessPoliciesResponseModel.cs
  12. 39
      src/Api/SecretsManager/Utilities/AccessPolicyHelpers.cs
  13. 12
      src/Core/SecretsManager/AuthorizationRequirements/ProjectServiceAccountsAccessPoliciesOperationRequirement.cs
  14. 22
      src/Core/SecretsManager/Models/Data/ProjectServiceAccountsAccessPolicies.cs
  15. 1
      src/Core/SecretsManager/Queries/AccessPolicies/Interfaces/ISameOrganizationQuery.cs
  16. 2
      src/Core/SecretsManager/Repositories/IAccessPolicyRepository.cs
  17. 1
      src/Core/SecretsManager/Repositories/IServiceAccountRepository.cs
  18. 1
      src/Core/SecretsManager/Repositories/Noop/NoopServiceAccountRepository.cs
  19. 247
      test/Api.IntegrationTest/SecretsManager/Controllers/AccessPoliciesControllerTests.cs
  20. 206
      test/Api.Test/SecretsManager/Controllers/AccessPoliciesControllerTests.cs
  21. 79
      test/Api.Test/SecretsManager/SecretsManager/Utilities/AccessPolicyHelpersTests.cs

89
bitwarden_license/src/Commercial.Core/SecretsManager/AuthorizationHandlers/AccessPolicies/ProjectServiceAccountsAccessPoliciesAuthorizationHandler.cs

@ -0,0 +1,89 @@ @@ -0,0 +1,89 @@
using Bit.Core.Context;
using Bit.Core.Enums;
using Bit.Core.SecretsManager.AuthorizationRequirements;
using Bit.Core.SecretsManager.Models.Data;
using Bit.Core.SecretsManager.Queries.AccessPolicies.Interfaces;
using Bit.Core.SecretsManager.Queries.Interfaces;
using Bit.Core.SecretsManager.Repositories;
using Microsoft.AspNetCore.Authorization;
namespace Bit.Commercial.Core.SecretsManager.AuthorizationHandlers.AccessPolicies;
public class
ProjectServiceAccountsAccessPoliciesAuthorizationHandler : AuthorizationHandler<
ProjectServiceAccountsAccessPoliciesOperationRequirement,
ProjectServiceAccountsAccessPolicies>
{
private readonly IAccessClientQuery _accessClientQuery;
private readonly ICurrentContext _currentContext;
private readonly IProjectRepository _projectRepository;
private readonly ISameOrganizationQuery _sameOrganizationQuery;
private readonly IServiceAccountRepository _serviceAccountRepository;
public ProjectServiceAccountsAccessPoliciesAuthorizationHandler(ICurrentContext currentContext,
IAccessClientQuery accessClientQuery,
ISameOrganizationQuery sameOrganizationQuery,
IProjectRepository projectRepository,
IServiceAccountRepository serviceAccountRepository)
{
_currentContext = currentContext;
_accessClientQuery = accessClientQuery;
_sameOrganizationQuery = sameOrganizationQuery;
_projectRepository = projectRepository;
_serviceAccountRepository = serviceAccountRepository;
}
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context,
ProjectServiceAccountsAccessPoliciesOperationRequirement requirement,
ProjectServiceAccountsAccessPolicies resource)
{
if (!_currentContext.AccessSecretsManager(resource.OrganizationId))
{
return;
}
// Only users and admins should be able to manipulate access policies
var (accessClient, userId) =
await _accessClientQuery.GetAccessClientAsync(context.User, resource.OrganizationId);
if (accessClient != AccessClientType.User && accessClient != AccessClientType.NoAccessCheck)
{
return;
}
switch (requirement)
{
case not null when requirement == ProjectServiceAccountsAccessPoliciesOperations.Replace:
await CanReplaceProjectServiceAccountsAsync(context, requirement, resource, accessClient, userId);
break;
default:
throw new ArgumentException("Unsupported operation requirement type provided.",
nameof(requirement));
}
}
private async Task CanReplaceProjectServiceAccountsAsync(AuthorizationHandlerContext context,
ProjectServiceAccountsAccessPoliciesOperationRequirement requirement, ProjectServiceAccountsAccessPolicies resource,
AccessClientType accessClient, Guid userId)
{
var projectAccess = await _projectRepository.AccessToProjectAsync(resource.Id, userId, accessClient);
if (projectAccess.Write)
{
if (resource.ServiceAccountProjectsAccessPolicies != null && resource.ServiceAccountProjectsAccessPolicies.Any())
{
var serviceAccountIds = resource.ServiceAccountProjectsAccessPolicies.Select(ap => ap.ServiceAccountId!.Value).ToList();
if (!await _sameOrganizationQuery.ServiceAccountsInTheSameOrgAsync(serviceAccountIds, resource.OrganizationId))
{
return;
}
var serviceAccountAccess = await _serviceAccountRepository.AccessToServiceAccountsAsync(serviceAccountIds, userId, accessClient);
if (!serviceAccountAccess.Write)
{
return;
}
}
context.Succeed(requirement);
}
}
}

12
bitwarden_license/src/Commercial.Core/SecretsManager/Queries/AccessPolicies/SameOrganizationQuery.cs

@ -1,19 +1,22 @@ @@ -1,19 +1,22 @@
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Repositories;
using Bit.Core.SecretsManager.Queries.AccessPolicies.Interfaces;
using Bit.Core.SecretsManager.Repositories;
namespace Bit.Commercial.Core.SecretsManager.Queries.AccessPolicies;
public class SameOrganizationQuery : ISameOrganizationQuery
{
private readonly IGroupRepository _groupRepository;
private readonly IServiceAccountRepository _serviceAccountRepository;
private readonly IOrganizationUserRepository _organizationUserRepository;
public SameOrganizationQuery(IOrganizationUserRepository organizationUserRepository,
IGroupRepository groupRepository)
IGroupRepository groupRepository, IServiceAccountRepository serviceAccountRepository)
{
_organizationUserRepository = organizationUserRepository;
_groupRepository = groupRepository;
_serviceAccountRepository = serviceAccountRepository;
}
public async Task<bool> OrgUsersInTheSameOrgAsync(List<Guid> organizationUserIds, Guid organizationId)
@ -29,4 +32,11 @@ public class SameOrganizationQuery : ISameOrganizationQuery @@ -29,4 +32,11 @@ public class SameOrganizationQuery : ISameOrganizationQuery
return groups.All(group => group.OrganizationId == organizationId) &&
groups.Count == groupIds.Count;
}
public async Task<bool> ServiceAccountsInTheSameOrgAsync(List<Guid> serviceAccountIds, Guid organizationId)
{
var serviceAccounts = (await _serviceAccountRepository.GetManyByIds(serviceAccountIds)).ToList();
return serviceAccounts.All(serviceAccount => serviceAccount.OrganizationId == organizationId) &&
serviceAccounts.Count == serviceAccountIds.Count;
}
}

1
bitwarden_license/src/Commercial.Core/SecretsManager/SecretsManagerCollectionExtensions.cs

@ -61,5 +61,6 @@ public static class SecretsManagerCollectionExtensions @@ -61,5 +61,6 @@ public static class SecretsManagerCollectionExtensions
services.AddScoped<IImportCommand, ImportCommand>();
services.AddScoped<IEmptyTrashCommand, EmptyTrashCommand>();
services.AddScoped<IRestoreTrashCommand, RestoreTrashCommand>();
services.AddScoped<IAuthorizationHandler, ProjectServiceAccountsAccessPoliciesAuthorizationHandler>();
}
}

76
bitwarden_license/src/Commercial.Infrastructure.EntityFramework/SecretsManager/Repositories/AccessPolicyRepository.cs

@ -218,6 +218,54 @@ public class AccessPolicyRepository : BaseEntityFrameworkRepository, IAccessPoli @@ -218,6 +218,54 @@ public class AccessPolicyRepository : BaseEntityFrameworkRepository, IAccessPoli
return entities.Select(MapToCore);
}
public async Task<IEnumerable<Core.SecretsManager.Entities.BaseAccessPolicy>> ReplaceProjectServiceAccountsAsync(
ProjectServiceAccountsAccessPolicies newProjectServiceAccountsAccessPolicies)
{
using var scope = ServiceScopeFactory.CreateScope();
var dbContext = GetDatabaseContext(scope);
var currentProjectServiceAccountsPolicyEntities = await dbContext.AccessPolicies.Where(ap =>
ap.Discriminator == AccessPolicyDiscriminator.ServiceAccountProject &&
((ServiceAccountProjectAccessPolicy)ap).GrantedProjectId == newProjectServiceAccountsAccessPolicies.Id).ToListAsync();
if (newProjectServiceAccountsAccessPolicies.ServiceAccountProjectsAccessPolicies == null || !newProjectServiceAccountsAccessPolicies.ServiceAccountProjectsAccessPolicies.Any())
{
dbContext.RemoveRange(currentProjectServiceAccountsPolicyEntities);
}
else
{
foreach (var projectServiceAccountsPolicyEntity in currentProjectServiceAccountsPolicyEntities.Where(entity =>
newProjectServiceAccountsAccessPolicies.ServiceAccountProjectsAccessPolicies.All(ap =>
((Core.SecretsManager.Entities.ServiceAccountProjectAccessPolicy)ap).ServiceAccountId !=
((ServiceAccountProjectAccessPolicy)entity).ServiceAccountId)))
{
dbContext.Remove(projectServiceAccountsPolicyEntity);
}
}
await UpsertProjectServiceAccountsPoliciesAsync(dbContext,
newProjectServiceAccountsAccessPolicies.ToBaseAccessPolicies().Select(MapToEntity).ToList(), currentProjectServiceAccountsPolicyEntities);
await dbContext.SaveChangesAsync();
return await GetServiceAccountPoliciesByGrantedProjectIdAsync(newProjectServiceAccountsAccessPolicies.Id);
}
public async Task<IEnumerable<Core.SecretsManager.Entities.BaseAccessPolicy>>
GetServiceAccountPoliciesByGrantedProjectIdAsync(Guid projectId)
{
using var scope = ServiceScopeFactory.CreateScope();
var dbContext = GetDatabaseContext(scope);
var entities = await dbContext.AccessPolicies.Where(ap =>
ap.Discriminator == AccessPolicyDiscriminator.ServiceAccountProject &&
(((ServiceAccountProjectAccessPolicy)ap).GrantedProjectId == projectId))
.Include(ap => ((ServiceAccountProjectAccessPolicy)ap).ServiceAccount)
.ToListAsync();
return entities.Select(MapToCore);
}
public async Task<PeopleGrantees> GetPeopleGranteesAsync(Guid organizationId, Guid currentUserId)
{
using var scope = ServiceScopeFactory.CreateScope();
@ -494,4 +542,32 @@ public class AccessPolicyRepository : BaseEntityFrameworkRepository, IAccessPoli @@ -494,4 +542,32 @@ public class AccessPolicyRepository : BaseEntityFrameworkRepository, IAccessPoli
return MapToCore(baseAccessPolicyEntity);
}
}
private static async Task UpsertProjectServiceAccountsPoliciesAsync(DatabaseContext dbContext,
List<BaseAccessPolicy> newPolicies, IReadOnlyCollection<AccessPolicy> currentProjectServiceAccountAccessPolicies)
{
var currentDate = DateTime.UtcNow;
foreach (var updatedEntity in newPolicies)
{
var currentEntity = updatedEntity switch
{
ServiceAccountProjectAccessPolicy ap => currentProjectServiceAccountAccessPolicies.FirstOrDefault(e =>
((ServiceAccountProjectAccessPolicy)e).ServiceAccountId == ap.ServiceAccountId),
_ => null
};
if (currentEntity != null)
{
dbContext.AccessPolicies.Attach(currentEntity);
currentEntity.Read = updatedEntity.Read;
currentEntity.Write = updatedEntity.Write;
currentEntity.RevisionDate = currentDate;
}
else
{
updatedEntity.SetNewId();
await dbContext.AddAsync(updatedEntity);
}
}
}
}

66
bitwarden_license/src/Commercial.Infrastructure.EntityFramework/SecretsManager/Repositories/ServiceAccountRepository.cs

@ -108,26 +108,10 @@ public class ServiceAccountRepository : Repository<Core.SecretsManager.Entities. @@ -108,26 +108,10 @@ public class ServiceAccountRepository : Repository<Core.SecretsManager.Entities.
using var scope = ServiceScopeFactory.CreateScope();
var dbContext = GetDatabaseContext(scope);
var serviceAccount = dbContext.ServiceAccount.Where(sa => sa.Id == id);
var query = accessType switch
{
AccessClientType.NoAccessCheck => serviceAccount.Select(_ => new { Read = true, Write = true }),
AccessClientType.User => serviceAccount.Select(sa => new
{
Read = sa.UserAccessPolicies.Any(ap => ap.OrganizationUser.User.Id == userId && ap.Read) ||
sa.GroupAccessPolicies.Any(ap =>
ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.User.Id == userId && ap.Read)),
Write = sa.UserAccessPolicies.Any(ap => ap.OrganizationUser.User.Id == userId && ap.Write) ||
sa.GroupAccessPolicies.Any(ap =>
ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.User.Id == userId && ap.Write)),
}),
AccessClientType.ServiceAccount => serviceAccount.Select(_ => new { Read = false, Write = false }),
_ => serviceAccount.Select(_ => new { Read = false, Write = false }),
};
var serviceAccountQuery = dbContext.ServiceAccount.Where(sa => sa.Id == id);
var query = ToAccessQuery(serviceAccountQuery, userId, accessType);
var policy = await query.FirstOrDefaultAsync();
return policy == null ? (false, false) : (policy.Read, policy.Write);
}
@ -179,6 +163,52 @@ public class ServiceAccountRepository : Repository<Core.SecretsManager.Entities. @@ -179,6 +163,52 @@ public class ServiceAccountRepository : Repository<Core.SecretsManager.Entities.
return results;
}
public async Task<(bool Read, bool Write)> AccessToServiceAccountsAsync(IEnumerable<Guid> ids, Guid userId, AccessClientType accessType)
{
using var scope = ServiceScopeFactory.CreateScope();
var dbContext = GetDatabaseContext(scope);
var serviceAccountQuery = dbContext.ServiceAccount.Where(sa => ids.Contains(sa.Id));
var query = ToAccessQuery(serviceAccountQuery, userId, accessType);
var policies = await query.ToListAsync();
if (!policies.Any())
{
return (false, false);
}
var read = policies.All(p => p.Read);
var write = policies.All(p => p.Write);
return (read, write);
}
private class Access
{
public bool Read;
public bool Write;
}
private static IQueryable<Access> ToAccessQuery(IQueryable<ServiceAccount> serviceAccount, Guid userId, AccessClientType accessType)
{
return accessType switch
{
AccessClientType.NoAccessCheck => serviceAccount.Select(_ => new Access { Read = true, Write = true }),
AccessClientType.User => serviceAccount.Select(sa => new Access
{
Read = sa.UserAccessPolicies.Any(ap => ap.OrganizationUser.User.Id == userId && ap.Read) ||
sa.GroupAccessPolicies.Any(ap =>
ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.User.Id == userId && ap.Read)),
Write = sa.UserAccessPolicies.Any(ap => ap.OrganizationUser.User.Id == userId && ap.Write) ||
sa.GroupAccessPolicies.Any(ap =>
ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.User.Id == userId && ap.Write)),
}),
AccessClientType.ServiceAccount => serviceAccount.Select(_ => new Access { Read = false, Write = false }),
_ => serviceAccount.Select(_ => new Access { Read = false, Write = false }),
};
}
private static Expression<Func<ServiceAccount, bool>> UserHasReadAccessToServiceAccount(Guid userId) => sa =>
sa.UserAccessPolicies.Any(ap => ap.OrganizationUser.User.Id == userId && ap.Read) ||
sa.GroupAccessPolicies.Any(ap => ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.User.Id == userId && ap.Read));

222
bitwarden_license/test/Commercial.Core.Test/SecretsManager/AuthorizationHandlers/AccessPolicies/ProjectServiceAccountsAccessPoliciesAuthorizationHandlerTests.cs

@ -0,0 +1,222 @@ @@ -0,0 +1,222 @@
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.AccessPolicies.Interfaces;
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 ProjectServiceAccountAccessPoliciesAuthorizationHandlerTests
{
private static void SetupUserPermission(SutProvider<ProjectServiceAccountsAccessPoliciesAuthorizationHandler> sutProvider,
AccessClientType accessClientType, ProjectServiceAccountsAccessPolicies resource, Guid userId = new(), bool read = true,
bool write = true)
{
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(resource.OrganizationId)
.Returns(true);
sutProvider.GetDependency<IAccessClientQuery>().GetAccessClientAsync(default, resource.OrganizationId)
.ReturnsForAnyArgs(
(accessClientType, userId));
sutProvider.GetDependency<IProjectRepository>().AccessToProjectAsync(resource.Id, userId, accessClientType)
.Returns((read, write));
sutProvider.GetDependency<IServiceAccountRepository>().AccessToServiceAccountsAsync(Arg.Any<List<Guid>>(), userId, accessClientType)
.Returns((read, write));
}
private static void SetupOrganizationServiceAccounts(SutProvider<ProjectServiceAccountsAccessPoliciesAuthorizationHandler> sutProvider,
ProjectServiceAccountsAccessPolicies resource) =>
sutProvider.GetDependency<ISameOrganizationQuery>()
.ServiceAccountsInTheSameOrgAsync(Arg.Any<List<Guid>>(), resource.OrganizationId)
.Returns(true);
[Fact]
public void ServiceAccountAccessPoliciesOperations_OnlyPublicStatic()
{
var publicStaticFields =
typeof(ProjectServiceAccountsAccessPoliciesOperations).GetFields(BindingFlags.Public | BindingFlags.Static);
var allFields = typeof(ProjectServiceAccountsAccessPoliciesOperations).GetFields();
Assert.Equal(publicStaticFields.Length, allFields.Length);
}
[Theory]
[BitAutoData]
public async Task Handler_UnsupportedProjectServiceAccountsAccessPoliciesOperationRequirement_Throws(
SutProvider<ProjectServiceAccountsAccessPoliciesAuthorizationHandler> sutProvider, ProjectServiceAccountsAccessPolicies resource,
ClaimsPrincipal claimsPrincipal)
{
var requirement = new ProjectServiceAccountsAccessPoliciesOperationRequirement();
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(resource.OrganizationId)
.Returns(true);
sutProvider.GetDependency<IAccessClientQuery>().GetAccessClientAsync(default, resource.OrganizationId)
.ReturnsForAnyArgs(
(AccessClientType.NoAccessCheck, new Guid()));
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, resource);
await Assert.ThrowsAsync<ArgumentException>(() => sutProvider.Sut.HandleAsync(authzContext));
}
[Theory]
[BitAutoData]
public async Task Handler_AccessSecretsManagerFalse_DoesNotSucceed(
SutProvider<ProjectServiceAccountsAccessPoliciesAuthorizationHandler> sutProvider, ProjectServiceAccountsAccessPolicies resource,
ClaimsPrincipal claimsPrincipal)
{
var requirement = new ProjectServiceAccountsAccessPoliciesOperationRequirement();
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.User)]
[BitAutoData(AccessClientType.NoAccessCheck)]
public async Task ReplaceProjectServiceAccount_ServiceAccountNotInOrg_DoesNotSucceed(AccessClientType accessClient,
SutProvider<ProjectServiceAccountsAccessPoliciesAuthorizationHandler> sutProvider, ProjectServiceAccountsAccessPolicies resource,
ClaimsPrincipal claimsPrincipal, Guid userId)
{
var requirement = ProjectServiceAccountsAccessPoliciesOperations.Replace;
SetupUserPermission(sutProvider, accessClient, resource, userId);
sutProvider.GetDependency<ISameOrganizationQuery>()
.ServiceAccountsInTheSameOrgAsync(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.User, false, false, false)]
[BitAutoData(AccessClientType.User, false, true, true)]
[BitAutoData(AccessClientType.User, true, false, false)]
[BitAutoData(AccessClientType.User, true, true, true)]
[BitAutoData(AccessClientType.NoAccessCheck, false, false, false)]
[BitAutoData(AccessClientType.NoAccessCheck, false, true, true)]
[BitAutoData(AccessClientType.NoAccessCheck, true, false, false)]
[BitAutoData(AccessClientType.NoAccessCheck, true, true, true)]
public async Task ReplaceProjectServiceAccount_ProjectAccessCheck(AccessClientType accessClient, bool read, bool write,
bool expected,
SutProvider<ProjectServiceAccountsAccessPoliciesAuthorizationHandler> sutProvider, ProjectServiceAccountsAccessPolicies resource,
ClaimsPrincipal claimsPrincipal, Guid userId)
{
var requirement = ProjectServiceAccountsAccessPoliciesOperations.Replace;
SetupUserPermission(sutProvider, accessClient, resource, userId, read, write);
SetupOrganizationServiceAccounts(sutProvider, resource);
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, resource);
await sutProvider.Sut.HandleAsync(authzContext);
Assert.Equal(expected, authzContext.HasSucceeded);
}
[Theory]
[BitAutoData(AccessClientType.User, false, false, false)]
[BitAutoData(AccessClientType.User, false, true, true)]
[BitAutoData(AccessClientType.User, true, false, false)]
[BitAutoData(AccessClientType.User, true, true, true)]
[BitAutoData(AccessClientType.NoAccessCheck, false, false, false)]
[BitAutoData(AccessClientType.NoAccessCheck, false, true, true)]
[BitAutoData(AccessClientType.NoAccessCheck, true, false, false)]
[BitAutoData(AccessClientType.NoAccessCheck, true, true, true)]
public async Task ReplaceProjectServiceAccount_ServiceAccountsAccessCheck(AccessClientType accessClient, bool read, bool write,
bool expected,
SutProvider<ProjectServiceAccountsAccessPoliciesAuthorizationHandler> sutProvider, ProjectServiceAccountsAccessPolicies resource,
ClaimsPrincipal claimsPrincipal, Guid userId)
{
var requirement = ProjectServiceAccountsAccessPoliciesOperations.Replace;
SetupUserPermission(sutProvider, accessClient, resource, userId, true, true);
SetupOrganizationServiceAccounts(sutProvider, resource);
sutProvider.GetDependency<IServiceAccountRepository>().AccessToServiceAccountsAsync(Arg.Any<List<Guid>>(), userId, accessClient).Returns((read, write));
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, resource);
await sutProvider.Sut.HandleAsync(authzContext);
Assert.Equal(expected, authzContext.HasSucceeded);
}
[Theory]
[BitAutoData(AccessClientType.User, false, false, false)]
[BitAutoData(AccessClientType.User, false, true, false)]
[BitAutoData(AccessClientType.User, true, false, false)]
[BitAutoData(AccessClientType.User, true, true, false)]
[BitAutoData(AccessClientType.NoAccessCheck, false, false, false)]
[BitAutoData(AccessClientType.NoAccessCheck, false, true, false)]
[BitAutoData(AccessClientType.NoAccessCheck, true, false, false)]
[BitAutoData(AccessClientType.NoAccessCheck, true, true, false)]
public async Task ReplaceProjectServiceAccount_ProjectAccessFalseCheck(AccessClientType accessClient, bool read, bool write,
bool expected,
SutProvider<ProjectServiceAccountsAccessPoliciesAuthorizationHandler> sutProvider, ProjectServiceAccountsAccessPolicies resource,
ClaimsPrincipal claimsPrincipal, Guid userId)
{
var requirement = ProjectServiceAccountsAccessPoliciesOperations.Replace;
SetupUserPermission(sutProvider, accessClient, resource, userId, false, false);
SetupOrganizationServiceAccounts(sutProvider, resource);
sutProvider.GetDependency<IServiceAccountRepository>().AccessToServiceAccountsAsync(resource.ServiceAccountProjectsAccessPolicies.Select(ap => ap.ServiceAccountId!.Value).ToList(), userId, accessClient)
.Returns((read, write));
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, resource);
await sutProvider.Sut.HandleAsync(authzContext);
Assert.Equal(expected, authzContext.HasSucceeded);
}
[Theory]
[BitAutoData(AccessClientType.ServiceAccount, true, true, false)]
[BitAutoData(AccessClientType.ServiceAccount, true, false, false)]
[BitAutoData(AccessClientType.ServiceAccount, false, true, false)]
[BitAutoData(AccessClientType.ServiceAccount, false, false, false)]
[BitAutoData(AccessClientType.Organization, true, true, false)]
[BitAutoData(AccessClientType.Organization, true, false, false)]
[BitAutoData(AccessClientType.Organization, false, true, false)]
[BitAutoData(AccessClientType.Organization, false, false, false)]
public async Task ReplaceProjectServiceAccount_UnsupportedAccessType(AccessClientType accessClient, bool read, bool write,
bool expected,
SutProvider<ProjectServiceAccountsAccessPoliciesAuthorizationHandler> sutProvider, ProjectServiceAccountsAccessPolicies resource,
ClaimsPrincipal claimsPrincipal, Guid userId)
{
var requirement = ProjectServiceAccountsAccessPoliciesOperations.Replace;
SetupUserPermission(sutProvider, accessClient, resource, userId, false, false);
SetupOrganizationServiceAccounts(sutProvider, resource);
sutProvider.GetDependency<IServiceAccountRepository>().AccessToServiceAccountsAsync(resource.ServiceAccountProjectsAccessPolicies.Select(ap => ap.ServiceAccountId!.Value).ToList(), userId, accessClient)
.Returns((read, write));
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, resource);
await sutProvider.Sut.HandleAsync(authzContext);
Assert.Equal(expected, authzContext.HasSucceeded);
}
}

35
src/Api/SecretsManager/Controllers/AccessPoliciesController.cs

@ -303,6 +303,41 @@ public class AccessPoliciesController : Controller @@ -303,6 +303,41 @@ public class AccessPoliciesController : Controller
return new ServiceAccountPeopleAccessPoliciesResponseModel(results, userId);
}
[HttpGet("/projects/{id}/access-policies/service-accounts")]
public async Task<ProjectServiceAccountsAccessPoliciesResponseModel> GetProjectServiceAccountsAccessPoliciesAsync(
[FromRoute] Guid id)
{
var project = await _projectRepository.GetByIdAsync(id);
await CheckUserHasWriteAccessToProjectAsync(project);
var results = await _accessPolicyRepository.GetServiceAccountPoliciesByGrantedProjectIdAsync(id);
return new ProjectServiceAccountsAccessPoliciesResponseModel(results);
}
[HttpPut("/projects/{id}/access-policies/service-accounts")]
public async Task<ProjectServiceAccountsAccessPoliciesResponseModel> PutProjectServiceAccountsAccessPoliciesAsync(
[FromRoute] Guid id,
[FromBody] ServiceAccountsAccessPoliciesRequestModel request)
{
var project = await _projectRepository.GetByIdAsync(id);
if (project == null)
{
throw new NotFoundException();
}
var projectServiceAccountsAccessPolicies = request.ToProjectServiceAccountsAccessPolicies(id, project.OrganizationId);
var authorizationResult = await _authorizationService.AuthorizeAsync(User, projectServiceAccountsAccessPolicies,
ProjectServiceAccountsAccessPoliciesOperations.Replace);
if (!authorizationResult.Succeeded)
{
throw new NotFoundException();
}
var results = await _accessPolicyRepository.ReplaceProjectServiceAccountsAsync(projectServiceAccountsAccessPolicies);
return new ProjectServiceAccountsAccessPoliciesResponseModel(results);
}
private async Task<(AccessClientType AccessClientType, Guid UserId)> CheckUserHasWriteAccessToProjectAsync(Project project)
{
if (project == null)

28
src/Api/SecretsManager/Models/Request/AccessPoliciesCreateRequest.cs

@ -1,5 +1,6 @@ @@ -1,5 +1,6 @@
#nullable enable
using System.ComponentModel.DataAnnotations;
using Bit.Api.SecretsManager.Utilities;
using Bit.Core.Exceptions;
using Bit.Core.SecretsManager.Entities;
@ -7,29 +8,6 @@ namespace Bit.Api.SecretsManager.Models.Request; @@ -7,29 +8,6 @@ namespace Bit.Api.SecretsManager.Models.Request;
public class AccessPoliciesCreateRequest
{
private 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 IEnumerable<AccessPolicyRequest>? UserAccessPolicyRequests { get; set; }
public IEnumerable<AccessPolicyRequest>? GroupAccessPolicyRequests { get; set; }
@ -68,7 +46,7 @@ public class AccessPoliciesCreateRequest @@ -68,7 +46,7 @@ public class AccessPoliciesCreateRequest
policies.AddRange(serviceAccountAccessPolicies);
}
CheckForDistinctAccessPolicies(policies);
AccessPolicyHelpers.CheckForDistinctAccessPolicies(policies);
return policies;
}
@ -96,7 +74,7 @@ public class AccessPoliciesCreateRequest @@ -96,7 +74,7 @@ public class AccessPoliciesCreateRequest
policies.AddRange(groupAccessPolicies);
}
CheckForDistinctAccessPolicies(policies);
AccessPolicyHelpers.CheckForDistinctAccessPolicies(policies);
return policies;
}

5
src/Api/SecretsManager/Models/Request/PeopleAccessPoliciesRequestModel.cs

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
using Bit.Core.Exceptions;
using Bit.Api.SecretsManager.Utilities;
using Bit.Core.Exceptions;
using Bit.Core.SecretsManager.Entities;
using Bit.Core.SecretsManager.Models.Data;
@ -51,7 +52,7 @@ public class PeopleAccessPoliciesRequestModel @@ -51,7 +52,7 @@ public class PeopleAccessPoliciesRequestModel
policies.AddRange(groupAccessPolicies);
}
CheckForDistinctAccessPolicies(policies);
AccessPolicyHelpers.CheckForDistinctAccessPolicies(policies);
return new ProjectPeopleAccessPolicies
{

32
src/Api/SecretsManager/Models/Request/ServiceAccountsAccessPoliciesRequestModel.cs

@ -0,0 +1,32 @@ @@ -0,0 +1,32 @@
using Bit.Api.SecretsManager.Utilities;
using Bit.Core.SecretsManager.Entities;
using Bit.Core.SecretsManager.Models.Data;
namespace Bit.Api.SecretsManager.Models.Request;
public class ServiceAccountsAccessPoliciesRequestModel
{
public IEnumerable<AccessPolicyRequest> ProjectServiceAccountsAccessPolicyRequests { get; set; }
public ProjectServiceAccountsAccessPolicies ToProjectServiceAccountsAccessPolicies(Guid grantedProjectId, Guid organizationId)
{
var projectServiceAccountsAccessPolicies = ProjectServiceAccountsAccessPolicyRequests?
.Select(x => x.ToServiceAccountProjectAccessPolicy(grantedProjectId, organizationId)).ToList();
var policies = new List<BaseAccessPolicy>();
if (projectServiceAccountsAccessPolicies != null)
{
policies.AddRange(projectServiceAccountsAccessPolicies);
}
AccessPolicyHelpers.CheckForDistinctAccessPolicies(policies);
AccessPolicyHelpers.CheckAccessPoliciesHasReadPermission(policies);
return new ProjectServiceAccountsAccessPolicies
{
Id = grantedProjectId,
OrganizationId = organizationId,
ServiceAccountProjectsAccessPolicies = projectServiceAccountsAccessPolicies,
};
}
}

35
src/Api/SecretsManager/Models/Response/ProjectServiceAccountsAccessPoliciesResponseModel.cs

@ -0,0 +1,35 @@ @@ -0,0 +1,35 @@
using Bit.Core.Models.Api;
using Bit.Core.SecretsManager.Entities;
namespace Bit.Api.SecretsManager.Models.Response;
public class ProjectServiceAccountsAccessPoliciesResponseModel : ResponseModel
{
private const string _objectName = "projectServiceAccountsAccessPolicies";
public ProjectServiceAccountsAccessPoliciesResponseModel(IEnumerable<BaseAccessPolicy> baseAccessPolicies)
: base(_objectName)
{
if (baseAccessPolicies == null)
{
return;
}
foreach (var baseAccessPolicy in baseAccessPolicies)
{
switch (baseAccessPolicy)
{
case ServiceAccountProjectAccessPolicy accessPolicy:
ServiceAccountsAccessPolicies.Add(new ServiceAccountProjectAccessPolicyResponseModel(accessPolicy));
break;
}
}
}
public ProjectServiceAccountsAccessPoliciesResponseModel() : base(_objectName)
{
}
public List<ServiceAccountProjectAccessPolicyResponseModel> ServiceAccountsAccessPolicies { get; set; } = new();
}

39
src/Api/SecretsManager/Utilities/AccessPolicyHelpers.cs

@ -0,0 +1,39 @@ @@ -0,0 +1,39 @@
using Bit.Core.Exceptions;
using Bit.Core.SecretsManager.Entities;
namespace Bit.Api.SecretsManager.Utilities;
public 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 CheckAccessPoliciesHasReadPermission(IReadOnlyCollection<BaseAccessPolicy> accessPolicies)
{
var accessPoliciesPermission = accessPolicies.All(Policy => Policy.Read); //Has to be read, write can be true or false.
if (!accessPoliciesPermission)
{
throw new BadRequestException("Resources must be Read = true");
}
}
}

12
src/Core/SecretsManager/AuthorizationRequirements/ProjectServiceAccountsAccessPoliciesOperationRequirement.cs

@ -0,0 +1,12 @@ @@ -0,0 +1,12 @@
using Microsoft.AspNetCore.Authorization.Infrastructure;
namespace Bit.Core.SecretsManager.AuthorizationRequirements;
public class ProjectServiceAccountsAccessPoliciesOperationRequirement : OperationAuthorizationRequirement
{
}
public static class ProjectServiceAccountsAccessPoliciesOperations
{
public static readonly ProjectServiceAccountsAccessPoliciesOperationRequirement Replace = new() { Name = nameof(Replace) };
}

22
src/Core/SecretsManager/Models/Data/ProjectServiceAccountsAccessPolicies.cs

@ -0,0 +1,22 @@ @@ -0,0 +1,22 @@
using Bit.Core.SecretsManager.Entities;
namespace Bit.Core.SecretsManager.Models.Data;
public class ProjectServiceAccountsAccessPolicies
{
public Guid Id { get; set; }
public Guid OrganizationId { get; set; }
public IEnumerable<ServiceAccountProjectAccessPolicy> ServiceAccountProjectsAccessPolicies { get; set; }
public IEnumerable<BaseAccessPolicy> ToBaseAccessPolicies()
{
var policies = new List<BaseAccessPolicy>();
if (ServiceAccountProjectsAccessPolicies != null && ServiceAccountProjectsAccessPolicies.Any())
{
policies.AddRange(ServiceAccountProjectsAccessPolicies);
}
return policies;
}
}

1
src/Core/SecretsManager/Queries/AccessPolicies/Interfaces/ISameOrganizationQuery.cs

@ -4,4 +4,5 @@ public interface ISameOrganizationQuery @@ -4,4 +4,5 @@ public interface ISameOrganizationQuery
{
Task<bool> OrgUsersInTheSameOrgAsync(List<Guid> organizationUserIds, Guid organizationId);
Task<bool> GroupsInTheSameOrgAsync(List<Guid> groupIds, Guid organizationId);
Task<bool> ServiceAccountsInTheSameOrgAsync(List<Guid> serviceAccountIds, Guid organizationId);
}

2
src/Core/SecretsManager/Repositories/IAccessPolicyRepository.cs

@ -20,4 +20,6 @@ public interface IAccessPolicyRepository @@ -20,4 +20,6 @@ public interface IAccessPolicyRepository
Task<PeopleGrantees> GetPeopleGranteesAsync(Guid organizationId, Guid currentUserId);
Task<IEnumerable<BaseAccessPolicy>> GetPeoplePoliciesByGrantedServiceAccountIdAsync(Guid id, Guid userId);
Task<IEnumerable<BaseAccessPolicy>> ReplaceServiceAccountPeopleAsync(ServiceAccountPeopleAccessPolicies peopleAccessPolicies, Guid userId);
Task<IEnumerable<BaseAccessPolicy>> GetServiceAccountPoliciesByGrantedProjectIdAsync(Guid projectId);
Task<IEnumerable<BaseAccessPolicy>> ReplaceProjectServiceAccountsAsync(ProjectServiceAccountsAccessPolicies serviceAccountsAccessPolicies);
}

1
src/Core/SecretsManager/Repositories/IServiceAccountRepository.cs

@ -18,4 +18,5 @@ public interface IServiceAccountRepository @@ -18,4 +18,5 @@ public interface IServiceAccountRepository
Task<(bool Read, bool Write)> AccessToServiceAccountAsync(Guid id, Guid userId, AccessClientType accessType);
Task<int> GetServiceAccountCountByOrganizationIdAsync(Guid organizationId);
Task<IEnumerable<ServiceAccountSecretsDetails>> GetManyByOrganizationIdWithSecretsDetailsAsync(Guid organizationId, Guid userId, AccessClientType accessType);
Task<(bool Read, bool Write)> AccessToServiceAccountsAsync(IEnumerable<Guid> ids, Guid userId, AccessClientType accessType);
}

1
src/Core/SecretsManager/Repositories/Noop/NoopServiceAccountRepository.cs

@ -59,4 +59,5 @@ public class NoopServiceAccountRepository : IServiceAccountRepository @@ -59,4 +59,5 @@ public class NoopServiceAccountRepository : IServiceAccountRepository
}
public Task<IEnumerable<ServiceAccountSecretsDetails>> GetManyByOrganizationIdWithSecretsDetailsAsync(Guid organizationId, Guid userId, AccessClientType accessType) => throw new NotImplementedException();
public Task<(bool Read, bool Write)> AccessToServiceAccountsAsync(IEnumerable<Guid> ids, Guid userId, AccessClientType accessType) => throw new NotImplementedException();
}

247
test/Api.IntegrationTest/SecretsManager/Controllers/AccessPoliciesControllerTests.cs

@ -1189,6 +1189,190 @@ public class AccessPoliciesControllerTests : IClassFixture<ApiApplicationFactory @@ -1189,6 +1189,190 @@ public class AccessPoliciesControllerTests : IClassFixture<ApiApplicationFactory
Assert.Equal(result.UserAccessPolicies.First().Id, createdAccessPolicy.Id);
}
[Theory]
[InlineData(false, false, false)]
[InlineData(false, false, true)]
[InlineData(false, true, false)]
[InlineData(false, true, true)]
[InlineData(true, false, false)]
[InlineData(true, false, true)]
[InlineData(true, true, false)]
public async Task GetProjectServiceAccountAccessPolicies_SmAccessDenied_NotFound(bool useSecrets, bool accessSecrets, bool organizationEnabled)
{
var (org, _) = await _organizationHelper.Initialize(useSecrets, accessSecrets, organizationEnabled);
await LoginAsync(_email);
var project = await _projectRepository.CreateAsync(new Project
{
OrganizationId = org.Id,
Name = _mockEncryptedString
});
var response = await _client.GetAsync($"/projects/{project.Id}/access-policies/service-accounts");
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Fact]
public async Task GetProjectServiceAccountAccessPolicies_ReturnsEmpty()
{
var (org, _) = await _organizationHelper.Initialize(true, true, true);
await LoginAsync(_email);
var project = await _projectRepository.CreateAsync(new Project
{
OrganizationId = org.Id,
Name = _mockEncryptedString
});
var response = await _client.GetAsync($"/projects/{project.Id}/access-policies/service-accounts");
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<ProjectPeopleAccessPoliciesResponseModel>();
Assert.NotNull(result);
Assert.Empty(result!.UserAccessPolicies);
Assert.Empty(result.GroupAccessPolicies);
}
[Fact]
public async Task GetProjectServiceAccountAccessPolicies_NoPermission_NotFound()
{
await _organizationHelper.Initialize(true, true, true);
var (email, orgUser) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);
await LoginAsync(email);
var project = await _projectRepository.CreateAsync(new Project
{
OrganizationId = orgUser.OrganizationId,
Name = _mockEncryptedString
});
var response = await _client.GetAsync($"/projects/{project.Id}/access-policies/service-accounts");
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Theory]
[InlineData(PermissionType.RunAsAdmin)]
[InlineData(PermissionType.RunAsUserWithPermission)]
public async Task GetProjectServiceAccountAccessPolicies_Success(PermissionType permissionType)
{
var (_, organizationUser) = await _organizationHelper.Initialize(true, true, true);
await LoginAsync(_email);
var (serviceAccount, project) = await SetupProjectServiceAccountPermissionAsync(permissionType, organizationUser);
var response = await _client.GetAsync($"/projects/{project.Id}/access-policies/service-accounts");
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<ProjectServiceAccountsAccessPoliciesResponseModel>();
Assert.NotNull(result?.ServiceAccountsAccessPolicies);
Assert.Single(result!.ServiceAccountsAccessPolicies);
}
[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 PutProjectServiceAccountAccessPolicies_SmAccessDenied_NotFound(bool useSecrets, bool accessSecrets, bool organizationEnabled)
{
var (_, organizationUser) = await _organizationHelper.Initialize(useSecrets, accessSecrets, organizationEnabled);
await LoginAsync(_email);
var (project, request) = await SetupProjectServiceAccountRequestAsync(PermissionType.RunAsAdmin, organizationUser);
var response = await _client.PutAsJsonAsync($"/projects/{project.Id}/access-policies/service-accounts", request);
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Fact]
public async Task PutProjectServiceAccountAccessPolicies_NoPermission()
{
var (org, _) = await _organizationHelper.Initialize(true, true, true);
var (email, organizationUser) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);
await LoginAsync(email);
var project = await _projectRepository.CreateAsync(new Project
{
OrganizationId = org.Id,
Name = _mockEncryptedString
});
var request = new PeopleAccessPoliciesRequestModel
{
UserAccessPolicyRequests = new List<AccessPolicyRequest>
{
new() { GranteeId = organizationUser.Id, Read = true, Write = true }
}
};
var response = await _client.PutAsJsonAsync($"/projects/{project.Id}/access-policies/service-accounts", request);
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Theory]
[InlineData(PermissionType.RunAsAdmin)]
[InlineData(PermissionType.RunAsUserWithPermission)]
public async Task PutProjectServiceAccountAccessPolicies_MismatchedOrgIds_NotFound(PermissionType permissionType)
{
var (_, organizationUser) = await _organizationHelper.Initialize(true, true, true);
await LoginAsync(_email);
var (project, request) = await SetupProjectServiceAccountRequestAsync(permissionType, organizationUser);
var newOrg = await _organizationHelper.CreateSmOrganizationAsync();
var serviceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount
{
OrganizationId = newOrg.Id,
Name = _mockEncryptedString
});
request.ProjectServiceAccountsAccessPolicyRequests = new List<AccessPolicyRequest>
{
new() { GranteeId = serviceAccount.Id, Read = true, Write = true }
};
var response = await _client.PutAsJsonAsync($"/projects/{project.Id}/access-policies/service-accounts", request);
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Theory]
[InlineData(PermissionType.RunAsAdmin)]
[InlineData(PermissionType.RunAsUserWithPermission)]
public async Task PutProjectServiceAccountAccessPolicies_Success(PermissionType permissionType)
{
var (_, organizationUser) = await _organizationHelper.Initialize(true, true, true);
await LoginAsync(_email);
var (project, request) = await SetupProjectServiceAccountRequestAsync(permissionType, organizationUser);
var response = await _client.PutAsJsonAsync($"/projects/{project.Id}/access-policies/service-accounts", request);
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<ProjectServiceAccountsAccessPoliciesResponseModel>();
Assert.NotNull(result);
Assert.Equal(request.ProjectServiceAccountsAccessPolicyRequests.First().GranteeId,
result!.ServiceAccountsAccessPolicies.First().ServiceAccountId);
Assert.True(result.ServiceAccountsAccessPolicies.First().Read);
Assert.True(result.ServiceAccountsAccessPolicies.First().Write);
var createdAccessPolicy =
await _accessPolicyRepository.GetByIdAsync(result.ServiceAccountsAccessPolicies.First().Id);
Assert.NotNull(createdAccessPolicy);
Assert.Equal(result.ServiceAccountsAccessPolicies.First().Read, createdAccessPolicy!.Read);
Assert.Equal(result.ServiceAccountsAccessPolicies.First().Write, createdAccessPolicy.Write);
Assert.Equal(result.ServiceAccountsAccessPolicies.First().Id, createdAccessPolicy.Id);
}
private async Task<RequestSetupData> SetupAccessPolicyRequest(Guid organizationId)
{
var project = await _projectRepository.CreateAsync(new Project
@ -1284,6 +1468,55 @@ public class AccessPoliciesControllerTests : IClassFixture<ApiApplicationFactory @@ -1284,6 +1468,55 @@ public class AccessPoliciesControllerTests : IClassFixture<ApiApplicationFactory
return (serviceAccount, organizationUser);
}
private async Task<(ServiceAccount serviceAccount, Project project)> SetupProjectServiceAccountPermissionAsync(
PermissionType permissionType,
OrganizationUser organizationUser)
{
var project = await _projectRepository.CreateAsync(new Project
{
OrganizationId = organizationUser.OrganizationId,
Name = _mockEncryptedString
});
if (permissionType == PermissionType.RunAsUserWithPermission)
{
var (email, orgUser) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);
await LoginAsync(email);
organizationUser = orgUser;
}
var serviceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount
{
OrganizationId = organizationUser.OrganizationId,
Name = _mockEncryptedString,
});
var accessPolicies = new List<BaseAccessPolicy>
{
new UserServiceAccountAccessPolicy
{
GrantedServiceAccountId = serviceAccount.Id, OrganizationUserId = organizationUser.Id, Read = true, Write = true,
},
new UserProjectAccessPolicy
{
GrantedProjectId = project.Id, OrganizationUserId = organizationUser.Id, Read = true, Write = true,
},
new ServiceAccountProjectAccessPolicy
{
ServiceAccountId = serviceAccount.Id,
GrantedProjectId = project.Id,
Read = true,
Write = true,
},
};
await _accessPolicyRepository.CreateManyAsync(accessPolicies);
return (serviceAccount, project);
}
private async Task<(Project project, PeopleAccessPoliciesRequestModel request)> SetupProjectPeopleRequestAsync(
PermissionType permissionType, OrganizationUser organizationUser)
{
@ -1298,6 +1531,20 @@ public class AccessPoliciesControllerTests : IClassFixture<ApiApplicationFactory @@ -1298,6 +1531,20 @@ public class AccessPoliciesControllerTests : IClassFixture<ApiApplicationFactory
return (project, request);
}
private async Task<(Project project, ServiceAccountsAccessPoliciesRequestModel request)> SetupProjectServiceAccountRequestAsync(
PermissionType permissionType, OrganizationUser organizationUser)
{
var (serviceAccount, project) = await SetupProjectServiceAccountPermissionAsync(permissionType, organizationUser);
var request = new ServiceAccountsAccessPoliciesRequestModel
{
ProjectServiceAccountsAccessPolicyRequests = new List<AccessPolicyRequest>
{
new() { GranteeId = serviceAccount.Id, Read = true, Write = true }
}
};
return (project, request);
}
private async Task<(ServiceAccount serviceAccount, PeopleAccessPoliciesRequestModel request)> SetupServiceAccountPeopleRequestAsync(
PermissionType permissionType, OrganizationUser organizationUser)
{

206
test/Api.Test/SecretsManager/Controllers/AccessPoliciesControllerTests.cs

@ -1165,4 +1165,210 @@ public class AccessPoliciesControllerTests @@ -1165,4 +1165,210 @@ public class AccessPoliciesControllerTests
await sutProvider.GetDependency<IAccessPolicyRepository>().Received(1)
.ReplaceServiceAccountPeopleAsync(Arg.Any<ServiceAccountPeopleAccessPolicies>(), Arg.Any<Guid>());
}
[Theory]
[BitAutoData(PermissionType.RunAsAdmin)]
[BitAutoData(PermissionType.RunAsUserWithPermission)]
public async void GetProjectServiceAccountAccessPolicies_ReturnsEmptyList(
PermissionType permissionType,
SutProvider<AccessPoliciesController> sutProvider,
Guid id, Project data)
{
sutProvider.GetDependency<IProjectRepository>().GetByIdAsync(default).ReturnsForAnyArgs(data);
switch (permissionType)
{
case PermissionType.RunAsAdmin:
SetupAdmin(sutProvider, data.OrganizationId);
sutProvider.GetDependency<IProjectRepository>().AccessToProjectAsync(Arg.Any<Guid>(), Arg.Any<Guid>(),
AccessClientType.NoAccessCheck)
.Returns((true, true));
break;
case PermissionType.RunAsUserWithPermission:
SetupUserWithPermission(sutProvider, data.OrganizationId);
sutProvider.GetDependency<IProjectRepository>()
.AccessToProjectAsync(Arg.Any<Guid>(), Arg.Any<Guid>(), AccessClientType.User)
.Returns((true, true));
break;
}
var result = await sutProvider.Sut.GetProjectServiceAccountsAccessPoliciesAsync(id);
await sutProvider.GetDependency<IAccessPolicyRepository>().Received(1)
.GetServiceAccountPoliciesByGrantedProjectIdAsync(Arg.Is(AssertHelper.AssertPropertyEqual(id)));
Assert.Empty(result.ServiceAccountsAccessPolicies);
}
[Theory]
[BitAutoData]
public async void GetProjectServiceAccountAccessPolicies_UserWithoutPermission_Throws(
SutProvider<AccessPoliciesController> sutProvider,
Guid id,
Project data)
{
SetupUserWithoutPermission(sutProvider, data.OrganizationId);
sutProvider.GetDependency<IProjectRepository>().GetByIdAsync(default).ReturnsForAnyArgs(data);
sutProvider.GetDependency<IProjectRepository>().AccessToProjectAsync(default, default, default)
.Returns((false, false));
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.GetProjectServiceAccountsAccessPoliciesAsync(id));
await sutProvider.GetDependency<IAccessPolicyRepository>().DidNotReceiveWithAnyArgs()
.GetPeoplePoliciesByGrantedProjectIdAsync(Arg.Any<Guid>(), Arg.Any<Guid>());
}
[Theory]
[BitAutoData]
public async void GetProjectServiceAccountAccessPolicies_ProjectsDoesNotExist_UserWithPermission_Throws(
SutProvider<AccessPoliciesController> sutProvider,
Guid id,
Project data,
UserProjectAccessPolicy resultAccessPolicy)
{
SetupUserWithPermission(sutProvider, data.OrganizationId);
sutProvider.GetDependency<IAccessPolicyRepository>().GetPeoplePoliciesByGrantedProjectIdAsync(default, default)
.ReturnsForAnyArgs(new List<BaseAccessPolicy> { resultAccessPolicy });
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.GetProjectServiceAccountsAccessPoliciesAsync(id));
await sutProvider.GetDependency<IAccessPolicyRepository>().DidNotReceiveWithAnyArgs()
.GetPeoplePoliciesByGrantedProjectIdAsync(Arg.Any<Guid>(), Arg.Any<Guid>());
}
[Theory]
[BitAutoData(PermissionType.RunAsAdmin)]
[BitAutoData(PermissionType.RunAsUserWithPermission)]
public async void GetProjectServiceAccountAccessPolicies_Success(
PermissionType permissionType,
SutProvider<AccessPoliciesController> sutProvider,
Guid id,
Project data,
ServiceAccountProjectAccessPolicy resultServiceAccountProject)
{
sutProvider.GetDependency<IProjectRepository>().GetByIdAsync(default).ReturnsForAnyArgs(data);
switch (permissionType)
{
case PermissionType.RunAsAdmin:
SetupAdmin(sutProvider, data.OrganizationId);
sutProvider.GetDependency<IProjectRepository>().AccessToProjectAsync(Arg.Any<Guid>(), Arg.Any<Guid>(),
AccessClientType.NoAccessCheck)
.Returns((true, true));
break;
case PermissionType.RunAsUserWithPermission:
SetupUserWithPermission(sutProvider, data.OrganizationId);
sutProvider.GetDependency<IProjectRepository>()
.AccessToProjectAsync(Arg.Any<Guid>(), Arg.Any<Guid>(), AccessClientType.User)
.Returns((true, true));
break;
}
sutProvider.GetDependency<IAccessPolicyRepository>().GetServiceAccountPoliciesByGrantedProjectIdAsync(default)
.ReturnsForAnyArgs(new List<BaseAccessPolicy> { resultServiceAccountProject });
var result = await sutProvider.Sut.GetProjectServiceAccountsAccessPoliciesAsync(id);
await sutProvider.GetDependency<IAccessPolicyRepository>().Received(1)
.GetServiceAccountPoliciesByGrantedProjectIdAsync(Arg.Is(AssertHelper.AssertPropertyEqual(id)));
Assert.NotEmpty(result.ServiceAccountsAccessPolicies);
}
[Theory]
[BitAutoData]
public async void PutProjectServiceAccountAccessPolicies_ProjectDoesNotExist_Throws(
SutProvider<AccessPoliciesController> sutProvider,
Guid id,
ServiceAccountsAccessPoliciesRequestModel request)
{
await Assert.ThrowsAsync<NotFoundException>(() =>
sutProvider.Sut.PutProjectServiceAccountsAccessPoliciesAsync(id, request));
await sutProvider.GetDependency<IAccessPolicyRepository>().DidNotReceiveWithAnyArgs()
.ReplaceProjectServiceAccountsAsync(Arg.Any<ProjectServiceAccountsAccessPolicies>());
}
[Theory]
[BitAutoData]
public async void PutProjectServiceAccountAccessPoliciesAsync_DuplicatePolicy_Throws(
SutProvider<AccessPoliciesController> sutProvider,
Project project,
ServiceAccountsAccessPoliciesRequestModel request)
{
var dup = new AccessPolicyRequest { GranteeId = Guid.NewGuid(), Read = true, Write = true };
request.ProjectServiceAccountsAccessPolicyRequests = new[] { dup, dup };
sutProvider.GetDependency<IProjectRepository>().GetByIdAsync(default).ReturnsForAnyArgs(project);
await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.PutProjectServiceAccountsAccessPoliciesAsync(project.Id, request));
await sutProvider.GetDependency<IAccessPolicyRepository>().DidNotReceiveWithAnyArgs()
.ReplaceProjectServiceAccountsAsync(Arg.Any<ProjectServiceAccountsAccessPolicies>());
}
[Theory]
[BitAutoData]
public async void PutProjectServiceAccountAccessPoliciesAsync_UnsupportedPermissions_Throws(
SutProvider<AccessPoliciesController> sutProvider,
Project project,
ServiceAccountsAccessPoliciesRequestModel request)
{
var dup = new AccessPolicyRequest { GranteeId = Guid.NewGuid(), Read = true, Write = false };
request.ProjectServiceAccountsAccessPolicyRequests = new[] { dup, dup };
sutProvider.GetDependency<IProjectRepository>().GetByIdAsync(default).ReturnsForAnyArgs(project);
await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.PutProjectServiceAccountsAccessPoliciesAsync(project.Id, request));
await sutProvider.GetDependency<IAccessPolicyRepository>().DidNotReceiveWithAnyArgs()
.ReplaceProjectServiceAccountsAsync(Arg.Any<ProjectServiceAccountsAccessPolicies>());
}
[Theory]
[BitAutoData]
public async void PutProjectServiceAccountAccessPoliciesAsync_NoAccess_Throws(
SutProvider<AccessPoliciesController> sutProvider,
Project project,
ServiceAccountsAccessPoliciesRequestModel request)
{
sutProvider.GetDependency<IProjectRepository>().GetByIdAsync(default).ReturnsForAnyArgs(project);
var peoplePolicies = request.ToProjectServiceAccountsAccessPolicies(project.Id, project.OrganizationId);
sutProvider.GetDependency<IAuthorizationService>()
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), peoplePolicies,
Arg.Any<IEnumerable<IAuthorizationRequirement>>()).ReturnsForAnyArgs(AuthorizationResult.Failed());
await Assert.ThrowsAsync<NotFoundException>(() =>
sutProvider.Sut.PutProjectServiceAccountsAccessPoliciesAsync(project.Id, request));
await sutProvider.GetDependency<IAccessPolicyRepository>().DidNotReceiveWithAnyArgs()
.ReplaceProjectServiceAccountsAsync(Arg.Any<ProjectServiceAccountsAccessPolicies>());
}
[Theory]
[BitAutoData]
public async void PutProjectServiceAccountAccessPoliciesAsync_Success(
SutProvider<AccessPoliciesController> sutProvider,
Guid userId,
Project project,
ServiceAccountsAccessPoliciesRequestModel request)
{
sutProvider.GetDependency<IProjectRepository>().GetByIdAsync(default).ReturnsForAnyArgs(project);
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
var peoplePolicies = request.ToProjectServiceAccountsAccessPolicies(project.Id, project.OrganizationId);
sutProvider.GetDependency<IAuthorizationService>()
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), peoplePolicies,
Arg.Any<IEnumerable<IAuthorizationRequirement>>()).ReturnsForAnyArgs(AuthorizationResult.Success());
sutProvider.GetDependency<IAccessPolicyRepository>().ReplaceProjectServiceAccountsAsync(peoplePolicies)
.Returns(peoplePolicies.ToBaseAccessPolicies());
await sutProvider.Sut.PutProjectServiceAccountsAccessPoliciesAsync(project.Id, request);
await sutProvider.GetDependency<IAccessPolicyRepository>().Received(1)
.ReplaceProjectServiceAccountsAsync(Arg.Any<ProjectServiceAccountsAccessPolicies>());
}
}

79
test/Api.Test/SecretsManager/SecretsManager/Utilities/AccessPolicyHelpersTests.cs

@ -0,0 +1,79 @@ @@ -0,0 +1,79 @@
using Bit.Api.SecretsManager.Utilities;
using Bit.Core.Exceptions;
using Bit.Core.SecretsManager.Entities;
using Xunit;
namespace Bit.Api.Test.SecretsManager.Utilities;
public class AccessPolicyHelpersTests
{
[Fact]
public void CheckForDistinctAccessPolicies_ThrowsExceptionWhenDuplicateExists()
{
var duplicatePolicy = new UserProjectAccessPolicy
{
OrganizationUserId = Guid.NewGuid(),
GrantedProjectId = Guid.NewGuid()
};
var accessPolicies = new List<BaseAccessPolicy>
{
duplicatePolicy,
duplicatePolicy // Duplicate policy
};
Assert.Throws<BadRequestException>(() =>
{
AccessPolicyHelpers.CheckForDistinctAccessPolicies(accessPolicies);
});
}
[Fact]
public void CheckForDistinctAccessPolicies_Success()
{
var accessPolicies = new List<BaseAccessPolicy>
{
new UserProjectAccessPolicy
{
OrganizationUserId = Guid.NewGuid(),
GrantedProjectId = Guid.NewGuid()
},
new GroupProjectAccessPolicy
{
GroupId = Guid.NewGuid(),
GrantedProjectId = Guid.NewGuid()
}
};
var exception = Record.Exception(() => AccessPolicyHelpers.CheckForDistinctAccessPolicies(accessPolicies));
Assert.Null(exception);
}
[Fact]
public void CheckAccessPoliciesHasReadPermission_ThrowsExceptionWhenReadPermissionIsFalse()
{
var accessPolicies = new List<BaseAccessPolicy>
{
new UserProjectAccessPolicy { Read = false, Write = true },
new GroupProjectAccessPolicy { Read = true, Write = false }
};
Assert.Throws<BadRequestException>(() =>
{
AccessPolicyHelpers.CheckAccessPoliciesHasReadPermission(accessPolicies);
});
}
[Fact]
public void CheckAccessPoliciesHasReadPermission_Success()
{
var accessPolicies = new List<BaseAccessPolicy>
{
new UserProjectAccessPolicy { Read = true, Write = true },
new GroupProjectAccessPolicy { Read = true, Write = false }
};
var exception = Record.Exception(() => AccessPolicyHelpers.CheckAccessPoliciesHasReadPermission(accessPolicies));
Assert.Null(exception);
}
}
Loading…
Cancel
Save