diff --git a/bitwarden_license/src/Commercial.Core/SecretsManager/AuthorizationHandlers/AccessPolicies/ProjectPeopleAccessPoliciesAuthorizationHandler.cs b/bitwarden_license/src/Commercial.Core/SecretsManager/AuthorizationHandlers/AccessPolicies/ProjectPeopleAccessPoliciesAuthorizationHandler.cs new file mode 100644 index 000000000..c99d6474a --- /dev/null +++ b/bitwarden_license/src/Commercial.Core/SecretsManager/AuthorizationHandlers/AccessPolicies/ProjectPeopleAccessPoliciesAuthorizationHandler.cs @@ -0,0 +1,96 @@ +using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Context; +using Bit.Core.Enums; +using Bit.Core.Repositories; +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 + ProjectPeopleAccessPoliciesAuthorizationHandler : AuthorizationHandler +{ + private readonly IAccessClientQuery _accessClientQuery; + private readonly ICurrentContext _currentContext; + private readonly IGroupRepository _groupRepository; + private readonly IOrganizationUserRepository _organizationUserRepository; + private readonly IProjectRepository _projectRepository; + + public ProjectPeopleAccessPoliciesAuthorizationHandler(ICurrentContext currentContext, + IAccessClientQuery accessClientQuery, + IGroupRepository groupRepository, + IOrganizationUserRepository organizationUserRepository, + IProjectRepository projectRepository) + { + _currentContext = currentContext; + _accessClientQuery = accessClientQuery; + _groupRepository = groupRepository; + _organizationUserRepository = organizationUserRepository; + _projectRepository = projectRepository; + } + + protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, + ProjectPeopleAccessPoliciesOperationRequirement requirement, + ProjectPeopleAccessPolicies 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 == ProjectPeopleAccessPoliciesOperations.Replace: + await CanReplaceProjectPeopleAsync(context, requirement, resource, accessClient, userId); + break; + default: + throw new ArgumentException("Unsupported operation requirement type provided.", + nameof(requirement)); + } + } + + private async Task CanReplaceProjectPeopleAsync(AuthorizationHandlerContext context, + ProjectPeopleAccessPoliciesOperationRequirement requirement, ProjectPeopleAccessPolicies resource, + AccessClientType accessClient, Guid userId) + { + var access = await _projectRepository.AccessToProjectAsync(resource.Id, userId, accessClient); + if (access.Write) + { + if (resource.UserAccessPolicies != null && resource.UserAccessPolicies.Any()) + { + var orgUserIds = resource.UserAccessPolicies.Select(ap => ap.OrganizationUserId!.Value).ToList(); + var users = await _organizationUserRepository.GetManyAsync(orgUserIds); + if (users.Any(user => user.OrganizationId != resource.OrganizationId) || + users.Count != orgUserIds.Count) + { + return; + } + } + + if (resource.GroupAccessPolicies != null && resource.GroupAccessPolicies.Any()) + { + var groupIds = resource.GroupAccessPolicies.Select(ap => ap.GroupId!.Value).ToList(); + var groups = await _groupRepository.GetManyByManyIds(groupIds); + if (groups.Any(group => group.OrganizationId != resource.OrganizationId) || + groups.Count != groupIds.Count) + { + return; + } + } + + context.Succeed(requirement); + } + } +} diff --git a/bitwarden_license/src/Commercial.Core/SecretsManager/SecretsManagerCollectionExtensions.cs b/bitwarden_license/src/Commercial.Core/SecretsManager/SecretsManagerCollectionExtensions.cs index 47547eb0b..2eeee7cfe 100644 --- a/bitwarden_license/src/Commercial.Core/SecretsManager/SecretsManagerCollectionExtensions.cs +++ b/bitwarden_license/src/Commercial.Core/SecretsManager/SecretsManagerCollectionExtensions.cs @@ -35,6 +35,7 @@ public static class SecretsManagerCollectionExtensions services.AddScoped(); services.AddScoped(); services.AddScoped(); + 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 805922ec1..dca6f9c93 100644 --- a/bitwarden_license/src/Commercial.Infrastructure.EntityFramework/SecretsManager/Repositories/AccessPolicyRepository.cs +++ b/bitwarden_license/src/Commercial.Infrastructure.EntityFramework/SecretsManager/Repositories/AccessPolicyRepository.cs @@ -1,8 +1,10 @@ using System.Linq.Expressions; using AutoMapper; using Bit.Core.Enums; +using Bit.Core.SecretsManager.Models.Data; using Bit.Core.SecretsManager.Repositories; using Bit.Infrastructure.EntityFramework.Repositories; +using Bit.Infrastructure.EntityFramework.SecretsManager.Discriminators; using Bit.Infrastructure.EntityFramework.SecretsManager.Models; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; @@ -238,6 +240,153 @@ public class AccessPolicyRepository : BaseEntityFrameworkRepository, IAccessPoli return entities.Select(MapToCore); } + public async Task GetPeopleGranteesAsync(Guid organizationId, Guid currentUserId) + { + using var scope = ServiceScopeFactory.CreateScope(); + var dbContext = GetDatabaseContext(scope); + + var userGrantees = await dbContext.OrganizationUsers + .Where(ou => + ou.OrganizationId == organizationId && + ou.AccessSecretsManager && + ou.Status == OrganizationUserStatusType.Confirmed) + .Include(ou => ou.User) + .Select(ou => new + UserGrantee + { + OrganizationUserId = ou.Id, + Name = ou.User.Name, + Email = ou.User.Email, + CurrentUser = ou.UserId == currentUserId + }).ToListAsync(); + + var groupGrantees = await dbContext.Groups + .Where(g => g.OrganizationId == organizationId) + .Include(g => g.GroupUsers) + .Select(g => new GroupGrantee + { + GroupId = g.Id, + Name = g.Name, + CurrentUserInGroup = g.GroupUsers.Any(gu => + gu.OrganizationUser.User.Id == currentUserId) + }).ToListAsync(); + + return new PeopleGrantees { UserGrantees = userGrantees, GroupGrantees = groupGrantees }; + } + + public async Task> + GetPeoplePoliciesByGrantedProjectIdAsync(Guid id, Guid userId) + { + using var scope = ServiceScopeFactory.CreateScope(); + var dbContext = GetDatabaseContext(scope); + + var entities = await dbContext.AccessPolicies.Where(ap => + ap.Discriminator != AccessPolicyDiscriminator.ServiceAccountProject && + (((UserProjectAccessPolicy)ap).GrantedProjectId == id || + ((GroupProjectAccessPolicy)ap).GrantedProjectId == id)) + .Include(ap => ((UserProjectAccessPolicy)ap).OrganizationUser.User) + .Include(ap => ((GroupProjectAccessPolicy)ap).Group) + .Select(ap => new + { + ap, + CurrentUserInGroup = ap is GroupProjectAccessPolicy && + ((GroupProjectAccessPolicy)ap).Group.GroupUsers.Any(g => + g.OrganizationUser.UserId == userId), + }) + .ToListAsync(); + + return entities.Select(e => MapToCore(e.ap, e.CurrentUserInGroup)); + } + + public async Task> ReplaceProjectPeopleAsync( + ProjectPeopleAccessPolicies peopleAccessPolicies, Guid userId) + { + using var scope = ServiceScopeFactory.CreateScope(); + var dbContext = GetDatabaseContext(scope); + var peoplePolicyEntities = await dbContext.AccessPolicies.Where(ap => + ap.Discriminator != AccessPolicyDiscriminator.ServiceAccountProject && + (((UserProjectAccessPolicy)ap).GrantedProjectId == peopleAccessPolicies.Id || + ((GroupProjectAccessPolicy)ap).GrantedProjectId == peopleAccessPolicies.Id)).ToListAsync(); + + var userPolicyEntities = + peoplePolicyEntities.Where(ap => ap.GetType() == typeof(UserProjectAccessPolicy)).ToList(); + var groupPolicyEntities = + peoplePolicyEntities.Where(ap => ap.GetType() == typeof(GroupProjectAccessPolicy)).ToList(); + + + if (peopleAccessPolicies.UserAccessPolicies == null || !peopleAccessPolicies.UserAccessPolicies.Any()) + { + dbContext.RemoveRange(userPolicyEntities); + } + else + { + foreach (var userPolicyEntity in userPolicyEntities.Where(entity => + peopleAccessPolicies.UserAccessPolicies.All(ap => + ((Core.SecretsManager.Entities.UserProjectAccessPolicy)ap).OrganizationUserId != + ((UserProjectAccessPolicy)entity).OrganizationUserId))) + { + dbContext.Remove(userPolicyEntity); + } + } + + if (peopleAccessPolicies.GroupAccessPolicies == null || !peopleAccessPolicies.GroupAccessPolicies.Any()) + { + dbContext.RemoveRange(groupPolicyEntities); + } + else + { + foreach (var groupPolicyEntity in groupPolicyEntities.Where(entity => + peopleAccessPolicies.GroupAccessPolicies.All(ap => + ((Core.SecretsManager.Entities.GroupProjectAccessPolicy)ap).GroupId != + ((GroupProjectAccessPolicy)entity).GroupId))) + { + dbContext.Remove(groupPolicyEntity); + } + } + + await UpsertPeoplePoliciesAsync(dbContext, + peopleAccessPolicies.ToBaseAccessPolicies().Select(MapToEntity).ToList(), userPolicyEntities, + groupPolicyEntities); + + await dbContext.SaveChangesAsync(); + return await GetPeoplePoliciesByGrantedProjectIdAsync(peopleAccessPolicies.Id, userId); + } + + private static async Task UpsertPeoplePoliciesAsync(DatabaseContext dbContext, + List policies, IReadOnlyCollection userPolicyEntities, + IReadOnlyCollection groupPolicyEntities) + { + var currentDate = DateTime.UtcNow; + foreach (var updatedEntity in policies) + { + var currentEntity = updatedEntity switch + { + UserProjectAccessPolicy ap => userPolicyEntities.FirstOrDefault(e => + ((UserProjectAccessPolicy)e).OrganizationUserId == ap.OrganizationUserId), + GroupProjectAccessPolicy ap => groupPolicyEntities.FirstOrDefault(e => + ((GroupProjectAccessPolicy)e).GroupId == ap.GroupId), + UserServiceAccountAccessPolicy ap => userPolicyEntities.FirstOrDefault(e => + ((UserServiceAccountAccessPolicy)e).OrganizationUserId == ap.OrganizationUserId), + GroupServiceAccountAccessPolicy ap => groupPolicyEntities.FirstOrDefault(e => + ((GroupServiceAccountAccessPolicy)e).GroupId == ap.GroupId), + _ => null + }; + + if (currentEntity != null) + { + dbContext.AccessPolicies.Attach(currentEntity); + currentEntity.Read = updatedEntity.Read; + currentEntity.Write = updatedEntity.Write; + currentEntity.RevisionDate = currentDate; + } + else + { + updatedEntity.SetNewId(); + await dbContext.AddAsync(updatedEntity); + } + } + } + private Core.SecretsManager.Entities.BaseAccessPolicy MapToCore( BaseAccessPolicy baseAccessPolicyEntity) => baseAccessPolicyEntity switch @@ -250,9 +399,27 @@ public class AccessPolicyRepository : BaseEntityFrameworkRepository, IAccessPoli Mapper.Map(ap), GroupServiceAccountAccessPolicy ap => Mapper .Map(ap), - _ => throw new ArgumentException("Unsupported access policy type"), + _ => throw new ArgumentException("Unsupported access policy type") }; + private BaseAccessPolicy MapToEntity(Core.SecretsManager.Entities.BaseAccessPolicy baseAccessPolicy) + { + return baseAccessPolicy switch + { + Core.SecretsManager.Entities.UserProjectAccessPolicy accessPolicy => Mapper.Map( + accessPolicy), + Core.SecretsManager.Entities.UserServiceAccountAccessPolicy accessPolicy => Mapper + .Map(accessPolicy), + Core.SecretsManager.Entities.GroupProjectAccessPolicy accessPolicy => Mapper.Map( + accessPolicy), + Core.SecretsManager.Entities.GroupServiceAccountAccessPolicy accessPolicy => Mapper + .Map(accessPolicy), + Core.SecretsManager.Entities.ServiceAccountProjectAccessPolicy accessPolicy => Mapper + .Map(accessPolicy), + _ => throw new ArgumentException("Unsupported access policy type") + }; + } + private Core.SecretsManager.Entities.BaseAccessPolicy MapToCore( BaseAccessPolicy baseAccessPolicyEntity, bool currentUserInGroup) { diff --git a/bitwarden_license/test/Commercial.Core.Test/SecretsManager/AuthorizationHandlers/AccessPolicies/ProjectPeopleAccessPoliciesAuthorizationHandlerTests.cs b/bitwarden_license/test/Commercial.Core.Test/SecretsManager/AuthorizationHandlers/AccessPolicies/ProjectPeopleAccessPoliciesAuthorizationHandlerTests.cs new file mode 100644 index 000000000..855f28b43 --- /dev/null +++ b/bitwarden_license/test/Commercial.Core.Test/SecretsManager/AuthorizationHandlers/AccessPolicies/ProjectPeopleAccessPoliciesAuthorizationHandlerTests.cs @@ -0,0 +1,246 @@ +using System.Reflection; +using System.Security.Claims; +using Bit.Commercial.Core.SecretsManager.AuthorizationHandlers.AccessPolicies; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Context; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Repositories; +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 ProjectPeopleAccessPoliciesAuthorizationHandlerTests +{ + private static void SetupUserPermission(SutProvider sutProvider, + AccessClientType accessClientType, ProjectPeopleAccessPolicies resource, Guid userId = new(), bool read = true, + bool write = true) + { + sutProvider.GetDependency().AccessSecretsManager(resource.OrganizationId) + .Returns(true); + sutProvider.GetDependency().GetAccessClientAsync(default, resource.OrganizationId) + .ReturnsForAnyArgs( + (accessClientType, userId)); + sutProvider.GetDependency().AccessToProjectAsync(resource.Id, userId, accessClientType) + .Returns((read, write)); + } + + private static void SetupOrganizationUsers(SutProvider sutProvider, + ProjectPeopleAccessPolicies resource) + { + var orgUsers = resource.UserAccessPolicies.Select(userPolicy => + new OrganizationUser + { + OrganizationId = resource.OrganizationId, + Id = userPolicy.OrganizationUserId!.Value + }).ToList(); + sutProvider.GetDependency().GetManyAsync(default) + .ReturnsForAnyArgs(orgUsers); + } + + private static void SetupGroups(SutProvider sutProvider, + ProjectPeopleAccessPolicies resource) + { + var groups = resource.GroupAccessPolicies.Select(groupPolicy => + new Group { OrganizationId = resource.OrganizationId, Id = groupPolicy.GroupId!.Value }).ToList(); + sutProvider.GetDependency().GetManyByManyIds(default) + .ReturnsForAnyArgs(groups); + } + + [Fact] + public void PeopleAccessPoliciesOperations_OnlyPublicStatic() + { + var publicStaticFields = + typeof(ProjectPeopleAccessPoliciesOperations).GetFields(BindingFlags.Public | BindingFlags.Static); + var allFields = typeof(ProjectPeopleAccessPoliciesOperations).GetFields(); + Assert.Equal(publicStaticFields.Length, allFields.Length); + } + + [Theory] + [BitAutoData] + public async Task Handler_UnsupportedProjectPeopleAccessPoliciesOperationRequirement_Throws( + SutProvider sutProvider, ProjectPeopleAccessPolicies resource, + ClaimsPrincipal claimsPrincipal) + { + var requirement = new ProjectPeopleAccessPoliciesOperationRequirement(); + sutProvider.GetDependency().AccessSecretsManager(resource.OrganizationId) + .Returns(true); + sutProvider.GetDependency().GetAccessClientAsync(default, resource.OrganizationId) + .ReturnsForAnyArgs( + (AccessClientType.NoAccessCheck, new Guid())); + var authzContext = new AuthorizationHandlerContext(new List { requirement }, + claimsPrincipal, resource); + + await Assert.ThrowsAsync(() => sutProvider.Sut.HandleAsync(authzContext)); + } + + [Theory] + [BitAutoData] + public async Task Handler_AccessSecretsManagerFalse_DoesNotSucceed( + SutProvider sutProvider, ProjectPeopleAccessPolicies resource, + ClaimsPrincipal claimsPrincipal) + { + var requirement = new ProjectPeopleAccessPoliciesOperationRequirement(); + 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 clientType, + SutProvider sutProvider, ProjectPeopleAccessPolicies resource, + ClaimsPrincipal claimsPrincipal) + { + var requirement = new ProjectPeopleAccessPoliciesOperationRequirement(); + SetupUserPermission(sutProvider, clientType, resource); + var authzContext = new AuthorizationHandlerContext(new List { requirement }, + claimsPrincipal, resource); + + await sutProvider.Sut.HandleAsync(authzContext); + + Assert.False(authzContext.HasSucceeded); + } + + [Theory] + [BitAutoData(AccessClientType.User)] + [BitAutoData(AccessClientType.NoAccessCheck)] + public async Task ReplaceProjectPeople_UserNotInOrg_DoesNotSucceed(AccessClientType accessClient, + SutProvider sutProvider, ProjectPeopleAccessPolicies resource, + ClaimsPrincipal claimsPrincipal, Guid userId) + { + var requirement = ProjectPeopleAccessPoliciesOperations.Replace; + SetupUserPermission(sutProvider, accessClient, resource, userId); + var orgUsers = resource.UserAccessPolicies.Select(userPolicy => + new OrganizationUser { OrganizationId = Guid.NewGuid(), Id = userPolicy.OrganizationUserId!.Value }) + .ToList(); + sutProvider.GetDependency().GetManyAsync(default) + .ReturnsForAnyArgs(orgUsers); + var authzContext = new AuthorizationHandlerContext(new List { requirement }, + claimsPrincipal, resource); + + await sutProvider.Sut.HandleAsync(authzContext); + + Assert.False(authzContext.HasSucceeded); + } + + [Theory] + [BitAutoData(AccessClientType.User)] + [BitAutoData(AccessClientType.NoAccessCheck)] + public async Task ReplaceProjectPeople_UserCountMismatch_DoesNotSucceed(AccessClientType accessClient, + SutProvider sutProvider, ProjectPeopleAccessPolicies resource, + ClaimsPrincipal claimsPrincipal, Guid userId) + { + var requirement = ProjectPeopleAccessPoliciesOperations.Replace; + SetupUserPermission(sutProvider, accessClient, resource, userId); + var orgUsers = resource.UserAccessPolicies.Select(userPolicy => + new OrganizationUser + { + OrganizationId = resource.OrganizationId, + Id = userPolicy.OrganizationUserId!.Value + }).ToList(); + orgUsers.RemoveAt(0); + sutProvider.GetDependency().GetManyAsync(default) + .ReturnsForAnyArgs(orgUsers); + var authzContext = new AuthorizationHandlerContext(new List { requirement }, + claimsPrincipal, resource); + + await sutProvider.Sut.HandleAsync(authzContext); + + Assert.False(authzContext.HasSucceeded); + } + + [Theory] + [BitAutoData(AccessClientType.User)] + [BitAutoData(AccessClientType.NoAccessCheck)] + public async Task ReplaceProjectPeople_GroupNotInOrg_DoesNotSucceed(AccessClientType accessClient, + SutProvider sutProvider, ProjectPeopleAccessPolicies resource, + ClaimsPrincipal claimsPrincipal, Guid userId) + { + var requirement = ProjectPeopleAccessPoliciesOperations.Replace; + SetupUserPermission(sutProvider, accessClient, resource, userId); + SetupOrganizationUsers(sutProvider, resource); + + var groups = resource.GroupAccessPolicies.Select(groupPolicy => + new Group { OrganizationId = Guid.NewGuid(), Id = groupPolicy.GroupId!.Value }).ToList(); + sutProvider.GetDependency().GetManyByManyIds(default) + .ReturnsForAnyArgs(groups); + + var authzContext = new AuthorizationHandlerContext(new List { requirement }, + claimsPrincipal, resource); + + await sutProvider.Sut.HandleAsync(authzContext); + + Assert.False(authzContext.HasSucceeded); + } + + [Theory] + [BitAutoData(AccessClientType.User)] + [BitAutoData(AccessClientType.NoAccessCheck)] + public async Task ReplaceProjectPeople_GroupCountMismatch_DoesNotSucceed(AccessClientType accessClient, + SutProvider sutProvider, ProjectPeopleAccessPolicies resource, + ClaimsPrincipal claimsPrincipal, Guid userId) + { + var requirement = ProjectPeopleAccessPoliciesOperations.Replace; + SetupUserPermission(sutProvider, accessClient, resource, userId); + SetupOrganizationUsers(sutProvider, resource); + + var groups = resource.GroupAccessPolicies.Select(groupPolicy => + new Group { OrganizationId = resource.OrganizationId, Id = groupPolicy.GroupId!.Value }).ToList(); + groups.RemoveAt(0); + sutProvider.GetDependency().GetManyByManyIds(default) + .ReturnsForAnyArgs(groups); + + var authzContext = new AuthorizationHandlerContext(new List { requirement }, + claimsPrincipal, resource); + + await sutProvider.Sut.HandleAsync(authzContext); + + Assert.False(authzContext.HasSucceeded); + } + + [Theory] + [BitAutoData(AccessClientType.User, false, false, false)] + [BitAutoData(AccessClientType.User, false, true, true)] + [BitAutoData(AccessClientType.User, true, false, false)] + [BitAutoData(AccessClientType.User, true, true, true)] + [BitAutoData(AccessClientType.NoAccessCheck, false, false, false)] + [BitAutoData(AccessClientType.NoAccessCheck, false, true, true)] + [BitAutoData(AccessClientType.NoAccessCheck, true, false, false)] + [BitAutoData(AccessClientType.NoAccessCheck, true, true, true)] + public async Task ReplaceProjectPeople_AccessCheck(AccessClientType accessClient, bool read, bool write, + bool expected, + SutProvider sutProvider, ProjectPeopleAccessPolicies resource, + ClaimsPrincipal claimsPrincipal, Guid userId) + { + var requirement = ProjectPeopleAccessPoliciesOperations.Replace; + SetupUserPermission(sutProvider, accessClient, resource, userId, read, write); + SetupOrganizationUsers(sutProvider, resource); + SetupGroups(sutProvider, resource); + + var authzContext = new AuthorizationHandlerContext(new List { requirement }, + claimsPrincipal, resource); + + await sutProvider.Sut.HandleAsync(authzContext); + + Assert.Equal(expected, authzContext.HasSucceeded); + } +} diff --git a/src/Api/SecretsManager/Controllers/AccessPoliciesController.cs b/src/Api/SecretsManager/Controllers/AccessPoliciesController.cs index c8d43293e..fdcb76015 100644 --- a/src/Api/SecretsManager/Controllers/AccessPoliciesController.cs +++ b/src/Api/SecretsManager/Controllers/AccessPoliciesController.cs @@ -1,11 +1,9 @@ using Bit.Api.Models.Response; using Bit.Api.SecretsManager.Models.Request; using Bit.Api.SecretsManager.Models.Response; -using Bit.Core.AdminConsole.Repositories; using Bit.Core.Context; using Bit.Core.Enums; using Bit.Core.Exceptions; -using Bit.Core.Repositories; using Bit.Core.SecretsManager.AuthorizationRequirements; using Bit.Core.SecretsManager.Commands.AccessPolicies.Interfaces; using Bit.Core.SecretsManager.Entities; @@ -25,8 +23,6 @@ public class AccessPoliciesController : Controller private readonly ICreateAccessPoliciesCommand _createAccessPoliciesCommand; private readonly ICurrentContext _currentContext; private readonly IDeleteAccessPolicyCommand _deleteAccessPolicyCommand; - private readonly IGroupRepository _groupRepository; - private readonly IOrganizationUserRepository _organizationUserRepository; private readonly IProjectRepository _projectRepository; private readonly IServiceAccountRepository _serviceAccountRepository; private readonly IUpdateAccessPolicyCommand _updateAccessPolicyCommand; @@ -39,9 +35,7 @@ public class AccessPoliciesController : Controller ICurrentContext currentContext, IAccessPolicyRepository accessPolicyRepository, IServiceAccountRepository serviceAccountRepository, - IGroupRepository groupRepository, IProjectRepository projectRepository, - IOrganizationUserRepository organizationUserRepository, ICreateAccessPoliciesCommand createAccessPoliciesCommand, IDeleteAccessPolicyCommand deleteAccessPolicyCommand, IUpdateAccessPolicyCommand updateAccessPolicyCommand) @@ -51,8 +45,6 @@ public class AccessPoliciesController : Controller _currentContext = currentContext; _serviceAccountRepository = serviceAccountRepository; _projectRepository = projectRepository; - _groupRepository = groupRepository; - _organizationUserRepository = organizationUserRepository; _accessPolicyRepository = accessPolicyRepository; _createAccessPoliciesCommand = createAccessPoliciesCommand; _deleteAccessPolicyCommand = deleteAccessPolicyCommand; @@ -243,15 +235,11 @@ public class AccessPoliciesController : Controller throw new NotFoundException(); } - var groups = await _groupRepository.GetManyByOrganizationIdAsync(id); - var groupResponses = groups.Select(g => new PotentialGranteeResponseModel(g)); - - var organizationUsers = - await _organizationUserRepository.GetManyDetailsByOrganizationAsync(id); - var userResponses = organizationUsers - .Where(user => user.AccessSecretsManager && user.Status == OrganizationUserStatusType.Confirmed) - .Select(userDetails => new PotentialGranteeResponseModel(userDetails)); + var userId = _userService.GetProperUserId(User).Value; + var peopleGrantees = await _accessPolicyRepository.GetPeopleGranteesAsync(id, userId); + var userResponses = peopleGrantees.UserGrantees.Select(ug => new PotentialGranteeResponseModel(ug)); + var groupResponses = peopleGrantees.GroupGrantees.Select(g => new PotentialGranteeResponseModel(g)); return new ListResponseModel(userResponses.Concat(groupResponses)); } @@ -287,6 +275,40 @@ public class AccessPoliciesController : Controller return new ListResponseModel(projectResponses); } + [HttpGet("/projects/{id}/access-policies/people")] + public async Task GetProjectPeopleAccessPoliciesAsync([FromRoute] Guid id) + { + var project = await _projectRepository.GetByIdAsync(id); + var (_, userId) = await CheckUserHasWriteAccessToProjectAsync(project); + var results = await _accessPolicyRepository.GetPeoplePoliciesByGrantedProjectIdAsync(id, userId); + return new ProjectPeopleAccessPoliciesResponseModel(results, userId); + } + + [HttpPut("/projects/{id}/access-policies/people")] + public async Task PutProjectPeopleAccessPoliciesAsync([FromRoute] Guid id, + [FromBody] PeopleAccessPoliciesRequestModel request) + { + var project = await _projectRepository.GetByIdAsync(id); + if (project == null) + { + throw new NotFoundException(); + } + + var peopleAccessPolicies = request.ToProjectPeopleAccessPolicies(id, project.OrganizationId); + + var authorizationResult = await _authorizationService.AuthorizeAsync(User, peopleAccessPolicies, + ProjectPeopleAccessPoliciesOperations.Replace); + if (!authorizationResult.Succeeded) + { + throw new NotFoundException(); + } + + var userId = _userService.GetProperUserId(User).Value; + var results = await _accessPolicyRepository.ReplaceProjectPeopleAsync(peopleAccessPolicies, userId); + return new ProjectPeopleAccessPoliciesResponseModel(results, userId); + } + + private async Task<(AccessClientType AccessClientType, Guid UserId)> CheckUserHasWriteAccessToProjectAsync(Project project) { if (project == null) diff --git a/src/Api/SecretsManager/Models/Request/PeopleAccessPoliciesRequestModel.cs b/src/Api/SecretsManager/Models/Request/PeopleAccessPoliciesRequestModel.cs new file mode 100644 index 000000000..6cc5c287d --- /dev/null +++ b/src/Api/SecretsManager/Models/Request/PeopleAccessPoliciesRequestModel.cs @@ -0,0 +1,64 @@ +using Bit.Core.Exceptions; +using Bit.Core.SecretsManager.Entities; +using Bit.Core.SecretsManager.Models.Data; + +namespace Bit.Api.SecretsManager.Models.Request; + +public class PeopleAccessPoliciesRequestModel +{ + public IEnumerable UserAccessPolicyRequests { get; set; } + + public IEnumerable GroupAccessPolicyRequests { get; set; } + + private 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 ProjectPeopleAccessPolicies ToProjectPeopleAccessPolicies(Guid grantedProjectId, Guid organizationId) + { + var userAccessPolicies = UserAccessPolicyRequests? + .Select(x => x.ToUserProjectAccessPolicy(grantedProjectId, organizationId)).ToList(); + + var groupAccessPolicies = GroupAccessPolicyRequests? + .Select(x => x.ToGroupProjectAccessPolicy(grantedProjectId, organizationId)).ToList(); + var policies = new List(); + if (userAccessPolicies != null) + { + policies.AddRange(userAccessPolicies); + } + + if (groupAccessPolicies != null) + { + policies.AddRange(groupAccessPolicies); + } + + CheckForDistinctAccessPolicies(policies); + + return new ProjectPeopleAccessPolicies + { + Id = grantedProjectId, + OrganizationId = organizationId, + UserAccessPolicies = userAccessPolicies, + GroupAccessPolicies = groupAccessPolicies + }; + } +} diff --git a/src/Api/SecretsManager/Models/Response/AccessPolicyResponseModel.cs b/src/Api/SecretsManager/Models/Response/AccessPolicyResponseModel.cs index cedaf66f4..05926bd6c 100644 --- a/src/Api/SecretsManager/Models/Response/AccessPolicyResponseModel.cs +++ b/src/Api/SecretsManager/Models/Response/AccessPolicyResponseModel.cs @@ -34,10 +34,13 @@ public class UserProjectAccessPolicyResponseModel : BaseAccessPolicyResponseMode public UserProjectAccessPolicyResponseModel(UserProjectAccessPolicy accessPolicy) : base(accessPolicy, _objectName) { - OrganizationUserId = accessPolicy.OrganizationUserId; - GrantedProjectId = accessPolicy.GrantedProjectId; - OrganizationUserName = GetUserDisplayName(accessPolicy.User); - UserId = accessPolicy.User?.Id; + SetProperties(accessPolicy); + } + + public UserProjectAccessPolicyResponseModel(UserProjectAccessPolicy accessPolicy, Guid currentUserId) : base(accessPolicy, _objectName) + { + CurrentUser = currentUserId == accessPolicy.User?.Id; + SetProperties(accessPolicy); } public UserProjectAccessPolicyResponseModel() : base(new UserProjectAccessPolicy(), _objectName) @@ -48,6 +51,15 @@ public class UserProjectAccessPolicyResponseModel : BaseAccessPolicyResponseMode public string? OrganizationUserName { get; set; } public Guid? UserId { get; set; } public Guid? GrantedProjectId { get; set; } + public bool? CurrentUser { get; set; } + + private void SetProperties(UserProjectAccessPolicy accessPolicy) + { + OrganizationUserId = accessPolicy.OrganizationUserId; + GrantedProjectId = accessPolicy.GrantedProjectId; + OrganizationUserName = GetUserDisplayName(accessPolicy.User); + UserId = accessPolicy.User?.Id; + } } public class UserServiceAccountAccessPolicyResponseModel : BaseAccessPolicyResponseModel diff --git a/src/Api/SecretsManager/Models/Response/PotentialGranteeResponseModel.cs b/src/Api/SecretsManager/Models/Response/PotentialGranteeResponseModel.cs index a64c0d86d..002ba1525 100644 --- a/src/Api/SecretsManager/Models/Response/PotentialGranteeResponseModel.cs +++ b/src/Api/SecretsManager/Models/Response/PotentialGranteeResponseModel.cs @@ -1,7 +1,6 @@ -using Bit.Core.AdminConsole.Entities; -using Bit.Core.Models.Api; -using Bit.Core.Models.Data.Organizations.OrganizationUsers; +using Bit.Core.Models.Api; using Bit.Core.SecretsManager.Entities; +using Bit.Core.SecretsManager.Models.Data; namespace Bit.Api.SecretsManager.Models.Response; @@ -9,31 +8,33 @@ public class PotentialGranteeResponseModel : ResponseModel { private const string _objectName = "potentialGrantee"; - public PotentialGranteeResponseModel(Group group) + public PotentialGranteeResponseModel(GroupGrantee grantee) : base(_objectName) { - if (group == null) + if (grantee == null) { - throw new ArgumentNullException(nameof(group)); + throw new ArgumentNullException(nameof(grantee)); } - Id = group.Id; - Name = group.Name; Type = "group"; + Id = grantee.GroupId; + Name = grantee.Name; + CurrentUserInGroup = grantee.CurrentUserInGroup; } - public PotentialGranteeResponseModel(OrganizationUserUserDetails user) + public PotentialGranteeResponseModel(UserGrantee grantee) : base(_objectName) { - if (user == null) + if (grantee == null) { - throw new ArgumentNullException(nameof(user)); + throw new ArgumentNullException(nameof(grantee)); } - Id = user.Id; - Name = user.Name; - Email = user.Email; Type = "user"; + Id = grantee.OrganizationUserId; + Name = grantee.Name; + Email = grantee.Email; + CurrentUser = grantee.CurrentUser; } public PotentialGranteeResponseModel(ServiceAccount serviceAccount) @@ -67,9 +68,9 @@ public class PotentialGranteeResponseModel : ResponseModel } public Guid Id { get; set; } - public string Name { get; set; } - public string Type { get; set; } public string Email { get; set; } + public bool CurrentUserInGroup { get; set; } + public bool CurrentUser { get; set; } } diff --git a/src/Api/SecretsManager/Models/Response/ProjectPeopleAccessPoliciesResponseModel.cs b/src/Api/SecretsManager/Models/Response/ProjectPeopleAccessPoliciesResponseModel.cs new file mode 100644 index 000000000..b1d949d5d --- /dev/null +++ b/src/Api/SecretsManager/Models/Response/ProjectPeopleAccessPoliciesResponseModel.cs @@ -0,0 +1,34 @@ +using Bit.Core.Models.Api; +using Bit.Core.SecretsManager.Entities; + +namespace Bit.Api.SecretsManager.Models.Response; + +public class ProjectPeopleAccessPoliciesResponseModel : ResponseModel +{ + private const string _objectName = "projectPeopleAccessPolicies"; + + public ProjectPeopleAccessPoliciesResponseModel(IEnumerable baseAccessPolicies, Guid userId) + : base(_objectName) + { + foreach (var baseAccessPolicy in baseAccessPolicies) + { + switch (baseAccessPolicy) + { + case UserProjectAccessPolicy accessPolicy: + UserAccessPolicies.Add(new UserProjectAccessPolicyResponseModel(accessPolicy, userId)); + break; + case GroupProjectAccessPolicy accessPolicy: + GroupAccessPolicies.Add(new GroupProjectAccessPolicyResponseModel(accessPolicy)); + break; + } + } + } + + public ProjectPeopleAccessPoliciesResponseModel() : base(_objectName) + { + } + + public List UserAccessPolicies { get; set; } = new(); + + public List GroupAccessPolicies { get; set; } = new(); +} diff --git a/src/Core/SecretsManager/AuthorizationRequirements/ProjectPeopleAccessPoliciesOperationRequirement.cs b/src/Core/SecretsManager/AuthorizationRequirements/ProjectPeopleAccessPoliciesOperationRequirement.cs new file mode 100644 index 000000000..3a3877606 --- /dev/null +++ b/src/Core/SecretsManager/AuthorizationRequirements/ProjectPeopleAccessPoliciesOperationRequirement.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Authorization.Infrastructure; + +namespace Bit.Core.SecretsManager.AuthorizationRequirements; + +public class ProjectPeopleAccessPoliciesOperationRequirement : OperationAuthorizationRequirement +{ +} + +public static class ProjectPeopleAccessPoliciesOperations +{ + public static readonly ProjectPeopleAccessPoliciesOperationRequirement Replace = new() { Name = nameof(Replace) }; +} diff --git a/src/Core/SecretsManager/Models/Data/PeopleGrantees.cs b/src/Core/SecretsManager/Models/Data/PeopleGrantees.cs new file mode 100644 index 000000000..db7031269 --- /dev/null +++ b/src/Core/SecretsManager/Models/Data/PeopleGrantees.cs @@ -0,0 +1,22 @@ +namespace Bit.Core.SecretsManager.Models.Data; + +public class PeopleGrantees +{ + public IEnumerable UserGrantees { get; set; } + public IEnumerable GroupGrantees { get; set; } +} + +public class UserGrantee +{ + public Guid OrganizationUserId { get; set; } + public string Name { get; set; } + public string Email { get; set; } + public bool CurrentUser { get; set; } +} + +public class GroupGrantee +{ + public Guid GroupId { get; set; } + public string Name { get; set; } + public bool CurrentUserInGroup { get; set; } +} diff --git a/src/Core/SecretsManager/Models/Data/ProjectPeopleAccessPolicies.cs b/src/Core/SecretsManager/Models/Data/ProjectPeopleAccessPolicies.cs new file mode 100644 index 000000000..ee3a4e614 --- /dev/null +++ b/src/Core/SecretsManager/Models/Data/ProjectPeopleAccessPolicies.cs @@ -0,0 +1,27 @@ +using Bit.Core.SecretsManager.Entities; + +namespace Bit.Core.SecretsManager.Models.Data; + +public class ProjectPeopleAccessPolicies +{ + public Guid Id { get; set; } + public Guid OrganizationId { get; set; } + public IEnumerable UserAccessPolicies { get; set; } + public IEnumerable GroupAccessPolicies { get; set; } + + public IEnumerable ToBaseAccessPolicies() + { + var policies = new List(); + if (UserAccessPolicies != null && UserAccessPolicies.Any()) + { + policies.AddRange(UserAccessPolicies); + } + + if (GroupAccessPolicies != null && GroupAccessPolicies.Any()) + { + policies.AddRange(GroupAccessPolicies); + } + + return policies; + } +} diff --git a/src/Core/SecretsManager/Repositories/IAccessPolicyRepository.cs b/src/Core/SecretsManager/Repositories/IAccessPolicyRepository.cs index 198ea7614..233b67695 100644 --- a/src/Core/SecretsManager/Repositories/IAccessPolicyRepository.cs +++ b/src/Core/SecretsManager/Repositories/IAccessPolicyRepository.cs @@ -1,6 +1,7 @@ #nullable enable using Bit.Core.Enums; using Bit.Core.SecretsManager.Entities; +using Bit.Core.SecretsManager.Models.Data; namespace Bit.Core.SecretsManager.Repositories; @@ -15,4 +16,7 @@ public interface IAccessPolicyRepository AccessClientType accessType); Task ReplaceAsync(BaseAccessPolicy baseAccessPolicy); Task DeleteAsync(Guid id); + Task> GetPeoplePoliciesByGrantedProjectIdAsync(Guid id, Guid userId); + Task> ReplaceProjectPeopleAsync(ProjectPeopleAccessPolicies peopleAccessPolicies, Guid userId); + Task GetPeopleGranteesAsync(Guid organizationId, Guid currentUserId); } diff --git a/src/Infrastructure.EntityFramework/SecretsManager/Configurations/AccessPolicyEntityTypeConfiguration.cs b/src/Infrastructure.EntityFramework/SecretsManager/Configurations/AccessPolicyEntityTypeConfiguration.cs index 9e8732a85..2f39cffef 100644 --- a/src/Infrastructure.EntityFramework/SecretsManager/Configurations/AccessPolicyEntityTypeConfiguration.cs +++ b/src/Infrastructure.EntityFramework/SecretsManager/Configurations/AccessPolicyEntityTypeConfiguration.cs @@ -1,4 +1,5 @@ -using Bit.Infrastructure.EntityFramework.SecretsManager.Models; +using Bit.Infrastructure.EntityFramework.SecretsManager.Discriminators; +using Bit.Infrastructure.EntityFramework.SecretsManager.Models; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; @@ -10,11 +11,11 @@ public class AccessPolicyEntityTypeConfiguration : IEntityTypeConfiguration("Discriminator") - .HasValue("user_project") - .HasValue("user_service_account") - .HasValue("group_project") - .HasValue("group_service_account") - .HasValue("service_account_project"); + .HasValue(AccessPolicyDiscriminator.UserProject) + .HasValue(AccessPolicyDiscriminator.UserServiceAccount) + .HasValue(AccessPolicyDiscriminator.GroupProject) + .HasValue(AccessPolicyDiscriminator.GroupServiceAccount) + .HasValue(AccessPolicyDiscriminator.ServiceAccountProject); builder .Property(s => s.Id) diff --git a/src/Infrastructure.EntityFramework/SecretsManager/Discriminators/AccessPolicyDiscriminator.cs b/src/Infrastructure.EntityFramework/SecretsManager/Discriminators/AccessPolicyDiscriminator.cs new file mode 100644 index 000000000..12254a5e3 --- /dev/null +++ b/src/Infrastructure.EntityFramework/SecretsManager/Discriminators/AccessPolicyDiscriminator.cs @@ -0,0 +1,11 @@ +namespace Bit.Infrastructure.EntityFramework.SecretsManager.Discriminators; + +public static class AccessPolicyDiscriminator +{ + public const string UserProject = "user_project"; + public const string UserServiceAccount = "user_service_account"; + public const string GroupProject = "group_project"; + public const string GroupServiceAccount = "group_service_account"; + public const string ServiceAccountProject = "service_account_project"; + +} diff --git a/test/Api.IntegrationTest/SecretsManager/Controllers/AccessPoliciesControllerTests.cs b/test/Api.IntegrationTest/SecretsManager/Controllers/AccessPoliciesControllerTests.cs index c5873b12b..c3050db65 100644 --- a/test/Api.IntegrationTest/SecretsManager/Controllers/AccessPoliciesControllerTests.cs +++ b/test/Api.IntegrationTest/SecretsManager/Controllers/AccessPoliciesControllerTests.cs @@ -5,6 +5,8 @@ using Bit.Api.IntegrationTest.SecretsManager.Enums; using Bit.Api.Models.Response; using Bit.Api.SecretsManager.Models.Request; using Bit.Api.SecretsManager.Models.Response; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Repositories; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.SecretsManager.Entities; @@ -25,6 +27,7 @@ public class AccessPoliciesControllerTests : IClassFixture(); _serviceAccountRepository = _factory.GetService(); _projectRepository = _factory.GetService(); + _groupRepository = _factory.GetService(); } public async Task InitializeAsync() @@ -660,7 +664,7 @@ public class AccessPoliciesControllerTests : IClassFixture(); + + Assert.NotNull(result); + Assert.Empty(result!.UserAccessPolicies); + Assert.Empty(result.GroupAccessPolicies); + } + + [Fact] + public async Task GetProjectPeopleAccessPolicies_NoPermission_NotFound() + { + await _organizationHelper.Initialize(true, true, true); + var (email, orgUser) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true); + await LoginAsync(email); + + var project = await _projectRepository.CreateAsync(new Project + { + OrganizationId = orgUser.OrganizationId, + Name = _mockEncryptedString + }); + + var response = await _client.GetAsync($"/projects/{project.Id}/access-policies/people"); + + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Theory] + [InlineData(PermissionType.RunAsAdmin)] + [InlineData(PermissionType.RunAsUserWithPermission)] + public async Task GetProjectPeopleAccessPolicies_Success(PermissionType permissionType) + { + var (_, organizationUser) = await _organizationHelper.Initialize(true, true, true); + await LoginAsync(_email); + + var (project, _) = await SetupProjectPeoplePermissionAsync(permissionType, organizationUser); + + var response = await _client.GetAsync($"/projects/{project.Id}/access-policies/people"); + response.EnsureSuccessStatusCode(); + + var result = await response.Content.ReadFromJsonAsync(); + + Assert.NotNull(result?.UserAccessPolicies); + Assert.Single(result!.UserAccessPolicies); + } + + [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 PutProjectPeopleAccessPolicies_SmAccessDenied_NotFound(bool useSecrets, bool accessSecrets, bool organizationEnabled) + { + var (_, organizationUser) = await _organizationHelper.Initialize(useSecrets, accessSecrets, organizationEnabled); + await LoginAsync(_email); + + var (project, request) = await SetupProjectPeopleRequestAsync(PermissionType.RunAsAdmin, organizationUser); + + var response = await _client.PutAsJsonAsync($"/projects/{project.Id}/access-policies/people", request); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Fact] + public async Task PutProjectPeopleAccessPolicies_NoPermission() + { + var (org, _) = await _organizationHelper.Initialize(true, true, true); + var (email, organizationUser) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true); + await LoginAsync(email); + + var project = await _projectRepository.CreateAsync(new Project + { + OrganizationId = org.Id, + Name = _mockEncryptedString + }); + + var request = new PeopleAccessPoliciesRequestModel + { + UserAccessPolicyRequests = new List + { + new() { GranteeId = organizationUser.Id, Read = true, Write = true } + } + }; + + var response = await _client.PutAsJsonAsync($"/projects/{project.Id}/access-policies/people", request); + + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Theory] + [InlineData(PermissionType.RunAsAdmin)] + [InlineData(PermissionType.RunAsUserWithPermission)] + public async Task PutProjectPeopleAccessPolicies_MismatchedOrgIds_NotFound(PermissionType permissionType) + { + var (_, organizationUser) = await _organizationHelper.Initialize(true, true, true); + await LoginAsync(_email); + + var (project, request) = await SetupProjectPeopleRequestAsync(permissionType, organizationUser); + var newOrg = await _organizationHelper.CreateSmOrganizationAsync(); + var group = await _groupRepository.CreateAsync(new Group + { + OrganizationId = newOrg.Id, + Name = _mockEncryptedString + }); + request.GroupAccessPolicyRequests = new List + { + new() { GranteeId = group.Id, Read = true, Write = true } + }; + + var response = await _client.PutAsJsonAsync($"/projects/{project.Id}/access-policies/people", request); + + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Theory] + [InlineData(PermissionType.RunAsAdmin)] + [InlineData(PermissionType.RunAsUserWithPermission)] + public async Task PutProjectPeopleAccessPolicies_Success(PermissionType permissionType) + { + var (_, organizationUser) = await _organizationHelper.Initialize(true, true, true); + await LoginAsync(_email); + + var (project, request) = await SetupProjectPeopleRequestAsync(permissionType, organizationUser); + + var response = await _client.PutAsJsonAsync($"/projects/{project.Id}/access-policies/people", request); + response.EnsureSuccessStatusCode(); + + var result = await response.Content.ReadFromJsonAsync(); + + Assert.NotNull(result); + Assert.Equal(request.UserAccessPolicyRequests.First().GranteeId, + result!.UserAccessPolicies.First().OrganizationUserId); + Assert.True(result.UserAccessPolicies.First().Read); + Assert.True(result.UserAccessPolicies.First().Write); + + var createdAccessPolicy = + await _accessPolicyRepository.GetByIdAsync(result.UserAccessPolicies.First().Id); + Assert.NotNull(createdAccessPolicy); + Assert.Equal(result.UserAccessPolicies.First().Read, createdAccessPolicy!.Read); + Assert.Equal(result.UserAccessPolicies.First().Write, createdAccessPolicy.Write); + Assert.Equal(result.UserAccessPolicies.First().Id, createdAccessPolicy.Id); + } + private async Task SetupAccessPolicyRequest(Guid organizationId) { var project = await _projectRepository.CreateAsync(new Project @@ -1082,6 +1261,52 @@ public class AccessPoliciesControllerTests : IClassFixture SetupProjectPeoplePermissionAsync( + PermissionType permissionType, + OrganizationUser organizationUser) + { + var project = await _projectRepository.CreateAsync(new Project + { + OrganizationId = organizationUser.OrganizationId, + Name = _mockEncryptedString + }); + + if (permissionType == PermissionType.RunAsUserWithPermission) + { + var (email, orgUser) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true); + await LoginAsync(email); + organizationUser = orgUser; + } + + var accessPolicies = new List + { + new UserProjectAccessPolicy + { + GrantedProjectId = project.Id, + OrganizationUserId = organizationUser.Id, + Read = true, + Write = true + } + }; + await _accessPolicyRepository.CreateManyAsync(accessPolicies); + + return (project, organizationUser); + } + + private async Task<(Project project, PeopleAccessPoliciesRequestModel request)> SetupProjectPeopleRequestAsync( + PermissionType permissionType, OrganizationUser organizationUser) + { + var (project, currentUser) = await SetupProjectPeoplePermissionAsync(permissionType, organizationUser); + var request = new PeopleAccessPoliciesRequestModel + { + UserAccessPolicyRequests = new List + { + new() { GranteeId = currentUser.Id, Read = true, Write = true } + } + }; + return (project, request); + } + private async Task<(Guid ProjectId, Guid ServiceAccountId)> CreateProjectAndServiceAccountAsync(Guid organizationId, bool misMatchOrganization = false) { diff --git a/test/Api.Test/SecretsManager/Controllers/AccessPoliciesControllerTests.cs b/test/Api.Test/SecretsManager/Controllers/AccessPoliciesControllerTests.cs index 6c3d85e42..7a58889cf 100644 --- a/test/Api.Test/SecretsManager/Controllers/AccessPoliciesControllerTests.cs +++ b/test/Api.Test/SecretsManager/Controllers/AccessPoliciesControllerTests.cs @@ -2,14 +2,12 @@ using Bit.Api.SecretsManager.Controllers; using Bit.Api.SecretsManager.Models.Request; using Bit.Api.Test.SecretsManager.Enums; -using Bit.Core.AdminConsole.Entities; -using Bit.Core.AdminConsole.Repositories; using Bit.Core.Context; using Bit.Core.Enums; using Bit.Core.Exceptions; -using Bit.Core.Repositories; using Bit.Core.SecretsManager.Commands.AccessPolicies.Interfaces; using Bit.Core.SecretsManager.Entities; +using Bit.Core.SecretsManager.Models.Data; using Bit.Core.SecretsManager.Repositories; using Bit.Core.Services; using Bit.Core.Test.SecretsManager.AutoFixture.ProjectsFixture; @@ -806,14 +804,17 @@ public class AccessPoliciesControllerTests Guid id) { SetupPermission(sutProvider, permissionType, id); + sutProvider.GetDependency().GetPeopleGranteesAsync(default, default) + .ReturnsForAnyArgs(new PeopleGrantees + { + UserGrantees = new List(), + GroupGrantees = new List() + }); + var result = await sutProvider.Sut.GetPeoplePotentialGranteesAsync(id); - await sutProvider.GetDependency().Received(1) - .GetManyByOrganizationIdAsync(Arg.Is(AssertHelper.AssertPropertyEqual(id))); - - await sutProvider.GetDependency().Received(1) - .GetManyDetailsByOrganizationAsync(Arg.Is(AssertHelper.AssertPropertyEqual(id))); - + await sutProvider.GetDependency().Received(1) + .GetPeopleGranteesAsync(id, Arg.Any()); Assert.Empty(result.Data); } @@ -826,17 +827,17 @@ public class AccessPoliciesControllerTests sutProvider.GetDependency().OrganizationAdmin(id).Returns(false); sutProvider.GetDependency().AccessSecretsManager(default).ReturnsForAnyArgs(false); sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(Guid.NewGuid()); + sutProvider.GetDependency().GetPeopleGranteesAsync(default, default) + .ReturnsForAnyArgs(new PeopleGrantees + { + UserGrantees = new List(), + GroupGrantees = new List() + }); await Assert.ThrowsAsync(() => sutProvider.Sut.GetPeoplePotentialGranteesAsync(id)); - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() - .GetManyByOrganizationIdAsync(Arg.Any()); - - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() - .GetManyDetailsByOrganizationAsync(Arg.Any()); - - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() - .GetManyByOrganizationIdWriteAccessAsync(Arg.Any(), Arg.Any(), Arg.Any()); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .GetPeopleGranteesAsync(id, Arg.Any()); } [Theory] @@ -846,19 +847,20 @@ public class AccessPoliciesControllerTests PermissionType permissionType, SutProvider sutProvider, Guid id, - Group mockGroup) + GroupGrantee groupGrantee) { SetupPermission(sutProvider, permissionType, id); - sutProvider.GetDependency().GetManyByOrganizationIdAsync(default) - .ReturnsForAnyArgs(new List { mockGroup }); + sutProvider.GetDependency().GetPeopleGranteesAsync(default, default) + .ReturnsForAnyArgs(new PeopleGrantees + { + UserGrantees = new List(), + GroupGrantees = new List { groupGrantee } + }); var result = await sutProvider.Sut.GetPeoplePotentialGranteesAsync(id); - await sutProvider.GetDependency().Received(1) - .GetManyByOrganizationIdAsync(Arg.Is(AssertHelper.AssertPropertyEqual(id))); - - await sutProvider.GetDependency().Received(1) - .GetManyDetailsByOrganizationAsync(Arg.Is(AssertHelper.AssertPropertyEqual(id))); + await sutProvider.GetDependency().Received(1) + .GetPeopleGranteesAsync(id, Arg.Any()); Assert.NotEmpty(result.Data); } @@ -980,4 +982,195 @@ public class AccessPoliciesControllerTests Assert.NotEmpty(result.Data); } + + [Theory] + [BitAutoData(PermissionType.RunAsAdmin)] + [BitAutoData(PermissionType.RunAsUserWithPermission)] + public async void GetProjectPeopleAccessPolicies_ReturnsEmptyList( + PermissionType permissionType, + SutProvider sutProvider, + Guid id, Project data) + { + sutProvider.GetDependency().GetByIdAsync(default).ReturnsForAnyArgs(data); + + switch (permissionType) + { + case PermissionType.RunAsAdmin: + SetupAdmin(sutProvider, data.OrganizationId); + sutProvider.GetDependency().AccessToProjectAsync(Arg.Any(), Arg.Any(), + AccessClientType.NoAccessCheck) + .Returns((true, true)); + break; + case PermissionType.RunAsUserWithPermission: + SetupUserWithPermission(sutProvider, data.OrganizationId); + sutProvider.GetDependency() + .AccessToProjectAsync(Arg.Any(), Arg.Any(), AccessClientType.User) + .Returns((true, true)); + break; + } + + var result = await sutProvider.Sut.GetProjectPeopleAccessPoliciesAsync(id); + + await sutProvider.GetDependency().Received(1) + .GetPeoplePoliciesByGrantedProjectIdAsync(Arg.Is(AssertHelper.AssertPropertyEqual(id)), Arg.Any()); + + Assert.Empty(result.GroupAccessPolicies); + Assert.Empty(result.UserAccessPolicies); + } + + [Theory] + [BitAutoData] + public async void GetProjectPeopleAccessPolicies_UserWithoutPermission_Throws( + SutProvider sutProvider, + Guid id, + Project data) + { + SetupUserWithoutPermission(sutProvider, data.OrganizationId); + sutProvider.GetDependency().GetByIdAsync(default).ReturnsForAnyArgs(data); + sutProvider.GetDependency().AccessToProjectAsync(default, default, default) + .Returns((false, false)); + + await Assert.ThrowsAsync(() => sutProvider.Sut.GetProjectPeopleAccessPoliciesAsync(id)); + + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .GetPeoplePoliciesByGrantedProjectIdAsync(Arg.Any(), Arg.Any()); + } + + [Theory] + [BitAutoData] + public async void GetProjectPeopleAccessPolicies_ProjectsExist_UserWithoutPermission_Throws( + SutProvider sutProvider, + Guid id, + Project data, + UserProjectAccessPolicy resultAccessPolicy) + { + SetupUserWithoutPermission(sutProvider, data.OrganizationId); + sutProvider.GetDependency().GetByIdAsync(default).ReturnsForAnyArgs(data); + sutProvider.GetDependency().AccessToProjectAsync(default, default, default) + .Returns((false, false)); + + sutProvider.GetDependency().GetPeoplePoliciesByGrantedProjectIdAsync(default, default) + .ReturnsForAnyArgs(new List { resultAccessPolicy }); + + await Assert.ThrowsAsync(() => sutProvider.Sut.GetProjectPeopleAccessPoliciesAsync(id)); + + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .GetPeoplePoliciesByGrantedProjectIdAsync(Arg.Any(), Arg.Any()); + } + + [Theory] + [BitAutoData(PermissionType.RunAsAdmin)] + [BitAutoData(PermissionType.RunAsUserWithPermission)] + public async void GetProjectPeopleAccessPolicies_Success( + PermissionType permissionType, + SutProvider sutProvider, + Guid id, + Project data, + UserProjectAccessPolicy resultUserPolicy, + GroupProjectAccessPolicy resultGroupPolicy) + { + sutProvider.GetDependency().GetByIdAsync(default).ReturnsForAnyArgs(data); + switch (permissionType) + { + case PermissionType.RunAsAdmin: + SetupAdmin(sutProvider, data.OrganizationId); + sutProvider.GetDependency().AccessToProjectAsync(Arg.Any(), Arg.Any(), + AccessClientType.NoAccessCheck) + .Returns((true, true)); + break; + case PermissionType.RunAsUserWithPermission: + SetupUserWithPermission(sutProvider, data.OrganizationId); + sutProvider.GetDependency() + .AccessToProjectAsync(Arg.Any(), Arg.Any(), AccessClientType.User) + .Returns((true, true)); + break; + } + + sutProvider.GetDependency().GetPeoplePoliciesByGrantedProjectIdAsync(default, default) + .ReturnsForAnyArgs(new List { resultUserPolicy, resultGroupPolicy }); + + var result = await sutProvider.Sut.GetProjectPeopleAccessPoliciesAsync(id); + + await sutProvider.GetDependency().Received(1) + .GetPeoplePoliciesByGrantedProjectIdAsync(Arg.Is(AssertHelper.AssertPropertyEqual(id)), Arg.Any()); + + Assert.NotEmpty(result.GroupAccessPolicies); + Assert.NotEmpty(result.UserAccessPolicies); + } + + [Theory] + [BitAutoData] + public async void PutProjectPeopleAccessPolicies_ProjectDoesNotExist_Throws( + SutProvider sutProvider, + Guid id, + PeopleAccessPoliciesRequestModel request) + { + await Assert.ThrowsAsync(() => + sutProvider.Sut.PutProjectPeopleAccessPoliciesAsync(id, request)); + + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .ReplaceProjectPeopleAsync(Arg.Any(), Arg.Any()); + } + + [Theory] + [BitAutoData] + public async void PutProjectPeopleAccessPoliciesAsync_DuplicatePolicy_Throws( + SutProvider sutProvider, + Project project, + PeopleAccessPoliciesRequestModel request) + { + var dup = new AccessPolicyRequest { GranteeId = Guid.NewGuid(), Read = true, Write = true }; + request.UserAccessPolicyRequests = new[] { dup, dup }; + sutProvider.GetDependency().GetByIdAsync(default).ReturnsForAnyArgs(project); + + await Assert.ThrowsAsync(() => + sutProvider.Sut.PutProjectPeopleAccessPoliciesAsync(project.Id, request)); + + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .ReplaceProjectPeopleAsync(Arg.Any(), Arg.Any()); + } + + [Theory] + [BitAutoData] + public async void PutProjectPeopleAccessPoliciesAsync_NoAccess_Throws( + SutProvider sutProvider, + Project project, + PeopleAccessPoliciesRequestModel request) + { + sutProvider.GetDependency().GetByIdAsync(default).ReturnsForAnyArgs(project); + var peoplePolicies = request.ToProjectPeopleAccessPolicies(project.Id, project.OrganizationId); + sutProvider.GetDependency() + .AuthorizeAsync(Arg.Any(), peoplePolicies, + Arg.Any>()).ReturnsForAnyArgs(AuthorizationResult.Failed()); + + await Assert.ThrowsAsync(() => + sutProvider.Sut.PutProjectPeopleAccessPoliciesAsync(project.Id, request)); + + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .ReplaceProjectPeopleAsync(Arg.Any(), Arg.Any()); + } + + [Theory] + [BitAutoData] + public async void PutProjectPeopleAccessPoliciesAsync_Success( + SutProvider sutProvider, + Guid userId, + Project project, + PeopleAccessPoliciesRequestModel request) + { + sutProvider.GetDependency().GetByIdAsync(default).ReturnsForAnyArgs(project); + sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId); + var peoplePolicies = request.ToProjectPeopleAccessPolicies(project.Id, project.OrganizationId); + sutProvider.GetDependency() + .AuthorizeAsync(Arg.Any(), peoplePolicies, + Arg.Any>()).ReturnsForAnyArgs(AuthorizationResult.Success()); + + sutProvider.GetDependency().ReplaceProjectPeopleAsync(peoplePolicies, Arg.Any()) + .Returns(peoplePolicies.ToBaseAccessPolicies()); + + await sutProvider.Sut.PutProjectPeopleAccessPoliciesAsync(project.Id, request); + + await sutProvider.GetDependency().Received(1) + .ReplaceProjectPeopleAsync(Arg.Any(), Arg.Any()); + } }