diff --git a/bitwarden_license/src/Commercial.Core/SecretsManager/AuthorizationHandlers/AccessPolicies/ProjectPeopleAccessPoliciesAuthorizationHandler.cs b/bitwarden_license/src/Commercial.Core/SecretsManager/AuthorizationHandlers/AccessPolicies/ProjectPeopleAccessPoliciesAuthorizationHandler.cs index c99d6474a..64f7effd4 100644 --- a/bitwarden_license/src/Commercial.Core/SecretsManager/AuthorizationHandlers/AccessPolicies/ProjectPeopleAccessPoliciesAuthorizationHandler.cs +++ b/bitwarden_license/src/Commercial.Core/SecretsManager/AuthorizationHandlers/AccessPolicies/ProjectPeopleAccessPoliciesAuthorizationHandler.cs @@ -1,9 +1,8 @@ -using Bit.Core.AdminConsole.Repositories; -using Bit.Core.Context; +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.AccessPolicies.Interfaces; using Bit.Core.SecretsManager.Queries.Interfaces; using Bit.Core.SecretsManager.Repositories; using Microsoft.AspNetCore.Authorization; @@ -11,25 +10,23 @@ 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; + private readonly ISameOrganizationQuery _sameOrganizationQuery; public ProjectPeopleAccessPoliciesAuthorizationHandler(ICurrentContext currentContext, IAccessClientQuery accessClientQuery, - IGroupRepository groupRepository, - IOrganizationUserRepository organizationUserRepository, + ISameOrganizationQuery sameOrganizationQuery, IProjectRepository projectRepository) { _currentContext = currentContext; _accessClientQuery = accessClientQuery; - _groupRepository = groupRepository; - _organizationUserRepository = organizationUserRepository; + _sameOrganizationQuery = sameOrganizationQuery; _projectRepository = projectRepository; } @@ -71,9 +68,7 @@ public class 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) + if (!await _sameOrganizationQuery.OrgUsersInTheSameOrgAsync(orgUserIds, resource.OrganizationId)) { return; } @@ -82,9 +77,7 @@ public class 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) + if (!await _sameOrganizationQuery.GroupsInTheSameOrgAsync(groupIds, resource.OrganizationId)) { return; } diff --git a/bitwarden_license/src/Commercial.Core/SecretsManager/AuthorizationHandlers/AccessPolicies/ServiceAccountPeopleAccessPoliciesAuthorizationHandler.cs b/bitwarden_license/src/Commercial.Core/SecretsManager/AuthorizationHandlers/AccessPolicies/ServiceAccountPeopleAccessPoliciesAuthorizationHandler.cs new file mode 100644 index 000000000..6e1c7cbc1 --- /dev/null +++ b/bitwarden_license/src/Commercial.Core/SecretsManager/AuthorizationHandlers/AccessPolicies/ServiceAccountPeopleAccessPoliciesAuthorizationHandler.cs @@ -0,0 +1,89 @@ +using Bit.Core.Context; +using Bit.Core.Enums; +using Bit.Core.SecretsManager.AuthorizationRequirements; +using Bit.Core.SecretsManager.Models.Data; +using Bit.Core.SecretsManager.Queries.AccessPolicies.Interfaces; +using Bit.Core.SecretsManager.Queries.Interfaces; +using Bit.Core.SecretsManager.Repositories; +using Microsoft.AspNetCore.Authorization; + +namespace Bit.Commercial.Core.SecretsManager.AuthorizationHandlers.AccessPolicies; + +public class + ServiceAccountPeopleAccessPoliciesAuthorizationHandler : AuthorizationHandler< + ServiceAccountPeopleAccessPoliciesOperationRequirement, + ServiceAccountPeopleAccessPolicies> +{ + private readonly IAccessClientQuery _accessClientQuery; + private readonly ICurrentContext _currentContext; + private readonly ISameOrganizationQuery _sameOrganizationQuery; + private readonly IServiceAccountRepository _serviceAccountRepository; + + public ServiceAccountPeopleAccessPoliciesAuthorizationHandler(ICurrentContext currentContext, + IAccessClientQuery accessClientQuery, + ISameOrganizationQuery sameOrganizationQuery, + IServiceAccountRepository serviceAccountRepository) + { + _currentContext = currentContext; + _accessClientQuery = accessClientQuery; + _sameOrganizationQuery = sameOrganizationQuery; + _serviceAccountRepository = serviceAccountRepository; + } + + protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, + ServiceAccountPeopleAccessPoliciesOperationRequirement requirement, + ServiceAccountPeopleAccessPolicies 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 == ServiceAccountPeopleAccessPoliciesOperations.Replace: + await CanReplaceServiceAccountPeopleAsync(context, requirement, resource, accessClient, userId); + break; + default: + throw new ArgumentException("Unsupported operation requirement type provided.", + nameof(requirement)); + } + } + + private async Task CanReplaceServiceAccountPeopleAsync(AuthorizationHandlerContext context, + ServiceAccountPeopleAccessPoliciesOperationRequirement requirement, ServiceAccountPeopleAccessPolicies resource, + AccessClientType accessClient, Guid userId) + { + var access = await _serviceAccountRepository.AccessToServiceAccountAsync(resource.Id, userId, accessClient); + if (access.Write) + { + if (resource.UserAccessPolicies != null && resource.UserAccessPolicies.Any()) + { + var orgUserIds = resource.UserAccessPolicies.Select(ap => ap.OrganizationUserId!.Value).ToList(); + if (!await _sameOrganizationQuery.OrgUsersInTheSameOrgAsync(orgUserIds, resource.OrganizationId)) + { + return; + } + } + + if (resource.GroupAccessPolicies != null && resource.GroupAccessPolicies.Any()) + { + var groupIds = resource.GroupAccessPolicies.Select(ap => ap.GroupId!.Value).ToList(); + if (!await _sameOrganizationQuery.GroupsInTheSameOrgAsync(groupIds, resource.OrganizationId)) + { + return; + } + } + + context.Succeed(requirement); + } + } +} diff --git a/bitwarden_license/src/Commercial.Core/SecretsManager/Queries/AccessPolicies/SameOrganizationQuery.cs b/bitwarden_license/src/Commercial.Core/SecretsManager/Queries/AccessPolicies/SameOrganizationQuery.cs new file mode 100644 index 000000000..e2241078d --- /dev/null +++ b/bitwarden_license/src/Commercial.Core/SecretsManager/Queries/AccessPolicies/SameOrganizationQuery.cs @@ -0,0 +1,32 @@ +using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Repositories; +using Bit.Core.SecretsManager.Queries.AccessPolicies.Interfaces; + +namespace Bit.Commercial.Core.SecretsManager.Queries.AccessPolicies; + +public class SameOrganizationQuery : ISameOrganizationQuery +{ + private readonly IGroupRepository _groupRepository; + private readonly IOrganizationUserRepository _organizationUserRepository; + + public SameOrganizationQuery(IOrganizationUserRepository organizationUserRepository, + IGroupRepository groupRepository) + { + _organizationUserRepository = organizationUserRepository; + _groupRepository = groupRepository; + } + + public async Task OrgUsersInTheSameOrgAsync(List organizationUserIds, Guid organizationId) + { + var users = await _organizationUserRepository.GetManyAsync(organizationUserIds); + return users.All(user => user.OrganizationId == organizationId) && + users.Count == organizationUserIds.Count; + } + + public async Task GroupsInTheSameOrgAsync(List groupIds, Guid organizationId) + { + var groups = await _groupRepository.GetManyByManyIds(groupIds); + return groups.All(group => group.OrganizationId == organizationId) && + groups.Count == groupIds.Count; + } +} diff --git a/bitwarden_license/src/Commercial.Core/SecretsManager/SecretsManagerCollectionExtensions.cs b/bitwarden_license/src/Commercial.Core/SecretsManager/SecretsManagerCollectionExtensions.cs index 2eeee7cfe..239f3c098 100644 --- a/bitwarden_license/src/Commercial.Core/SecretsManager/SecretsManagerCollectionExtensions.cs +++ b/bitwarden_license/src/Commercial.Core/SecretsManager/SecretsManagerCollectionExtensions.cs @@ -10,6 +10,7 @@ using Bit.Commercial.Core.SecretsManager.Commands.Secrets; using Bit.Commercial.Core.SecretsManager.Commands.ServiceAccounts; using Bit.Commercial.Core.SecretsManager.Commands.Trash; using Bit.Commercial.Core.SecretsManager.Queries; +using Bit.Commercial.Core.SecretsManager.Queries.AccessPolicies; using Bit.Commercial.Core.SecretsManager.Queries.Projects; using Bit.Commercial.Core.SecretsManager.Queries.ServiceAccounts; using Bit.Core.SecretsManager.Commands.AccessPolicies.Interfaces; @@ -19,6 +20,7 @@ using Bit.Core.SecretsManager.Commands.Projects.Interfaces; using Bit.Core.SecretsManager.Commands.Secrets.Interfaces; using Bit.Core.SecretsManager.Commands.ServiceAccounts.Interfaces; using Bit.Core.SecretsManager.Commands.Trash.Interfaces; +using Bit.Core.SecretsManager.Queries.AccessPolicies.Interfaces; using Bit.Core.SecretsManager.Queries.Interfaces; using Bit.Core.SecretsManager.Queries.Projects.Interfaces; using Bit.Core.SecretsManager.Queries.ServiceAccounts.Interfaces; @@ -36,8 +38,10 @@ public static class SecretsManagerCollectionExtensions services.AddScoped(); services.AddScoped(); services.AddScoped(); + 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 dca6f9c93..8a11cdc61 100644 --- a/bitwarden_license/src/Commercial.Infrastructure.EntityFramework/SecretsManager/Repositories/AccessPolicyRepository.cs +++ b/bitwarden_license/src/Commercial.Infrastructure.EntityFramework/SecretsManager/Repositories/AccessPolicyRepository.cs @@ -183,28 +183,6 @@ public class AccessPolicyRepository : BaseEntityFrameworkRepository, IAccessPoli return entities.Select(e => MapToCore(e.ap, e.CurrentUserInGroup)); } - public async Task> GetManyByGrantedServiceAccountIdAsync(Guid id, Guid userId) - { - using var scope = ServiceScopeFactory.CreateScope(); - var dbContext = GetDatabaseContext(scope); - - var entities = await dbContext.AccessPolicies.Where(ap => - ((UserServiceAccountAccessPolicy)ap).GrantedServiceAccountId == id || - ((GroupServiceAccountAccessPolicy)ap).GrantedServiceAccountId == id) - .Include(ap => ((UserServiceAccountAccessPolicy)ap).OrganizationUser.User) - .Include(ap => ((GroupServiceAccountAccessPolicy)ap).Group) - .Select(ap => new - { - ap, - CurrentUserInGroup = ap is GroupServiceAccountAccessPolicy && - ((GroupServiceAccountAccessPolicy)ap).Group.GroupUsers.Any(g => - g.OrganizationUser.User.Id == userId), - }) - .ToListAsync(); - - return entities.Select(e => MapToCore(e.ap, e.CurrentUserInGroup)); - } - public async Task DeleteAsync(Guid id) { using var scope = ServiceScopeFactory.CreateScope(); @@ -352,6 +330,81 @@ public class AccessPolicyRepository : BaseEntityFrameworkRepository, IAccessPoli return await GetPeoplePoliciesByGrantedProjectIdAsync(peopleAccessPolicies.Id, userId); } + public async Task> + GetPeoplePoliciesByGrantedServiceAccountIdAsync(Guid id, Guid userId) + { + using var scope = ServiceScopeFactory.CreateScope(); + var dbContext = GetDatabaseContext(scope); + + var entities = await dbContext.AccessPolicies.Where(ap => + ((UserServiceAccountAccessPolicy)ap).GrantedServiceAccountId == id || + ((GroupServiceAccountAccessPolicy)ap).GrantedServiceAccountId == id) + .Include(ap => ((UserServiceAccountAccessPolicy)ap).OrganizationUser.User) + .Include(ap => ((GroupServiceAccountAccessPolicy)ap).Group) + .Select(ap => new + { + ap, + CurrentUserInGroup = ap is GroupServiceAccountAccessPolicy && + ((GroupServiceAccountAccessPolicy)ap).Group.GroupUsers.Any(g => + g.OrganizationUser.UserId == userId) + }) + .ToListAsync(); + + return entities.Select(e => MapToCore(e.ap, e.CurrentUserInGroup)); + } + + public async Task> ReplaceServiceAccountPeopleAsync( + ServiceAccountPeopleAccessPolicies peopleAccessPolicies, Guid userId) + { + using var scope = ServiceScopeFactory.CreateScope(); + var dbContext = GetDatabaseContext(scope); + var peoplePolicyEntities = await dbContext.AccessPolicies.Where(ap => + ((UserServiceAccountAccessPolicy)ap).GrantedServiceAccountId == peopleAccessPolicies.Id || + ((GroupServiceAccountAccessPolicy)ap).GrantedServiceAccountId == peopleAccessPolicies.Id).ToListAsync(); + + var userPolicyEntities = + peoplePolicyEntities.Where(ap => ap.GetType() == typeof(UserServiceAccountAccessPolicy)).ToList(); + var groupPolicyEntities = + peoplePolicyEntities.Where(ap => ap.GetType() == typeof(GroupServiceAccountAccessPolicy)).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.UserServiceAccountAccessPolicy)ap).OrganizationUserId != + ((UserServiceAccountAccessPolicy)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.GroupServiceAccountAccessPolicy)ap).GroupId != + ((GroupServiceAccountAccessPolicy)entity).GroupId))) + { + dbContext.Remove(groupPolicyEntity); + } + } + + await UpsertPeoplePoliciesAsync(dbContext, + peopleAccessPolicies.ToBaseAccessPolicies().Select(MapToEntity).ToList(), userPolicyEntities, + groupPolicyEntities); + await dbContext.SaveChangesAsync(); + return await GetPeoplePoliciesByGrantedServiceAccountIdAsync(peopleAccessPolicies.Id, userId); + } + private static async Task UpsertPeoplePoliciesAsync(DatabaseContext dbContext, List policies, IReadOnlyCollection userPolicyEntities, IReadOnlyCollection groupPolicyEntities) 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 index 855f28b43..f12ede617 100644 --- a/bitwarden_license/test/Commercial.Core.Test/SecretsManager/AuthorizationHandlers/AccessPolicies/ProjectPeopleAccessPoliciesAuthorizationHandlerTests.cs +++ b/bitwarden_license/test/Commercial.Core.Test/SecretsManager/AuthorizationHandlers/AccessPolicies/ProjectPeopleAccessPoliciesAuthorizationHandlerTests.cs @@ -1,14 +1,11 @@ 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.AccessPolicies.Interfaces; using Bit.Core.SecretsManager.Queries.Interfaces; using Bit.Core.SecretsManager.Repositories; using Bit.Core.Test.SecretsManager.AutoFixture.ProjectsFixture; @@ -38,26 +35,16 @@ public class ProjectPeopleAccessPoliciesAuthorizationHandlerTests } 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); - } + ProjectPeopleAccessPolicies resource) => + sutProvider.GetDependency() + .OrgUsersInTheSameOrgAsync(Arg.Any>(), resource.OrganizationId) + .Returns(true); 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); - } + ProjectPeopleAccessPolicies resource) => + sutProvider.GetDependency() + .GroupsInTheSameOrgAsync(Arg.Any>(), resource.OrganizationId) + .Returns(true); [Fact] public void PeopleAccessPoliciesOperations_OnlyPublicStatic() @@ -129,37 +116,10 @@ public class ProjectPeopleAccessPoliciesAuthorizationHandlerTests { 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); + sutProvider.GetDependency() + .OrgUsersInTheSameOrgAsync(Arg.Any>(), resource.OrganizationId) + .Returns(false); - 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); @@ -179,35 +139,8 @@ public class ProjectPeopleAccessPoliciesAuthorizationHandlerTests 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); + sutProvider.GetDependency() + .GroupsInTheSameOrgAsync(Arg.Any>(), resource.OrganizationId).Returns(false); var authzContext = new AuthorizationHandlerContext(new List { requirement }, claimsPrincipal, resource); diff --git a/bitwarden_license/test/Commercial.Core.Test/SecretsManager/AuthorizationHandlers/AccessPolicies/ServiceAccountPeopleAccessPoliciesAuthorizationHandlerTests.cs b/bitwarden_license/test/Commercial.Core.Test/SecretsManager/AuthorizationHandlers/AccessPolicies/ServiceAccountPeopleAccessPoliciesAuthorizationHandlerTests.cs new file mode 100644 index 000000000..f2a18e227 --- /dev/null +++ b/bitwarden_license/test/Commercial.Core.Test/SecretsManager/AuthorizationHandlers/AccessPolicies/ServiceAccountPeopleAccessPoliciesAuthorizationHandlerTests.cs @@ -0,0 +1,186 @@ +using System.Reflection; +using System.Security.Claims; +using Bit.Commercial.Core.SecretsManager.AuthorizationHandlers.AccessPolicies; +using Bit.Core.Context; +using Bit.Core.Enums; +using Bit.Core.SecretsManager.AuthorizationRequirements; +using Bit.Core.SecretsManager.Models.Data; +using Bit.Core.SecretsManager.Queries.AccessPolicies.Interfaces; +using Bit.Core.SecretsManager.Queries.Interfaces; +using Bit.Core.SecretsManager.Repositories; +using Bit.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] +public class ServiceAccountPeopleAccessPoliciesAuthorizationHandlerTests +{ + private static void SetupUserPermission( + SutProvider sutProvider, + AccessClientType accessClientType, ServiceAccountPeopleAccessPolicies 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() + .AccessToServiceAccountAsync(resource.Id, userId, accessClientType) + .Returns((read, write)); + } + + private static void SetupOrganizationUsers( + SutProvider sutProvider, + ServiceAccountPeopleAccessPolicies resource) => + sutProvider.GetDependency() + .OrgUsersInTheSameOrgAsync(Arg.Any>(), resource.OrganizationId) + .Returns(true); + + private static void SetupGroups(SutProvider sutProvider, + ServiceAccountPeopleAccessPolicies resource) => + sutProvider.GetDependency() + .GroupsInTheSameOrgAsync(Arg.Any>(), resource.OrganizationId) + .Returns(true); + + [Fact] + public void ServiceAccountPeopleAccessPoliciesOperations_OnlyPublicStatic() + { + var publicStaticFields = + typeof(ServiceAccountPeopleAccessPoliciesOperations).GetFields(BindingFlags.Public | BindingFlags.Static); + var allFields = typeof(ServiceAccountPeopleAccessPoliciesOperations).GetFields(); + Assert.Equal(publicStaticFields.Length, allFields.Length); + } + + [Theory] + [BitAutoData] + public async Task Handler_UnsupportedServiceAccountPeopleAccessPoliciesOperationRequirement_Throws( + SutProvider sutProvider, + ServiceAccountPeopleAccessPolicies resource, + ClaimsPrincipal claimsPrincipal) + { + var requirement = new ServiceAccountPeopleAccessPoliciesOperationRequirement(); + 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, + ServiceAccountPeopleAccessPolicies resource, + ClaimsPrincipal claimsPrincipal) + { + var requirement = new ServiceAccountPeopleAccessPoliciesOperationRequirement(); + 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, + ServiceAccountPeopleAccessPolicies resource, + ClaimsPrincipal claimsPrincipal) + { + var requirement = new ServiceAccountPeopleAccessPoliciesOperationRequirement(); + 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 ReplaceServiceAccountPeople_UserNotInOrg_DoesNotSucceed(AccessClientType accessClient, + SutProvider sutProvider, + ServiceAccountPeopleAccessPolicies resource, + ClaimsPrincipal claimsPrincipal, Guid userId) + { + var requirement = ServiceAccountPeopleAccessPoliciesOperations.Replace; + SetupUserPermission(sutProvider, accessClient, resource, userId); + sutProvider.GetDependency() + .OrgUsersInTheSameOrgAsync(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.User)] + [BitAutoData(AccessClientType.NoAccessCheck)] + public async Task ReplaceServiceAccountPeople_GroupNotInOrg_DoesNotSucceed(AccessClientType accessClient, + SutProvider sutProvider, + ServiceAccountPeopleAccessPolicies resource, + ClaimsPrincipal claimsPrincipal, Guid userId) + { + var requirement = ServiceAccountPeopleAccessPoliciesOperations.Replace; + SetupUserPermission(sutProvider, accessClient, resource, userId); + SetupOrganizationUsers(sutProvider, resource); + + sutProvider.GetDependency() + .GroupsInTheSameOrgAsync(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.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 ReplaceServiceAccountPeople_AccessCheck(AccessClientType accessClient, bool read, bool write, + bool expected, + SutProvider sutProvider, + ServiceAccountPeopleAccessPolicies resource, + ClaimsPrincipal claimsPrincipal, Guid userId) + { + var requirement = ServiceAccountPeopleAccessPoliciesOperations.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/bitwarden_license/test/Commercial.Core.Test/SecretsManager/Queries/AccessPolicies/SameOrganizationQueryTests.cs b/bitwarden_license/test/Commercial.Core.Test/SecretsManager/Queries/AccessPolicies/SameOrganizationQueryTests.cs new file mode 100644 index 000000000..12bbbce80 --- /dev/null +++ b/bitwarden_license/test/Commercial.Core.Test/SecretsManager/Queries/AccessPolicies/SameOrganizationQueryTests.cs @@ -0,0 +1,151 @@ +using Bit.Commercial.Core.SecretsManager.Queries.AccessPolicies; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Entities; +using Bit.Core.Repositories; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; + +namespace Bit.Commercial.Core.Test.SecretsManager.Queries.AccessPolicies; + +[SutProviderCustomize] +public class SameOrganizationQueryTests +{ + [Theory] + [BitAutoData] + public async Task OrgUsersInTheSameOrg_NoOrgUsers_ReturnsFalse(SutProvider sutProvider, + List orgUsers, Guid organizationId) + { + var orgUserIds = orgUsers.Select(ou => ou.Id).ToList(); + sutProvider.GetDependency().GetManyAsync(orgUserIds) + .ReturnsForAnyArgs(new List()); + + var result = await sutProvider.Sut.OrgUsersInTheSameOrgAsync(orgUserIds, organizationId); + + Assert.False(result); + } + + [Theory] + [BitAutoData] + public async Task OrgUsersInTheSameOrg_OrgMismatch_ReturnsFalse(SutProvider sutProvider, + List orgUsers, Guid organizationId) + { + var orgUserIds = orgUsers.Select(ou => ou.Id).ToList(); + sutProvider.GetDependency().GetManyAsync(orgUserIds) + .ReturnsForAnyArgs(orgUsers); + + var result = await sutProvider.Sut.OrgUsersInTheSameOrgAsync(orgUserIds, organizationId); + + Assert.False(result); + } + + [Theory] + [BitAutoData] + public async Task OrgUsersInTheSameOrg_CountMismatch_ReturnsFalse(SutProvider sutProvider, + List orgUsers, Guid organizationId) + { + var orgUserIds = orgUsers.Select(ou => ou.Id).ToList(); + foreach (var organizationUser in orgUsers) + { + organizationUser.OrganizationId = organizationId; + } + + orgUsers.RemoveAt(0); + sutProvider.GetDependency().GetManyAsync(orgUserIds) + .ReturnsForAnyArgs(orgUsers); + + var result = await sutProvider.Sut.OrgUsersInTheSameOrgAsync(orgUserIds, organizationId); + + Assert.False(result); + } + + [Theory] + [BitAutoData] + public async Task OrgUsersInTheSameOrg_Success_ReturnsTrue(SutProvider sutProvider, + List orgUsers, Guid organizationId) + { + var orgUserIds = orgUsers.Select(ou => ou.Id).ToList(); + foreach (var organizationUser in orgUsers) + { + organizationUser.OrganizationId = organizationId; + } + + sutProvider.GetDependency().GetManyAsync(orgUserIds) + .ReturnsForAnyArgs(orgUsers); + + var result = await sutProvider.Sut.OrgUsersInTheSameOrgAsync(orgUserIds, organizationId); + + Assert.True(result); + } + + [Theory] + [BitAutoData] + public async Task GroupsInTheSameOrg_NoGroups_ReturnsFalse(SutProvider sutProvider, + List groups, Guid organizationId) + { + var groupIds = groups.Select(ou => ou.Id).ToList(); + sutProvider.GetDependency().GetManyByManyIds(groupIds) + .ReturnsForAnyArgs(new List()); + + var result = await sutProvider.Sut.GroupsInTheSameOrgAsync(groupIds, organizationId); + + Assert.False(result); + } + + [Theory] + [BitAutoData] + public async Task GroupsInTheSameOrg_OrgMismatch_ReturnsFalse(SutProvider sutProvider, + List groups, Guid organizationId) + { + var groupIds = groups.Select(ou => ou.Id).ToList(); + sutProvider.GetDependency().GetManyByManyIds(groupIds) + .ReturnsForAnyArgs(groups); + + var result = await sutProvider.Sut.GroupsInTheSameOrgAsync(groupIds, organizationId); + + Assert.False(result); + } + + [Theory] + [BitAutoData] + public async Task GroupsInTheSameOrg_CountMismatch_ReturnsFalse(SutProvider sutProvider, + List groups, Guid organizationId) + { + var groupIds = groups.Select(ou => ou.Id).ToList(); + foreach (var group in groups) + { + group.OrganizationId = organizationId; + } + + groups.RemoveAt(0); + + sutProvider.GetDependency().GetManyByManyIds(groupIds) + .ReturnsForAnyArgs(groups); + + var result = await sutProvider.Sut.GroupsInTheSameOrgAsync(groupIds, organizationId); + + Assert.False(result); + } + + [Theory] + [BitAutoData] + public async Task GroupsInTheSameOrg_Success_ReturnsTrue(SutProvider sutProvider, + List groups, Guid organizationId) + { + var groupIds = groups.Select(ou => ou.Id).ToList(); + foreach (var group in groups) + { + group.OrganizationId = organizationId; + } + + sutProvider.GetDependency().GetManyByManyIds(groupIds) + .ReturnsForAnyArgs(groups); + + + var result = await sutProvider.Sut.GroupsInTheSameOrgAsync(groupIds, organizationId); + + Assert.True(result); + } +} diff --git a/src/Api/SecretsManager/Controllers/AccessPoliciesController.cs b/src/Api/SecretsManager/Controllers/AccessPoliciesController.cs index fdcb76015..734fcfe30 100644 --- a/src/Api/SecretsManager/Controllers/AccessPoliciesController.cs +++ b/src/Api/SecretsManager/Controllers/AccessPoliciesController.cs @@ -89,46 +89,6 @@ public class AccessPoliciesController : Controller return new ProjectAccessPoliciesResponseModel(results); } - [HttpPost("/service-accounts/{id}/access-policies")] - public async Task CreateServiceAccountAccessPoliciesAsync( - [FromRoute] Guid id, - [FromBody] AccessPoliciesCreateRequest request) - { - if (request.Count() > _maxBulkCreation) - { - throw new BadRequestException($"Can process no more than {_maxBulkCreation} creation requests at once."); - } - - var serviceAccount = await _serviceAccountRepository.GetByIdAsync(id); - if (serviceAccount == null) - { - throw new NotFoundException(); - } - - var policies = request.ToBaseAccessPoliciesForServiceAccount(id, serviceAccount.OrganizationId); - 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(policies); - return new ServiceAccountAccessPoliciesResponseModel(results); - } - - [HttpGet("/service-accounts/{id}/access-policies")] - public async Task GetServiceAccountAccessPoliciesAsync( - [FromRoute] Guid id) - { - var serviceAccount = await _serviceAccountRepository.GetByIdAsync(id); - var (_, userId) = await CheckUserHasWriteAccessToServiceAccountAsync(serviceAccount); - var results = await _accessPolicyRepository.GetManyByGrantedServiceAccountIdAsync(id, userId); - return new ServiceAccountAccessPoliciesResponseModel(results); - } - [HttpPost("/service-accounts/{id}/granted-policies")] public async Task> CreateServiceAccountGrantedPoliciesAsync([FromRoute] Guid id, @@ -308,6 +268,40 @@ public class AccessPoliciesController : Controller return new ProjectPeopleAccessPoliciesResponseModel(results, userId); } + [HttpGet("/service-accounts/{id}/access-policies/people")] + public async Task GetServiceAccountPeopleAccessPoliciesAsync( + [FromRoute] Guid id) + { + var serviceAccount = await _serviceAccountRepository.GetByIdAsync(id); + var (_, userId) = await CheckUserHasWriteAccessToServiceAccountAsync(serviceAccount); + var results = await _accessPolicyRepository.GetPeoplePoliciesByGrantedServiceAccountIdAsync(id, userId); + return new ServiceAccountPeopleAccessPoliciesResponseModel(results, userId); + } + + [HttpPut("/service-accounts/{id}/access-policies/people")] + public async Task PutServiceAccountPeopleAccessPoliciesAsync( + [FromRoute] Guid id, + [FromBody] PeopleAccessPoliciesRequestModel request) + { + var serviceAccount = await _serviceAccountRepository.GetByIdAsync(id); + if (serviceAccount == null) + { + throw new NotFoundException(); + } + + var peopleAccessPolicies = request.ToServiceAccountPeopleAccessPolicies(id, serviceAccount.OrganizationId); + + var authorizationResult = await _authorizationService.AuthorizeAsync(User, peopleAccessPolicies, + ServiceAccountPeopleAccessPoliciesOperations.Replace); + if (!authorizationResult.Succeeded) + { + throw new NotFoundException(); + } + + var userId = _userService.GetProperUserId(User)!.Value; + var results = await _accessPolicyRepository.ReplaceServiceAccountPeopleAsync(peopleAccessPolicies, userId); + return new ServiceAccountPeopleAccessPoliciesResponseModel(results, userId); + } private async Task<(AccessClientType AccessClientType, Guid UserId)> CheckUserHasWriteAccessToProjectAsync(Project project) { @@ -345,6 +339,7 @@ public class AccessPoliciesController : Controller { throw new NotFoundException(); } + return (accessClient, userId); } diff --git a/src/Api/SecretsManager/Models/Request/PeopleAccessPoliciesRequestModel.cs b/src/Api/SecretsManager/Models/Request/PeopleAccessPoliciesRequestModel.cs index 6cc5c287d..cfe2c2323 100644 --- a/src/Api/SecretsManager/Models/Request/PeopleAccessPoliciesRequestModel.cs +++ b/src/Api/SecretsManager/Models/Request/PeopleAccessPoliciesRequestModel.cs @@ -61,4 +61,39 @@ public class PeopleAccessPoliciesRequestModel GroupAccessPolicies = groupAccessPolicies }; } + + public ServiceAccountPeopleAccessPolicies ToServiceAccountPeopleAccessPolicies(Guid grantedServiceAccountId, Guid organizationId) + { + var userAccessPolicies = UserAccessPolicyRequests? + .Select(x => x.ToUserServiceAccountAccessPolicy(grantedServiceAccountId, organizationId)).ToList(); + + var groupAccessPolicies = GroupAccessPolicyRequests? + .Select(x => x.ToGroupServiceAccountAccessPolicy(grantedServiceAccountId, organizationId)).ToList(); + + var policies = new List(); + if (userAccessPolicies != null) + { + policies.AddRange(userAccessPolicies); + } + + if (groupAccessPolicies != null) + { + policies.AddRange(groupAccessPolicies); + } + + CheckForDistinctAccessPolicies(policies); + + if (!policies.All(ap => ap.Read && ap.Write)) + { + throw new BadRequestException("Service account access must be Can read, write"); + } + + return new ServiceAccountPeopleAccessPolicies + { + Id = grantedServiceAccountId, + 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 05926bd6c..819b60e1f 100644 --- a/src/Api/SecretsManager/Models/Response/AccessPolicyResponseModel.cs +++ b/src/Api/SecretsManager/Models/Response/AccessPolicyResponseModel.cs @@ -69,10 +69,14 @@ public class UserServiceAccountAccessPolicyResponseModel : BaseAccessPolicyRespo public UserServiceAccountAccessPolicyResponseModel(UserServiceAccountAccessPolicy accessPolicy) : base(accessPolicy, _objectName) { - OrganizationUserId = accessPolicy.OrganizationUserId; - GrantedServiceAccountId = accessPolicy.GrantedServiceAccountId; - OrganizationUserName = GetUserDisplayName(accessPolicy.User); - UserId = accessPolicy.User?.Id; + SetProperties(accessPolicy); + } + + public UserServiceAccountAccessPolicyResponseModel(UserServiceAccountAccessPolicy accessPolicy, Guid userId) + : base(accessPolicy, _objectName) + { + SetProperties(accessPolicy); + CurrentUser = accessPolicy.User?.Id == userId; } public UserServiceAccountAccessPolicyResponseModel() : base(new UserServiceAccountAccessPolicy(), _objectName) @@ -83,6 +87,15 @@ public class UserServiceAccountAccessPolicyResponseModel : BaseAccessPolicyRespo public string? OrganizationUserName { get; set; } public Guid? UserId { get; set; } public Guid? GrantedServiceAccountId { get; set; } + public bool CurrentUser { get; set; } + + private void SetProperties(UserServiceAccountAccessPolicy accessPolicy) + { + OrganizationUserId = accessPolicy.OrganizationUserId; + GrantedServiceAccountId = accessPolicy.GrantedServiceAccountId; + OrganizationUserName = GetUserDisplayName(accessPolicy.User); + UserId = accessPolicy.User?.Id; + } } public class GroupProjectAccessPolicyResponseModel : BaseAccessPolicyResponseModel diff --git a/src/Api/SecretsManager/Models/Response/ServiceAccountAccessPoliciesResponseModel.cs b/src/Api/SecretsManager/Models/Response/ServiceAccountPeopleAccessPoliciesResponseModel.cs similarity index 76% rename from src/Api/SecretsManager/Models/Response/ServiceAccountAccessPoliciesResponseModel.cs rename to src/Api/SecretsManager/Models/Response/ServiceAccountPeopleAccessPoliciesResponseModel.cs index 6f047cf88..899eb7dba 100644 --- a/src/Api/SecretsManager/Models/Response/ServiceAccountAccessPoliciesResponseModel.cs +++ b/src/Api/SecretsManager/Models/Response/ServiceAccountPeopleAccessPoliciesResponseModel.cs @@ -3,11 +3,11 @@ using Bit.Core.SecretsManager.Entities; namespace Bit.Api.SecretsManager.Models.Response; -public class ServiceAccountAccessPoliciesResponseModel : ResponseModel +public class ServiceAccountPeopleAccessPoliciesResponseModel : ResponseModel { private const string _objectName = "serviceAccountAccessPolicies"; - public ServiceAccountAccessPoliciesResponseModel(IEnumerable baseAccessPolicies) + public ServiceAccountPeopleAccessPoliciesResponseModel(IEnumerable baseAccessPolicies, Guid userId) : base(_objectName) { if (baseAccessPolicies == null) @@ -20,7 +20,7 @@ public class ServiceAccountAccessPoliciesResponseModel : ResponseModel switch (baseAccessPolicy) { case UserServiceAccountAccessPolicy accessPolicy: - UserAccessPolicies.Add(new UserServiceAccountAccessPolicyResponseModel(accessPolicy)); + UserAccessPolicies.Add(new UserServiceAccountAccessPolicyResponseModel(accessPolicy, userId)); break; case GroupServiceAccountAccessPolicy accessPolicy: GroupAccessPolicies.Add(new GroupServiceAccountAccessPolicyResponseModel(accessPolicy)); @@ -29,7 +29,7 @@ public class ServiceAccountAccessPoliciesResponseModel : ResponseModel } } - public ServiceAccountAccessPoliciesResponseModel() : base(_objectName) + public ServiceAccountPeopleAccessPoliciesResponseModel() : base(_objectName) { } diff --git a/src/Core/SecretsManager/AuthorizationRequirements/ServiceAccountPeopleAccessPoliciesOperationRequirement.cs b/src/Core/SecretsManager/AuthorizationRequirements/ServiceAccountPeopleAccessPoliciesOperationRequirement.cs new file mode 100644 index 000000000..6088f502c --- /dev/null +++ b/src/Core/SecretsManager/AuthorizationRequirements/ServiceAccountPeopleAccessPoliciesOperationRequirement.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Authorization.Infrastructure; + +namespace Bit.Core.SecretsManager.AuthorizationRequirements; + +public class ServiceAccountPeopleAccessPoliciesOperationRequirement : OperationAuthorizationRequirement +{ +} + +public static class ServiceAccountPeopleAccessPoliciesOperations +{ + public static readonly ServiceAccountPeopleAccessPoliciesOperationRequirement Replace = new() { Name = nameof(Replace) }; +} diff --git a/src/Core/SecretsManager/Models/Data/ServiceAccountPeopleAccessPolicies.cs b/src/Core/SecretsManager/Models/Data/ServiceAccountPeopleAccessPolicies.cs new file mode 100644 index 000000000..b0cd37d5c --- /dev/null +++ b/src/Core/SecretsManager/Models/Data/ServiceAccountPeopleAccessPolicies.cs @@ -0,0 +1,27 @@ +using Bit.Core.SecretsManager.Entities; + +namespace Bit.Core.SecretsManager.Models.Data; + +public class ServiceAccountPeopleAccessPolicies +{ + 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/Queries/AccessPolicies/Interfaces/ISameOrganizationQuery.cs b/src/Core/SecretsManager/Queries/AccessPolicies/Interfaces/ISameOrganizationQuery.cs new file mode 100644 index 000000000..94977f9fe --- /dev/null +++ b/src/Core/SecretsManager/Queries/AccessPolicies/Interfaces/ISameOrganizationQuery.cs @@ -0,0 +1,7 @@ +namespace Bit.Core.SecretsManager.Queries.AccessPolicies.Interfaces; + +public interface ISameOrganizationQuery +{ + Task OrgUsersInTheSameOrgAsync(List organizationUserIds, Guid organizationId); + Task GroupsInTheSameOrgAsync(List groupIds, Guid organizationId); +} diff --git a/src/Core/SecretsManager/Repositories/IAccessPolicyRepository.cs b/src/Core/SecretsManager/Repositories/IAccessPolicyRepository.cs index 233b67695..f40d847a1 100644 --- a/src/Core/SecretsManager/Repositories/IAccessPolicyRepository.cs +++ b/src/Core/SecretsManager/Repositories/IAccessPolicyRepository.cs @@ -11,7 +11,6 @@ public interface IAccessPolicyRepository Task AccessPolicyExists(BaseAccessPolicy baseAccessPolicy); Task GetByIdAsync(Guid id); Task> GetManyByGrantedProjectIdAsync(Guid id, Guid userId); - Task> GetManyByGrantedServiceAccountIdAsync(Guid id, Guid userId); Task> GetManyByServiceAccountIdAsync(Guid id, Guid userId, AccessClientType accessType); Task ReplaceAsync(BaseAccessPolicy baseAccessPolicy); @@ -19,4 +18,6 @@ public interface IAccessPolicyRepository Task> GetPeoplePoliciesByGrantedProjectIdAsync(Guid id, Guid userId); Task> ReplaceProjectPeopleAsync(ProjectPeopleAccessPolicies peopleAccessPolicies, Guid userId); Task GetPeopleGranteesAsync(Guid organizationId, Guid currentUserId); + Task> GetPeoplePoliciesByGrantedServiceAccountIdAsync(Guid id, Guid userId); + Task> ReplaceServiceAccountPeopleAsync(ServiceAccountPeopleAccessPolicies peopleAccessPolicies, Guid userId); } diff --git a/test/Api.IntegrationTest/SecretsManager/Controllers/AccessPoliciesControllerTests.cs b/test/Api.IntegrationTest/SecretsManager/Controllers/AccessPoliciesControllerTests.cs index c3050db65..b8eb4a770 100644 --- a/test/Api.IntegrationTest/SecretsManager/Controllers/AccessPoliciesControllerTests.cs +++ b/test/Api.IntegrationTest/SecretsManager/Controllers/AccessPoliciesControllerTests.cs @@ -627,228 +627,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 CreateServiceAccountAccessPolicies_SmAccessDenied_NotFound(bool useSecrets, bool accessSecrets, bool organizationEnabled) - { - var (org, orgUser) = await _organizationHelper.Initialize(useSecrets, accessSecrets, organizationEnabled); - await LoginAsync(_email); - - var serviceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount - { - OrganizationId = org.Id, - Name = _mockEncryptedString, - }); - - var request = new AccessPoliciesCreateRequest - { - UserAccessPolicyRequests = new List - { - new() { GranteeId = orgUser.Id, Read = true, Write = true }, - }, - }; - - var response = - await _client.PostAsJsonAsync($"/service-accounts/{serviceAccount.Id}/access-policies", request); - Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); - } - - [Theory] - [InlineData(PermissionType.RunAsAdmin)] - [InlineData(PermissionType.RunAsUserWithPermission)] - public async Task CreateServiceAccountAccessPolicies_MismatchOrgId_NotFound(PermissionType permissionType) - { - var (_, orgUser) = await _organizationHelper.Initialize(true, true, true); - await LoginAsync(_email); - var anotherOrg = await _organizationHelper.CreateSmOrganizationAsync(); - - var serviceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount - { - OrganizationId = anotherOrg.Id, - Name = _mockEncryptedString, - }); - var request = - await SetupUserServiceAccountAccessPolicyRequestAsync(permissionType, orgUser.Id, serviceAccount.Id); - - var response = - await _client.PostAsJsonAsync($"/service-accounts/{serviceAccount.Id}/access-policies", request); - Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); - } - - [Theory] - [InlineData(PermissionType.RunAsAdmin)] - [InlineData(PermissionType.RunAsUserWithPermission)] - public async Task CreateServiceAccountAccessPolicies_Success(PermissionType permissionType) - { - var (org, orgUser) = await _organizationHelper.Initialize(true, true, true); - await LoginAsync(_email); - var ownerOrgUserId = orgUser.Id; - - var serviceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount - { - OrganizationId = org.Id, - Name = _mockEncryptedString, - }); - var request = - await SetupUserServiceAccountAccessPolicyRequestAsync(permissionType, orgUser.Id, serviceAccount.Id); - - var response = - await _client.PostAsJsonAsync($"/service-accounts/{serviceAccount.Id}/access-policies", request); - response.EnsureSuccessStatusCode(); - - var result = await response.Content.ReadFromJsonAsync(); - - Assert.NotNull(result); - Assert.Equal(ownerOrgUserId, - result!.UserAccessPolicies.First(ap => ap.OrganizationUserId == ownerOrgUserId).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); - } - - [Fact] - public async Task CreateServiceAccountAccessPolicies_NoPermission() - { - // Create a new account as a user - var (org, _) = await _organizationHelper.Initialize(true, true, true); - var (email, orgUser) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true); - await LoginAsync(email); - - var serviceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount - { - OrganizationId = org.Id, - Name = _mockEncryptedString, - }); - - var request = new AccessPoliciesCreateRequest - { - UserAccessPolicyRequests = new List - { - new() { GranteeId = orgUser.Id, Read = true, Write = true }, - }, - }; - - var response = - await _client.PostAsJsonAsync($"/service-accounts/{serviceAccount.Id}/access-policies", request); - Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); - } - - [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 GetServiceAccountAccessPolicies_SmAccessDenied_NotFound(bool useSecrets, bool accessSecrets, bool organizationEnabled) - { - var (org, _) = await _organizationHelper.Initialize(useSecrets, accessSecrets, organizationEnabled); - await LoginAsync(_email); - var initData = await SetupAccessPolicyRequest(org.Id); - - var response = await _client.GetAsync($"/service-accounts/{initData.ServiceAccountId}/access-policies"); - Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); - } - - [Fact] - public async Task GetServiceAccountAccessPolicies_ReturnsEmpty() - { - var (org, _) = await _organizationHelper.Initialize(true, true, true); - await LoginAsync(_email); - - var serviceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount - { - OrganizationId = org.Id, - Name = _mockEncryptedString, - }); - - var response = await _client.GetAsync($"/service-accounts/{serviceAccount.Id}/access-policies"); - response.EnsureSuccessStatusCode(); - - var result = await response.Content.ReadFromJsonAsync(); - - Assert.NotNull(result); - Assert.Empty(result!.UserAccessPolicies); - Assert.Empty(result.GroupAccessPolicies); - } - - [Fact] - public async Task GetServiceAccountAccessPolicies_NoPermission() - { - // Create a new account as a user - await _organizationHelper.Initialize(true, true, true); - var (email, orgUser) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true); - await LoginAsync(email); - - var initData = await SetupAccessPolicyRequest(orgUser.OrganizationId); - - var response = await _client.GetAsync($"/service-accounts/{initData.ServiceAccountId}/access-policies"); - - Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); - } - - [Theory] - [InlineData(PermissionType.RunAsAdmin)] - [InlineData(PermissionType.RunAsUserWithPermission)] - public async Task GetServiceAccountAccessPolicies(PermissionType permissionType) - { - var (org, ownerOrgUser) = await _organizationHelper.Initialize(true, true, true); - await LoginAsync(_email); - var initData = await SetupAccessPolicyRequest(org.Id); - - if (permissionType == PermissionType.RunAsUserWithPermission) - { - var (email, orgUser) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true); - await LoginAsync(email); - var accessPolicies = new List - { - new UserServiceAccountAccessPolicy - { - GrantedServiceAccountId = initData.ServiceAccountId, - OrganizationUserId = orgUser.Id, - Read = true, - Write = true, - }, - }; - await _accessPolicyRepository.CreateManyAsync(accessPolicies); - } - - var policies = new List - { - new UserServiceAccountAccessPolicy - { - GrantedServiceAccountId = initData.ServiceAccountId, - OrganizationUserId = ownerOrgUser.Id, - Read = true, - Write = true, - }, - }; - await _accessPolicyRepository.CreateManyAsync(policies); - - var response = await _client.GetAsync($"/service-accounts/{initData.ServiceAccountId}/access-policies"); - response.EnsureSuccessStatusCode(); - - var result = await response.Content.ReadFromJsonAsync(); - - Assert.NotNull(result?.UserAccessPolicies); - Assert.NotEmpty(result!.UserAccessPolicies); - Assert.Equal(ownerOrgUser.Id, - result.UserAccessPolicies.First(x => x.OrganizationUserId == ownerOrgUser.Id).OrganizationUserId); - } - [Theory] [InlineData(false, false, false)] [InlineData(false, false, true)] @@ -1066,9 +844,13 @@ public class AccessPoliciesControllerTests : IClassFixture(); + + Assert.NotNull(result); + Assert.Empty(result!.UserAccessPolicies); + Assert.Empty(result.GroupAccessPolicies); + } + + [Fact] + public async Task GetServiceAccountPeopleAccessPolicies_NoPermission() + { + var (org, _) = await _organizationHelper.Initialize(true, true, true); + var (email, _) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true); + await LoginAsync(email); + + var serviceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount + { + OrganizationId = org.Id, + Name = _mockEncryptedString, + }); + + var response = await _client.GetAsync($"/service-accounts/{serviceAccount.Id}/access-policies/people"); + + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Theory] + [InlineData(PermissionType.RunAsAdmin)] + [InlineData(PermissionType.RunAsUserWithPermission)] + public async Task GetServiceAccountPeopleAccessPolicies_Success(PermissionType permissionType) + { + var (_, organizationUser) = await _organizationHelper.Initialize(true, true, true); + await LoginAsync(_email); + + var (serviceAccount, _) = await SetupServiceAccountPeoplePermissionAsync(permissionType, organizationUser); + + var response = await _client.GetAsync($"/service-accounts/{serviceAccount.Id}/access-policies/people"); + response.EnsureSuccessStatusCode(); + + var result = await response.Content.ReadFromJsonAsync(); + + Assert.NotNull(result?.UserAccessPolicies); + Assert.Single(result!.UserAccessPolicies); + } + + [Theory] + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(false, true)] + public async Task PutServiceAccountPeopleAccessPolicies_SmNotEnabled_NotFound(bool useSecrets, bool accessSecrets) + { + var (_, organizationUser) = await _organizationHelper.Initialize(useSecrets, accessSecrets, true); + await LoginAsync(_email); + + var (serviceAccount, request) = await SetupServiceAccountPeopleRequestAsync(PermissionType.RunAsAdmin, organizationUser); + + var response = await _client.PutAsJsonAsync($"/service-accounts/{serviceAccount.Id}/access-policies/people", request); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Fact] + public async Task PutServiceAccountPeopleAccessPolicies_NoPermission() + { + var (org, _) = await _organizationHelper.Initialize(true, true, true); + var (email, organizationUser) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true); + await LoginAsync(email); + + var serviceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount + { + 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($"/service-accounts/{serviceAccount.Id}/access-policies/people", request); + + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Theory] + [InlineData(PermissionType.RunAsAdmin)] + [InlineData(PermissionType.RunAsUserWithPermission)] + public async Task PutServiceAccountPeopleAccessPolicies_MismatchedOrgIds_NotFound(PermissionType permissionType) + { + var (_, organizationUser) = await _organizationHelper.Initialize(true, true, true); + await LoginAsync(_email); + + var (serviceAccount, request) = await SetupServiceAccountPeopleRequestAsync(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($"/service-accounts/{serviceAccount.Id}/access-policies/people", request); + + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Theory] + [InlineData(PermissionType.RunAsAdmin)] + [InlineData(PermissionType.RunAsUserWithPermission)] + public async Task PutServiceAccountPeopleAccessPolicies_Success(PermissionType permissionType) + { + var (_, organizationUser) = await _organizationHelper.Initialize(true, true, true); + await LoginAsync(_email); + + var (serviceAccount, request) = await SetupServiceAccountPeopleRequestAsync(permissionType, organizationUser); + + var response = await _client.PutAsJsonAsync($"/service-accounts/{serviceAccount.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 @@ -1293,6 +1252,38 @@ public class AccessPoliciesControllerTests : IClassFixture SetupServiceAccountPeoplePermissionAsync( + PermissionType permissionType, + OrganizationUser organizationUser) + { + var serviceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount + { + 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 UserServiceAccountAccessPolicy + { + GrantedServiceAccountId = serviceAccount.Id, + OrganizationUserId = organizationUser.Id, + Read = true, + Write = true + } + }; + await _accessPolicyRepository.CreateManyAsync(accessPolicies); + + return (serviceAccount, organizationUser); + } + private async Task<(Project project, PeopleAccessPoliciesRequestModel request)> SetupProjectPeopleRequestAsync( PermissionType permissionType, OrganizationUser organizationUser) { @@ -1307,6 +1298,20 @@ public class AccessPoliciesControllerTests : IClassFixture SetupServiceAccountPeopleRequestAsync( + PermissionType permissionType, OrganizationUser organizationUser) + { + var (serviceAccount, currentUser) = await SetupServiceAccountPeoplePermissionAsync(permissionType, organizationUser); + var request = new PeopleAccessPoliciesRequestModel + { + UserAccessPolicyRequests = new List + { + new() { GranteeId = currentUser.Id, Read = true, Write = true } + } + }; + return (serviceAccount, request); + } + private async Task<(Guid ProjectId, Guid ServiceAccountId)> CreateProjectAndServiceAccountAsync(Guid organizationId, bool misMatchOrganization = false) { diff --git a/test/Api.IntegrationTest/SecretsManager/Controllers/ServiceAccountsControllerTests.cs b/test/Api.IntegrationTest/SecretsManager/Controllers/ServiceAccountsControllerTests.cs index bfda63adf..a482d9b04 100644 --- a/test/Api.IntegrationTest/SecretsManager/Controllers/ServiceAccountsControllerTests.cs +++ b/test/Api.IntegrationTest/SecretsManager/Controllers/ServiceAccountsControllerTests.cs @@ -248,7 +248,7 @@ public class ServiceAccountsControllerTests : IClassFixture sutProvider, Guid organizationId) { sutProvider.GetDependency().AccessSecretsManager(default).ReturnsForAnyArgs(true); @@ -103,12 +120,14 @@ public class AccessPoliciesControllerTests { case PermissionType.RunAsAdmin: SetupAdmin(sutProvider, data.OrganizationId); - sutProvider.GetDependency().AccessToProjectAsync(Arg.Any(), Arg.Any(), AccessClientType.NoAccessCheck) + 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) + sutProvider.GetDependency() + .AccessToProjectAsync(Arg.Any(), Arg.Any(), AccessClientType.User) .Returns((true, true)); break; } @@ -156,12 +175,14 @@ public class AccessPoliciesControllerTests { case PermissionType.RunAsAdmin: SetupAdmin(sutProvider, data.OrganizationId); - sutProvider.GetDependency().AccessToProjectAsync(Arg.Any(), Arg.Any(), AccessClientType.NoAccessCheck) + 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) + sutProvider.GetDependency() + .AccessToProjectAsync(Arg.Any(), Arg.Any(), AccessClientType.User) .Returns((true, true)); break; } @@ -201,114 +222,6 @@ public class AccessPoliciesControllerTests .GetManyByGrantedProjectIdAsync(Arg.Any(), Arg.Any()); } - [Theory] - [BitAutoData(PermissionType.RunAsAdmin)] - [BitAutoData(PermissionType.RunAsUserWithPermission)] - public async void GetServiceAccountAccessPolicies_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.GetServiceAccountAccessPoliciesAsync(id); - - await sutProvider.GetDependency().Received(1) - .GetManyByGrantedServiceAccountIdAsync(Arg.Is(AssertHelper.AssertPropertyEqual(id)), Arg.Any()); - - Assert.Empty(result.UserAccessPolicies); - Assert.Empty(result.GroupAccessPolicies); - } - - [Theory] - [BitAutoData] - public async void GetServiceAccountAccessPolicies_UserWithoutPermission_Throws( - SutProvider sutProvider, - Guid id, - ServiceAccount data) - { - SetupUserWithoutPermission(sutProvider, data.OrganizationId); - sutProvider.GetDependency().GetByIdAsync(default).ReturnsForAnyArgs(data); - sutProvider.GetDependency().UserHasWriteAccessToServiceAccount(default, default) - .ReturnsForAnyArgs(false); - - await Assert.ThrowsAsync(() => sutProvider.Sut.GetServiceAccountAccessPoliciesAsync(id)); - - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() - .GetManyByGrantedServiceAccountIdAsync(Arg.Any(), Arg.Any()); - } - - [Theory] - [BitAutoData(PermissionType.RunAsAdmin)] - [BitAutoData(PermissionType.RunAsUserWithPermission)] - public async void GetServiceAccountAccessPolicies_Success( - PermissionType permissionType, - SutProvider sutProvider, - Guid id, - ServiceAccount data, - UserServiceAccountAccessPolicy resultAccessPolicy) - { - sutProvider.GetDependency().GetByIdAsync(default).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; - } - - sutProvider.GetDependency().GetManyByGrantedServiceAccountIdAsync(default, default) - .ReturnsForAnyArgs(new List { resultAccessPolicy }); - - var result = await sutProvider.Sut.GetServiceAccountAccessPoliciesAsync(id); - - await sutProvider.GetDependency().Received(1) - .GetManyByGrantedServiceAccountIdAsync(Arg.Is(AssertHelper.AssertPropertyEqual(id)), Arg.Any()); - - Assert.Empty(result.GroupAccessPolicies); - Assert.NotEmpty(result.UserAccessPolicies); - } - - [Theory] - [BitAutoData] - public async void GetServiceAccountAccessPolicies_ServiceAccountExists_UserWithoutPermission_Throws( - SutProvider sutProvider, - Guid id, - ServiceAccount data, - UserServiceAccountAccessPolicy resultAccessPolicy) - { - SetupUserWithoutPermission(sutProvider, data.OrganizationId); - sutProvider.GetDependency().GetByIdAsync(default).ReturnsForAnyArgs(data); - sutProvider.GetDependency().UserHasWriteAccessToServiceAccount(default, default) - .ReturnsForAnyArgs(false); - - sutProvider.GetDependency().GetManyByGrantedServiceAccountIdAsync(default, default) - .ReturnsForAnyArgs(new List { resultAccessPolicy }); - - await Assert.ThrowsAsync(() => sutProvider.Sut.GetServiceAccountAccessPoliciesAsync(id)); - - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() - .GetManyByGrantedServiceAccountIdAsync(Arg.Any(), Arg.Any()); - } - [Theory] [BitAutoData(PermissionType.RunAsAdmin)] [BitAutoData(PermissionType.RunAsUserWithPermission)] @@ -419,12 +332,12 @@ public class AccessPoliciesControllerTests UserProjectAccessPolicy data, AccessPoliciesCreateRequest request) { - var dup = new AccessPolicyRequest() { GranteeId = Guid.NewGuid(), Read = true, Write = true }; + var dup = new AccessPolicyRequest { GranteeId = Guid.NewGuid(), Read = true, Write = true }; request.UserAccessPolicyRequests = new[] { dup, dup }; mockProject.Id = id; sutProvider.GetDependency().GetByIdAsync(default).ReturnsForAnyArgs(mockProject); sutProvider.GetDependency().CreateManyAsync(default) - .ReturnsForAnyArgs(new List { data }); + .ReturnsForAnyArgs(new List { data }); await Assert.ThrowsAsync(() => sutProvider.Sut.CreateProjectAccessPoliciesAsync(id, request)); @@ -451,6 +364,7 @@ public class AccessPoliciesControllerTests .AuthorizeAsync(Arg.Any(), policy, Arg.Any>()).ReturnsForAnyArgs(AuthorizationResult.Failed()); } + sutProvider.GetDependency().CreateManyAsync(default) .ReturnsForAnyArgs(new List { data }); @@ -479,6 +393,7 @@ public class AccessPoliciesControllerTests .AuthorizeAsync(Arg.Any(), policy, Arg.Any>()).ReturnsForAnyArgs(AuthorizationResult.Success()); } + sutProvider.GetDependency().CreateManyAsync(default) .ReturnsForAnyArgs(new List { data }); @@ -488,124 +403,6 @@ public class AccessPoliciesControllerTests .CreateManyAsync(Arg.Any>()); } - [Theory] - [BitAutoData] - public async void CreateServiceAccountAccessPolicies_RequestMoreThanMax_Throws( - SutProvider sutProvider, - Guid id, - ServiceAccount serviceAccount, - UserServiceAccountAccessPolicy data, - AccessPoliciesCreateRequest request) - { - sutProvider.GetDependency().GetByIdAsync(default).ReturnsForAnyArgs(serviceAccount); - sutProvider.GetDependency() - .CreateManyAsync(default) - .ReturnsForAnyArgs(new List { data }); - - request = AddRequestsOverMax(request); - - await Assert.ThrowsAsync(() => - sutProvider.Sut.CreateServiceAccountAccessPoliciesAsync(id, request)); - - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() - .CreateManyAsync(Arg.Any>()); - } - - [Theory] - [BitAutoData] - public async void CreateServiceAccountAccessPolicies_ServiceAccountDoesNotExist_Throws( - SutProvider sutProvider, - Guid id, - AccessPoliciesCreateRequest request) - { - await Assert.ThrowsAsync(() => - sutProvider.Sut.CreateServiceAccountAccessPoliciesAsync(id, request)); - - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() - .CreateManyAsync(Arg.Any>()); - } - - [Theory] - [BitAutoData] - public async void CreateServiceAccountAccessPolicies_DuplicatePolicy_Throws( - SutProvider sutProvider, - Guid id, - ServiceAccount serviceAccount, - UserServiceAccountAccessPolicy data, - AccessPoliciesCreateRequest request) - { - var dup = new AccessPolicyRequest() { GranteeId = Guid.NewGuid(), Read = true, Write = true }; - request.UserAccessPolicyRequests = new[] { dup, dup }; - sutProvider.GetDependency().GetByIdAsync(default).ReturnsForAnyArgs(serviceAccount); - - sutProvider.GetDependency() - .CreateManyAsync(default) - .ReturnsForAnyArgs(new List { data }); - - await Assert.ThrowsAsync(() => - sutProvider.Sut.CreateServiceAccountAccessPoliciesAsync(id, request)); - - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() - .CreateManyAsync(Arg.Any>()); - } - - [Theory] - [BitAutoData] - public async void CreateServiceAccountAccessPolicies_NoAccess_Throws( - SutProvider sutProvider, - Guid id, - ServiceAccount serviceAccount, - UserServiceAccountAccessPolicy data, - AccessPoliciesCreateRequest request) - { - sutProvider.GetDependency().GetByIdAsync(default).ReturnsForAnyArgs(serviceAccount); - var policies = request.ToBaseAccessPoliciesForServiceAccount(id, serviceAccount.OrganizationId); - foreach (var policy in policies) - { - sutProvider.GetDependency() - .AuthorizeAsync(Arg.Any(), policy, - Arg.Any>()).ReturnsForAnyArgs(AuthorizationResult.Failed()); - } - - sutProvider.GetDependency() - .CreateManyAsync(default) - .ReturnsForAnyArgs(new List { data }); - - await Assert.ThrowsAsync(() => - sutProvider.Sut.CreateServiceAccountAccessPoliciesAsync(id, request)); - - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() - .CreateManyAsync(Arg.Any>()); - } - - [Theory] - [BitAutoData] - public async void CreateServiceAccountAccessPolicies_Success( - SutProvider sutProvider, - Guid id, - ServiceAccount serviceAccount, - UserServiceAccountAccessPolicy data, - AccessPoliciesCreateRequest request) - { - sutProvider.GetDependency().GetByIdAsync(default).ReturnsForAnyArgs(serviceAccount); - var policies = request.ToBaseAccessPoliciesForServiceAccount(id, serviceAccount.OrganizationId); - foreach (var policy in policies) - { - sutProvider.GetDependency() - .AuthorizeAsync(Arg.Any(), policy, - Arg.Any>()).ReturnsForAnyArgs(AuthorizationResult.Success()); - } - - sutProvider.GetDependency() - .CreateManyAsync(default) - .ReturnsForAnyArgs(new List { data }); - - await sutProvider.Sut.CreateServiceAccountAccessPoliciesAsync(id, request); - - await sutProvider.GetDependency().Received(1) - .CreateManyAsync(Arg.Any>()); - } - [Theory] [BitAutoData] public async void CreateServiceAccountGrantedPolicies_RequestMoreThanMax_Throws( @@ -652,7 +449,7 @@ public class AccessPoliciesControllerTests ServiceAccountProjectAccessPolicy data, List request) { - var dup = new GrantedAccessPolicyRequest() { GrantedId = Guid.NewGuid(), Read = true, Write = true }; + var dup = new GrantedAccessPolicyRequest { GrantedId = Guid.NewGuid(), Read = true, Write = true }; request.Add(dup); request.Add(dup); sutProvider.GetDependency().GetByIdAsync(default).ReturnsForAnyArgs(serviceAccount); @@ -1173,4 +970,199 @@ public class AccessPoliciesControllerTests await sutProvider.GetDependency().Received(1) .ReplaceProjectPeopleAsync(Arg.Any(), Arg.Any()); } + + [Theory] + [BitAutoData] + public async void GetServiceAccountPeopleAccessPoliciesAsync_ServiceAccountDoesntExist_ThrowsNotFound( + SutProvider sutProvider, + ServiceAccount data) + { + sutProvider.GetDependency().GetByIdAsync(data.Id).ReturnsNull(); + + await Assert.ThrowsAsync(() => + sutProvider.Sut.GetServiceAccountPeopleAccessPoliciesAsync(data.Id)); + + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .GetPeoplePoliciesByGrantedServiceAccountIdAsync(Arg.Any(), Arg.Any()); + } + + [Theory] + [BitAutoData(PermissionType.RunAsAdmin)] + [BitAutoData(PermissionType.RunAsUserWithPermission)] + public async void GetServiceAccountPeopleAccessPoliciesAsync_ReturnsEmptyList( + PermissionType permissionType, + SutProvider sutProvider, + 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.GetServiceAccountPeopleAccessPoliciesAsync(data.Id); + + await sutProvider.GetDependency().Received(1) + .GetPeoplePoliciesByGrantedServiceAccountIdAsync(Arg.Is(AssertHelper.AssertPropertyEqual(data.Id)), Arg.Any()); + + Assert.Empty(result.UserAccessPolicies); + Assert.Empty(result.GroupAccessPolicies); + } + + [Theory] + [BitAutoData] + public async void GetServiceAccountPeopleAccessPoliciesAsync_UserWithoutPermission_Throws( + SutProvider sutProvider, + ServiceAccount data) + { + SetupUserWithoutPermission(sutProvider, data.OrganizationId); + sutProvider.GetDependency().GetByIdAsync(default).ReturnsForAnyArgs(data); + sutProvider.GetDependency().UserHasWriteAccessToServiceAccount(default, default) + .ReturnsForAnyArgs(false); + + await Assert.ThrowsAsync(() => + sutProvider.Sut.GetServiceAccountPeopleAccessPoliciesAsync(data.Id)); + + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .GetPeoplePoliciesByGrantedServiceAccountIdAsync(Arg.Any(), Arg.Any()); + } + + [Theory] + [BitAutoData(PermissionType.RunAsAdmin)] + [BitAutoData(PermissionType.RunAsUserWithPermission)] + public async void GetServiceAccountPeopleAccessPoliciesAsync_Success( + PermissionType permissionType, + SutProvider sutProvider, + ServiceAccount data, + UserServiceAccountAccessPolicy resultAccessPolicy) + { + sutProvider.GetDependency().GetByIdAsync(default).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; + } + + sutProvider.GetDependency().GetPeoplePoliciesByGrantedServiceAccountIdAsync(default, default) + .ReturnsForAnyArgs(new List { resultAccessPolicy }); + + var result = await sutProvider.Sut.GetServiceAccountPeopleAccessPoliciesAsync(data.Id); + + await sutProvider.GetDependency().Received(1) + .GetPeoplePoliciesByGrantedServiceAccountIdAsync(Arg.Is(AssertHelper.AssertPropertyEqual(data.Id)), Arg.Any()); + + Assert.Empty(result.GroupAccessPolicies); + Assert.NotEmpty(result.UserAccessPolicies); + } + + [Theory] + [BitAutoData] + public async void PutServiceAccountPeopleAccessPolicies_ServiceAccountDoesNotExist_Throws( + SutProvider sutProvider, + ServiceAccount data, + PeopleAccessPoliciesRequestModel request) + { + await Assert.ThrowsAsync(() => + sutProvider.Sut.PutServiceAccountPeopleAccessPoliciesAsync(data.Id, request)); + + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .ReplaceServiceAccountPeopleAsync(Arg.Any(), Arg.Any()); + } + + [Theory] + [BitAutoData] + public async void PutServiceAccountPeopleAccessPolicies_DuplicatePolicy_Throws( + SutProvider sutProvider, + ServiceAccount data, + PeopleAccessPoliciesRequestModel request) + { + var dup = new AccessPolicyRequest { GranteeId = Guid.NewGuid(), Read = true, Write = true }; + request.UserAccessPolicyRequests = new[] { dup, dup }; + sutProvider.GetDependency().GetByIdAsync(data.Id).ReturnsForAnyArgs(data); + + await Assert.ThrowsAsync(() => + sutProvider.Sut.PutServiceAccountPeopleAccessPoliciesAsync(data.Id, request)); + + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .ReplaceServiceAccountPeopleAsync(Arg.Any(), Arg.Any()); + } + + [Theory] + [BitAutoData] + public async void PutServiceAccountPeopleAccessPolicies_NotCanReadWrite_Throws( + SutProvider sutProvider, + ServiceAccount data, + PeopleAccessPoliciesRequestModel request) + { + request.UserAccessPolicyRequests.First().Read = false; + sutProvider.GetDependency().GetByIdAsync(data.Id).ReturnsForAnyArgs(data); + + await Assert.ThrowsAsync(() => + sutProvider.Sut.PutServiceAccountPeopleAccessPoliciesAsync(data.Id, request)); + + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .ReplaceServiceAccountPeopleAsync(Arg.Any(), Arg.Any()); + } + + [Theory] + [BitAutoData] + public async void PutServiceAccountPeopleAccessPolicies_NoAccess_Throws( + SutProvider sutProvider, + ServiceAccount data, + PeopleAccessPoliciesRequestModel request) + { + request = SetRequestToCanReadWrite(request); + sutProvider.GetDependency().GetByIdAsync(data.Id).ReturnsForAnyArgs(data); + var peoplePolicies = request.ToServiceAccountPeopleAccessPolicies(data.Id, data.OrganizationId); + sutProvider.GetDependency() + .AuthorizeAsync(Arg.Any(), peoplePolicies, + Arg.Any>()).ReturnsForAnyArgs(AuthorizationResult.Failed()); + + await Assert.ThrowsAsync(() => + sutProvider.Sut.PutServiceAccountPeopleAccessPoliciesAsync(data.Id, request)); + + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .ReplaceServiceAccountPeopleAsync(Arg.Any(), Arg.Any()); + } + + [Theory] + [BitAutoData] + public async void PutServiceAccountPeopleAccessPolicies_Success( + SutProvider sutProvider, + ServiceAccount data, + Guid userId, + PeopleAccessPoliciesRequestModel request) + { + request = SetRequestToCanReadWrite(request); + sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId); + sutProvider.GetDependency().GetByIdAsync(data.Id).ReturnsForAnyArgs(data); + var peoplePolicies = request.ToServiceAccountPeopleAccessPolicies(data.Id, data.OrganizationId); + sutProvider.GetDependency() + .AuthorizeAsync(Arg.Any(), peoplePolicies, + Arg.Any>()).ReturnsForAnyArgs(AuthorizationResult.Success()); + + sutProvider.GetDependency().ReplaceServiceAccountPeopleAsync(peoplePolicies, Arg.Any()) + .Returns(peoplePolicies.ToBaseAccessPolicies()); + + await sutProvider.Sut.PutServiceAccountPeopleAccessPoliciesAsync(data.Id, request); + + await sutProvider.GetDependency().Received(1) + .ReplaceServiceAccountPeopleAsync(Arg.Any(), Arg.Any()); + } }