From ebd88393c86b4db711b0b5e380efc4184fb4e184 Mon Sep 17 00:00:00 2001 From: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> Date: Wed, 1 May 2024 11:47:11 -0500 Subject: [PATCH] [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 --- ...ountGrantedPoliciesAuthorizationHandler.cs | 88 +++ ...ateServiceAccountGrantedPoliciesCommand.cs | 26 + ...ServiceAccountGrantedPolicyUpdatesQuery.cs | 41 ++ .../SecretsManagerCollectionExtensions.cs | 3 + .../Repositories/AccessPolicyRepository.cs | 176 ++++-- .../Repositories/ProjectRepository.cs | 63 ++- ...rantedPoliciesAuthorizationHandlerTests.cs | 273 +++++++++ ...rviceAccountGrantedPoliciesCommandTests.cs | 43 ++ ...ceAccountGrantedPolicyUpdatesQueryTests.cs | 86 +++ .../Controllers/AccessPoliciesController.cs | 110 ++-- ...rviceAccountGrantedPoliciesRequestModel.cs | 28 + ...dPoliciesPermissionDetailsResponseModel.cs | 30 + ...essPolicyPermissionDetailsResponseModel.cs | 25 + .../Utilities/AccessPolicyHelpers.cs | 40 ++ ...ountGrantedPoliciesOperationRequirement.cs | 14 + ...ateServiceAccountGrantedPoliciesCommand.cs | 9 + .../AccessPolicies/AccessPolicyOperation.cs | 8 + ...ServiceAccountProjectAccessPolicyUpdate.cs | 11 + .../Data/ServiceAccountGrantedPolicies.cs | 83 +++ ...AccountGrantedPoliciesPermissionDetails.cs | 17 + ...ServiceAccountGrantedPolicyUpdatesQuery.cs | 9 + .../Repositories/IAccessPolicyRepository.cs | 6 +- .../Repositories/IProjectRepository.cs | 2 + .../Noop/NoopProjectRepository.cs | 6 + .../AccessPoliciesControllerTests.cs | 456 ++++++++------- .../AccessPoliciesControllerTests.cs | 519 +++++++++--------- .../Utilities/AccessPolicyHelpersTests.cs | 101 ++++ .../ServiceAccountGrantedPoliciesTests.cs | 77 +++ 28 files changed, 1772 insertions(+), 578 deletions(-) create mode 100644 bitwarden_license/src/Commercial.Core/SecretsManager/AuthorizationHandlers/AccessPolicies/ServiceAccountGrantedPoliciesAuthorizationHandler.cs create mode 100644 bitwarden_license/src/Commercial.Core/SecretsManager/Commands/AccessPolicies/UpdateServiceAccountGrantedPoliciesCommand.cs create mode 100644 bitwarden_license/src/Commercial.Core/SecretsManager/Queries/AccessPolicies/ServiceAccountGrantedPolicyUpdatesQuery.cs create mode 100644 bitwarden_license/test/Commercial.Core.Test/SecretsManager/AuthorizationHandlers/AccessPolicies/ServiceAccountGrantedPoliciesAuthorizationHandlerTests.cs create mode 100644 bitwarden_license/test/Commercial.Core.Test/SecretsManager/Commands/AccessPolicies/UpdateServiceAccountGrantedPoliciesCommandTests.cs create mode 100644 bitwarden_license/test/Commercial.Core.Test/SecretsManager/Queries/AccessPolicies/ServiceAccountGrantedPolicyUpdatesQueryTests.cs create mode 100644 src/Api/SecretsManager/Models/Request/ServiceAccountGrantedPoliciesRequestModel.cs create mode 100644 src/Api/SecretsManager/Models/Response/ServiceAccountGrantedPoliciesPermissionDetailsResponseModel.cs create mode 100644 src/Api/SecretsManager/Models/Response/ServiceAccountProjectAccessPolicyPermissionDetailsResponseModel.cs create mode 100644 src/Api/SecretsManager/Utilities/AccessPolicyHelpers.cs create mode 100644 src/Core/SecretsManager/AuthorizationRequirements/ServiceAccountGrantedPoliciesOperationRequirement.cs create mode 100644 src/Core/SecretsManager/Commands/AccessPolicies/Interfaces/IUpdateServiceAccountGrantedPoliciesCommand.cs create mode 100644 src/Core/SecretsManager/Enums/AccessPolicies/AccessPolicyOperation.cs create mode 100644 src/Core/SecretsManager/Models/Data/AccessPolicyUpdates/ServiceAccountProjectAccessPolicyUpdate.cs create mode 100644 src/Core/SecretsManager/Models/Data/ServiceAccountGrantedPolicies.cs create mode 100644 src/Core/SecretsManager/Models/Data/ServiceAccountGrantedPoliciesPermissionDetails.cs create mode 100644 src/Core/SecretsManager/Queries/AccessPolicies/Interfaces/IServiceAccountGrantedPolicyUpdatesQuery.cs create mode 100644 test/Api.Test/SecretsManager/Utilities/AccessPolicyHelpersTests.cs create mode 100644 test/Core.Test/SecretsManager/Models/ServiceAccountGrantedPoliciesTests.cs diff --git a/bitwarden_license/src/Commercial.Core/SecretsManager/AuthorizationHandlers/AccessPolicies/ServiceAccountGrantedPoliciesAuthorizationHandler.cs b/bitwarden_license/src/Commercial.Core/SecretsManager/AuthorizationHandlers/AccessPolicies/ServiceAccountGrantedPoliciesAuthorizationHandler.cs new file mode 100644 index 0000000000..d5b4054169 --- /dev/null +++ b/bitwarden_license/src/Commercial.Core/SecretsManager/AuthorizationHandlers/AccessPolicies/ServiceAccountGrantedPoliciesAuthorizationHandler.cs @@ -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); + } + } + } +} diff --git a/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/AccessPolicies/UpdateServiceAccountGrantedPoliciesCommand.cs b/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/AccessPolicies/UpdateServiceAccountGrantedPoliciesCommand.cs new file mode 100644 index 0000000000..e64315f1c7 --- /dev/null +++ b/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/AccessPolicies/UpdateServiceAccountGrantedPoliciesCommand.cs @@ -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); + } +} diff --git a/bitwarden_license/src/Commercial.Core/SecretsManager/Queries/AccessPolicies/ServiceAccountGrantedPolicyUpdatesQuery.cs b/bitwarden_license/src/Commercial.Core/SecretsManager/Queries/AccessPolicies/ServiceAccountGrantedPolicyUpdatesQuery.cs new file mode 100644 index 0000000000..dce91f47bc --- /dev/null +++ b/bitwarden_license/src/Commercial.Core/SecretsManager/Queries/AccessPolicies/ServiceAccountGrantedPolicyUpdatesQuery.cs @@ -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 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); + } +} diff --git a/bitwarden_license/src/Commercial.Core/SecretsManager/SecretsManagerCollectionExtensions.cs b/bitwarden_license/src/Commercial.Core/SecretsManager/SecretsManagerCollectionExtensions.cs index 1fbb21f4b9..da7804b8ef 100644 --- a/bitwarden_license/src/Commercial.Core/SecretsManager/SecretsManagerCollectionExtensions.cs +++ b/bitwarden_license/src/Commercial.Core/SecretsManager/SecretsManagerCollectionExtensions.cs @@ -41,10 +41,12 @@ public static class SecretsManagerCollectionExtensions services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); @@ -64,5 +66,6 @@ public static class SecretsManagerCollectionExtensions services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); } } diff --git a/bitwarden_license/src/Commercial.Infrastructure.EntityFramework/SecretsManager/Repositories/AccessPolicyRepository.cs b/bitwarden_license/src/Commercial.Infrastructure.EntityFramework/SecretsManager/Repositories/AccessPolicyRepository.cs index 691c188aa9..9c07c910a9 100644 --- a/bitwarden_license/src/Commercial.Infrastructure.EntityFramework/SecretsManager/Repositories/AccessPolicyRepository.cs +++ b/bitwarden_license/src/Commercial.Infrastructure.EntityFramework/SecretsManager/Repositories/AccessPolicyRepository.cs @@ -1,7 +1,8 @@ -using System.Linq.Expressions; -using AutoMapper; +using AutoMapper; using Bit.Core.Enums; +using Bit.Core.SecretsManager.Enums.AccessPolicies; using Bit.Core.SecretsManager.Models.Data; +using Bit.Core.SecretsManager.Models.Data.AccessPolicyUpdates; using Bit.Core.SecretsManager.Repositories; using Bit.Infrastructure.EntityFramework.Repositories; using Bit.Infrastructure.EntityFramework.SecretsManager.Discriminators; @@ -19,14 +20,7 @@ public class AccessPolicyRepository : BaseEntityFrameworkRepository, IAccessPoli { } - private static Expression> UserHasWriteAccessToProject(Guid userId) => - policy => - policy.GrantedProject.UserAccessPolicies.Any(ap => ap.OrganizationUser.User.Id == userId && ap.Write) || - policy.GrantedProject.GroupAccessPolicies.Any(ap => - ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.User.Id == userId && ap.Write)); - - public async Task> CreateManyAsync( - List baseAccessPolicies) + public async Task> CreateManyAsync(List baseAccessPolicies) { await using var scope = ServiceScopeFactory.CreateAsyncScope(); var dbContext = GetDatabaseContext(scope); @@ -219,29 +213,6 @@ public class AccessPolicyRepository : BaseEntityFrameworkRepository, IAccessPoli } } - public async Task> 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 GetPeopleGranteesAsync(Guid organizationId, Guid currentUserId) { using var scope = ServiceScopeFactory.CreateScope(); @@ -429,6 +400,77 @@ public class AccessPolicyRepository : BaseEntityFrameworkRepository, IAccessPoli return await GetPeoplePoliciesByGrantedServiceAccountIdAsync(peopleAccessPolicies.Id, userId); } + public async Task 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 + GetServiceAccountGrantedPoliciesPermissionDetailsAsync(Guid serviceAccountId, Guid userId, + AccessClientType accessClientType) + { + await using var scope = ServiceScopeFactory.CreateAsyncScope(); + var dbContext = GetDatabaseContext(scope); + var accessPolicyQuery = dbContext.ServiceAccountProjectAccessPolicy + .Where(ap => ap.ServiceAccountId == serviceAccountId) + .Include(ap => ap.ServiceAccount) + .Include(ap => ap.GrantedProject); + + var accessPoliciesPermissionDetails = + await ToPermissionDetails(accessPolicyQuery, userId, accessClientType).ToListAsync(); + if (accessPoliciesPermissionDetails.Count == 0) + { + return null; + } + + return new ServiceAccountGrantedPoliciesPermissionDetails + { + ServiceAccountId = serviceAccountId, + OrganizationId = accessPoliciesPermissionDetails.First().AccessPolicy.GrantedProject!.OrganizationId, + ProjectGrantedPolicies = accessPoliciesPermissionDetails + }; + } + + public async Task UpdateServiceAccountGrantedPoliciesAsync(ServiceAccountGrantedPoliciesUpdates updates) + { + await using var scope = ServiceScopeFactory.CreateAsyncScope(); + var dbContext = GetDatabaseContext(scope); + var currentAccessPolicies = await dbContext.ServiceAccountProjectAccessPolicy + .Where(ap => ap.ServiceAccountId == updates.ServiceAccountId) + .ToListAsync(); + + if (currentAccessPolicies.Count != 0) + { + var projectIdsToDelete = updates.ProjectGrantedPolicyUpdates + .Where(pu => pu.Operation == AccessPolicyOperation.Delete) + .Select(pu => pu.AccessPolicy.GrantedProjectId!.Value) + .ToList(); + + var policiesToDelete = currentAccessPolicies + .Where(entity => projectIdsToDelete.Contains(entity.GrantedProjectId!.Value)) + .ToList(); + + dbContext.RemoveRange(policiesToDelete); + } + + await UpsertServiceAccountGrantedPoliciesAsync(dbContext, currentAccessPolicies, + updates.ProjectGrantedPolicyUpdates.Where(pu => pu.Operation != AccessPolicyOperation.Delete).ToList()); + await UpdateServiceAccountRevisionAsync(dbContext, updates.ServiceAccountId); + await dbContext.SaveChangesAsync(); + } + private static async Task UpsertPeoplePoliciesAsync(DatabaseContext dbContext, List policies, IReadOnlyCollection userPolicyEntities, IReadOnlyCollection groupPolicyEntities) @@ -464,6 +506,36 @@ public class AccessPolicyRepository : BaseEntityFrameworkRepository, IAccessPoli } } + private async Task UpsertServiceAccountGrantedPoliciesAsync(DatabaseContext dbContext, + IReadOnlyCollection currentPolices, + List policyUpdates) + { + var currentDate = DateTime.UtcNow; + foreach (var policyUpdate in policyUpdates) + { + var updatedEntity = MapToEntity(policyUpdate.AccessPolicy); + var currentEntity = currentPolices.FirstOrDefault(e => + e.GrantedProjectId == policyUpdate.AccessPolicy.GrantedProjectId!.Value); + + switch (policyUpdate.Operation) + { + case AccessPolicyOperation.Create when currentEntity == null: + updatedEntity.SetNewId(); + await dbContext.AddAsync(updatedEntity); + break; + + case AccessPolicyOperation.Update when currentEntity != null: + dbContext.AccessPolicies.Attach(currentEntity); + currentEntity.Read = updatedEntity.Read; + currentEntity.Write = updatedEntity.Write; + currentEntity.RevisionDate = currentDate; + break; + default: + throw new InvalidOperationException("Policy updates failed due to unexpected state."); + } + } + } + private Core.SecretsManager.Entities.BaseAccessPolicy MapToCore( BaseAccessPolicy baseAccessPolicyEntity) => baseAccessPolicyEntity switch @@ -518,4 +590,42 @@ public class AccessPolicyRepository : BaseEntityFrameworkRepository, IAccessPoli return MapToCore(baseAccessPolicyEntity); } } + + private IQueryable ToPermissionDetails( + IQueryable + query, Guid userId, AccessClientType accessClientType) + { + var permissionDetails = accessClientType switch + { + AccessClientType.NoAccessCheck => query.Select(ap => new ServiceAccountProjectAccessPolicyPermissionDetails + { + AccessPolicy = + Mapper.Map(ap), + HasPermission = true + }), + AccessClientType.User => query.Select(ap => new ServiceAccountProjectAccessPolicyPermissionDetails + { + AccessPolicy = + Mapper.Map(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; + } + } } diff --git a/bitwarden_license/src/Commercial.Infrastructure.EntityFramework/SecretsManager/Repositories/ProjectRepository.cs b/bitwarden_license/src/Commercial.Infrastructure.EntityFramework/SecretsManager/Repositories/ProjectRepository.cs index 51d6a88785..55360a7248 100644 --- a/bitwarden_license/src/Commercial.Infrastructure.EntityFramework/SecretsManager/Repositories/ProjectRepository.cs +++ b/bitwarden_license/src/Commercial.Infrastructure.EntityFramework/SecretsManager/Repositories/ProjectRepository.cs @@ -140,27 +140,8 @@ public class ProjectRepository : Repository s.Id == id); - var query = accessType switch - { - AccessClientType.NoAccessCheck => projectQuery.Select(_ => new { Read = true, Write = true }), - AccessClientType.User => projectQuery.Select(p => new - { - Read = p.UserAccessPolicies.Any(ap => ap.OrganizationUser.User.Id == userId && ap.Read) - || p.GroupAccessPolicies.Any(ap => - ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.User.Id == userId && ap.Read)), - Write = p.UserAccessPolicies.Any(ap => ap.OrganizationUser.User.Id == userId && ap.Write) || - p.GroupAccessPolicies.Any(ap => - ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.User.Id == userId && ap.Write)), - }), - AccessClientType.ServiceAccount => projectQuery.Select(p => new - { - Read = p.ServiceAccountAccessPolicies.Any(ap => ap.ServiceAccountId == userId && ap.Read), - Write = p.ServiceAccountAccessPolicies.Any(ap => ap.ServiceAccountId == userId && ap.Write), - }), - _ => projectQuery.Select(_ => new { Read = false, Write = false }), - }; - - var policy = await query.FirstOrDefaultAsync(); + var accessQuery = BuildProjectAccessQuery(projectQuery, userId, accessType); + var policy = await accessQuery.FirstOrDefaultAsync(); return policy == null ? (false, false) : (policy.Read, policy.Write); } @@ -174,6 +155,46 @@ public class ProjectRepository : Repository> AccessToProjectsAsync( + IEnumerable 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 BuildProjectAccessQuery(IQueryable 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 ProjectToPermissionDetails(IQueryable query, Guid userId, AccessClientType accessType) { var projects = accessType switch diff --git a/bitwarden_license/test/Commercial.Core.Test/SecretsManager/AuthorizationHandlers/AccessPolicies/ServiceAccountGrantedPoliciesAuthorizationHandlerTests.cs b/bitwarden_license/test/Commercial.Core.Test/SecretsManager/AuthorizationHandlers/AccessPolicies/ServiceAccountGrantedPoliciesAuthorizationHandlerTests.cs new file mode 100644 index 0000000000..6f36684c44 --- /dev/null +++ b/bitwarden_license/test/Commercial.Core.Test/SecretsManager/AuthorizationHandlers/AccessPolicies/ServiceAccountGrantedPoliciesAuthorizationHandlerTests.cs @@ -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 sutProvider, + ServiceAccountGrantedPoliciesUpdates resource, + ClaimsPrincipal claimsPrincipal) + { + var requirement = ServiceAccountGrantedPoliciesOperations.Updates; + sutProvider.GetDependency().AccessSecretsManager(resource.OrganizationId) + .Returns(false); + var authzContext = new AuthorizationHandlerContext(new List { 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 sutProvider, + ServiceAccountGrantedPoliciesUpdates resource, + ClaimsPrincipal claimsPrincipal) + { + var requirement = ServiceAccountGrantedPoliciesOperations.Updates; + SetupUserSubstitutes(sutProvider, accessClientType, resource); + var authzContext = new AuthorizationHandlerContext(new List { requirement }, + claimsPrincipal, resource); + + await sutProvider.Sut.HandleAsync(authzContext); + + Assert.False(authzContext.HasSucceeded); + } + + [Theory] + [BitAutoData] + public async Task Handler_UnsupportedServiceAccountGrantedPoliciesOperationRequirement_Throws( + SutProvider sutProvider, + ServiceAccountGrantedPoliciesUpdates resource, + ClaimsPrincipal claimsPrincipal) + { + var requirement = new ServiceAccountGrantedPoliciesOperationRequirement(); + SetupUserSubstitutes(sutProvider, AccessClientType.NoAccessCheck, resource); + var authzContext = new AuthorizationHandlerContext(new List { requirement }, + claimsPrincipal, resource); + + await Assert.ThrowsAsync(() => 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 sutProvider, + ServiceAccountGrantedPoliciesUpdates resource, + Guid userId, + ClaimsPrincipal claimsPrincipal) + { + var requirement = ServiceAccountGrantedPoliciesOperations.Updates; + SetupUserSubstitutes(sutProvider, accessClientType, resource, userId); + sutProvider.GetDependency() + .AccessToServiceAccountAsync(resource.ServiceAccountId, userId, accessClientType) + .Returns((saReadAccess, saWriteAccess)); + var authzContext = new AuthorizationHandlerContext(new List { requirement }, + claimsPrincipal, resource); + + await sutProvider.Sut.HandleAsync(authzContext); + + Assert.False(authzContext.HasSucceeded); + } + + [Theory] + [BitAutoData] + public async Task Handler_GrantedProjectsInDifferentOrganization_DoesNotSucceed( + SutProvider sutProvider, + ServiceAccountGrantedPoliciesUpdates resource, + Guid userId, + ClaimsPrincipal claimsPrincipal) + { + var requirement = ServiceAccountGrantedPoliciesOperations.Updates; + SetupUserSubstitutes(sutProvider, AccessClientType.NoAccessCheck, resource, userId); + sutProvider.GetDependency() + .AccessToServiceAccountAsync(resource.ServiceAccountId, userId, AccessClientType.NoAccessCheck) + .Returns((true, true)); + sutProvider.GetDependency() + .ProjectsAreInOrganization(Arg.Any>(), resource.OrganizationId) + .Returns(false); + var authzContext = new AuthorizationHandlerContext(new List { 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 sutProvider, + ServiceAccountGrantedPoliciesUpdates resource, + Guid userId, + ClaimsPrincipal claimsPrincipal) + { + var requirement = ServiceAccountGrantedPoliciesOperations.Updates; + var projectIds = SetupProjectAccessTest(sutProvider, accessClientType, resource, userId); + + sutProvider.GetDependency() + .AccessToProjectsAsync(Arg.Any>(), userId, accessClientType) + .Returns(projectIds.ToDictionary(projectId => projectId, _ => (false, false))); + + + var authzContext = new AuthorizationHandlerContext(new List { 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 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() + .AccessToProjectsAsync(Arg.Any>(), userId, accessClientType) + .Returns(accessResult); + + + var authzContext = new AuthorizationHandlerContext(new List { 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 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() + .AccessToProjectsAsync(Arg.Any>(), userId, accessClientType) + .Returns(accessResult); + + var authzContext = new AuthorizationHandlerContext(new List { 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 sutProvider, + ServiceAccountGrantedPoliciesUpdates resource, + Guid userId, + ClaimsPrincipal claimsPrincipal) + { + var requirement = ServiceAccountGrantedPoliciesOperations.Updates; + var projectIds = SetupProjectAccessTest(sutProvider, accessClientType, resource, userId); + + sutProvider.GetDependency() + .AccessToProjectsAsync(Arg.Any>(), userId, accessClientType) + .Returns(projectIds.ToDictionary(projectId => projectId, _ => (true, true))); + + var authzContext = new AuthorizationHandlerContext(new List { requirement }, + claimsPrincipal, resource); + + await sutProvider.Sut.HandleAsync(authzContext); + + Assert.True(authzContext.HasSucceeded); + } + + private static void SetupUserSubstitutes( + SutProvider sutProvider, + AccessClientType accessClientType, + ServiceAccountGrantedPoliciesUpdates resource, + Guid userId = new()) + { + sutProvider.GetDependency().AccessSecretsManager(resource.OrganizationId) + .Returns(true); + sutProvider.GetDependency().GetAccessClientAsync(default, resource.OrganizationId) + .ReturnsForAnyArgs((accessClientType, userId)); + } + + private static List SetupProjectAccessTest( + SutProvider sutProvider, + AccessClientType accessClientType, + ServiceAccountGrantedPoliciesUpdates resource, + Guid userId = new()) + { + SetupUserSubstitutes(sutProvider, accessClientType, resource, userId); + + sutProvider.GetDependency() + .AccessToServiceAccountAsync(resource.ServiceAccountId, userId, accessClientType) + .Returns((true, true)); + sutProvider.GetDependency() + .ProjectsAreInOrganization(Arg.Any>(), resource.OrganizationId) + .Returns(true); + + return resource.ProjectGrantedPolicyUpdates + .Select(pu => pu.AccessPolicy.GrantedProjectId!.Value) + .ToList(); + } +} diff --git a/bitwarden_license/test/Commercial.Core.Test/SecretsManager/Commands/AccessPolicies/UpdateServiceAccountGrantedPoliciesCommandTests.cs b/bitwarden_license/test/Commercial.Core.Test/SecretsManager/Commands/AccessPolicies/UpdateServiceAccountGrantedPoliciesCommandTests.cs new file mode 100644 index 0000000000..ca3e902376 --- /dev/null +++ b/bitwarden_license/test/Commercial.Core.Test/SecretsManager/Commands/AccessPolicies/UpdateServiceAccountGrantedPoliciesCommandTests.cs @@ -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 sutProvider, + ServiceAccountGrantedPoliciesUpdates data) + { + data.ProjectGrantedPolicyUpdates = []; + await sutProvider.Sut.UpdateAsync(data); + + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .UpdateServiceAccountGrantedPoliciesAsync(Arg.Any()); + } + + [Theory] + [BitAutoData] + public async Task UpdateAsync_HasUpdates_CallsRepository( + SutProvider sutProvider, + ServiceAccountGrantedPoliciesUpdates data) + { + await sutProvider.Sut.UpdateAsync(data); + + await sutProvider.GetDependency() + .Received(1) + .UpdateServiceAccountGrantedPoliciesAsync(Arg.Any()); + } +} diff --git a/bitwarden_license/test/Commercial.Core.Test/SecretsManager/Queries/AccessPolicies/ServiceAccountGrantedPolicyUpdatesQueryTests.cs b/bitwarden_license/test/Commercial.Core.Test/SecretsManager/Queries/AccessPolicies/ServiceAccountGrantedPolicyUpdatesQueryTests.cs new file mode 100644 index 0000000000..64ee4cd34e --- /dev/null +++ b/bitwarden_license/test/Commercial.Core.Test/SecretsManager/Queries/AccessPolicies/ServiceAccountGrantedPolicyUpdatesQueryTests.cs @@ -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 sutProvider, + ServiceAccountGrantedPolicies data) + { + sutProvider.GetDependency() + .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 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() + .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)); + } +} diff --git a/src/Api/SecretsManager/Controllers/AccessPoliciesController.cs b/src/Api/SecretsManager/Controllers/AccessPoliciesController.cs index 734fcfe307..59e178a9b6 100644 --- a/src/Api/SecretsManager/Controllers/AccessPoliciesController.cs +++ b/src/Api/SecretsManager/Controllers/AccessPoliciesController.cs @@ -7,6 +7,8 @@ using Bit.Core.Exceptions; using Bit.Core.SecretsManager.AuthorizationRequirements; using Bit.Core.SecretsManager.Commands.AccessPolicies.Interfaces; using Bit.Core.SecretsManager.Entities; +using Bit.Core.SecretsManager.Queries.AccessPolicies.Interfaces; +using Bit.Core.SecretsManager.Queries.Interfaces; using Bit.Core.SecretsManager.Repositories; using Bit.Core.Services; using Microsoft.AspNetCore.Authorization; @@ -26,6 +28,9 @@ public class AccessPoliciesController : Controller private readonly IProjectRepository _projectRepository; private readonly IServiceAccountRepository _serviceAccountRepository; private readonly IUpdateAccessPolicyCommand _updateAccessPolicyCommand; + private readonly IUpdateServiceAccountGrantedPoliciesCommand _updateServiceAccountGrantedPoliciesCommand; + private readonly IAccessClientQuery _accessClientQuery; + private readonly IServiceAccountGrantedPolicyUpdatesQuery _serviceAccountGrantedPolicyUpdatesQuery; private readonly IUserService _userService; private readonly IAuthorizationService _authorizationService; @@ -36,6 +41,9 @@ public class AccessPoliciesController : Controller IAccessPolicyRepository accessPolicyRepository, IServiceAccountRepository serviceAccountRepository, IProjectRepository projectRepository, + IAccessClientQuery accessClientQuery, + IServiceAccountGrantedPolicyUpdatesQuery serviceAccountGrantedPolicyUpdatesQuery, + IUpdateServiceAccountGrantedPoliciesCommand updateServiceAccountGrantedPoliciesCommand, ICreateAccessPoliciesCommand createAccessPoliciesCommand, IDeleteAccessPolicyCommand deleteAccessPolicyCommand, IUpdateAccessPolicyCommand updateAccessPolicyCommand) @@ -49,6 +57,9 @@ public class AccessPoliciesController : Controller _createAccessPoliciesCommand = createAccessPoliciesCommand; _deleteAccessPolicyCommand = deleteAccessPolicyCommand; _updateAccessPolicyCommand = updateAccessPolicyCommand; + _updateServiceAccountGrantedPoliciesCommand = updateServiceAccountGrantedPoliciesCommand; + _accessClientQuery = accessClientQuery; + _serviceAccountGrantedPolicyUpdatesQuery = serviceAccountGrantedPolicyUpdatesQuery; } [HttpPost("/projects/{id}/access-policies")] @@ -89,61 +100,6 @@ public class AccessPoliciesController : Controller return new ProjectAccessPoliciesResponseModel(results); } - [HttpPost("/service-accounts/{id}/granted-policies")] - public async Task> - CreateServiceAccountGrantedPoliciesAsync([FromRoute] Guid id, - [FromBody] List 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(policies)); - var responses = results.Select(ap => - new ServiceAccountProjectAccessPolicyResponseModel((ServiceAccountProjectAccessPolicy)ap)); - return new ListResponseModel(responses); - } - - [HttpGet("/service-accounts/{id}/granted-policies")] - public async Task> - 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(responses); - } - [HttpPut("{id}")] public async Task UpdateAccessPolicyAsync([FromRoute] Guid id, [FromBody] AccessPolicyUpdateRequest request) @@ -303,6 +259,43 @@ public class AccessPoliciesController : Controller return new ServiceAccountPeopleAccessPoliciesResponseModel(results, userId); } + [HttpGet("/service-accounts/{id}/granted-policies")] + public async Task + 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 + PutServiceAccountGrantedPoliciesAsync([FromRoute] Guid id, + [FromBody] ServiceAccountGrantedPoliciesRequestModel request) + { + var serviceAccount = await _serviceAccountRepository.GetByIdAsync(id) ?? throw new NotFoundException(); + var grantedPoliciesUpdates = + await _serviceAccountGrantedPolicyUpdatesQuery.GetAsync(request.ToGrantedPolicies(serviceAccount)); + + var authorizationResult = await _authorizationService.AuthorizeAsync(User, grantedPoliciesUpdates, + ServiceAccountGrantedPoliciesOperations.Updates); + if (!authorizationResult.Succeeded) + { + throw new NotFoundException(); + } + + await _updateServiceAccountGrantedPoliciesCommand.UpdateAsync(grantedPoliciesUpdates); + return await GetServiceAccountGrantedPoliciesAsync(serviceAccount); + } + private async Task<(AccessClientType AccessClientType, Guid UserId)> CheckUserHasWriteAccessToProjectAsync(Project project) { if (project == null) @@ -355,4 +348,11 @@ public class AccessPoliciesController : Controller var accessClient = AccessClientHelper.ToAccessClient(_currentContext.ClientType, orgAdmin); return (accessClient, userId); } + + private async Task 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); + } } diff --git a/src/Api/SecretsManager/Models/Request/ServiceAccountGrantedPoliciesRequestModel.cs b/src/Api/SecretsManager/Models/Request/ServiceAccountGrantedPoliciesRequestModel.cs new file mode 100644 index 0000000000..77b16c90ff --- /dev/null +++ b/src/Api/SecretsManager/Models/Request/ServiceAccountGrantedPoliciesRequestModel.cs @@ -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 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 + }; + } +} diff --git a/src/Api/SecretsManager/Models/Response/ServiceAccountGrantedPoliciesPermissionDetailsResponseModel.cs b/src/Api/SecretsManager/Models/Response/ServiceAccountGrantedPoliciesPermissionDetailsResponseModel.cs new file mode 100644 index 0000000000..4cc535ab12 --- /dev/null +++ b/src/Api/SecretsManager/Models/Response/ServiceAccountGrantedPoliciesPermissionDetailsResponseModel.cs @@ -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 GrantedProjectPolicies { get; set; } = + []; +} diff --git a/src/Api/SecretsManager/Models/Response/ServiceAccountProjectAccessPolicyPermissionDetailsResponseModel.cs b/src/Api/SecretsManager/Models/Response/ServiceAccountProjectAccessPolicyPermissionDetailsResponseModel.cs new file mode 100644 index 0000000000..abf7466be0 --- /dev/null +++ b/src/Api/SecretsManager/Models/Response/ServiceAccountProjectAccessPolicyPermissionDetailsResponseModel.cs @@ -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; } +} diff --git a/src/Api/SecretsManager/Utilities/AccessPolicyHelpers.cs b/src/Api/SecretsManager/Utilities/AccessPolicyHelpers.cs new file mode 100644 index 0000000000..553b68145a --- /dev/null +++ b/src/Api/SecretsManager/Utilities/AccessPolicyHelpers.cs @@ -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 accessPolicies) + { + var distinctAccessPolicies = accessPolicies.DistinctBy(baseAccessPolicy => + { + return baseAccessPolicy switch + { + UserProjectAccessPolicy ap => new Tuple(ap.OrganizationUserId, ap.GrantedProjectId), + GroupProjectAccessPolicy ap => new Tuple(ap.GroupId, ap.GrantedProjectId), + ServiceAccountProjectAccessPolicy ap => new Tuple(ap.ServiceAccountId, + ap.GrantedProjectId), + UserServiceAccountAccessPolicy ap => new Tuple(ap.OrganizationUserId, + ap.GrantedServiceAccountId), + GroupServiceAccountAccessPolicy ap => new Tuple(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 accessPolicies) + { + var accessPoliciesPermission = accessPolicies.All(policy => policy.Read); + if (!accessPoliciesPermission) + { + throw new BadRequestException("Resources must be Read = true"); + } + } +} diff --git a/src/Core/SecretsManager/AuthorizationRequirements/ServiceAccountGrantedPoliciesOperationRequirement.cs b/src/Core/SecretsManager/AuthorizationRequirements/ServiceAccountGrantedPoliciesOperationRequirement.cs new file mode 100644 index 0000000000..0c52fa5921 --- /dev/null +++ b/src/Core/SecretsManager/AuthorizationRequirements/ServiceAccountGrantedPoliciesOperationRequirement.cs @@ -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) }; +} diff --git a/src/Core/SecretsManager/Commands/AccessPolicies/Interfaces/IUpdateServiceAccountGrantedPoliciesCommand.cs b/src/Core/SecretsManager/Commands/AccessPolicies/Interfaces/IUpdateServiceAccountGrantedPoliciesCommand.cs new file mode 100644 index 0000000000..fd1a8c8cd8 --- /dev/null +++ b/src/Core/SecretsManager/Commands/AccessPolicies/Interfaces/IUpdateServiceAccountGrantedPoliciesCommand.cs @@ -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); +} diff --git a/src/Core/SecretsManager/Enums/AccessPolicies/AccessPolicyOperation.cs b/src/Core/SecretsManager/Enums/AccessPolicies/AccessPolicyOperation.cs new file mode 100644 index 0000000000..d20e6aa3b4 --- /dev/null +++ b/src/Core/SecretsManager/Enums/AccessPolicies/AccessPolicyOperation.cs @@ -0,0 +1,8 @@ +namespace Bit.Core.SecretsManager.Enums.AccessPolicies; + +public enum AccessPolicyOperation +{ + Create, + Update, + Delete +} diff --git a/src/Core/SecretsManager/Models/Data/AccessPolicyUpdates/ServiceAccountProjectAccessPolicyUpdate.cs b/src/Core/SecretsManager/Models/Data/AccessPolicyUpdates/ServiceAccountProjectAccessPolicyUpdate.cs new file mode 100644 index 0000000000..84d47500ca --- /dev/null +++ b/src/Core/SecretsManager/Models/Data/AccessPolicyUpdates/ServiceAccountProjectAccessPolicyUpdate.cs @@ -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; } +} diff --git a/src/Core/SecretsManager/Models/Data/ServiceAccountGrantedPolicies.cs b/src/Core/SecretsManager/Models/Data/ServiceAccountGrantedPolicies.cs new file mode 100644 index 0000000000..a69208a0c7 --- /dev/null +++ b/src/Core/SecretsManager/Models/Data/ServiceAccountGrantedPolicies.cs @@ -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 policies) + { + ServiceAccountId = serviceAccountId; + ProjectGrantedPolicies = policies.Where(x => x is ServiceAccountProjectAccessPolicy) + .Cast().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 ProjectGrantedPolicies { get; set; } = + new List(); + + 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 CreatePolicyUpdates( + IEnumerable policies, List projectIds, + AccessPolicyOperation operation) => + policies + .Where(ap => projectIds.Contains(ap.GrantedProjectId!.Value)) + .Select(ap => new ServiceAccountProjectAccessPolicyUpdate { Operation = operation, AccessPolicy = ap }) + .ToList(); + + private List 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 ProjectGrantedPolicyUpdates { get; set; } = + new List(); +} diff --git a/src/Core/SecretsManager/Models/Data/ServiceAccountGrantedPoliciesPermissionDetails.cs b/src/Core/SecretsManager/Models/Data/ServiceAccountGrantedPoliciesPermissionDetails.cs new file mode 100644 index 0000000000..6ea444ec77 --- /dev/null +++ b/src/Core/SecretsManager/Models/Data/ServiceAccountGrantedPoliciesPermissionDetails.cs @@ -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 ProjectGrantedPolicies { get; set; } +} + +public class ServiceAccountProjectAccessPolicyPermissionDetails +{ + public required ServiceAccountProjectAccessPolicy AccessPolicy { get; set; } + public bool HasPermission { get; set; } +} diff --git a/src/Core/SecretsManager/Queries/AccessPolicies/Interfaces/IServiceAccountGrantedPolicyUpdatesQuery.cs b/src/Core/SecretsManager/Queries/AccessPolicies/Interfaces/IServiceAccountGrantedPolicyUpdatesQuery.cs new file mode 100644 index 0000000000..c29033d0ad --- /dev/null +++ b/src/Core/SecretsManager/Queries/AccessPolicies/Interfaces/IServiceAccountGrantedPolicyUpdatesQuery.cs @@ -0,0 +1,9 @@ +#nullable enable +using Bit.Core.SecretsManager.Models.Data; + +namespace Bit.Core.SecretsManager.Queries.AccessPolicies.Interfaces; + +public interface IServiceAccountGrantedPolicyUpdatesQuery +{ + Task GetAsync(ServiceAccountGrantedPolicies grantedPolicies); +} diff --git a/src/Core/SecretsManager/Repositories/IAccessPolicyRepository.cs b/src/Core/SecretsManager/Repositories/IAccessPolicyRepository.cs index f40d847a1f..e8ef99d490 100644 --- a/src/Core/SecretsManager/Repositories/IAccessPolicyRepository.cs +++ b/src/Core/SecretsManager/Repositories/IAccessPolicyRepository.cs @@ -11,8 +11,6 @@ public interface IAccessPolicyRepository Task AccessPolicyExists(BaseAccessPolicy baseAccessPolicy); Task GetByIdAsync(Guid id); Task> GetManyByGrantedProjectIdAsync(Guid id, Guid userId); - Task> GetManyByServiceAccountIdAsync(Guid id, Guid userId, - AccessClientType accessType); Task ReplaceAsync(BaseAccessPolicy baseAccessPolicy); Task DeleteAsync(Guid id); Task> GetPeoplePoliciesByGrantedProjectIdAsync(Guid id, Guid userId); @@ -20,4 +18,8 @@ public interface IAccessPolicyRepository Task GetPeopleGranteesAsync(Guid organizationId, Guid currentUserId); Task> GetPeoplePoliciesByGrantedServiceAccountIdAsync(Guid id, Guid userId); Task> ReplaceServiceAccountPeopleAsync(ServiceAccountPeopleAccessPolicies peopleAccessPolicies, Guid userId); + Task GetServiceAccountGrantedPoliciesAsync(Guid serviceAccountId); + Task GetServiceAccountGrantedPoliciesPermissionDetailsAsync( + Guid serviceAccountId, Guid userId, AccessClientType accessClientType); + Task UpdateServiceAccountGrantedPoliciesAsync(ServiceAccountGrantedPoliciesUpdates policyUpdates); } diff --git a/src/Core/SecretsManager/Repositories/IProjectRepository.cs b/src/Core/SecretsManager/Repositories/IProjectRepository.cs index 62770d9720..cc3aa40cf7 100644 --- a/src/Core/SecretsManager/Repositories/IProjectRepository.cs +++ b/src/Core/SecretsManager/Repositories/IProjectRepository.cs @@ -17,4 +17,6 @@ public interface IProjectRepository Task<(bool Read, bool Write)> AccessToProjectAsync(Guid id, Guid userId, AccessClientType accessType); Task ProjectsAreInOrganization(List projectIds, Guid organizationId); Task GetProjectCountByOrganizationIdAsync(Guid organizationId); + Task> AccessToProjectsAsync(IEnumerable projectIds, Guid userId, + AccessClientType accessType); } diff --git a/src/Core/SecretsManager/Repositories/Noop/NoopProjectRepository.cs b/src/Core/SecretsManager/Repositories/Noop/NoopProjectRepository.cs index f8b16f276a..acd428a676 100644 --- a/src/Core/SecretsManager/Repositories/Noop/NoopProjectRepository.cs +++ b/src/Core/SecretsManager/Repositories/Noop/NoopProjectRepository.cs @@ -62,4 +62,10 @@ public class NoopProjectRepository : IProjectRepository { return Task.FromResult(0); } + + public Task> AccessToProjectsAsync(IEnumerable projectIds, + Guid userId, AccessClientType accessType) + { + return Task.FromResult(null as Dictionary); + } } diff --git a/test/Api.IntegrationTest/SecretsManager/Controllers/AccessPoliciesControllerTests.cs b/test/Api.IntegrationTest/SecretsManager/Controllers/AccessPoliciesControllerTests.cs index e1cce68704..8927f546c6 100644 --- a/test/Api.IntegrationTest/SecretsManager/Controllers/AccessPoliciesControllerTests.cs +++ b/test/Api.IntegrationTest/SecretsManager/Controllers/AccessPoliciesControllerTests.cs @@ -623,210 +623,6 @@ public class AccessPoliciesControllerTests : IClassFixture 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 { 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 { 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 { 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 { 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>(); - - 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>(); - - 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>(); - - 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 - { - 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>(); - - Assert.NotNull(result?.Data); - Assert.NotEmpty(result.Data); - Assert.Equal(initData.ServiceAccountId, result.Data.First().ServiceAccountId); - Assert.NotNull(result.Data.First().ServiceAccountName); - Assert.NotNull(result.Data.First().GrantedProjectName); - } - [Theory] [InlineData(false, false, false)] [InlineData(false, false, true)] @@ -1090,12 +886,16 @@ public class AccessPoliciesControllerTests : IClassFixture(); + + 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 + { + 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(); + + 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 + { + 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 + { + 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(); + + 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 SetupAccessPolicyRequest(Guid organizationId) { var project = await _projectRepository.CreateAsync(new Project @@ -1275,6 +1259,7 @@ public class AccessPoliciesControllerTests : IClassFixture 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 + { + 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 + { + new() { GrantedId = project.Id, Read = true, Write = true } + } + }; + return (serviceAccount, request); + } + private class RequestSetupData { public Guid ProjectId { get; set; } diff --git a/test/Api.Test/SecretsManager/Controllers/AccessPoliciesControllerTests.cs b/test/Api.Test/SecretsManager/Controllers/AccessPoliciesControllerTests.cs index 616c8af08c..dba6bc1d68 100644 --- a/test/Api.Test/SecretsManager/Controllers/AccessPoliciesControllerTests.cs +++ b/test/Api.Test/SecretsManager/Controllers/AccessPoliciesControllerTests.cs @@ -1,4 +1,5 @@ -using System.Security.Claims; +#nullable enable +using System.Security.Claims; using Bit.Api.SecretsManager.Controllers; using Bit.Api.SecretsManager.Models.Request; using Bit.Api.Test.SecretsManager.Enums; @@ -8,6 +9,7 @@ using Bit.Core.Exceptions; using Bit.Core.SecretsManager.Commands.AccessPolicies.Interfaces; using Bit.Core.SecretsManager.Entities; using Bit.Core.SecretsManager.Models.Data; +using Bit.Core.SecretsManager.Queries.Interfaces; using Bit.Core.SecretsManager.Repositories; using Bit.Core.Services; using Bit.Core.Test.SecretsManager.AutoFixture.ProjectsFixture; @@ -29,83 +31,6 @@ public class AccessPoliciesControllerTests { private const int _overMax = 16; - private static AccessPoliciesCreateRequest AddRequestsOverMax(AccessPoliciesCreateRequest request) - { - var newRequests = new List(); - 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 AddRequestsOverMax(List 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 sutProvider, Guid organizationId) - { - sutProvider.GetDependency().AccessSecretsManager(default).ReturnsForAnyArgs(true); - sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(Guid.NewGuid()); - sutProvider.GetDependency().OrganizationAdmin(organizationId).Returns(true); - } - - private static void SetupUserWithPermission(SutProvider sutProvider, Guid organizationId) - { - sutProvider.GetDependency().AccessSecretsManager(default).ReturnsForAnyArgs(true); - sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(Guid.NewGuid()); - sutProvider.GetDependency().OrganizationAdmin(organizationId).Returns(false); - sutProvider.GetDependency().OrganizationUser(default).ReturnsForAnyArgs(true); - } - - private static void SetupUserWithoutPermission(SutProvider sutProvider, - Guid organizationId) - { - sutProvider.GetDependency().AccessSecretsManager(default).ReturnsForAnyArgs(true); - sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(Guid.NewGuid()); - sutProvider.GetDependency().OrganizationAdmin(organizationId).Returns(false); - sutProvider.GetDependency().OrganizationUser(default).ReturnsForAnyArgs(true); - } - - private static void SetupPermission(SutProvider sutProvider, - PermissionType permissionType, Guid orgId) - { - switch (permissionType) - { - case PermissionType.RunAsAdmin: - SetupAdmin(sutProvider, orgId); - break; - case PermissionType.RunAsUserWithPermission: - SetupUserWithPermission(sutProvider, orgId); - break; - } - } - [Theory] [BitAutoData(PermissionType.RunAsAdmin)] [BitAutoData(PermissionType.RunAsUserWithPermission)] @@ -222,71 +147,6 @@ public class AccessPoliciesControllerTests .GetManyByGrantedProjectIdAsync(Arg.Any(), Arg.Any()); } - [Theory] - [BitAutoData(PermissionType.RunAsAdmin)] - [BitAutoData(PermissionType.RunAsUserWithPermission)] - public async Task GetServiceAccountGrantedPolicies_ReturnsEmptyList( - PermissionType permissionType, - SutProvider sutProvider, - Guid id, ServiceAccount data) - { - sutProvider.GetDependency().GetByIdAsync(data.Id).ReturnsForAnyArgs(data); - - switch (permissionType) - { - case PermissionType.RunAsAdmin: - SetupAdmin(sutProvider, data.OrganizationId); - break; - case PermissionType.RunAsUserWithPermission: - SetupUserWithPermission(sutProvider, data.OrganizationId); - sutProvider.GetDependency() - .UserHasWriteAccessToServiceAccount(default, default) - .ReturnsForAnyArgs(true); - break; - } - - var result = await sutProvider.Sut.GetServiceAccountGrantedPoliciesAsync(id); - - await sutProvider.GetDependency().Received(1) - .GetManyByServiceAccountIdAsync(Arg.Is(AssertHelper.AssertPropertyEqual(id)), Arg.Any(), - Arg.Any()); - - Assert.Empty(result.Data); - } - - [Theory] - [BitAutoData(PermissionType.RunAsAdmin)] - [BitAutoData(PermissionType.RunAsUserWithPermission)] - public async Task GetServiceAccountGrantedPolicies_Success( - PermissionType permissionType, - SutProvider sutProvider, - Guid id, - ServiceAccount data, - ServiceAccountProjectAccessPolicy resultAccessPolicy) - { - sutProvider.GetDependency().GetByIdAsync(default).ReturnsForAnyArgs(data); - switch (permissionType) - { - case PermissionType.RunAsAdmin: - SetupAdmin(sutProvider, data.OrganizationId); - break; - case PermissionType.RunAsUserWithPermission: - SetupUserWithPermission(sutProvider, data.OrganizationId); - break; - } - - sutProvider.GetDependency().GetManyByServiceAccountIdAsync(default, default, default) - .ReturnsForAnyArgs(new List { resultAccessPolicy }); - - var result = await sutProvider.Sut.GetServiceAccountGrantedPoliciesAsync(id); - - await sutProvider.GetDependency().Received(1) - .GetManyByServiceAccountIdAsync(Arg.Is(AssertHelper.AssertPropertyEqual(id)), Arg.Any(), - Arg.Any()); - - Assert.NotEmpty(result.Data); - } - [Theory] [BitAutoData] public async Task CreateProjectAccessPolicies_RequestMoreThanMax_Throws( @@ -403,121 +263,6 @@ public class AccessPoliciesControllerTests .CreateManyAsync(Arg.Any>()); } - [Theory] - [BitAutoData] - public async Task CreateServiceAccountGrantedPolicies_RequestMoreThanMax_Throws( - SutProvider sutProvider, - Guid id, - ServiceAccount serviceAccount, - ServiceAccountProjectAccessPolicy data, - List request) - { - sutProvider.GetDependency().GetByIdAsync(default).ReturnsForAnyArgs(serviceAccount); - sutProvider.GetDependency() - .CreateManyAsync(default) - .ReturnsForAnyArgs(new List { data }); - - request = AddRequestsOverMax(request); - - await Assert.ThrowsAsync(() => - sutProvider.Sut.CreateServiceAccountGrantedPoliciesAsync(id, request)); - - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() - .CreateManyAsync(Arg.Any>()); - } - - [Theory] - [BitAutoData] - public async Task CreateServiceAccountGrantedPolicies_ServiceAccountDoesNotExist_Throws( - SutProvider sutProvider, - Guid id, - List request) - { - await Assert.ThrowsAsync(() => - sutProvider.Sut.CreateServiceAccountGrantedPoliciesAsync(id, request)); - - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() - .CreateManyAsync(Arg.Any>()); - } - - [Theory] - [BitAutoData] - public async Task CreateServiceAccountGrantedPolicies_DuplicatePolicy_Throws( - SutProvider sutProvider, - Guid id, - ServiceAccount serviceAccount, - ServiceAccountProjectAccessPolicy data, - List request) - { - var dup = new GrantedAccessPolicyRequest { GrantedId = Guid.NewGuid(), Read = true, Write = true }; - request.Add(dup); - request.Add(dup); - sutProvider.GetDependency().GetByIdAsync(default).ReturnsForAnyArgs(serviceAccount); - - sutProvider.GetDependency() - .CreateManyAsync(default) - .ReturnsForAnyArgs(new List { data }); - - await Assert.ThrowsAsync(() => - sutProvider.Sut.CreateServiceAccountGrantedPoliciesAsync(id, request)); - - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() - .CreateManyAsync(Arg.Any>()); - } - - [Theory] - [BitAutoData] - public async Task CreateServiceAccountGrantedPolicies_NoAccess_Throws( - SutProvider sutProvider, - Guid id, - ServiceAccount serviceAccount, - ServiceAccountProjectAccessPolicy data, - List request) - { - sutProvider.GetDependency().GetByIdAsync(default).ReturnsForAnyArgs(serviceAccount); - sutProvider.GetDependency() - .CreateManyAsync(default) - .ReturnsForAnyArgs(new List { data }); - foreach (var policy in request) - { - sutProvider.GetDependency() - .AuthorizeAsync(Arg.Any(), policy, - Arg.Any>()).ReturnsForAnyArgs(AuthorizationResult.Failed()); - } - - await Assert.ThrowsAsync(() => - sutProvider.Sut.CreateServiceAccountGrantedPoliciesAsync(id, request)); - - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() - .CreateManyAsync(Arg.Any>()); - } - - [Theory] - [BitAutoData] - public async Task CreateServiceAccountGrantedPolicies_Success( - SutProvider sutProvider, - Guid id, - ServiceAccount serviceAccount, - ServiceAccountProjectAccessPolicy data, - List request) - { - sutProvider.GetDependency().GetByIdAsync(default).ReturnsForAnyArgs(serviceAccount); - sutProvider.GetDependency() - .CreateManyAsync(default) - .ReturnsForAnyArgs(new List { data }); - foreach (var policy in request) - { - sutProvider.GetDependency() - .AuthorizeAsync(Arg.Any(), policy, - Arg.Any>()).ReturnsForAnyArgs(AuthorizationResult.Success()); - } - - await sutProvider.Sut.CreateServiceAccountGrantedPoliciesAsync(id, request); - - await sutProvider.GetDependency().Received(1) - .CreateManyAsync(Arg.Any>()); - } - [Theory] [BitAutoData] public async Task UpdateAccessPolicies_NoAccess_Throws( @@ -1165,4 +910,262 @@ public class AccessPoliciesControllerTests await sutProvider.GetDependency().Received(1) .ReplaceServiceAccountPeopleAsync(Arg.Any(), Arg.Any()); } + + [Theory] + [BitAutoData] + public async Task GetServiceAccountGrantedPoliciesAsync_NoAccess_ThrowsNotFound( + SutProvider sutProvider, + ServiceAccount data) + { + sutProvider.GetDependency().GetByIdAsync(data.Id).Returns(data); + + sutProvider.GetDependency() + .AuthorizeAsync(Arg.Any(), data, + Arg.Any>()).ReturnsForAnyArgs(AuthorizationResult.Failed()); + + await Assert.ThrowsAsync(() => + sutProvider.Sut.GetServiceAccountGrantedPoliciesAsync(data.Id)); + + await sutProvider.GetDependency().Received(0) + .GetServiceAccountGrantedPoliciesPermissionDetailsAsync(Arg.Any(), Arg.Any(), + Arg.Any()); + } + + [Theory] + [BitAutoData(AccessClientType.NoAccessCheck)] + [BitAutoData(AccessClientType.User)] + public async Task GetServiceAccountGrantedPoliciesAsync_HasAccessNoPolicies_ReturnsEmptyList( + AccessClientType accessClientType, + SutProvider sutProvider, + Guid userId, + ServiceAccount data) + { + sutProvider.GetDependency().GetByIdAsync(data.Id).Returns(data); + + sutProvider.GetDependency() + .AuthorizeAsync(Arg.Any(), data, + Arg.Any>()).ReturnsForAnyArgs(AuthorizationResult.Success()); + + sutProvider.GetDependency() + .GetAccessClientAsync(Arg.Any(), data.OrganizationId).Returns((accessClientType, userId)); + + sutProvider.GetDependency() + .GetServiceAccountGrantedPoliciesPermissionDetailsAsync(Arg.Any(), Arg.Any(), + Arg.Any()) + .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 sutProvider, + Guid userId, + ServiceAccountGrantedPoliciesPermissionDetails policies, + ServiceAccount data) + { + sutProvider.GetDependency().GetByIdAsync(data.Id).Returns(data); + + sutProvider.GetDependency() + .AuthorizeAsync(Arg.Any(), data, + Arg.Any>()).ReturnsForAnyArgs(AuthorizationResult.Success()); + + sutProvider.GetDependency() + .GetAccessClientAsync(Arg.Any(), data.OrganizationId).Returns((accessClientType, userId)); + + sutProvider.GetDependency() + .GetServiceAccountGrantedPoliciesPermissionDetailsAsync(Arg.Any(), Arg.Any(), + Arg.Any()) + .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 sutProvider, + ServiceAccount data, + ServiceAccountGrantedPoliciesRequestModel request) + { + await Assert.ThrowsAsync(() => + sutProvider.Sut.PutServiceAccountGrantedPoliciesAsync(data.Id, request)); + + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .UpdateAsync(Arg.Any()); + } + + [Theory] + [BitAutoData] + public async Task PutServiceAccountGrantedPoliciesAsync_DuplicatePolicyRequest_ThrowsBadRequestException( + SutProvider sutProvider, + ServiceAccount data, + ServiceAccountGrantedPoliciesRequestModel request) + { + var dup = new GrantedAccessPolicyRequest { GrantedId = Guid.NewGuid(), Read = true, Write = true }; + request.ProjectGrantedPolicyRequests = new[] { dup, dup }; + + sutProvider.GetDependency().GetByIdAsync(data.Id).ReturnsForAnyArgs(data); + + await Assert.ThrowsAsync(() => + sutProvider.Sut.PutServiceAccountGrantedPoliciesAsync(data.Id, request)); + + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .UpdateAsync(Arg.Any()); + } + + [Theory] + [BitAutoData] + public async Task PutServiceAccountGrantedPoliciesAsync_InvalidPolicyRequest_ThrowsBadRequestException( + SutProvider sutProvider, + ServiceAccount data, + ServiceAccountGrantedPoliciesRequestModel request) + { + var policyRequest = new GrantedAccessPolicyRequest { GrantedId = Guid.NewGuid(), Read = false, Write = true }; + request.ProjectGrantedPolicyRequests = new[] { policyRequest }; + + sutProvider.GetDependency().GetByIdAsync(data.Id).ReturnsForAnyArgs(data); + + await Assert.ThrowsAsync(() => + sutProvider.Sut.PutServiceAccountGrantedPoliciesAsync(data.Id, request)); + + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .UpdateAsync(Arg.Any()); + } + + [Theory] + [BitAutoData] + public async Task PutServiceAccountGrantedPoliciesAsync_UserHasNoAccess_ThrowsNotFoundException( + SutProvider sutProvider, + ServiceAccount data, + ServiceAccountGrantedPoliciesRequestModel request) + { + request = SetupValidRequest(request); + sutProvider.GetDependency().GetByIdAsync(data.Id).ReturnsForAnyArgs(data); + + sutProvider.GetDependency() + .AuthorizeAsync(Arg.Any(), Arg.Any(), + Arg.Any>()).Returns(AuthorizationResult.Failed()); + + await Assert.ThrowsAsync(() => + sutProvider.Sut.PutServiceAccountGrantedPoliciesAsync(data.Id, request)); + + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .UpdateAsync(Arg.Any()); + } + + [Theory] + [BitAutoData] + public async Task PutServiceAccountGrantedPoliciesAsync_Success( + SutProvider sutProvider, + ServiceAccount data, + ServiceAccountGrantedPoliciesRequestModel request) + { + request = SetupValidRequest(request); + sutProvider.GetDependency().GetByIdAsync(data.Id).ReturnsForAnyArgs(data); + + sutProvider.GetDependency() + .AuthorizeAsync(Arg.Any(), Arg.Any(), + Arg.Any>()).Returns(AuthorizationResult.Success()); + + await sutProvider.Sut.PutServiceAccountGrantedPoliciesAsync(data.Id, request); + + await sutProvider.GetDependency().Received(1) + .UpdateAsync(Arg.Any()); + } + + private static AccessPoliciesCreateRequest AddRequestsOverMax(AccessPoliciesCreateRequest request) + { + var newRequests = new List(); + 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 AddRequestsOverMax(List 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 sutProvider, Guid organizationId) + { + sutProvider.GetDependency().AccessSecretsManager(default).ReturnsForAnyArgs(true); + sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(Guid.NewGuid()); + sutProvider.GetDependency().OrganizationAdmin(organizationId).Returns(true); + } + + private static void SetupUserWithPermission(SutProvider sutProvider, Guid organizationId) + { + sutProvider.GetDependency().AccessSecretsManager(default).ReturnsForAnyArgs(true); + sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(Guid.NewGuid()); + sutProvider.GetDependency().OrganizationAdmin(organizationId).Returns(false); + sutProvider.GetDependency().OrganizationUser(default).ReturnsForAnyArgs(true); + } + + private static void SetupUserWithoutPermission(SutProvider sutProvider, + Guid organizationId) + { + sutProvider.GetDependency().AccessSecretsManager(default).ReturnsForAnyArgs(true); + sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(Guid.NewGuid()); + sutProvider.GetDependency().OrganizationAdmin(organizationId).Returns(false); + sutProvider.GetDependency().OrganizationUser(default).ReturnsForAnyArgs(true); + } + + private static void SetupPermission(SutProvider 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; + } } diff --git a/test/Api.Test/SecretsManager/Utilities/AccessPolicyHelpersTests.cs b/test/Api.Test/SecretsManager/Utilities/AccessPolicyHelpersTests.cs new file mode 100644 index 0000000000..debf49b20d --- /dev/null +++ b/test/Api.Test/SecretsManager/Utilities/AccessPolicyHelpersTests.cs @@ -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 + { + userProjectAccessPolicy, + userProjectAccessPolicy, + userServiceAccountAccessPolicy, + userServiceAccountAccessPolicy, + groupProjectAccessPolicy, + groupProjectAccessPolicy, + groupServiceAccountAccessPolicy, + groupServiceAccountAccessPolicy, + serviceAccountProjectAccessPolicy, + serviceAccountProjectAccessPolicy + }; + + Assert.Throws(() => + { + AccessPolicyHelpers.CheckForDistinctAccessPolicies(accessPolicies); + }); + } + + [Fact] + public void CheckForDistinctAccessPolicies_UnsupportedAccessPolicy_ThrowsArgumentException() + { + var accessPolicies = new List { new UnsupportedAccessPolicy() }; + + Assert.Throws(() => { AccessPolicyHelpers.CheckForDistinctAccessPolicies(accessPolicies); }); + } + + + [Theory] + [BitAutoData] + public void CheckForDistinctAccessPolicies_DistinctPolicies_Success(UserProjectAccessPolicy userProjectAccessPolicy, + UserServiceAccountAccessPolicy userServiceAccountAccessPolicy, + GroupProjectAccessPolicy groupProjectAccessPolicy, + GroupServiceAccountAccessPolicy groupServiceAccountAccessPolicy, + ServiceAccountProjectAccessPolicy serviceAccountProjectAccessPolicy) + { + var accessPolicies = new List + { + userProjectAccessPolicy, + userServiceAccountAccessPolicy, + groupProjectAccessPolicy, + groupServiceAccountAccessPolicy, + serviceAccountProjectAccessPolicy + }; + + AccessPolicyHelpers.CheckForDistinctAccessPolicies(accessPolicies); + } + + [Fact] + public void CheckAccessPoliciesHaveReadPermission_ReadPermissionFalse_ThrowsBadRequestException() + { + var accessPolicies = new List + { + new UserProjectAccessPolicy { Read = false, Write = true }, + new GroupProjectAccessPolicy { Read = true, Write = false } + }; + + Assert.Throws(() => + { + AccessPolicyHelpers.CheckAccessPoliciesHaveReadPermission(accessPolicies); + }); + } + + [Fact] + public void CheckAccessPoliciesHaveReadPermission_AllReadIsTrue_Success() + { + var accessPolicies = new List + { + new UserProjectAccessPolicy { Read = true, Write = true }, + new GroupProjectAccessPolicy { Read = true, Write = false } + }; + + AccessPolicyHelpers.CheckAccessPoliciesHaveReadPermission(accessPolicies); + } + + private class UnsupportedAccessPolicy : BaseAccessPolicy; +} diff --git a/test/Core.Test/SecretsManager/Models/ServiceAccountGrantedPoliciesTests.cs b/test/Core.Test/SecretsManager/Models/ServiceAccountGrantedPoliciesTests.cs new file mode 100644 index 0000000000..1ea5a49206 --- /dev/null +++ b/test/Core.Test/SecretsManager/Models/ServiceAccountGrantedPoliciesTests.cs @@ -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 + { + 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 + { + 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 + { + 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)); + } +}