From cf669286eda971e060a52b1852ba910df7e34a87 Mon Sep 17 00:00:00 2001 From: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> Date: Mon, 6 Feb 2023 11:26:06 -0600 Subject: [PATCH] [SM-429] Add permission checks to access policy endpoints (#2628) * Add permission checks to access policy endpoints * Fix unit tests * Add service account grant permission checks * Add service account grant tests * Add new endpoint unit tests * Cleanup unit tests add integration tests * User permission enum in create tests * Swap to NotFoundException for access checks * Add filter for potential grantees * Add in AccessSecretsManager check and test it * Add code review updates * Code review updates * Refactor potential grantees endpoint * Code review updates --- .../CreateAccessPoliciesCommand.cs | 46 +- .../DeleteAccessPolicyCommand.cs | 88 +++- .../UpdateAccessPolicyCommand.cs | 87 +++- .../Repositories/AccessPolicyRepository.cs | 7 + .../Repositories/ServiceAccountRepository.cs | 17 + .../CreateAccessPoliciesCommandTests.cs | 135 ++++- .../DeleteAccessPolicyCommandTests.cs | 204 +++++++- .../UpdateAccessPolicyCommandTests.cs | 236 ++++++++- .../SecretsManager/Enums/AccessPolicyType.cs | 11 + .../SecretsManager/Enums/PermissionType.cs | 7 + .../Controllers/AccessPoliciesController.cs | 108 +++- .../Response/PotentialGranteeResponseModel.cs | 62 +++ .../ICreateAccessPoliciesCommand.cs | 2 +- .../Interfaces/IDeleteAccessPolicyCommand.cs | 2 +- .../Interfaces/IUpdateAccessPolicyCommand.cs | 2 +- .../SecretsManager/Entities/AccessPolicy.cs | 15 +- .../Repositories/IServiceAccountRepository.cs | 1 + .../AccessPoliciesControllerTest.cs | 484 +++++++++++++++--- .../SecretsManager/Enums/PermissionType.cs | 7 + .../AccessPoliciesControllerTests.cs | 335 +++++++++++- 20 files changed, 1710 insertions(+), 146 deletions(-) create mode 100644 bitwarden_license/test/Commercial.Core.Test/SecretsManager/Enums/AccessPolicyType.cs create mode 100644 bitwarden_license/test/Commercial.Core.Test/SecretsManager/Enums/PermissionType.cs create mode 100644 src/Api/SecretsManager/Models/Response/PotentialGranteeResponseModel.cs create mode 100644 test/Api.IntegrationTest/SecretsManager/Enums/PermissionType.cs diff --git a/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/AccessPolicies/CreateAccessPoliciesCommand.cs b/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/AccessPolicies/CreateAccessPoliciesCommand.cs index 05af497e6..2b003aae4 100644 --- a/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/AccessPolicies/CreateAccessPoliciesCommand.cs +++ b/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/AccessPolicies/CreateAccessPoliciesCommand.cs @@ -1,4 +1,6 @@ -using Bit.Core.Exceptions; +using Bit.Core.Context; +using Bit.Core.Enums; +using Bit.Core.Exceptions; using Bit.Core.SecretsManager.Commands.AccessPolicies.Interfaces; using Bit.Core.SecretsManager.Entities; using Bit.Core.SecretsManager.Repositories; @@ -8,22 +10,52 @@ namespace Bit.Commercial.Core.SecretsManager.Commands.AccessPolicies; public class CreateAccessPoliciesCommand : ICreateAccessPoliciesCommand { private readonly IAccessPolicyRepository _accessPolicyRepository; + private readonly ICurrentContext _currentContext; + private readonly IProjectRepository _projectRepository; - public CreateAccessPoliciesCommand(IAccessPolicyRepository accessPolicyRepository) + public CreateAccessPoliciesCommand( + IAccessPolicyRepository accessPolicyRepository, + ICurrentContext currentContext, + IProjectRepository projectRepository) { + _projectRepository = projectRepository; _accessPolicyRepository = accessPolicyRepository; + _currentContext = currentContext; } - public async Task> CreateAsync(List accessPolicies) + public async Task> CreateForProjectAsync(Guid projectId, + List accessPolicies, Guid userId) { + var project = await _projectRepository.GetByIdAsync(projectId); + if (project == null || !_currentContext.AccessSecretsManager(project.OrganizationId)) + { + throw new NotFoundException(); + } + + var orgAdmin = await _currentContext.OrganizationAdmin(project.OrganizationId); + var accessClient = AccessClientHelper.ToAccessClient(_currentContext.ClientType, orgAdmin); + + var hasAccess = accessClient switch + { + AccessClientType.NoAccessCheck => true, + AccessClientType.User => await _projectRepository.UserHasWriteAccessToProject(project.Id, userId), + _ => false, + }; + + if (!hasAccess) + { + throw new NotFoundException(); + } + 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), - _ => throw new ArgumentException("Unsupported access policy type provided.", nameof(baseAccessPolicy)) + ServiceAccountProjectAccessPolicy ap => new Tuple(ap.ServiceAccountId, + ap.GrantedProjectId), + _ => throw new ArgumentException("Unsupported access policy type provided.", nameof(baseAccessPolicy)), }; }).ToList(); @@ -39,7 +71,7 @@ public class CreateAccessPoliciesCommand : ICreateAccessPoliciesCommand throw new BadRequestException("Resource already exists"); } } - - return await _accessPolicyRepository.CreateManyAsync(accessPolicies); + await _accessPolicyRepository.CreateManyAsync(accessPolicies); + return await _accessPolicyRepository.GetManyByGrantedProjectIdAsync(projectId); } } diff --git a/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/AccessPolicies/DeleteAccessPolicyCommand.cs b/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/AccessPolicies/DeleteAccessPolicyCommand.cs index 0795d6290..ad5b3da14 100644 --- a/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/AccessPolicies/DeleteAccessPolicyCommand.cs +++ b/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/AccessPolicies/DeleteAccessPolicyCommand.cs @@ -1,5 +1,8 @@ -using Bit.Core.Exceptions; +using Bit.Core.Context; +using Bit.Core.Enums; +using Bit.Core.Exceptions; using Bit.Core.SecretsManager.Commands.AccessPolicies.Interfaces; +using Bit.Core.SecretsManager.Entities; using Bit.Core.SecretsManager.Repositories; namespace Bit.Commercial.Core.SecretsManager.Commands.AccessPolicies; @@ -7,14 +10,23 @@ namespace Bit.Commercial.Core.SecretsManager.Commands.AccessPolicies; public class DeleteAccessPolicyCommand : IDeleteAccessPolicyCommand { private readonly IAccessPolicyRepository _accessPolicyRepository; + private readonly ICurrentContext _currentContext; + private readonly IProjectRepository _projectRepository; + private readonly IServiceAccountRepository _serviceAccountRepository; - public DeleteAccessPolicyCommand(IAccessPolicyRepository accessPolicyRepository) + public DeleteAccessPolicyCommand( + IAccessPolicyRepository accessPolicyRepository, + ICurrentContext currentContext, + IProjectRepository projectRepository, + IServiceAccountRepository serviceAccountRepository) { + _projectRepository = projectRepository; + _serviceAccountRepository = serviceAccountRepository; _accessPolicyRepository = accessPolicyRepository; + _currentContext = currentContext; } - - public async Task DeleteAsync(Guid id) + public async Task DeleteAsync(Guid id, Guid userId) { var accessPolicy = await _accessPolicyRepository.GetByIdAsync(id); if (accessPolicy == null) @@ -22,6 +34,74 @@ public class DeleteAccessPolicyCommand : IDeleteAccessPolicyCommand throw new NotFoundException(); } + if (!await IsAllowedToDeleteAsync(accessPolicy, userId)) + { + throw new NotFoundException(); + } + await _accessPolicyRepository.DeleteAsync(id); } + + private async Task IsAllowedToDeleteAsync(BaseAccessPolicy baseAccessPolicy, Guid userId) => + baseAccessPolicy switch + { + UserProjectAccessPolicy ap => await HasPermissionAsync(ap.GrantedProject!.OrganizationId, userId, + ap.GrantedProjectId), + GroupProjectAccessPolicy ap => await HasPermissionAsync(ap.GrantedProject!.OrganizationId, userId, + ap.GrantedProjectId), + ServiceAccountProjectAccessPolicy ap => await HasPermissionAsync(ap.GrantedProject!.OrganizationId, + userId, ap.GrantedProjectId), + UserServiceAccountAccessPolicy ap => await HasPermissionAsync( + ap.GrantedServiceAccount!.OrganizationId, + userId, serviceAccountIdToCheck: ap.GrantedServiceAccountId), + GroupServiceAccountAccessPolicy ap => await HasPermissionAsync( + ap.GrantedServiceAccount!.OrganizationId, + userId, serviceAccountIdToCheck: ap.GrantedServiceAccountId), + _ => throw new ArgumentException("Unsupported access policy type provided."), + }; + + private async Task HasPermissionAsync( + Guid organizationId, + Guid userId, + Guid? projectIdToCheck = null, + Guid? serviceAccountIdToCheck = null) + { + if (!_currentContext.AccessSecretsManager(organizationId)) + { + return false; + } + + var orgAdmin = await _currentContext.OrganizationAdmin(organizationId); + var accessClient = AccessClientHelper.ToAccessClient(_currentContext.ClientType, orgAdmin); + + bool hasAccess; + switch (accessClient) + { + case AccessClientType.NoAccessCheck: + hasAccess = true; + break; + case AccessClientType.User: + if (projectIdToCheck.HasValue) + { + hasAccess = await _projectRepository.UserHasWriteAccessToProject(projectIdToCheck.Value, userId); + } + else if (serviceAccountIdToCheck.HasValue) + { + hasAccess = + await _serviceAccountRepository.UserHasWriteAccessToServiceAccount( + serviceAccountIdToCheck.Value, userId); + } + else + { + throw new ArgumentException("No ID to check provided."); + } + + break; + default: + hasAccess = false; + break; + } + + return hasAccess; + } } diff --git a/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/AccessPolicies/UpdateAccessPolicyCommand.cs b/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/AccessPolicies/UpdateAccessPolicyCommand.cs index b7ebd1706..bf83b735c 100644 --- a/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/AccessPolicies/UpdateAccessPolicyCommand.cs +++ b/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/AccessPolicies/UpdateAccessPolicyCommand.cs @@ -1,4 +1,6 @@ -using Bit.Core.Exceptions; +using Bit.Core.Context; +using Bit.Core.Enums; +using Bit.Core.Exceptions; using Bit.Core.SecretsManager.Commands.AccessPolicies.Interfaces; using Bit.Core.SecretsManager.Entities; using Bit.Core.SecretsManager.Repositories; @@ -8,13 +10,23 @@ namespace Bit.Commercial.Core.SecretsManager.Commands.AccessPolicies; public class UpdateAccessPolicyCommand : IUpdateAccessPolicyCommand { private readonly IAccessPolicyRepository _accessPolicyRepository; + private readonly ICurrentContext _currentContext; + private readonly IProjectRepository _projectRepository; + private readonly IServiceAccountRepository _serviceAccountRepository; - public UpdateAccessPolicyCommand(IAccessPolicyRepository accessPolicyRepository) + public UpdateAccessPolicyCommand( + IAccessPolicyRepository accessPolicyRepository, + ICurrentContext currentContext, + IProjectRepository projectRepository, + IServiceAccountRepository serviceAccountRepository) { _accessPolicyRepository = accessPolicyRepository; + _currentContext = currentContext; + _projectRepository = projectRepository; + _serviceAccountRepository = serviceAccountRepository; } - public async Task UpdateAsync(Guid id, bool read, bool write) + public async Task UpdateAsync(Guid id, bool read, bool write, Guid userId) { var accessPolicy = await _accessPolicyRepository.GetByIdAsync(id); if (accessPolicy == null) @@ -22,11 +34,78 @@ public class UpdateAccessPolicyCommand : IUpdateAccessPolicyCommand throw new NotFoundException(); } + if (!await IsAllowedToUpdateAsync(accessPolicy, userId)) + { + throw new NotFoundException(); + } + accessPolicy.Read = read; accessPolicy.Write = write; accessPolicy.RevisionDate = DateTime.UtcNow; - await _accessPolicyRepository.ReplaceAsync(accessPolicy); return accessPolicy; } + + private async Task IsAllowedToUpdateAsync(BaseAccessPolicy baseAccessPolicy, Guid userId) => + baseAccessPolicy switch + { + UserProjectAccessPolicy ap => await HasPermissionsAsync(ap.GrantedProject!.OrganizationId, userId, + ap.GrantedProjectId), + GroupProjectAccessPolicy ap => await HasPermissionsAsync(ap.GrantedProject!.OrganizationId, userId, + ap.GrantedProjectId), + ServiceAccountProjectAccessPolicy ap => await HasPermissionsAsync(ap.GrantedProject!.OrganizationId, + userId, ap.GrantedProjectId), + UserServiceAccountAccessPolicy ap => await HasPermissionsAsync( + ap.GrantedServiceAccount!.OrganizationId, + userId, serviceAccountIdToCheck: ap.GrantedServiceAccountId), + GroupServiceAccountAccessPolicy ap => await HasPermissionsAsync( + ap.GrantedServiceAccount!.OrganizationId, + userId, serviceAccountIdToCheck: ap.GrantedServiceAccountId), + _ => throw new ArgumentException("Unsupported access policy type provided."), + }; + + private async Task HasPermissionsAsync( + Guid organizationId, + Guid userId, + Guid? projectIdToCheck = null, + Guid? serviceAccountIdToCheck = null) + { + if (!_currentContext.AccessSecretsManager(organizationId)) + { + return false; + } + + var orgAdmin = await _currentContext.OrganizationAdmin(organizationId); + var accessClient = AccessClientHelper.ToAccessClient(_currentContext.ClientType, orgAdmin); + + bool hasAccess; + switch (accessClient) + { + case AccessClientType.NoAccessCheck: + hasAccess = true; + break; + case AccessClientType.User: + if (projectIdToCheck.HasValue) + { + hasAccess = await _projectRepository.UserHasWriteAccessToProject(projectIdToCheck.Value, userId); + } + else if (serviceAccountIdToCheck.HasValue) + { + hasAccess = + await _serviceAccountRepository.UserHasWriteAccessToServiceAccount( + serviceAccountIdToCheck.Value, userId); + } + else + { + throw new ArgumentException("No ID to check provided."); + } + + break; + default: + hasAccess = false; + break; + } + + return hasAccess; + } } 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 1cf066190..b61bdcbfd 100644 --- a/bitwarden_license/src/Commercial.Infrastructure.EntityFramework/SecretsManager/Repositories/AccessPolicyRepository.cs +++ b/bitwarden_license/src/Commercial.Infrastructure.EntityFramework/SecretsManager/Repositories/AccessPolicyRepository.cs @@ -108,8 +108,15 @@ public class AccessPolicyRepository : BaseEntityFrameworkRepository, IAccessPoli var dbContext = GetDatabaseContext(scope); var entity = await dbContext.AccessPolicies.Where(ap => ap.Id == id) .Include(ap => ((UserProjectAccessPolicy)ap).OrganizationUser.User) + .Include(ap => ((UserProjectAccessPolicy)ap).GrantedProject) .Include(ap => ((GroupProjectAccessPolicy)ap).Group) + .Include(ap => ((GroupProjectAccessPolicy)ap).GrantedProject) .Include(ap => ((ServiceAccountProjectAccessPolicy)ap).ServiceAccount) + .Include(ap => ((ServiceAccountProjectAccessPolicy)ap).GrantedProject) + .Include(ap => ((UserServiceAccountAccessPolicy)ap).OrganizationUser.User) + .Include(ap => ((UserServiceAccountAccessPolicy)ap).GrantedServiceAccount) + .Include(ap => ((GroupServiceAccountAccessPolicy)ap).Group) + .Include(ap => ((GroupServiceAccountAccessPolicy)ap).GrantedServiceAccount) .FirstOrDefaultAsync(); if (entity == null) diff --git a/bitwarden_license/src/Commercial.Infrastructure.EntityFramework/SecretsManager/Repositories/ServiceAccountRepository.cs b/bitwarden_license/src/Commercial.Infrastructure.EntityFramework/SecretsManager/Repositories/ServiceAccountRepository.cs index 4632abc8f..5dd560c45 100644 --- a/bitwarden_license/src/Commercial.Infrastructure.EntityFramework/SecretsManager/Repositories/ServiceAccountRepository.cs +++ b/bitwarden_license/src/Commercial.Infrastructure.EntityFramework/SecretsManager/Repositories/ServiceAccountRepository.cs @@ -54,6 +54,23 @@ public class ServiceAccountRepository : Repository> GetManyByOrganizationIdWriteAccessAsync(Guid organizationId, Guid userId, AccessClientType accessType) + { + using var scope = ServiceScopeFactory.CreateScope(); + var dbContext = GetDatabaseContext(scope); + var query = dbContext.ServiceAccount.Where(c => c.OrganizationId == organizationId); + + query = accessType switch + { + AccessClientType.NoAccessCheck => query, + AccessClientType.User => query.Where(UserHasWriteAccessToServiceAccount(userId)), + _ => throw new ArgumentOutOfRangeException(nameof(accessType), accessType, null), + }; + + var serviceAccounts = await query.OrderBy(c => c.RevisionDate).ToListAsync(); + return Mapper.Map>(serviceAccounts); + } + private static Expression> UserHasReadAccessToServiceAccount(Guid userId) => sa => sa.UserAccessPolicies.Any(ap => ap.OrganizationUser.User.Id == userId && ap.Read) || sa.GroupAccessPolicies.Any(ap => ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.User.Id == userId && ap.Read)); diff --git a/bitwarden_license/test/Commercial.Core.Test/SecretsManager/AccessPolicies/CreateAccessPoliciesCommandTests.cs b/bitwarden_license/test/Commercial.Core.Test/SecretsManager/AccessPolicies/CreateAccessPoliciesCommandTests.cs index 6005f1a9a..451caeffe 100644 --- a/bitwarden_license/test/Commercial.Core.Test/SecretsManager/AccessPolicies/CreateAccessPoliciesCommandTests.cs +++ b/bitwarden_license/test/Commercial.Core.Test/SecretsManager/AccessPolicies/CreateAccessPoliciesCommandTests.cs @@ -1,7 +1,10 @@ using Bit.Commercial.Core.SecretsManager.Commands.AccessPolicies; +using Bit.Commercial.Core.Test.SecretsManager.Enums; +using Bit.Core.Context; using Bit.Core.Exceptions; using Bit.Core.SecretsManager.Entities; using Bit.Core.SecretsManager.Repositories; +using Bit.Core.Test.SecretsManager.AutoFixture.ProjectsFixture; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.Helpers; @@ -11,29 +14,14 @@ using Xunit; namespace Bit.Commercial.Core.Test.SecretsManager.AccessPolicies; [SutProviderCustomize] +[ProjectCustomize] public class CreateAccessPoliciesCommandTests { [Theory] [BitAutoData] - public async Task CreateAsync_CallsCreate(List userProjectAccessPolicies, - List groupProjectAccessPolicies, - List serviceAccountProjectAccessPolicies, - SutProvider sutProvider) - { - var data = new List(); - data.AddRange(userProjectAccessPolicies); - data.AddRange(groupProjectAccessPolicies); - data.AddRange(serviceAccountProjectAccessPolicies); - - await sutProvider.Sut.CreateAsync(data); - - await sutProvider.GetDependency().Received(1) - .CreateManyAsync(Arg.Is(AssertHelper.AssertPropertyEqual(data))); - } - - [Theory] - [BitAutoData] - public async Task CreateAsync_AlreadyExists_Throws_BadRequestException( + public async Task CreateAsync_SmNotEnabled_Throws( + Guid userId, + Project project, List userProjectAccessPolicies, List groupProjectAccessPolicies, List serviceAccountProjectAccessPolicies, @@ -44,14 +32,41 @@ public class CreateAccessPoliciesCommandTests data.AddRange(groupProjectAccessPolicies); data.AddRange(serviceAccountProjectAccessPolicies); - sutProvider.GetDependency().AccessPolicyExists(Arg.Any()) - .Returns(true); + sutProvider.GetDependency().AccessSecretsManager(Arg.Any()).Returns(false); - var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.CreateAsync(data)); + await Assert.ThrowsAsync(() => + sutProvider.Sut.CreateForProjectAsync(project.Id, data, userId)); await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().CreateManyAsync(default); } + [Theory] + [BitAutoData] + public async Task CreateAsync_AlreadyExists_Throws_BadRequestException( + Guid userId, + Project project, + List userProjectAccessPolicies, + List groupProjectAccessPolicies, + List serviceAccountProjectAccessPolicies, + SutProvider sutProvider) + { + var data = new List(); + data.AddRange(userProjectAccessPolicies); + data.AddRange(groupProjectAccessPolicies); + data.AddRange(serviceAccountProjectAccessPolicies); + + sutProvider.GetDependency().AccessSecretsManager(Arg.Any()).Returns(true); + sutProvider.GetDependency().GetByIdAsync(project.Id).Returns(project); + sutProvider.GetDependency().OrganizationAdmin(project.OrganizationId).Returns(true); + + sutProvider.GetDependency().AccessPolicyExists(Arg.Any()) + .Returns(true); + + await Assert.ThrowsAsync(() => + sutProvider.Sut.CreateForProjectAsync(project.Id, data, userId)); + + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().CreateManyAsync(default); + } [Theory] [BitAutoData(true, false, false)] @@ -65,6 +80,8 @@ public class CreateAccessPoliciesCommandTests bool testUserPolicies, bool testGroupPolicies, bool testServiceAccountPolicies, + Guid userId, + Project project, List userProjectAccessPolicies, List groupProjectAccessPolicies, List serviceAccountProjectAccessPolicies, @@ -76,12 +93,16 @@ public class CreateAccessPoliciesCommandTests data.AddRange(groupProjectAccessPolicies); data.AddRange(serviceAccountProjectAccessPolicies); + sutProvider.GetDependency().AccessSecretsManager(Arg.Any()).Returns(true); + sutProvider.GetDependency().GetByIdAsync(project.Id).Returns(project); + sutProvider.GetDependency().OrganizationAdmin(project.OrganizationId).Returns(true); + if (testUserPolicies) { var mockUserPolicy = new UserProjectAccessPolicy { OrganizationUserId = Guid.NewGuid(), - GrantedProjectId = Guid.NewGuid() + GrantedProjectId = Guid.NewGuid(), }; data.Add(mockUserPolicy); @@ -94,7 +115,7 @@ public class CreateAccessPoliciesCommandTests var mockGroupPolicy = new GroupProjectAccessPolicy { GroupId = Guid.NewGuid(), - GrantedProjectId = Guid.NewGuid() + GrantedProjectId = Guid.NewGuid(), }; data.Add(mockGroupPolicy); @@ -107,7 +128,7 @@ public class CreateAccessPoliciesCommandTests var mockServiceAccountPolicy = new ServiceAccountProjectAccessPolicy { ServiceAccountId = Guid.NewGuid(), - GrantedProjectId = Guid.NewGuid() + GrantedProjectId = Guid.NewGuid(), }; data.Add(mockServiceAccountPolicy); @@ -119,7 +140,69 @@ public class CreateAccessPoliciesCommandTests sutProvider.GetDependency().AccessPolicyExists(Arg.Any()) .Returns(true); - var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.CreateAsync(data)); + await Assert.ThrowsAsync(() => + sutProvider.Sut.CreateForProjectAsync(project.Id, data, userId)); + + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().CreateManyAsync(default); + } + + [Theory] + [BitAutoData(PermissionType.RunAsAdmin)] + [BitAutoData(PermissionType.RunAsUserWithPermission)] + public async Task CreateAsync_Success( + PermissionType permissionType, + Guid userId, + Project project, + List userProjectAccessPolicies, + List groupProjectAccessPolicies, + List serviceAccountProjectAccessPolicies, + SutProvider sutProvider) + { + var data = new List(); + data.AddRange(userProjectAccessPolicies); + data.AddRange(groupProjectAccessPolicies); + data.AddRange(serviceAccountProjectAccessPolicies); + + sutProvider.GetDependency().AccessSecretsManager(Arg.Any()).Returns(true); + sutProvider.GetDependency().GetByIdAsync(project.Id).Returns(project); + + switch (permissionType) + { + case PermissionType.RunAsAdmin: + sutProvider.GetDependency().OrganizationAdmin(project.OrganizationId).Returns(true); + break; + case PermissionType.RunAsUserWithPermission: + sutProvider.GetDependency().UserHasWriteAccessToProject(project.Id, userId).Returns(true); + break; + } + + await sutProvider.Sut.CreateForProjectAsync(project.Id, data, userId); + + await sutProvider.GetDependency().Received(1) + .CreateManyAsync(Arg.Is(AssertHelper.AssertPropertyEqual(data))); + } + + [Theory] + [BitAutoData] + public async Task CreateAsync_User_NoPermission( + Guid userId, + Project project, + List userProjectAccessPolicies, + List groupProjectAccessPolicies, + List serviceAccountProjectAccessPolicies, + SutProvider sutProvider) + { + var data = new List(); + data.AddRange(userProjectAccessPolicies); + data.AddRange(groupProjectAccessPolicies); + data.AddRange(serviceAccountProjectAccessPolicies); + + sutProvider.GetDependency().AccessSecretsManager(Arg.Any()).Returns(true); + sutProvider.GetDependency().GetByIdAsync(project.Id).Returns(project); + sutProvider.GetDependency().UserHasWriteAccessToProject(project.Id, userId).Returns(false); + + await Assert.ThrowsAsync(() => + sutProvider.Sut.CreateForProjectAsync(project.Id, data, userId)); await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().CreateManyAsync(default); } diff --git a/bitwarden_license/test/Commercial.Core.Test/SecretsManager/AccessPolicies/DeleteAccessPolicyCommandTests.cs b/bitwarden_license/test/Commercial.Core.Test/SecretsManager/AccessPolicies/DeleteAccessPolicyCommandTests.cs index dfb09eb44..c1c5743d7 100644 --- a/bitwarden_license/test/Commercial.Core.Test/SecretsManager/AccessPolicies/DeleteAccessPolicyCommandTests.cs +++ b/bitwarden_license/test/Commercial.Core.Test/SecretsManager/AccessPolicies/DeleteAccessPolicyCommandTests.cs @@ -1,7 +1,11 @@ using Bit.Commercial.Core.SecretsManager.Commands.AccessPolicies; +using Bit.Commercial.Core.Test.SecretsManager.Enums; +using Bit.Core.Context; +using Bit.Core.Entities; using Bit.Core.Exceptions; using Bit.Core.SecretsManager.Entities; using Bit.Core.SecretsManager.Repositories; +using Bit.Core.Test.SecretsManager.AutoFixture.ProjectsFixture; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; @@ -11,28 +15,216 @@ using Xunit; namespace Bit.Commercial.Core.Test.SecretsManager.AccessPolicies; [SutProviderCustomize] +[ProjectCustomize] public class DeleteAccessPolicyCommandTests { + private static void SetupPermission(SutProvider sutProvider, + PermissionType permissionType, Project grantedProject, Guid userId) + { + switch (permissionType) + { + case PermissionType.RunAsAdmin: + sutProvider.GetDependency().OrganizationAdmin(grantedProject.OrganizationId) + .Returns(true); + break; + case PermissionType.RunAsUserWithPermission: + sutProvider.GetDependency().UserHasWriteAccessToProject(grantedProject.Id, userId) + .Returns(true); + break; + default: + throw new ArgumentOutOfRangeException(nameof(permissionType), permissionType, null); + } + } + + private static void SetupPermission(SutProvider sutProvider, + PermissionType permissionType, ServiceAccount grantedServiceAccount, Guid userId) + { + switch (permissionType) + { + case PermissionType.RunAsAdmin: + sutProvider.GetDependency().OrganizationAdmin(grantedServiceAccount.OrganizationId) + .Returns(true); + break; + case PermissionType.RunAsUserWithPermission: + sutProvider.GetDependency() + .UserHasWriteAccessToServiceAccount(grantedServiceAccount.Id, userId) + .Returns(true); + break; + default: + throw new ArgumentOutOfRangeException(nameof(permissionType), permissionType, null); + } + } + + private static BaseAccessPolicy CreatePolicyToReturn(AccessPolicyType accessPolicyType, Guid data, + Project grantedProject, Group mockGroup, ServiceAccount mockServiceAccount) => + accessPolicyType switch + { + AccessPolicyType.UserProjectAccessPolicy => new UserProjectAccessPolicy + { + Id = data, + GrantedProjectId = grantedProject.Id, + GrantedProject = grantedProject, + }, + AccessPolicyType.GroupProjectAccessPolicy => new GroupProjectAccessPolicy + { + Id = data, + GrantedProjectId = grantedProject.Id, + Group = mockGroup, + GrantedProject = grantedProject, + }, + AccessPolicyType.ServiceAccountProjectAccessPolicy => new ServiceAccountProjectAccessPolicy + { + Id = data, + GrantedProjectId = grantedProject.Id, + ServiceAccount = mockServiceAccount, + GrantedProject = grantedProject, + }, + _ => null, + }; + + private static BaseAccessPolicy CreatePolicyToReturn(AccessPolicyType accessPolicyType, Guid data, + ServiceAccount grantedServiceAccount, Group mockGroup) => + accessPolicyType switch + { + AccessPolicyType.UserServiceAccountAccessPolicy => new UserServiceAccountAccessPolicy + { + Id = data, + GrantedServiceAccountId = grantedServiceAccount.Id, + GrantedServiceAccount = grantedServiceAccount, + }, + AccessPolicyType.GroupServiceAccountAccessPolicy => new GroupServiceAccountAccessPolicy + { + Id = data, + GrantedServiceAccountId = grantedServiceAccount.Id, + Group = mockGroup, + GrantedServiceAccount = grantedServiceAccount, + }, + _ => null, + }; + [Theory] [BitAutoData] - public async Task DeleteAccessPolicy_Throws_NotFoundException(Guid data, + public async Task DeleteAccessPolicy_Throws_NotFoundException(Guid data, Guid userId, SutProvider sutProvider) { + sutProvider.GetDependency().AccessSecretsManager(Arg.Any()).Returns(true); sutProvider.GetDependency().GetByIdAsync(data).ReturnsNull(); - var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.DeleteAsync(data)); + await Assert.ThrowsAsync(() => sutProvider.Sut.DeleteAsync(data, userId)); await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().DeleteAsync(default); } [Theory] [BitAutoData] - public async Task DeleteAccessPolicy_Success(Guid data, + public async Task DeleteAccessPolicy_SmNotEnabled_Throws_NotFoundException(Guid data, Guid userId, SutProvider sutProvider) { - sutProvider.GetDependency().GetByIdAsync(data) - .Returns(new UserProjectAccessPolicy { Id = data }); + sutProvider.GetDependency().AccessSecretsManager(Arg.Any()).Returns(false); + sutProvider.GetDependency().GetByIdAsync(data).ReturnsNull(); + await Assert.ThrowsAsync(() => sutProvider.Sut.DeleteAsync(data, userId)); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().DeleteAsync(default); + } - await sutProvider.Sut.DeleteAsync(data); + [Theory] + [BitAutoData(AccessPolicyType.UserProjectAccessPolicy, PermissionType.RunAsAdmin)] + [BitAutoData(AccessPolicyType.UserProjectAccessPolicy, PermissionType.RunAsUserWithPermission)] + [BitAutoData(AccessPolicyType.GroupProjectAccessPolicy, PermissionType.RunAsAdmin)] + [BitAutoData(AccessPolicyType.GroupProjectAccessPolicy, PermissionType.RunAsUserWithPermission)] + [BitAutoData(AccessPolicyType.ServiceAccountProjectAccessPolicy, PermissionType.RunAsAdmin)] + [BitAutoData(AccessPolicyType.ServiceAccountProjectAccessPolicy, PermissionType.RunAsUserWithPermission)] + public async Task DeleteAccessPolicy_ProjectGrants_PermissionsCheck_Success( + AccessPolicyType accessPolicyType, + PermissionType permissionType, + Guid data, + Guid userId, + Project grantedProject, + Group mockGroup, + ServiceAccount mockServiceAccount, + SutProvider sutProvider) + { + sutProvider.GetDependency().AccessSecretsManager(Arg.Any()).Returns(true); + var policyToReturn = + CreatePolicyToReturn(accessPolicyType, data, grantedProject, mockGroup, mockServiceAccount); + SetupPermission(sutProvider, permissionType, grantedProject, userId); + + sutProvider.GetDependency().GetByIdAsync(data) + .Returns(policyToReturn); + + await sutProvider.Sut.DeleteAsync(data, userId); await sutProvider.GetDependency().Received(1).DeleteAsync(Arg.Is(data)); } + + [Theory] + [BitAutoData(AccessPolicyType.UserProjectAccessPolicy)] + [BitAutoData(AccessPolicyType.GroupProjectAccessPolicy)] + [BitAutoData(AccessPolicyType.ServiceAccountProjectAccessPolicy)] + public async Task DeleteAccessPolicy_UserProjectAccessPolicy_PermissionsCheck_ThrowsNotAuthorized( + AccessPolicyType accessPolicyType, + Guid data, + Guid userId, + Group mockGroup, + ServiceAccount mockServiceAccount, + Project grantedProject, + SutProvider sutProvider) + { + sutProvider.GetDependency().AccessSecretsManager(Arg.Any()).Returns(true); + var policyToReturn = + CreatePolicyToReturn(accessPolicyType, data, grantedProject, mockGroup, mockServiceAccount); + + sutProvider.GetDependency().GetByIdAsync(data) + .Returns(policyToReturn); + + await Assert.ThrowsAsync(() => + sutProvider.Sut.DeleteAsync(data, userId)); + + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().DeleteAsync(default); + } + + [Theory] + [BitAutoData(AccessPolicyType.UserServiceAccountAccessPolicy, PermissionType.RunAsAdmin)] + [BitAutoData(AccessPolicyType.UserServiceAccountAccessPolicy, PermissionType.RunAsUserWithPermission)] + [BitAutoData(AccessPolicyType.GroupServiceAccountAccessPolicy, PermissionType.RunAsAdmin)] + [BitAutoData(AccessPolicyType.GroupServiceAccountAccessPolicy, PermissionType.RunAsUserWithPermission)] + public async Task DeleteAccessPolicy_ServiceAccountGrants_PermissionsCheck_Success( + AccessPolicyType accessPolicyType, + PermissionType permissionType, + Guid data, + Guid userId, + ServiceAccount grantedServiceAccount, + Group mockGroup, + SutProvider sutProvider) + { + sutProvider.GetDependency().AccessSecretsManager(Arg.Any()).Returns(true); + var policyToReturn = CreatePolicyToReturn(accessPolicyType, data, grantedServiceAccount, mockGroup); + SetupPermission(sutProvider, permissionType, grantedServiceAccount, userId); + sutProvider.GetDependency().GetByIdAsync(data) + .Returns(policyToReturn); + + await sutProvider.Sut.DeleteAsync(data, userId); + + await sutProvider.GetDependency().Received(1).DeleteAsync(Arg.Is(data)); + } + + [Theory] + [BitAutoData(AccessPolicyType.UserServiceAccountAccessPolicy)] + [BitAutoData(AccessPolicyType.GroupServiceAccountAccessPolicy)] + public async Task DeleteAccessPolicy_ServiceAccountGrants_PermissionsCheck_Throws( + AccessPolicyType accessPolicyType, + Guid data, + Guid userId, + ServiceAccount grantedServiceAccount, + Group mockGroup, + SutProvider sutProvider) + { + sutProvider.GetDependency().AccessSecretsManager(Arg.Any()).Returns(true); + var policyToReturn = CreatePolicyToReturn(accessPolicyType, data, grantedServiceAccount, mockGroup); + + sutProvider.GetDependency().GetByIdAsync(data) + .Returns(policyToReturn); + + await Assert.ThrowsAsync(() => + sutProvider.Sut.DeleteAsync(data, userId)); + + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().DeleteAsync(default); + } } diff --git a/bitwarden_license/test/Commercial.Core.Test/SecretsManager/AccessPolicies/UpdateAccessPolicyCommandTests.cs b/bitwarden_license/test/Commercial.Core.Test/SecretsManager/AccessPolicies/UpdateAccessPolicyCommandTests.cs index 482be4839..a0f69c9f2 100644 --- a/bitwarden_license/test/Commercial.Core.Test/SecretsManager/AccessPolicies/UpdateAccessPolicyCommandTests.cs +++ b/bitwarden_license/test/Commercial.Core.Test/SecretsManager/AccessPolicies/UpdateAccessPolicyCommandTests.cs @@ -1,7 +1,11 @@ using Bit.Commercial.Core.SecretsManager.Commands.AccessPolicies; +using Bit.Commercial.Core.Test.SecretsManager.Enums; +using Bit.Core.Context; +using Bit.Core.Entities; using Bit.Core.Exceptions; using Bit.Core.SecretsManager.Entities; using Bit.Core.SecretsManager.Repositories; +using Bit.Core.Test.SecretsManager.AutoFixture.ProjectsFixture; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.Helpers; @@ -11,30 +15,246 @@ using Xunit; namespace Bit.Commercial.Core.Test.SecretsManager.AccessPolicies; [SutProviderCustomize] +[ProjectCustomize] public class UpdateAccessPolicyCommandTests { + private static void SetupPermission(SutProvider sutProvider, + PermissionType permissionType, Project grantedProject, Guid userId) + { + switch (permissionType) + { + case PermissionType.RunAsAdmin: + sutProvider.GetDependency().OrganizationAdmin(grantedProject.OrganizationId) + .Returns(true); + break; + case PermissionType.RunAsUserWithPermission: + sutProvider.GetDependency().UserHasWriteAccessToProject(grantedProject.Id, userId) + .Returns(true); + break; + default: + throw new ArgumentOutOfRangeException(nameof(permissionType), permissionType, null); + } + } + + private static void SetupPermission(SutProvider sutProvider, + PermissionType permissionType, ServiceAccount grantedServiceAccount, Guid userId) + { + switch (permissionType) + { + case PermissionType.RunAsAdmin: + sutProvider.GetDependency().OrganizationAdmin(grantedServiceAccount.OrganizationId) + .Returns(true); + break; + case PermissionType.RunAsUserWithPermission: + sutProvider.GetDependency() + .UserHasWriteAccessToServiceAccount(grantedServiceAccount.Id, userId) + .Returns(true); + break; + default: + throw new ArgumentOutOfRangeException(nameof(permissionType), permissionType, null); + } + } + + private static BaseAccessPolicy CreatePolicyToReturn(AccessPolicyType accessPolicyType, + ServiceAccount grantedServiceAccount, Guid data, Group mockGroup) + { + switch (accessPolicyType) + { + case AccessPolicyType.UserServiceAccountAccessPolicy: + return + new UserServiceAccountAccessPolicy + { + Id = data, + Read = true, + Write = true, + GrantedServiceAccountId = grantedServiceAccount.Id, + GrantedServiceAccount = grantedServiceAccount, + }; + case AccessPolicyType.GroupServiceAccountAccessPolicy: + mockGroup.OrganizationId = grantedServiceAccount.OrganizationId; + return new GroupServiceAccountAccessPolicy + { + Id = data, + GrantedServiceAccountId = grantedServiceAccount.Id, + GrantedServiceAccount = grantedServiceAccount, + Read = true, + Write = true, + Group = mockGroup, + }; + default: + throw new ArgumentOutOfRangeException(nameof(accessPolicyType), accessPolicyType, null); + } + } + + private static BaseAccessPolicy CreatePolicyToReturn(AccessPolicyType accessPolicyType, Guid data, + Project grantedProject, Group mockGroup, ServiceAccount mockServiceAccount) + { + switch (accessPolicyType) + { + case AccessPolicyType.UserProjectAccessPolicy: + return + new UserProjectAccessPolicy + { + Id = data, + Read = true, + Write = true, + GrantedProjectId = grantedProject.Id, + GrantedProject = grantedProject, + }; + case AccessPolicyType.GroupProjectAccessPolicy: + mockGroup.OrganizationId = grantedProject.OrganizationId; + return + new GroupProjectAccessPolicy + { + Id = data, + GrantedProjectId = grantedProject.Id, + Read = true, + Write = true, + Group = mockGroup, + GrantedProject = grantedProject, + }; + case AccessPolicyType.ServiceAccountProjectAccessPolicy: + mockServiceAccount.OrganizationId = grantedProject.OrganizationId; + return new ServiceAccountProjectAccessPolicy + { + Id = data, + GrantedProjectId = grantedProject.Id, + Read = true, + Write = true, + ServiceAccount = mockServiceAccount, + GrantedProject = grantedProject, + }; + default: + throw new ArgumentOutOfRangeException(nameof(accessPolicyType), accessPolicyType, null); + } + } + [Theory] [BitAutoData] - public async Task UpdateAsync_Throws_NotFoundException(Guid data, bool read, bool write, + public async Task UpdateAsync_Throws_NotFoundException(Guid data, bool read, bool write, Guid userId, SutProvider sutProvider) { - var exception = - await Assert.ThrowsAsync(() => sutProvider.Sut.UpdateAsync(data, read, write)); + sutProvider.GetDependency().AccessSecretsManager(Arg.Any()).Returns(true); + await Assert.ThrowsAsync(() => sutProvider.Sut.UpdateAsync(data, read, write, userId)); await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().ReplaceAsync(default); } [Theory] [BitAutoData] - public async Task UpdateAsync_Calls_Replace(Guid data, bool read, bool write, + public async Task UpdateAsync_SmNotEnabled_Throws_NotFoundException(Guid data, bool read, bool write, Guid userId, SutProvider sutProvider) { - var existingPolicy = new UserProjectAccessPolicy { Id = data, Read = true, Write = true }; - sutProvider.GetDependency().GetByIdAsync(data).Returns(existingPolicy); - var result = await sutProvider.Sut.UpdateAsync(data, read, write); - await sutProvider.GetDependency().Received(1).ReplaceAsync(existingPolicy); + sutProvider.GetDependency().AccessSecretsManager(Arg.Any()).Returns(false); + await Assert.ThrowsAsync(() => sutProvider.Sut.UpdateAsync(data, read, write, userId)); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().ReplaceAsync(default); + } + + [Theory] + [BitAutoData(AccessPolicyType.UserProjectAccessPolicy, PermissionType.RunAsAdmin)] + [BitAutoData(AccessPolicyType.UserProjectAccessPolicy, PermissionType.RunAsUserWithPermission)] + [BitAutoData(AccessPolicyType.GroupProjectAccessPolicy, PermissionType.RunAsAdmin)] + [BitAutoData(AccessPolicyType.GroupProjectAccessPolicy, PermissionType.RunAsUserWithPermission)] + [BitAutoData(AccessPolicyType.ServiceAccountProjectAccessPolicy, PermissionType.RunAsAdmin)] + [BitAutoData(AccessPolicyType.ServiceAccountProjectAccessPolicy, PermissionType.RunAsUserWithPermission)] + public async Task UpdateAsync_ProjectGrants_PermissionsCheck_Success( + AccessPolicyType accessPolicyType, + PermissionType permissionType, + Guid data, + bool read, + bool write, + Guid userId, + Project grantedProject, + Group mockGroup, + ServiceAccount mockServiceAccount, + SutProvider sutProvider) + { + sutProvider.GetDependency().AccessSecretsManager(Arg.Any()).Returns(true); + var policyToReturn = + CreatePolicyToReturn(accessPolicyType, data, grantedProject, mockGroup, mockServiceAccount); + SetupPermission(sutProvider, permissionType, grantedProject, userId); + + sutProvider.GetDependency().GetByIdAsync(data).Returns(policyToReturn); + var result = await sutProvider.Sut.UpdateAsync(data, read, write, userId); + await sutProvider.GetDependency().Received(1).ReplaceAsync(policyToReturn); AssertHelper.AssertRecent(result.RevisionDate); Assert.Equal(read, result.Read); Assert.Equal(write, result.Write); } + + [Theory] + [BitAutoData(AccessPolicyType.UserProjectAccessPolicy)] + [BitAutoData(AccessPolicyType.GroupProjectAccessPolicy)] + [BitAutoData(AccessPolicyType.ServiceAccountProjectAccessPolicy)] + public async Task UpdateAsync_ProjectGrants_PermissionsCheck_Throws( + AccessPolicyType accessPolicyType, + Guid data, + bool read, + bool write, + Guid userId, + Project grantedProject, + Group mockGroup, + ServiceAccount mockServiceAccount, + SutProvider sutProvider) + { + sutProvider.GetDependency().AccessSecretsManager(Arg.Any()).Returns(true); + var policyToReturn = + CreatePolicyToReturn(accessPolicyType, data, grantedProject, mockGroup, mockServiceAccount); + sutProvider.GetDependency().GetByIdAsync(data).Returns(policyToReturn); + + await Assert.ThrowsAsync(() => + sutProvider.Sut.UpdateAsync(data, read, write, userId)); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().ReplaceAsync(default); + } + + [Theory] + [BitAutoData(AccessPolicyType.UserServiceAccountAccessPolicy, PermissionType.RunAsAdmin)] + [BitAutoData(AccessPolicyType.UserServiceAccountAccessPolicy, PermissionType.RunAsUserWithPermission)] + [BitAutoData(AccessPolicyType.GroupServiceAccountAccessPolicy, PermissionType.RunAsAdmin)] + [BitAutoData(AccessPolicyType.GroupServiceAccountAccessPolicy, PermissionType.RunAsUserWithPermission)] + public async Task UpdateAsync_ServiceAccountGrants_PermissionsCheck_Success( + AccessPolicyType accessPolicyType, + PermissionType permissionType, + Guid data, + bool read, + bool write, + Guid userId, + ServiceAccount grantedServiceAccount, + Group mockGroup, + SutProvider sutProvider) + { + sutProvider.GetDependency().AccessSecretsManager(Arg.Any()).Returns(true); + var policyToReturn = CreatePolicyToReturn(accessPolicyType, grantedServiceAccount, data, mockGroup); + SetupPermission(sutProvider, permissionType, grantedServiceAccount, userId); + + sutProvider.GetDependency().GetByIdAsync(data).Returns(policyToReturn); + var result = await sutProvider.Sut.UpdateAsync(data, read, write, userId); + await sutProvider.GetDependency().Received(1).ReplaceAsync(policyToReturn); + + AssertHelper.AssertRecent(result.RevisionDate); + Assert.Equal(read, result.Read); + Assert.Equal(write, result.Write); + } + + [Theory] + [BitAutoData(AccessPolicyType.UserServiceAccountAccessPolicy)] + [BitAutoData(AccessPolicyType.GroupServiceAccountAccessPolicy)] + public async Task UpdateAsync_ServiceAccountGrants_PermissionsCheck_Throws( + AccessPolicyType accessPolicyType, + Guid data, + bool read, + bool write, + Guid userId, + ServiceAccount grantedServiceAccount, + Group mockGroup, + SutProvider sutProvider) + { + sutProvider.GetDependency().AccessSecretsManager(Arg.Any()).Returns(true); + var policyToReturn = CreatePolicyToReturn(accessPolicyType, grantedServiceAccount, data, mockGroup); + sutProvider.GetDependency().GetByIdAsync(data).Returns(policyToReturn); + + await Assert.ThrowsAsync(() => + sutProvider.Sut.UpdateAsync(data, read, write, userId)); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().ReplaceAsync(default); + } } diff --git a/bitwarden_license/test/Commercial.Core.Test/SecretsManager/Enums/AccessPolicyType.cs b/bitwarden_license/test/Commercial.Core.Test/SecretsManager/Enums/AccessPolicyType.cs new file mode 100644 index 000000000..c46505b16 --- /dev/null +++ b/bitwarden_license/test/Commercial.Core.Test/SecretsManager/Enums/AccessPolicyType.cs @@ -0,0 +1,11 @@ +namespace Bit.Commercial.Core.Test.SecretsManager.Enums; + +public enum AccessPolicyType +{ + UserProjectAccessPolicy, + GroupProjectAccessPolicy, + ServiceAccountProjectAccessPolicy, + UserServiceAccountAccessPolicy, + GroupServiceAccountAccessPolicy, + +} diff --git a/bitwarden_license/test/Commercial.Core.Test/SecretsManager/Enums/PermissionType.cs b/bitwarden_license/test/Commercial.Core.Test/SecretsManager/Enums/PermissionType.cs new file mode 100644 index 000000000..1c4e88e91 --- /dev/null +++ b/bitwarden_license/test/Commercial.Core.Test/SecretsManager/Enums/PermissionType.cs @@ -0,0 +1,7 @@ +namespace Bit.Commercial.Core.Test.SecretsManager.Enums; + +public enum PermissionType +{ + RunAsAdmin, + RunAsUserWithPermission, +} diff --git a/src/Api/SecretsManager/Controllers/AccessPoliciesController.cs b/src/Api/SecretsManager/Controllers/AccessPoliciesController.cs index d2d3a4f28..254dd09e2 100644 --- a/src/Api/SecretsManager/Controllers/AccessPoliciesController.cs +++ b/src/Api/SecretsManager/Controllers/AccessPoliciesController.cs @@ -1,27 +1,53 @@ -using Bit.Api.SecretsManager.Models.Request; +using Bit.Api.Models.Response; +using Bit.Api.SecretsManager.Models.Request; using Bit.Api.SecretsManager.Models.Response; +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.Repositories; +using Bit.Core.Services; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; namespace Bit.Api.SecretsManager.Controllers; [SecretsManager] +[Authorize("secrets")] [Route("access-policies")] public class AccessPoliciesController : Controller { private readonly IAccessPolicyRepository _accessPolicyRepository; 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; + private readonly IUserService _userService; public AccessPoliciesController( + IUserService userService, + ICurrentContext currentContext, IAccessPolicyRepository accessPolicyRepository, + IServiceAccountRepository serviceAccountRepository, + IGroupRepository groupRepository, + IProjectRepository projectRepository, + IOrganizationUserRepository organizationUserRepository, ICreateAccessPoliciesCommand createAccessPoliciesCommand, IDeleteAccessPolicyCommand deleteAccessPolicyCommand, IUpdateAccessPolicyCommand updateAccessPolicyCommand) { + _userService = userService; + _currentContext = currentContext; + _serviceAccountRepository = serviceAccountRepository; + _projectRepository = projectRepository; + _groupRepository = groupRepository; + _organizationUserRepository = organizationUserRepository; _accessPolicyRepository = accessPolicyRepository; _createAccessPoliciesCommand = createAccessPoliciesCommand; _deleteAccessPolicyCommand = deleteAccessPolicyCommand; @@ -32,14 +58,18 @@ public class AccessPoliciesController : Controller public async Task CreateProjectAccessPoliciesAsync([FromRoute] Guid id, [FromBody] AccessPoliciesCreateRequest request) { + var userId = _userService.GetProperUserId(User).Value; var policies = request.ToBaseAccessPoliciesForProject(id); - var results = await _createAccessPoliciesCommand.CreateAsync(policies); + var results = await _createAccessPoliciesCommand.CreateForProjectAsync(id, policies, userId); return new ProjectAccessPoliciesResponseModel(results); } [HttpGet("/projects/{id}/access-policies")] public async Task GetProjectAccessPoliciesAsync([FromRoute] Guid id) { + var project = await _projectRepository.GetByIdAsync(id); + await CheckUserHasWriteAccessToProjectAsync(project); + var results = await _accessPolicyRepository.GetManyByGrantedProjectIdAsync(id); return new ProjectAccessPoliciesResponseModel(results); } @@ -48,7 +78,8 @@ public class AccessPoliciesController : Controller public async Task UpdateAccessPolicyAsync([FromRoute] Guid id, [FromBody] AccessPolicyUpdateRequest request) { - var result = await _updateAccessPolicyCommand.UpdateAsync(id, request.Read, request.Write); + var userId = _userService.GetProperUserId(User).Value; + var result = await _updateAccessPolicyCommand.UpdateAsync(id, request.Read, request.Write, userId); return result switch { @@ -56,13 +87,80 @@ public class AccessPoliciesController : Controller GroupProjectAccessPolicy accessPolicy => new GroupProjectAccessPolicyResponseModel(accessPolicy), ServiceAccountProjectAccessPolicy accessPolicy => new ServiceAccountProjectAccessPolicyResponseModel( accessPolicy), - _ => throw new ArgumentException("Unsupported access policy type provided.") + _ => throw new ArgumentException("Unsupported access policy type provided."), }; } [HttpDelete("{id}")] public async Task DeleteAccessPolicyAsync([FromRoute] Guid id) { - await _deleteAccessPolicyCommand.DeleteAsync(id); + var userId = _userService.GetProperUserId(User).Value; + await _deleteAccessPolicyCommand.DeleteAsync(id, userId); + } + + [HttpGet("/organizations/{id}/access-policies/people/potential-grantees")] + public async Task> GetPeoplePotentialGranteesAsync([FromRoute] Guid id) + { + if (!_currentContext.AccessSecretsManager(id)) + { + 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) + .Select(userDetails => new PotentialGranteeResponseModel(userDetails)); + + return new ListResponseModel(userResponses.Concat(groupResponses)); + } + + [HttpGet("/organizations/{id}/access-policies/service-accounts/potential-grantees")] + public async Task> GetServiceAccountsPotentialGranteesAsync([FromRoute] Guid id) + { + if (!_currentContext.AccessSecretsManager(id)) + { + throw new NotFoundException(); + } + + var userId = _userService.GetProperUserId(User).Value; + var orgAdmin = await _currentContext.OrganizationAdmin(id); + var accessClient = AccessClientHelper.ToAccessClient(_currentContext.ClientType, orgAdmin); + + var serviceAccounts = + await _serviceAccountRepository.GetManyByOrganizationIdWriteAccessAsync(id, + userId, + accessClient); + var serviceAccountResponses = + serviceAccounts.Select(serviceAccount => new PotentialGranteeResponseModel(serviceAccount)); + + return new ListResponseModel(serviceAccountResponses); + } + + private async Task CheckUserHasWriteAccessToProjectAsync(Project project) + { + if (project == null || !_currentContext.AccessSecretsManager(project.OrganizationId)) + { + throw new NotFoundException(); + } + + var userId = _userService.GetProperUserId(User).Value; + var orgAdmin = await _currentContext.OrganizationAdmin(project.OrganizationId); + var accessClient = AccessClientHelper.ToAccessClient(_currentContext.ClientType, orgAdmin); + + var hasAccess = accessClient switch + { + AccessClientType.NoAccessCheck => true, + AccessClientType.User => await _projectRepository.UserHasWriteAccessToProject(project.Id, userId), + _ => false, + }; + + if (!hasAccess) + { + throw new NotFoundException(); + } } } diff --git a/src/Api/SecretsManager/Models/Response/PotentialGranteeResponseModel.cs b/src/Api/SecretsManager/Models/Response/PotentialGranteeResponseModel.cs new file mode 100644 index 000000000..7b0632a45 --- /dev/null +++ b/src/Api/SecretsManager/Models/Response/PotentialGranteeResponseModel.cs @@ -0,0 +1,62 @@ +using Bit.Core.Entities; +using Bit.Core.Models.Api; +using Bit.Core.Models.Data.Organizations.OrganizationUsers; +using Bit.Core.SecretsManager.Entities; + +namespace Bit.Api.SecretsManager.Models.Response; + +public class PotentialGranteeResponseModel : ResponseModel +{ + private const string _objectName = "potentialGrantee"; + + public PotentialGranteeResponseModel(Group group) + : base(_objectName) + { + if (group == null) + { + throw new ArgumentNullException(nameof(group)); + } + + Id = group.Id.ToString(); + Name = group.Name; + Type = "group"; + } + + public PotentialGranteeResponseModel(OrganizationUserUserDetails user) + : base(_objectName) + { + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + Id = user.Id.ToString(); + Name = user.Name; + Email = user.Email; + Type = "user"; + } + + public PotentialGranteeResponseModel(ServiceAccount serviceAccount) + : base(_objectName) + { + if (serviceAccount == null) + { + throw new ArgumentNullException(nameof(serviceAccount)); + } + + Id = serviceAccount.Id.ToString(); + Name = serviceAccount.Name; + Type = "serviceAccount"; + } + + public PotentialGranteeResponseModel() : base(_objectName) + { + } + + public string Id { get; set; } + + public string Name { get; set; } + + public string Type { get; set; } + public string? Email { get; set; } +} diff --git a/src/Core/SecretsManager/Commands/AccessPolicies/Interfaces/ICreateAccessPoliciesCommand.cs b/src/Core/SecretsManager/Commands/AccessPolicies/Interfaces/ICreateAccessPoliciesCommand.cs index 9ec9ff0db..29aa34c0f 100644 --- a/src/Core/SecretsManager/Commands/AccessPolicies/Interfaces/ICreateAccessPoliciesCommand.cs +++ b/src/Core/SecretsManager/Commands/AccessPolicies/Interfaces/ICreateAccessPoliciesCommand.cs @@ -4,5 +4,5 @@ namespace Bit.Core.SecretsManager.Commands.AccessPolicies.Interfaces; public interface ICreateAccessPoliciesCommand { - Task> CreateAsync(List accessPolicies); + Task> CreateForProjectAsync(Guid projectId, List accessPolicies, Guid userId); } diff --git a/src/Core/SecretsManager/Commands/AccessPolicies/Interfaces/IDeleteAccessPolicyCommand.cs b/src/Core/SecretsManager/Commands/AccessPolicies/Interfaces/IDeleteAccessPolicyCommand.cs index de3215a02..e4d313b40 100644 --- a/src/Core/SecretsManager/Commands/AccessPolicies/Interfaces/IDeleteAccessPolicyCommand.cs +++ b/src/Core/SecretsManager/Commands/AccessPolicies/Interfaces/IDeleteAccessPolicyCommand.cs @@ -2,5 +2,5 @@ public interface IDeleteAccessPolicyCommand { - Task DeleteAsync(Guid id); + Task DeleteAsync(Guid id, Guid userId); } diff --git a/src/Core/SecretsManager/Commands/AccessPolicies/Interfaces/IUpdateAccessPolicyCommand.cs b/src/Core/SecretsManager/Commands/AccessPolicies/Interfaces/IUpdateAccessPolicyCommand.cs index 225f6a752..afeb47e9f 100644 --- a/src/Core/SecretsManager/Commands/AccessPolicies/Interfaces/IUpdateAccessPolicyCommand.cs +++ b/src/Core/SecretsManager/Commands/AccessPolicies/Interfaces/IUpdateAccessPolicyCommand.cs @@ -4,5 +4,5 @@ namespace Bit.Core.SecretsManager.Commands.AccessPolicies.Interfaces; public interface IUpdateAccessPolicyCommand { - public Task UpdateAsync(Guid id, bool read, bool write); + public Task UpdateAsync(Guid id, bool read, bool write, Guid userId); } diff --git a/src/Core/SecretsManager/Entities/AccessPolicy.cs b/src/Core/SecretsManager/Entities/AccessPolicy.cs index 62e79d0f2..bce9de062 100644 --- a/src/Core/SecretsManager/Entities/AccessPolicy.cs +++ b/src/Core/SecretsManager/Entities/AccessPolicy.cs @@ -24,34 +24,39 @@ public abstract class BaseAccessPolicy public class UserProjectAccessPolicy : BaseAccessPolicy { public Guid? OrganizationUserId { get; set; } - public Guid? GrantedProjectId { get; set; } public User? User { get; set; } + public Guid? GrantedProjectId { get; set; } + public Project? GrantedProject { get; set; } } public class UserServiceAccountAccessPolicy : BaseAccessPolicy { public Guid? OrganizationUserId { get; set; } - public Guid? GrantedServiceAccountId { get; set; } public User? User { get; set; } + public Guid? GrantedServiceAccountId { get; set; } + public ServiceAccount? GrantedServiceAccount { get; set; } } public class GroupProjectAccessPolicy : BaseAccessPolicy { public Guid? GroupId { get; set; } - public Guid? GrantedProjectId { get; set; } public Group? Group { get; set; } + public Guid? GrantedProjectId { get; set; } + public Project? GrantedProject { get; set; } } public class GroupServiceAccountAccessPolicy : BaseAccessPolicy { public Guid? GroupId { get; set; } - public Guid? GrantedServiceAccountId { get; set; } public Group? Group { get; set; } + public Guid? GrantedServiceAccountId { get; set; } + public ServiceAccount? GrantedServiceAccount { get; set; } } public class ServiceAccountProjectAccessPolicy : BaseAccessPolicy { public Guid? ServiceAccountId { get; set; } - public Guid? GrantedProjectId { get; set; } public ServiceAccount? ServiceAccount { get; set; } + public Guid? GrantedProjectId { get; set; } + public Project? GrantedProject { get; set; } } diff --git a/src/Core/SecretsManager/Repositories/IServiceAccountRepository.cs b/src/Core/SecretsManager/Repositories/IServiceAccountRepository.cs index 924a18f0b..740d597ec 100644 --- a/src/Core/SecretsManager/Repositories/IServiceAccountRepository.cs +++ b/src/Core/SecretsManager/Repositories/IServiceAccountRepository.cs @@ -11,4 +11,5 @@ public interface IServiceAccountRepository Task ReplaceAsync(ServiceAccount serviceAccount); Task UserHasReadAccessToServiceAccount(Guid id, Guid userId); Task UserHasWriteAccessToServiceAccount(Guid id, Guid userId); + Task> GetManyByOrganizationIdWriteAccessAsync(Guid organizationId, Guid userId, AccessClientType accessType); } diff --git a/test/Api.IntegrationTest/SecretsManager/Controllers/AccessPoliciesControllerTest.cs b/test/Api.IntegrationTest/SecretsManager/Controllers/AccessPoliciesControllerTest.cs index 9d42b67e8..9d48a8d3a 100644 --- a/test/Api.IntegrationTest/SecretsManager/Controllers/AccessPoliciesControllerTest.cs +++ b/test/Api.IntegrationTest/SecretsManager/Controllers/AccessPoliciesControllerTest.cs @@ -1,9 +1,11 @@ -using System.Net.Http.Headers; +using System.Net; +using System.Net.Http.Headers; using Bit.Api.IntegrationTest.Factories; -using Bit.Api.IntegrationTest.Helpers; +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.Entities; +using Bit.Core.Enums; using Bit.Core.SecretsManager.Entities; using Bit.Core.SecretsManager.Repositories; using Bit.Test.Common.Helpers; @@ -13,16 +15,17 @@ namespace Bit.Api.IntegrationTest.SecretsManager.Controllers; public class AccessPoliciesControllerTest : IClassFixture, IAsyncLifetime { + private const string _mockEncryptedString = + "2.3Uk+WNBIoU5xzmVFNcoWzz==|1MsPIYuRfdOHfu/0uY6H2Q==|/98sp4wb6pHP1VTZ9JcNCYgQjEUMFPlqJgCwRk1YXKg="; + private readonly IAccessPolicyRepository _accessPolicyRepository; private readonly HttpClient _client; private readonly ApiApplicationFactory _factory; - - private const string _mockEncryptedString = "2.3Uk+WNBIoU5xzmVFNcoWzz==|1MsPIYuRfdOHfu/0uY6H2Q==|/98sp4wb6pHP1VTZ9JcNCYgQjEUMFPlqJgCwRk1YXKg="; - private readonly IProjectRepository _projectRepository; private readonly IServiceAccountRepository _serviceAccountRepository; - private Organization _organization = null!; + private string _email = null!; + private SecretsManagerOrganizationHelper _organizationHelper = null!; public AccessPoliciesControllerTest(ApiApplicationFactory factory) { @@ -35,46 +38,105 @@ public class AccessPoliciesControllerTest : IClassFixture public async Task InitializeAsync() { - var ownerEmail = $"integration-test{Guid.NewGuid()}@bitwarden.com"; - var tokens = await _factory.LoginWithNewAccount(ownerEmail); - var (organization, _) = - await OrganizationTestHelpers.SignUpAsync(_factory, ownerEmail: ownerEmail, billingEmail: ownerEmail); - _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tokens.Token); - _organization = organization; + _email = $"integration-test{Guid.NewGuid()}@bitwarden.com"; + await _factory.LoginWithNewAccount(_email); + _organizationHelper = new SecretsManagerOrganizationHelper(_factory, _email); } - public Task DisposeAsync() => Task.CompletedTask; - - [Fact] - public async Task CreateProjectAccessPolicies() + public Task DisposeAsync() { - var initialProject = await _projectRepository.CreateAsync(new Project + _client.Dispose(); + return Task.CompletedTask; + } + + private async Task LoginAsync(string email) + { + var tokens = await _factory.LoginAsync(email); + _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tokens.Token); + } + + [Theory] + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(false, true)] + public async Task CreateProjectAccessPolicies_SmNotEnabled_NotFound(bool useSecrets, bool accessSecrets) + { + var (org, _) = await _organizationHelper.Initialize(useSecrets, accessSecrets); + await LoginAsync(_email); + + var project = await _projectRepository.CreateAsync(new Project { - OrganizationId = _organization.Id, - Name = _mockEncryptedString + OrganizationId = org.Id, + Name = _mockEncryptedString, }); - var initialServiceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount + var serviceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount { - OrganizationId = _organization.Id, - Name = _mockEncryptedString + OrganizationId = org.Id, + Name = _mockEncryptedString, }); var request = new AccessPoliciesCreateRequest { ServiceAccountAccessPolicyRequests = new List { - new() { GranteeId = initialServiceAccount.Id, Read = true, Write = true } - } + new() { GranteeId = serviceAccount.Id, Read = true, Write = true }, + }, }; - var response = await _client.PostAsJsonAsync($"/projects/{initialProject.Id}/access-policies", request); + var response = await _client.PostAsJsonAsync($"/projects/{project.Id}/access-policies", request); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Theory] + [InlineData(PermissionType.RunAsAdmin)] + [InlineData(PermissionType.RunAsUserWithPermission)] + public async Task CreateProjectAccessPolicies(PermissionType permissionType) + { + var (org, _) = await _organizationHelper.Initialize(true, true); + await LoginAsync(_email); + + var project = await _projectRepository.CreateAsync(new Project + { + OrganizationId = org.Id, + Name = _mockEncryptedString, + }); + + if (permissionType == PermissionType.RunAsUserWithPermission) + { + var (email, orgUser) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true); + await LoginAsync(email); + var accessPolicies = new List + { + new UserProjectAccessPolicy + { + GrantedProjectId = project.Id, OrganizationUserId = orgUser.Id, Read = true, Write = true, + }, + }; + await _accessPolicyRepository.CreateManyAsync(accessPolicies); + } + + var serviceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount + { + OrganizationId = org.Id, + Name = _mockEncryptedString, + }); + + var request = new AccessPoliciesCreateRequest + { + ServiceAccountAccessPolicyRequests = new List + { + new() { GranteeId = serviceAccount.Id, Read = true, Write = true }, + }, + }; + + var response = await _client.PostAsJsonAsync($"/projects/{project.Id}/access-policies", request); response.EnsureSuccessStatusCode(); var result = await response.Content.ReadFromJsonAsync(); Assert.NotNull(result); - Assert.Equal(initialServiceAccount.Id, result!.ServiceAccountAccessPolicies.First().ServiceAccountId); + Assert.Equal(serviceAccount.Id, result!.ServiceAccountAccessPolicies.First().ServiceAccountId); Assert.True(result.ServiceAccountAccessPolicies.First().Read); Assert.True(result.ServiceAccountAccessPolicies.First().Write); AssertHelper.AssertRecent(result.ServiceAccountAccessPolicies.First().RevisionDate); @@ -91,15 +153,103 @@ public class AccessPoliciesControllerTest : IClassFixture } [Fact] - public async Task UpdateAccessPolicy() + public async Task CreateProjectAccessPolicies_NoPermission() { - var initData = await SetupAccessPolicyRequest(); + // Create a new account as a user + var (org, _) = await _organizationHelper.Initialize(true, true); + var (email, orgUser) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true); + await LoginAsync(email); + + var project = await _projectRepository.CreateAsync(new Project + { + OrganizationId = org.Id, + Name = _mockEncryptedString, + }); + + var serviceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount + { + OrganizationId = org.Id, + Name = _mockEncryptedString, + }); + + var request = new AccessPoliciesCreateRequest + { + ServiceAccountAccessPolicyRequests = new List + { + new() { GranteeId = serviceAccount.Id, Read = true, Write = true }, + }, + }; + + var response = await _client.PostAsJsonAsync($"/projects/{project.Id}/access-policies", request); + + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Theory] + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(false, true)] + public async Task UpdateAccessPolicy_SmNotEnabled_NotFound(bool useSecrets, bool accessSecrets) + { + var (org, _) = await _organizationHelper.Initialize(useSecrets, accessSecrets); + await LoginAsync(_email); + var initData = await SetupAccessPolicyRequest(org.Id); const bool expectedRead = true; const bool expectedWrite = false; var request = new AccessPolicyUpdateRequest { Read = expectedRead, Write = expectedWrite }; - var response = await _client.PutAsJsonAsync($"/access-policies/{initData.InitialAccessPolicyId}", request); + var response = await _client.PutAsJsonAsync($"/access-policies/{initData.AccessPolicyId}", request); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Fact] + public async Task UpdateAccessPolicy_NoPermission() + { + // Create a new account as a user + var (org, _) = await _organizationHelper.Initialize(true, true); + var (email, orgUser) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true); + await LoginAsync(email); + + var initData = await SetupAccessPolicyRequest(orgUser.OrganizationId); + + const bool expectedRead = true; + const bool expectedWrite = false; + var request = new AccessPolicyUpdateRequest { Read = expectedRead, Write = expectedWrite }; + + var response = await _client.PutAsJsonAsync($"/access-policies/{initData.AccessPolicyId}", request); + + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Theory] + [InlineData(PermissionType.RunAsAdmin)] + [InlineData(PermissionType.RunAsUserWithPermission)] + public async Task UpdateAccessPolicy(PermissionType permissionType) + { + var (org, _) = await _organizationHelper.Initialize(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 UserProjectAccessPolicy + { + GrantedProjectId = initData.ProjectId, OrganizationUserId = orgUser.Id, Read = true, Write = true, + }, + }; + await _accessPolicyRepository.CreateManyAsync(accessPolicies); + } + + const bool expectedRead = true; + const bool expectedWrite = false; + var request = new AccessPolicyUpdateRequest { Read = expectedRead, Write = expectedWrite }; + + var response = await _client.PutAsJsonAsync($"/access-policies/{initData.AccessPolicyId}", request); response.EnsureSuccessStatusCode(); var result = await response.Content.ReadFromJsonAsync(); @@ -116,29 +266,78 @@ public class AccessPoliciesControllerTest : IClassFixture AssertHelper.AssertRecent(updatedAccessPolicy.RevisionDate); } + [Theory] + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(false, true)] + public async Task DeleteAccessPolicy_SmNotEnabled_NotFound(bool useSecrets, bool accessSecrets) + { + var (org, _) = await _organizationHelper.Initialize(useSecrets, accessSecrets); + await LoginAsync(_email); + var initData = await SetupAccessPolicyRequest(org.Id); + + var response = await _client.DeleteAsync($"/access-policies/{initData.AccessPolicyId}"); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } [Fact] - public async Task DeleteAccessPolicy() + public async Task DeleteAccessPolicy_NoPermission() { - var initData = await SetupAccessPolicyRequest(); + // Create a new account as a user + var (org, _) = await _organizationHelper.Initialize(true, true); + var (email, orgUser) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true); + await LoginAsync(email); - var response = await _client.DeleteAsync($"/access-policies/{initData.InitialAccessPolicyId}"); + var initData = await SetupAccessPolicyRequest(orgUser.OrganizationId); + + var response = await _client.DeleteAsync($"/access-policies/{initData.AccessPolicyId}"); + + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Theory] + [InlineData(PermissionType.RunAsAdmin)] + [InlineData(PermissionType.RunAsUserWithPermission)] + public async Task DeleteAccessPolicy(PermissionType permissionType) + { + var (org, _) = await _organizationHelper.Initialize(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 UserProjectAccessPolicy + { + GrantedProjectId = initData.ProjectId, OrganizationUserId = orgUser.Id, Read = true, Write = true, + }, + }; + await _accessPolicyRepository.CreateManyAsync(accessPolicies); + } + + var response = await _client.DeleteAsync($"/access-policies/{initData.AccessPolicyId}"); response.EnsureSuccessStatusCode(); - var test = await _accessPolicyRepository.GetByIdAsync(initData.InitialAccessPolicyId); + var test = await _accessPolicyRepository.GetByIdAsync(initData.AccessPolicyId); Assert.Null(test); } [Fact] public async Task GetProjectAccessPolicies_ReturnsEmpty() { - var initialProject = await _projectRepository.CreateAsync(new Project + var (org, _) = await _organizationHelper.Initialize(true, true); + await LoginAsync(_email); + + var project = await _projectRepository.CreateAsync(new Project { - OrganizationId = _organization.Id, + OrganizationId = org.Id, Name = _mockEncryptedString, }); - var response = await _client.GetAsync($"/projects/{initialProject.Id}/access-policies"); + var response = await _client.GetAsync($"/projects/{project.Id}/access-policies"); response.EnsureSuccessStatusCode(); var result = await response.Content.ReadFromJsonAsync(); @@ -149,12 +348,59 @@ public class AccessPoliciesControllerTest : IClassFixture Assert.Empty(result!.ServiceAccountAccessPolicies); } - [Fact] - public async Task GetProjectAccessPolicies() + [Theory] + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(false, true)] + public async Task GetProjectAccessPolicies_SmNotEnabled_NotFound(bool useSecrets, bool accessSecrets) { - var initData = await SetupAccessPolicyRequest(); + var (org, _) = await _organizationHelper.Initialize(useSecrets, accessSecrets); + await LoginAsync(_email); + var initData = await SetupAccessPolicyRequest(org.Id); - var response = await _client.GetAsync($"/projects/{initData.InitialProjectId}/access-policies"); + var response = await _client.GetAsync($"/projects/{initData.ProjectId}/access-policies"); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Fact] + public async Task GetProjectAccessPolicies_NoPermission() + { + // Create a new account as a user + var (org, _) = await _organizationHelper.Initialize(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($"/projects/{initData.ProjectId}/access-policies"); + + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Theory] + [InlineData(PermissionType.RunAsAdmin)] + [InlineData(PermissionType.RunAsUserWithPermission)] + public async Task GetProjectAccessPolicies(PermissionType permissionType) + { + var (org, _) = await _organizationHelper.Initialize(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 UserProjectAccessPolicy + { + GrantedProjectId = initData.ProjectId, OrganizationUserId = orgUser.Id, Read = true, Write = true, + }, + }; + await _accessPolicyRepository.CreateManyAsync(accessPolicies); + } + + var response = await _client.GetAsync($"/projects/{initData.ProjectId}/access-policies"); response.EnsureSuccessStatusCode(); var result = await response.Content.ReadFromJsonAsync(); @@ -163,44 +409,166 @@ public class AccessPoliciesControllerTest : IClassFixture Assert.Single(result!.ServiceAccountAccessPolicies); } - private async Task SetupAccessPolicyRequest() + [Theory] + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(false, true)] + public async Task GetPeoplePotentialGrantees_SmNotEnabled_NotFound(bool useSecrets, bool accessSecrets) { - var initialProject = await _projectRepository.CreateAsync(new Project + var (org, _) = await _organizationHelper.Initialize(useSecrets, accessSecrets); + await LoginAsync(_email); + + var response = + await _client.GetAsync( + $"/organizations/{org.Id}/access-policies/people/potential-grantees"); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Theory] + [InlineData(PermissionType.RunAsAdmin)] + [InlineData(PermissionType.RunAsUserWithPermission)] + public async Task GetPeoplePotentialGrantees_Success(PermissionType permissionType) + { + var (org, _) = await _organizationHelper.Initialize(true, true); + await LoginAsync(_email); + + if (permissionType == PermissionType.RunAsUserWithPermission) { - OrganizationId = _organization.Id, + var (email, orgUser) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true); + await LoginAsync(email); + } + + var response = + await _client.GetAsync( + $"/organizations/{org.Id}/access-policies/people/potential-grantees"); + response.EnsureSuccessStatusCode(); + + var result = await response.Content.ReadFromJsonAsync>(); + + Assert.NotNull(result?.Data); + Assert.NotEmpty(result!.Data); + } + + [Theory] + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(false, true)] + public async Task GetServiceAccountPotentialGrantees_SmNotEnabled_NotFound(bool useSecrets, bool accessSecrets) + { + var (org, _) = await _organizationHelper.Initialize(useSecrets, accessSecrets); + await LoginAsync(_email); + + var response = + await _client.GetAsync( + $"/organizations/{org.Id}/access-policies/service-accounts/potential-grantees"); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Fact] + public async Task GetServiceAccountPotentialGrantees_OnlyReturnsServiceAccountsWithWriteAccess() + { + // Create a new account as a user + var (org, _) = await _organizationHelper.Initialize(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 initialServiceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount + + var response = + await _client.GetAsync( + $"/organizations/{org.Id}/access-policies/service-accounts/potential-grantees"); + response.EnsureSuccessStatusCode(); + + var result = await response.Content.ReadFromJsonAsync>(); + + Assert.NotNull(result?.Data); + Assert.Empty(result!.Data); + } + + [Theory] + [InlineData(PermissionType.RunAsAdmin)] + [InlineData(PermissionType.RunAsUserWithPermission)] + public async Task GetServiceAccountsPotentialGrantees_Success(PermissionType permissionType) + { + var (org, _) = await _organizationHelper.Initialize(true, true); + await LoginAsync(_email); + + var serviceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount { - OrganizationId = _organization.Id, + OrganizationId = org.Id, Name = _mockEncryptedString, }); - var initialAccessPolicy = await _accessPolicyRepository.CreateManyAsync( + if (permissionType == PermissionType.RunAsUserWithPermission) + { + var (email, orgUser) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true); + await LoginAsync(email); + + await _accessPolicyRepository.CreateManyAsync( + new List + { + new UserServiceAccountAccessPolicy + { + GrantedServiceAccountId = serviceAccount.Id, + OrganizationUserId = orgUser.Id, + Read = true, + Write = true, + }, + }); + } + + var response = + await _client.GetAsync( + $"/organizations/{org.Id}/access-policies/service-accounts/potential-grantees"); + response.EnsureSuccessStatusCode(); + + var result = await response.Content.ReadFromJsonAsync>(); + + Assert.NotNull(result?.Data); + Assert.NotEmpty(result!.Data); + Assert.Equal(serviceAccount.Id.ToString(), result!.Data.First(x => x.Id == serviceAccount.Id.ToString()).Id); + } + + private async Task SetupAccessPolicyRequest(Guid organizationId) + { + var project = await _projectRepository.CreateAsync(new Project + { + OrganizationId = organizationId, + Name = _mockEncryptedString, + }); + + var serviceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount + { + OrganizationId = organizationId, + Name = _mockEncryptedString, + }); + + var accessPolicy = await _accessPolicyRepository.CreateManyAsync( new List { new ServiceAccountProjectAccessPolicy { - Read = true, - Write = true, - ServiceAccountId = initialServiceAccount.Id, - GrantedProjectId = initialProject.Id, - } + Read = true, Write = true, ServiceAccountId = serviceAccount.Id, GrantedProjectId = project.Id, + }, }); return new RequestSetupData { - InitialProjectId = initialProject.Id, - InitialServiceAccountId = initialServiceAccount.Id, - InitialAccessPolicyId = initialAccessPolicy.First().Id, + ProjectId = project.Id, + ServiceAccountId = serviceAccount.Id, + AccessPolicyId = accessPolicy.First().Id, }; } private class RequestSetupData { - public Guid InitialProjectId { get; set; } - public Guid InitialAccessPolicyId { get; set; } - public Guid InitialServiceAccountId { get; set; } + public Guid ProjectId { get; set; } + public Guid AccessPolicyId { get; set; } + public Guid ServiceAccountId { get; set; } } } diff --git a/test/Api.IntegrationTest/SecretsManager/Enums/PermissionType.cs b/test/Api.IntegrationTest/SecretsManager/Enums/PermissionType.cs new file mode 100644 index 000000000..7f1c4d7b9 --- /dev/null +++ b/test/Api.IntegrationTest/SecretsManager/Enums/PermissionType.cs @@ -0,0 +1,7 @@ +namespace Bit.Api.IntegrationTest.SecretsManager.Enums; + +public enum PermissionType +{ + RunAsAdmin, + RunAsUserWithPermission, +} diff --git a/test/Api.Test/SecretsManager/Controllers/AccessPoliciesControllerTests.cs b/test/Api.Test/SecretsManager/Controllers/AccessPoliciesControllerTests.cs index 1a12a5a7e..34bfce542 100644 --- a/test/Api.Test/SecretsManager/Controllers/AccessPoliciesControllerTests.cs +++ b/test/Api.Test/SecretsManager/Controllers/AccessPoliciesControllerTests.cs @@ -1,8 +1,15 @@ using Bit.Api.SecretsManager.Controllers; using Bit.Api.SecretsManager.Models.Request; +using Bit.Core.Context; +using Bit.Core.Entities; +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.Repositories; +using Bit.Core.Services; +using Bit.Core.Test.SecretsManager.AutoFixture.ProjectsFixture; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.Helpers; @@ -14,14 +21,64 @@ namespace Bit.Api.Test.SecretsManager.Controllers; [ControllerCustomize(typeof(AccessPoliciesController))] [SutProviderCustomize] +[ProjectCustomize] [JsonDocumentCustomize] public class AccessPoliciesControllerTests { - [Theory] - [BitAutoData] - public async void GetAccessPoliciesByProject_ReturnsEmptyList(SutProvider sutProvider, - Guid id) + public enum PermissionType { + RunAsAdmin, + RunAsUserWithPermission, + } + + private static void SetupAdmin(SutProvider sutProvider, Project data) + { + sutProvider.GetDependency().AccessSecretsManager(default).ReturnsForAnyArgs(true); + sutProvider.GetDependency().GetByIdAsync(default).ReturnsForAnyArgs(data); + sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(Guid.NewGuid()); + sutProvider.GetDependency().OrganizationAdmin(data.OrganizationId).Returns(true); + } + + private static void SetupUserWithPermission(SutProvider sutProvider, Project data) + { + sutProvider.GetDependency().AccessSecretsManager(default).ReturnsForAnyArgs(true); + sutProvider.GetDependency().GetByIdAsync(default).ReturnsForAnyArgs(data); + sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(Guid.NewGuid()); + sutProvider.GetDependency().OrganizationAdmin(data.OrganizationId).Returns(false); + sutProvider.GetDependency().OrganizationUser(default).ReturnsForAnyArgs(true); + sutProvider.GetDependency().UserHasWriteAccessToProject(default, default) + .ReturnsForAnyArgs(true); + } + + private static void SetupUserWithoutPermission(SutProvider sutProvider, Project data) + { + sutProvider.GetDependency().AccessSecretsManager(default).ReturnsForAnyArgs(true); + sutProvider.GetDependency().GetByIdAsync(default).ReturnsForAnyArgs(data); + sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(Guid.NewGuid()); + sutProvider.GetDependency().OrganizationAdmin(data.OrganizationId).Returns(false); + sutProvider.GetDependency().OrganizationUser(default).ReturnsForAnyArgs(true); + sutProvider.GetDependency().UserHasWriteAccessToProject(default, default) + .ReturnsForAnyArgs(false); + } + + [Theory] + [BitAutoData(PermissionType.RunAsAdmin)] + [BitAutoData(PermissionType.RunAsUserWithPermission)] + public async void GetAccessPoliciesByProject_ReturnsEmptyList( + PermissionType permissionType, + SutProvider sutProvider, + Guid id, Project data) + { + switch (permissionType) + { + case PermissionType.RunAsAdmin: + SetupAdmin(sutProvider, data); + break; + case PermissionType.RunAsUserWithPermission: + SetupUserWithPermission(sutProvider, data); + break; + } + var result = await sutProvider.Sut.GetProjectAccessPoliciesAsync(id); await sutProvider.GetDependency().Received(1) @@ -34,9 +91,39 @@ public class AccessPoliciesControllerTests [Theory] [BitAutoData] - public async void GetAccessPoliciesByProject_Success(SutProvider sutProvider, Guid id, + public async void GetAccessPoliciesByProject_UserWithoutPermission_Throws( + SutProvider sutProvider, + Guid id, + Project data) + { + SetupUserWithoutPermission(sutProvider, data); + + await Assert.ThrowsAsync(() => sutProvider.Sut.GetProjectAccessPoliciesAsync(id)); + + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .GetManyByGrantedProjectIdAsync(Arg.Any()); + } + + [Theory] + [BitAutoData(PermissionType.RunAsAdmin)] + [BitAutoData(PermissionType.RunAsUserWithPermission)] + public async void GetAccessPoliciesByProject_Admin_Success( + PermissionType permissionType, + SutProvider sutProvider, + Guid id, + Project data, UserProjectAccessPolicy resultAccessPolicy) { + switch (permissionType) + { + case PermissionType.RunAsAdmin: + SetupAdmin(sutProvider, data); + break; + case PermissionType.RunAsUserWithPermission: + SetupUserWithPermission(sutProvider, data); + break; + } + sutProvider.GetDependency().GetManyByGrantedProjectIdAsync(default) .ReturnsForAnyArgs(new List { resultAccessPolicy }); @@ -52,36 +139,244 @@ public class AccessPoliciesControllerTests [Theory] [BitAutoData] - public async void CreateAccessPolicies_Success(SutProvider sutProvider, Guid id, - UserProjectAccessPolicy data, AccessPoliciesCreateRequest request) + public async void GetAccessPoliciesByProject_ProjectsExist_UserWithoutPermission_Throws( + SutProvider sutProvider, + Guid id, + Project data, + UserProjectAccessPolicy resultAccessPolicy) { - sutProvider.GetDependency().CreateAsync(default) - .ReturnsForAnyArgs(new List { data }); - var result = await sutProvider.Sut.CreateProjectAccessPoliciesAsync(id, request); - await sutProvider.GetDependency().Received(1) - .CreateAsync(Arg.Any>()); - } + SetupUserWithoutPermission(sutProvider, data); + sutProvider.GetDependency().GetManyByGrantedProjectIdAsync(default) + .ReturnsForAnyArgs(new List { resultAccessPolicy }); + await Assert.ThrowsAsync(() => sutProvider.Sut.GetProjectAccessPoliciesAsync(id)); + + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .GetManyByGrantedProjectIdAsync(Arg.Any()); + } [Theory] [BitAutoData] - public async void UpdateAccessPolicies_Success(SutProvider sutProvider, Guid id, - UserProjectAccessPolicy data, AccessPolicyUpdateRequest request) + public async void CreateAccessPolicies_Success( + SutProvider sutProvider, + Guid id, + UserProjectAccessPolicy data, + AccessPoliciesCreateRequest request) { - sutProvider.GetDependency().UpdateAsync(default, default, default) + sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(Guid.NewGuid()); + sutProvider.GetDependency().CreateForProjectAsync(default, default, default) + .ReturnsForAnyArgs(new List { data }); + + await sutProvider.Sut.CreateProjectAccessPoliciesAsync(id, request); + + await sutProvider.GetDependency().Received(1) + .CreateForProjectAsync(Arg.Any(), Arg.Any>(), Arg.Any()); + } + + [Theory] + [BitAutoData] + public async void UpdateAccessPolicies_Success( + SutProvider sutProvider, + Guid id, + UserProjectAccessPolicy data, + AccessPolicyUpdateRequest request) + { + sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(Guid.NewGuid()); + sutProvider.GetDependency().UpdateAsync(default, default, default, default) .ReturnsForAnyArgs(data); - var result = await sutProvider.Sut.UpdateAccessPolicyAsync(id, request); + + await sutProvider.Sut.UpdateAccessPolicyAsync(id, request); + await sutProvider.GetDependency().Received(1) - .UpdateAsync(Arg.Any(), Arg.Is(request.Read), Arg.Is(request.Write)); + .UpdateAsync(Arg.Any(), Arg.Is(request.Read), Arg.Is(request.Write), Arg.Any()); } [Theory] [BitAutoData] public async void DeleteAccessPolicies_Success(SutProvider sutProvider, Guid id) { - sutProvider.GetDependency().DeleteAsync(default).ReturnsNull(); + sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(Guid.NewGuid()); + sutProvider.GetDependency().DeleteAsync(default, default).ReturnsNull(); + await sutProvider.Sut.DeleteAccessPolicyAsync(id); + await sutProvider.GetDependency().Received(1) - .DeleteAsync(Arg.Any()); + .DeleteAsync(Arg.Any(), Arg.Any()); + } + + [Theory] + [BitAutoData(PermissionType.RunAsAdmin)] + [BitAutoData(PermissionType.RunAsUserWithPermission)] + public async void GetPeoplePotentialGranteesAsync_ReturnsEmptyList( + PermissionType permissionType, + SutProvider sutProvider, + Guid id) + { + switch (permissionType) + { + case PermissionType.RunAsAdmin: + sutProvider.GetDependency().OrganizationAdmin(id).Returns(true); + sutProvider.GetDependency().AccessSecretsManager(default).ReturnsForAnyArgs(true); + sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(Guid.NewGuid()); + break; + case PermissionType.RunAsUserWithPermission: + sutProvider.GetDependency().OrganizationAdmin(id).Returns(false); + sutProvider.GetDependency().AccessSecretsManager(default).ReturnsForAnyArgs(true); + sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(Guid.NewGuid()); + break; + } + + 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))); + + Assert.Empty(result.Data); + } + + [Theory] + [BitAutoData] + public async void GetPeoplePotentialGranteesAsync_UserWithoutPermission_Throws( + SutProvider sutProvider, + Guid id) + { + sutProvider.GetDependency().OrganizationAdmin(id).Returns(false); + sutProvider.GetDependency().AccessSecretsManager(default).ReturnsForAnyArgs(false); + sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(Guid.NewGuid()); + + 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()); + } + + [Theory] + [BitAutoData(PermissionType.RunAsAdmin)] + [BitAutoData(PermissionType.RunAsUserWithPermission)] + public async void GetPeoplePotentialGranteesAsync_Success( + PermissionType permissionType, + SutProvider sutProvider, + Guid id, + Group mockGroup) + { + switch (permissionType) + { + case PermissionType.RunAsAdmin: + sutProvider.GetDependency().OrganizationAdmin(id).Returns(true); + sutProvider.GetDependency().AccessSecretsManager(default).ReturnsForAnyArgs(true); + sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(Guid.NewGuid()); + break; + case PermissionType.RunAsUserWithPermission: + sutProvider.GetDependency().OrganizationAdmin(id).Returns(false); + sutProvider.GetDependency().AccessSecretsManager(default).ReturnsForAnyArgs(true); + sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(Guid.NewGuid()); + break; + } + + sutProvider.GetDependency().GetManyByOrganizationIdAsync(default) + .ReturnsForAnyArgs(new List { mockGroup }); + + 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))); + + Assert.NotEmpty(result.Data); + } + + [Theory] + [BitAutoData(PermissionType.RunAsAdmin)] + [BitAutoData(PermissionType.RunAsUserWithPermission)] + public async void GetServiceAccountsPotentialGranteesAsync_ReturnsEmptyList( + PermissionType permissionType, + SutProvider sutProvider, + Guid id) + { + switch (permissionType) + { + case PermissionType.RunAsAdmin: + sutProvider.GetDependency().OrganizationAdmin(id).Returns(true); + sutProvider.GetDependency().AccessSecretsManager(default).ReturnsForAnyArgs(true); + sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(Guid.NewGuid()); + break; + case PermissionType.RunAsUserWithPermission: + sutProvider.GetDependency().OrganizationAdmin(id).Returns(false); + sutProvider.GetDependency().AccessSecretsManager(default).ReturnsForAnyArgs(true); + sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(Guid.NewGuid()); + break; + } + + var result = await sutProvider.Sut.GetServiceAccountsPotentialGranteesAsync(id); + + await sutProvider.GetDependency().Received(1) + .GetManyByOrganizationIdWriteAccessAsync(Arg.Is(AssertHelper.AssertPropertyEqual(id)), + Arg.Is(AssertHelper.AssertPropertyEqual(id)), + Arg.Any()); + + Assert.Empty(result.Data); + } + + [Theory] + [BitAutoData] + public async void GetServiceAccountsPotentialGranteesAsync_UserWithoutPermission_Throws( + SutProvider sutProvider, + Guid id) + { + sutProvider.GetDependency().OrganizationAdmin(id).Returns(false); + sutProvider.GetDependency().AccessSecretsManager(default).ReturnsForAnyArgs(false); + sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(Guid.NewGuid()); + + await Assert.ThrowsAsync(() => sutProvider.Sut.GetServiceAccountsPotentialGranteesAsync(id)); + + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .GetManyByOrganizationIdWriteAccessAsync(Arg.Any(), Arg.Any(), Arg.Any()); + } + + [Theory] + [BitAutoData(PermissionType.RunAsAdmin)] + [BitAutoData(PermissionType.RunAsUserWithPermission)] + public async void GetServiceAccountsPotentialGranteesAsync_Success( + PermissionType permissionType, + SutProvider sutProvider, + Guid id, + ServiceAccount mockServiceAccount) + { + switch (permissionType) + { + case PermissionType.RunAsAdmin: + sutProvider.GetDependency().OrganizationAdmin(id).Returns(true); + sutProvider.GetDependency().AccessSecretsManager(default).ReturnsForAnyArgs(true); + sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(Guid.NewGuid()); + break; + case PermissionType.RunAsUserWithPermission: + sutProvider.GetDependency().OrganizationAdmin(id).Returns(false); + sutProvider.GetDependency().AccessSecretsManager(default).ReturnsForAnyArgs(true); + sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(Guid.NewGuid()); + break; + } + + sutProvider.GetDependency().GetManyByOrganizationIdWriteAccessAsync(default, default, default) + .ReturnsForAnyArgs(new List { mockServiceAccount }); + + var result = await sutProvider.Sut.GetServiceAccountsPotentialGranteesAsync(id); + + await sutProvider.GetDependency().Received(1) + .GetManyByOrganizationIdWriteAccessAsync(Arg.Is(AssertHelper.AssertPropertyEqual(id)), + Arg.Is(AssertHelper.AssertPropertyEqual(id)), + Arg.Any()); + + Assert.NotEmpty(result.Data); } }