diff --git a/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/Secrets/CreateSecretCommand.cs b/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/Secrets/CreateSecretCommand.cs index d224247c1..61558ad22 100644 --- a/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/Secrets/CreateSecretCommand.cs +++ b/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/Secrets/CreateSecretCommand.cs @@ -1,4 +1,7 @@ -using Bit.Core.SecretsManager.Commands.Secrets.Interfaces; +using Bit.Core.Context; +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.SecretsManager.Commands.Secrets.Interfaces; using Bit.Core.SecretsManager.Entities; using Bit.Core.SecretsManager.Repositories; @@ -7,14 +10,34 @@ namespace Bit.Commercial.Core.SecretsManager.Commands.Secrets; public class CreateSecretCommand : ICreateSecretCommand { private readonly ISecretRepository _secretRepository; + private readonly IProjectRepository _projectRepository; + private readonly ICurrentContext _currentContext; - public CreateSecretCommand(ISecretRepository secretRepository) + public CreateSecretCommand(ISecretRepository secretRepository, IProjectRepository projectRepository, ICurrentContext currentContext) { _secretRepository = secretRepository; + _projectRepository = projectRepository; + _currentContext = currentContext; } - public async Task CreateAsync(Secret secret) + public async Task CreateAsync(Secret secret, Guid userId) { + var orgAdmin = await _currentContext.OrganizationAdmin(secret.OrganizationId); + var accessClient = AccessClientHelper.ToAccessClient(_currentContext.ClientType, orgAdmin); + var project = secret.Projects?.FirstOrDefault(); + + var hasAccess = accessClient switch + { + AccessClientType.NoAccessCheck => true, + AccessClientType.User => project != null && await _projectRepository.UserHasWriteAccessToProject(project.Id, userId), + _ => false, + }; + + if (!hasAccess) + { + throw new NotFoundException(); + } + return await _secretRepository.CreateAsync(secret); } } diff --git a/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/Secrets/DeleteSecretCommand.cs b/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/Secrets/DeleteSecretCommand.cs index c1872717b..2d59cffe3 100644 --- a/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/Secrets/DeleteSecretCommand.cs +++ b/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/Secrets/DeleteSecretCommand.cs @@ -1,4 +1,5 @@ using Bit.Core.Context; +using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.SecretsManager.Commands.Secrets.Interfaces; using Bit.Core.SecretsManager.Entities; @@ -10,25 +11,27 @@ public class DeleteSecretCommand : IDeleteSecretCommand { private readonly ICurrentContext _currentContext; private readonly ISecretRepository _secretRepository; + private readonly IProjectRepository _projectRepository; - public DeleteSecretCommand(ICurrentContext currentContext, ISecretRepository secretRepository) + public DeleteSecretCommand(ISecretRepository secretRepository, IProjectRepository projectRepository, ICurrentContext currentContext) { _currentContext = currentContext; _secretRepository = secretRepository; + _projectRepository = projectRepository; } - public async Task>> DeleteSecrets(List ids) + public async Task>> DeleteSecrets(List ids, Guid userId) { - var secrets = await _secretRepository.GetManyByIds(ids); + var secrets = (await _secretRepository.GetManyByIds(ids)).ToList(); - if (secrets?.Any() != true) + if (secrets.Any() != true) { throw new NotFoundException(); } // Ensure all secrets belongs to the same organization var organizationId = secrets.First().OrganizationId; - if (secrets.Any(p => p.OrganizationId != organizationId)) + if (secrets.Any(secret => secret.OrganizationId != organizationId)) { throw new BadRequestException(); } @@ -38,21 +41,46 @@ public class DeleteSecretCommand : IDeleteSecretCommand throw new NotFoundException(); } - var results = ids.Select(id => + var orgAdmin = await _currentContext.OrganizationAdmin(organizationId); + var accessClient = AccessClientHelper.ToAccessClient(_currentContext.ClientType, orgAdmin); + + var results = new List>(); + var deleteIds = new List(); + + foreach (var secret in secrets) { - var secret = secrets.FirstOrDefault(secret => secret.Id == id); - if (secret == null) + var hasAccess = orgAdmin; + + if (secret.Projects != null && secret.Projects?.Count > 0) { - throw new NotFoundException(); + var projectId = secret.Projects.First().Id; + + hasAccess = accessClient switch + { + AccessClientType.NoAccessCheck => true, + AccessClientType.User => await _projectRepository.UserHasWriteAccessToProject(projectId, userId), + _ => false, + }; + } + + if (!hasAccess) + { + results.Add(new Tuple(secret, "access denied")); } - // TODO Once permissions are implemented add check for each secret here. else { - return new Tuple(secret, ""); + deleteIds.Add(secret.Id); + results.Add(new Tuple(secret, "")); } - }).ToList(); + } + + + + if (deleteIds.Count > 0) + { + await _secretRepository.SoftDeleteManyByIdAsync(deleteIds); + } - await _secretRepository.SoftDeleteManyByIdAsync(ids); return results; } } diff --git a/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/Secrets/UpdateSecretCommand.cs b/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/Secrets/UpdateSecretCommand.cs index 4bd0cf4f3..583208adc 100644 --- a/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/Secrets/UpdateSecretCommand.cs +++ b/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/Secrets/UpdateSecretCommand.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.Secrets.Interfaces; using Bit.Core.SecretsManager.Entities; using Bit.Core.SecretsManager.Repositories; @@ -8,23 +10,45 @@ namespace Bit.Commercial.Core.SecretsManager.Commands.Secrets; public class UpdateSecretCommand : IUpdateSecretCommand { private readonly ISecretRepository _secretRepository; + private readonly IProjectRepository _projectRepository; + private readonly ICurrentContext _currentContext; - public UpdateSecretCommand(ISecretRepository secretRepository) + public UpdateSecretCommand(ISecretRepository secretRepository, IProjectRepository projectRepository, ICurrentContext currentContext) { _secretRepository = secretRepository; + _projectRepository = projectRepository; + _currentContext = currentContext; } - public async Task UpdateAsync(Secret secret) + public async Task UpdateAsync(Secret updatedSecret, Guid userId) { - var existingSecret = await _secretRepository.GetByIdAsync(secret.Id); - if (existingSecret == null) + var secret = await _secretRepository.GetByIdAsync(updatedSecret.Id); + if (secret == null || !_currentContext.AccessSecretsManager(secret.OrganizationId)) { throw new NotFoundException(); } - secret.OrganizationId = existingSecret.OrganizationId; - secret.CreationDate = existingSecret.CreationDate; - secret.DeletedDate = existingSecret.DeletedDate; + var orgAdmin = await _currentContext.OrganizationAdmin(secret.OrganizationId); + var accessClient = AccessClientHelper.ToAccessClient(_currentContext.ClientType, orgAdmin); + + var project = updatedSecret.Projects?.FirstOrDefault(); + + var hasAccess = accessClient switch + { + AccessClientType.NoAccessCheck => true, + AccessClientType.User => project != null && await _projectRepository.UserHasWriteAccessToProject(project.Id, userId), + _ => false, + }; + + if (!hasAccess) + { + throw new NotFoundException(); + } + + secret.Key = updatedSecret.Key; + secret.Value = updatedSecret.Value; + secret.Note = updatedSecret.Note; + secret.Projects = updatedSecret.Projects; secret.RevisionDate = DateTime.UtcNow; await _secretRepository.UpdateAsync(secret); diff --git a/bitwarden_license/src/Commercial.Infrastructure.EntityFramework/SecretsManager/Repositories/ProjectRepository.cs b/bitwarden_license/src/Commercial.Infrastructure.EntityFramework/SecretsManager/Repositories/ProjectRepository.cs index 20d80d42f..782486601 100644 --- a/bitwarden_license/src/Commercial.Infrastructure.EntityFramework/SecretsManager/Repositories/ProjectRepository.cs +++ b/bitwarden_license/src/Commercial.Infrastructure.EntityFramework/SecretsManager/Repositories/ProjectRepository.cs @@ -74,6 +74,9 @@ public class ProjectRepository : Repository> ServiceAccountHasReadAccessToProject(Guid serviceAccountId) => p => p.ServiceAccountAccessPolicies.Any(ap => ap.ServiceAccount.Id == serviceAccountId && ap.Read); + private static Expression> ServiceAccountHasWriteAccessToProject(Guid serviceAccountId) => p => + p.ServiceAccountAccessPolicies.Any(ap => ap.ServiceAccount.Id == serviceAccountId && ap.Write); + public async Task DeleteManyByIdAsync(IEnumerable ids) { using (var scope = ServiceScopeFactory.CreateScope()) @@ -100,6 +103,28 @@ public class ProjectRepository : Repository ServiceAccountHasReadAccessToProject(Guid id, Guid userId) + { + using var scope = ServiceScopeFactory.CreateScope(); + var dbContext = GetDatabaseContext(scope); + var query = dbContext.Project + .Where(p => p.Id == id) + .Where(ServiceAccountHasReadAccessToProject(userId)); + + return await query.AnyAsync(); + } + + public async Task ServiceAccountHasWriteAccessToProject(Guid id, Guid userId) + { + using var scope = ServiceScopeFactory.CreateScope(); + var dbContext = GetDatabaseContext(scope); + var query = dbContext.Project + .Where(p => p.Id == id) + .Where(ServiceAccountHasWriteAccessToProject(userId)); + + return await query.AnyAsync(); + } + public async Task UserHasReadAccessToProject(Guid id, Guid userId) { using var scope = ServiceScopeFactory.CreateScope(); diff --git a/bitwarden_license/src/Commercial.Infrastructure.EntityFramework/SecretsManager/Repositories/SecretRepository.cs b/bitwarden_license/src/Commercial.Infrastructure.EntityFramework/SecretsManager/Repositories/SecretRepository.cs index e2226c303..43f1adb59 100644 --- a/bitwarden_license/src/Commercial.Infrastructure.EntityFramework/SecretsManager/Repositories/SecretRepository.cs +++ b/bitwarden_license/src/Commercial.Infrastructure.EntityFramework/SecretsManager/Repositories/SecretRepository.cs @@ -1,4 +1,6 @@ -using AutoMapper; +using System.Linq.Expressions; +using AutoMapper; +using Bit.Core.Enums; using Bit.Core.SecretsManager.Repositories; using Bit.Infrastructure.EntityFramework; using Bit.Infrastructure.EntityFramework.Repositories; @@ -6,6 +8,7 @@ using Bit.Infrastructure.EntityFramework.SecretsManager.Models; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; + namespace Bit.Commercial.Infrastructure.EntityFramework.SecretsManager.Repositories; public class SecretRepository : Repository, ISecretRepository @@ -34,35 +37,58 @@ public class SecretRepository : Repository ids.Contains(c.Id) && c.DeletedDate == null) + .Include(c => c.Projects) .ToListAsync(); return Mapper.Map>(secrets); } } - public async Task> GetManyByOrganizationIdAsync(Guid organizationId) - { - using (var scope = ServiceScopeFactory.CreateScope()) - { - var dbContext = GetDatabaseContext(scope); - var secrets = await dbContext.Secret - .Where(c => c.OrganizationId == organizationId && c.DeletedDate == null) - .Include("Projects") - .OrderBy(c => c.RevisionDate) - .ToListAsync(); + private static Expression> ServiceAccountHasReadAccessToSecret(Guid serviceAccountId) => s => + s.Projects.Any(p => + p.ServiceAccountAccessPolicies.Any(ap => ap.ServiceAccount.Id == serviceAccountId && ap.Read)); - return Mapper.Map>(secrets); - } + private static Expression> UserHasReadAccessToSecret(Guid userId) => s => + s.Projects.Any(p => + p.UserAccessPolicies.Any(ap => ap.OrganizationUser.UserId == userId && ap.Read) || + p.GroupAccessPolicies.Any(ap => + ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.UserId == userId && ap.Read))); + + + public async Task> GetManyByOrganizationIdAsync(Guid organizationId, Guid userId, AccessClientType accessType) + { + using var scope = ServiceScopeFactory.CreateScope(); + var dbContext = GetDatabaseContext(scope); + var query = dbContext.Secret.Include(c => c.Projects).Where(c => c.OrganizationId == organizationId && c.DeletedDate == null); + + query = accessType switch + { + AccessClientType.NoAccessCheck => query, + AccessClientType.User => query.Where(UserHasReadAccessToSecret(userId)), + AccessClientType.ServiceAccount => query.Where(ServiceAccountHasReadAccessToSecret(userId)), + _ => throw new ArgumentOutOfRangeException(nameof(accessType), accessType, null), + }; + + var secrets = await query.OrderBy(c => c.RevisionDate).ToListAsync(); + return Mapper.Map>(secrets); } - public async Task> GetManyByProjectIdAsync(Guid projectId) + public async Task> GetManyByProjectIdAsync(Guid projectId, Guid userId, AccessClientType accessType) { using (var scope = ServiceScopeFactory.CreateScope()) { var dbContext = GetDatabaseContext(scope); - var secrets = await dbContext.Secret - .Where(s => s.Projects.Any(p => p.Id == projectId) && s.DeletedDate == null).Include("Projects") - .OrderBy(s => s.RevisionDate).ToListAsync(); + var query = dbContext.Secret.Include(s => s.Projects) + .Where(s => s.Projects.Any(p => p.Id == projectId) && s.DeletedDate == null); + query = accessType switch + { + AccessClientType.NoAccessCheck => query, + AccessClientType.User => query.Where(UserHasReadAccessToSecret(userId)), + AccessClientType.ServiceAccount => query.Where(ServiceAccountHasReadAccessToSecret(userId)), + _ => throw new ArgumentOutOfRangeException(nameof(accessType), accessType, null), + }; + + var secrets = await query.OrderBy(s => s.RevisionDate).ToListAsync(); return Mapper.Map>(secrets); } } @@ -96,6 +122,7 @@ public class SecretRepository : Repository(secret); + var entity = await dbContext.Secret .Include("Projects") .FirstAsync(s => s.Id == secret.Id); diff --git a/bitwarden_license/test/Commercial.Core.Test/SecretsManager/Secrets/CreateSecretCommandTests.cs b/bitwarden_license/test/Commercial.Core.Test/SecretsManager/Secrets/CreateSecretCommandTests.cs index 9accbc53f..78b8c2a02 100644 --- a/bitwarden_license/test/Commercial.Core.Test/SecretsManager/Secrets/CreateSecretCommandTests.cs +++ b/bitwarden_license/test/Commercial.Core.Test/SecretsManager/Secrets/CreateSecretCommandTests.cs @@ -1,4 +1,7 @@ using Bit.Commercial.Core.SecretsManager.Commands.Secrets; +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.SecretsFixture; @@ -14,14 +17,54 @@ namespace Bit.Commercial.Core.Test.SecretsManager.Secrets; public class CreateSecretCommandTests { [Theory] - [BitAutoData] - public async Task CreateAsync_CallsCreate(Secret data, - SutProvider sutProvider) + [BitAutoData(PermissionType.RunAsAdmin)] + [BitAutoData(PermissionType.RunAsUserWithPermission)] + public async Task CreateAsync_Success(PermissionType permissionType, Secret data, + SutProvider sutProvider, Guid userId, Project mockProject) { - await sutProvider.Sut.CreateAsync(data); + data.Projects = new List() { mockProject }; + + if (permissionType == PermissionType.RunAsAdmin) + { + sutProvider.GetDependency().OrganizationAdmin(data.OrganizationId).Returns(true); + } + else + { + sutProvider.GetDependency().OrganizationAdmin(data.OrganizationId).Returns(false); + sutProvider.GetDependency().UserHasWriteAccessToProject((Guid)(data.Projects?.First().Id), userId).Returns(true); + } + + await sutProvider.Sut.CreateAsync(data, userId); await sutProvider.GetDependency().Received(1) .CreateAsync(data); } + + + [Theory] + [BitAutoData] + public async Task CreateAsync_UserWithoutPermission_ThrowsNotFound(Secret data, + SutProvider sutProvider, Guid userId, Project mockProject) + { + data.Projects = new List() { mockProject }; + + sutProvider.GetDependency().OrganizationAdmin(data.OrganizationId).Returns(false); + sutProvider.GetDependency().UserHasWriteAccessToProject((Guid)(data.Projects?.First().Id), userId).Returns(false); + + await Assert.ThrowsAsync(() => + sutProvider.Sut.CreateAsync(data, userId)); + } + + [Theory] + [BitAutoData] + public async Task CreateAsync_NoProjects_User_ThrowsNotFound(Secret data, + SutProvider sutProvider, Guid userId) + { + data.Projects = null; + sutProvider.GetDependency().OrganizationAdmin(data.OrganizationId).Returns(false); + + await Assert.ThrowsAsync(() => + sutProvider.Sut.CreateAsync(data, userId)); + } } diff --git a/bitwarden_license/test/Commercial.Core.Test/SecretsManager/Secrets/DeleteSecretCommandTests.cs b/bitwarden_license/test/Commercial.Core.Test/SecretsManager/Secrets/DeleteSecretCommandTests.cs index cee70548e..8026fcd8e 100644 --- a/bitwarden_license/test/Commercial.Core.Test/SecretsManager/Secrets/DeleteSecretCommandTests.cs +++ b/bitwarden_license/test/Commercial.Core.Test/SecretsManager/Secrets/DeleteSecretCommandTests.cs @@ -1,16 +1,20 @@ using Bit.Commercial.Core.SecretsManager.Commands.Secrets; +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; using NSubstitute; using Xunit; namespace Bit.Commercial.Core.Test.SecretsManager.Secrets; [SutProviderCustomize] +[ProjectCustomize] public class DeleteSecretCommandTests { [Theory] @@ -20,7 +24,7 @@ public class DeleteSecretCommandTests { sutProvider.GetDependency().GetManyByIds(data).Returns(new List()); - var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.DeleteSecrets(data)); + var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.DeleteSecrets(data, default)); await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().SoftDeleteManyByIdAsync(default); } @@ -36,22 +40,39 @@ public class DeleteSecretCommandTests }; sutProvider.GetDependency().GetManyByIds(data).Returns(new List() { secret }); - var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.DeleteSecrets(data)); + var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.DeleteSecrets(data, default)); await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().SoftDeleteManyByIdAsync(default); } [Theory] - [BitAutoData] - public async Task DeleteSecrets_Success(List data, - SutProvider sutProvider) + [BitAutoData(PermissionType.RunAsAdmin)] + [BitAutoData(PermissionType.RunAsUserWithPermission)] + public async Task DeleteSecrets_Success(PermissionType permissionType, List data, + SutProvider sutProvider, Guid userId, Guid organizationId, Project mockProject) { + List projects = null; + + if (permissionType == PermissionType.RunAsAdmin) + { + sutProvider.GetDependency().OrganizationAdmin(organizationId).Returns(true); + } + else + { + sutProvider.GetDependency().OrganizationAdmin(organizationId).Returns(false); + sutProvider.GetDependency().UserHasWriteAccessToProject(mockProject.Id, userId).Returns(true); + projects = new List() { mockProject }; + } + + var secrets = new List(); foreach (Guid id in data) { var secret = new Secret() { - Id = id + Id = id, + OrganizationId = organizationId, + Projects = projects }; secrets.Add(secret); } @@ -59,9 +80,9 @@ public class DeleteSecretCommandTests sutProvider.GetDependency().GetManyByIds(data).Returns(secrets); sutProvider.GetDependency().AccessSecretsManager(default).ReturnsForAnyArgs(true); - var results = await sutProvider.Sut.DeleteSecrets(data); + var results = await sutProvider.Sut.DeleteSecrets(data, userId); + await sutProvider.GetDependency().Received(1).SoftDeleteManyByIdAsync(Arg.Is(AssertHelper.AssertPropertyEqual(data))); - await sutProvider.GetDependency().Received(1).SoftDeleteManyByIdAsync(Arg.Is(data)); foreach (var result in results) { Assert.Equal("", result.Item2); diff --git a/bitwarden_license/test/Commercial.Core.Test/SecretsManager/Secrets/UpdateSecretCommandTests.cs b/bitwarden_license/test/Commercial.Core.Test/SecretsManager/Secrets/UpdateSecretCommandTests.cs index 683868d7e..faa6e7ec5 100644 --- a/bitwarden_license/test/Commercial.Core.Test/SecretsManager/Secrets/UpdateSecretCommandTests.cs +++ b/bitwarden_license/test/Commercial.Core.Test/SecretsManager/Secrets/UpdateSecretCommandTests.cs @@ -1,7 +1,10 @@ using Bit.Commercial.Core.SecretsManager.Commands.Secrets; +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.Core.Test.SecretsManager.AutoFixture.SecretsFixture; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; @@ -13,23 +16,38 @@ namespace Bit.Commercial.Core.Test.SecretsManager.Secrets; [SutProviderCustomize] [SecretCustomize] +[ProjectCustomize] public class UpdateSecretCommandTests { [Theory] [BitAutoData] public async Task UpdateAsync_SecretDoesNotExist_ThrowsNotFound(Secret data, SutProvider sutProvider) { - var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.UpdateAsync(data)); + var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.UpdateAsync(data, default)); await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().UpdateAsync(default); } [Theory] - [BitAutoData] - public async Task UpdateAsync_CallsReplaceAsync(Secret data, SutProvider sutProvider) + [BitAutoData(PermissionType.RunAsAdmin)] + [BitAutoData(PermissionType.RunAsUserWithPermission)] + public async Task UpdateAsync_Success(PermissionType permissionType, Secret data, SutProvider sutProvider, Guid userId, Project mockProject) { + sutProvider.GetDependency().AccessSecretsManager(data.OrganizationId).Returns(true); + + if (permissionType == PermissionType.RunAsAdmin) + { + sutProvider.GetDependency().OrganizationAdmin(data.OrganizationId).Returns(true); + } + else + { + data.Projects = new List() { mockProject }; + sutProvider.GetDependency().OrganizationAdmin(data.OrganizationId).Returns(false); + sutProvider.GetDependency().UserHasWriteAccessToProject((Guid)(data.Projects?.First().Id), userId).Returns(true); + } + sutProvider.GetDependency().GetByIdAsync(data.Id).Returns(data); - await sutProvider.Sut.UpdateAsync(data); + await sutProvider.Sut.UpdateAsync(data, userId); await sutProvider.GetDependency().Received(1) .UpdateAsync(data); @@ -37,11 +55,14 @@ public class UpdateSecretCommandTests [Theory] [BitAutoData] - public async Task UpdateAsync_DoesNotModifyOrganizationId(Secret existingSecret, SutProvider sutProvider) + public async Task UpdateAsync_DoesNotModifyOrganizationId(Secret existingSecret, SutProvider sutProvider, Guid userId) { - sutProvider.GetDependency().GetByIdAsync(existingSecret.Id).Returns(existingSecret); - var updatedOrgId = Guid.NewGuid(); + sutProvider.GetDependency().OrganizationAdmin(existingSecret.OrganizationId).Returns(true); + sutProvider.GetDependency().AccessSecretsManager(existingSecret.OrganizationId).Returns(true); + sutProvider.GetDependency().GetByIdAsync(existingSecret.Id).Returns(existingSecret); + sutProvider.GetDependency().OrganizationAdmin(updatedOrgId).Returns(true); + var secretUpdate = new Secret() { OrganizationId = updatedOrgId, @@ -49,7 +70,7 @@ public class UpdateSecretCommandTests Key = existingSecret.Key, }; - var result = await sutProvider.Sut.UpdateAsync(secretUpdate); + var result = await sutProvider.Sut.UpdateAsync(secretUpdate, userId); Assert.Equal(existingSecret.OrganizationId, result.OrganizationId); Assert.NotEqual(existingSecret.OrganizationId, updatedOrgId); @@ -57,9 +78,11 @@ public class UpdateSecretCommandTests [Theory] [BitAutoData] - public async Task UpdateAsync_DoesNotModifyCreationDate(Secret existingSecret, SutProvider sutProvider) + public async Task UpdateAsync_DoesNotModifyCreationDate(Secret existingSecret, SutProvider sutProvider, Guid userId) { + sutProvider.GetDependency().AccessSecretsManager(existingSecret.OrganizationId).Returns(true); sutProvider.GetDependency().GetByIdAsync(existingSecret.Id).Returns(existingSecret); + sutProvider.GetDependency().OrganizationAdmin(existingSecret.OrganizationId).Returns(true); var updatedCreationDate = DateTime.UtcNow; var secretUpdate = new Secret() @@ -67,9 +90,10 @@ public class UpdateSecretCommandTests CreationDate = updatedCreationDate, Id = existingSecret.Id, Key = existingSecret.Key, + OrganizationId = existingSecret.OrganizationId }; - var result = await sutProvider.Sut.UpdateAsync(secretUpdate); + var result = await sutProvider.Sut.UpdateAsync(secretUpdate, userId); Assert.Equal(existingSecret.CreationDate, result.CreationDate); Assert.NotEqual(existingSecret.CreationDate, updatedCreationDate); @@ -77,9 +101,11 @@ public class UpdateSecretCommandTests [Theory] [BitAutoData] - public async Task UpdateAsync_DoesNotModifyDeletionDate(Secret existingSecret, SutProvider sutProvider) + public async Task UpdateAsync_DoesNotModifyDeletionDate(Secret existingSecret, SutProvider sutProvider, Guid userId) { + sutProvider.GetDependency().AccessSecretsManager(existingSecret.OrganizationId).Returns(true); sutProvider.GetDependency().GetByIdAsync(existingSecret.Id).Returns(existingSecret); + sutProvider.GetDependency().OrganizationAdmin(existingSecret.OrganizationId).Returns(true); var updatedDeletionDate = DateTime.UtcNow; var secretUpdate = new Secret() @@ -87,9 +113,10 @@ public class UpdateSecretCommandTests DeletedDate = updatedDeletionDate, Id = existingSecret.Id, Key = existingSecret.Key, + OrganizationId = existingSecret.OrganizationId }; - var result = await sutProvider.Sut.UpdateAsync(secretUpdate); + var result = await sutProvider.Sut.UpdateAsync(secretUpdate, userId); Assert.Equal(existingSecret.DeletedDate, result.DeletedDate); Assert.NotEqual(existingSecret.DeletedDate, updatedDeletionDate); @@ -98,9 +125,12 @@ public class UpdateSecretCommandTests [Theory] [BitAutoData] - public async Task UpdateAsync_RevisionDateIsUpdatedToUtcNow(Secret existingSecret, SutProvider sutProvider) + public async Task UpdateAsync_RevisionDateIsUpdatedToUtcNow(Secret existingSecret, SutProvider sutProvider, Guid userId) { + sutProvider.GetDependency().OrganizationAdmin(existingSecret.OrganizationId).Returns(true); + sutProvider.GetDependency().AccessSecretsManager(existingSecret.OrganizationId).Returns(true); sutProvider.GetDependency().GetByIdAsync(existingSecret.Id).Returns(existingSecret); + sutProvider.GetDependency().OrganizationAdmin(existingSecret.OrganizationId).Returns(true); var updatedRevisionDate = DateTime.UtcNow.AddDays(10); var secretUpdate = new Secret() @@ -108,11 +138,12 @@ public class UpdateSecretCommandTests RevisionDate = updatedRevisionDate, Id = existingSecret.Id, Key = existingSecret.Key, + OrganizationId = existingSecret.OrganizationId }; - var result = await sutProvider.Sut.UpdateAsync(secretUpdate); + var result = await sutProvider.Sut.UpdateAsync(secretUpdate, userId); - Assert.NotEqual(existingSecret.RevisionDate, result.RevisionDate); + Assert.NotEqual(secretUpdate.RevisionDate, result.RevisionDate); AssertHelper.AssertRecent(result.RevisionDate); } } diff --git a/src/Api/SecretsManager/Controllers/SecretsController.cs b/src/Api/SecretsManager/Controllers/SecretsController.cs index 3ddf4699d..7418fe0a1 100644 --- a/src/Api/SecretsManager/Controllers/SecretsController.cs +++ b/src/Api/SecretsManager/Controllers/SecretsController.cs @@ -2,9 +2,12 @@ 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.SecretsManager.Commands.Secrets.Interfaces; +using Bit.Core.SecretsManager.Entities; using Bit.Core.SecretsManager.Repositories; +using Bit.Core.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -16,22 +19,21 @@ public class SecretsController : Controller { private readonly ICurrentContext _currentContext; private readonly ISecretRepository _secretRepository; + private readonly IProjectRepository _projectRepository; private readonly ICreateSecretCommand _createSecretCommand; private readonly IUpdateSecretCommand _updateSecretCommand; private readonly IDeleteSecretCommand _deleteSecretCommand; + private readonly IUserService _userService; - public SecretsController( - ICurrentContext currentContext, - ISecretRepository secretRepository, - ICreateSecretCommand createSecretCommand, - IUpdateSecretCommand updateSecretCommand, - IDeleteSecretCommand deleteSecretCommand) + public SecretsController(ISecretRepository secretRepository, IProjectRepository projectRepository, ICreateSecretCommand createSecretCommand, IUpdateSecretCommand updateSecretCommand, IDeleteSecretCommand deleteSecretCommand, IUserService userService, ICurrentContext currentContext) { _currentContext = currentContext; _secretRepository = secretRepository; _createSecretCommand = createSecretCommand; _updateSecretCommand = updateSecretCommand; _deleteSecretCommand = deleteSecretCommand; + _projectRepository = projectRepository; + _userService = userService; } [HttpGet("organizations/{organizationId}/secrets")] @@ -42,7 +44,12 @@ public class SecretsController : Controller throw new NotFoundException(); } - var secrets = await _secretRepository.GetManyByOrganizationIdAsync(organizationId); + var userId = _userService.GetProperUserId(User).Value; + var orgAdmin = await _currentContext.OrganizationAdmin(organizationId); + var accessClient = AccessClientHelper.ToAccessClient(_currentContext.ClientType, orgAdmin); + + var secrets = await _secretRepository.GetManyByOrganizationIdAsync(organizationId, userId, accessClient); + return new SecretWithProjectsListResponseModel(secrets); } @@ -54,7 +61,8 @@ public class SecretsController : Controller throw new NotFoundException(); } - var result = await _createSecretCommand.CreateAsync(createRequest.ToSecret(organizationId)); + var userId = _userService.GetProperUserId(User).Value; + var result = await _createSecretCommand.CreateAsync(createRequest.ToSecret(organizationId), userId); return new SecretResponseModel(result); } @@ -62,34 +70,74 @@ public class SecretsController : Controller public async Task GetAsync([FromRoute] Guid id) { var secret = await _secretRepository.GetByIdAsync(id); - if (secret == null) + if (secret == null || !_currentContext.AccessSecretsManager(secret.OrganizationId)) { throw new NotFoundException(); } + + if (!await UserHasReadAccessToSecret(secret)) + { + throw new NotFoundException(); + } + return new SecretResponseModel(secret); } [HttpGet("projects/{projectId}/secrets")] public async Task GetSecretsByProjectAsync([FromRoute] Guid projectId) { - var secrets = await _secretRepository.GetManyByProjectIdAsync(projectId); - var responses = secrets.Select(s => new SecretResponseModel(s)); + var project = await _projectRepository.GetByIdAsync(projectId); + 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 secrets = await _secretRepository.GetManyByProjectIdAsync(projectId, userId, accessClient); + return new SecretWithProjectsListResponseModel(secrets); } [HttpPut("secrets/{id}")] - public async Task UpdateAsync([FromRoute] Guid id, [FromBody] SecretUpdateRequestModel updateRequest) + public async Task UpdateSecretAsync([FromRoute] Guid id, [FromBody] SecretUpdateRequestModel updateRequest) { - var result = await _updateSecretCommand.UpdateAsync(updateRequest.ToSecret(id)); + var userId = _userService.GetProperUserId(User).Value; + var secret = updateRequest.ToSecret(id); + var result = await _updateSecretCommand.UpdateAsync(secret, userId); return new SecretResponseModel(result); } - // TODO Once permissions are setup for Secrets Manager need to enforce them on delete. [HttpPost("secrets/delete")] public async Task> BulkDeleteAsync([FromBody] List ids) { - var results = await _deleteSecretCommand.DeleteSecrets(ids); + var userId = _userService.GetProperUserId(User).Value; + var results = await _deleteSecretCommand.DeleteSecrets(ids, userId); var responses = results.Select(r => new BulkDeleteResponseModel(r.Item1.Id, r.Item2)); return new ListResponseModel(responses); } + + public async Task UserHasReadAccessToSecret(Secret secret) + { + var userId = _userService.GetProperUserId(User).Value; + var orgAdmin = await _currentContext.OrganizationAdmin(secret.OrganizationId); + var accessClient = AccessClientHelper.ToAccessClient(_currentContext.ClientType, orgAdmin); + var hasAccess = orgAdmin; + + if (secret.Projects?.Count > 0) + { + Guid projectId = secret.Projects.FirstOrDefault().Id; + hasAccess = accessClient switch + { + AccessClientType.NoAccessCheck => true, + AccessClientType.User => await _projectRepository.UserHasReadAccessToProject(projectId, userId), + AccessClientType.ServiceAccount => await _projectRepository.ServiceAccountHasReadAccessToProject(projectId, userId), + _ => false, + }; + } + + return hasAccess; + } } diff --git a/src/Api/SecretsManager/Controllers/SecretsManagerPortingController.cs b/src/Api/SecretsManager/Controllers/SecretsManagerPortingController.cs index e85ace3b6..d1e75a328 100644 --- a/src/Api/SecretsManager/Controllers/SecretsManagerPortingController.cs +++ b/src/Api/SecretsManager/Controllers/SecretsManagerPortingController.cs @@ -38,7 +38,7 @@ public class SecretsManagerPortingController : Controller var userId = _userService.GetProperUserId(User).Value; var projects = await _projectRepository.GetManyByOrganizationIdAsync(organizationId, userId, AccessClientType.NoAccessCheck); - var secrets = await _secretRepository.GetManyByOrganizationIdAsync(organizationId); + var secrets = await _secretRepository.GetManyByOrganizationIdAsync(organizationId, userId, AccessClientType.NoAccessCheck); if (projects == null && secrets == null) { diff --git a/src/Core/SecretsManager/Commands/Secrets/Interfaces/ICreateSecretCommand.cs b/src/Core/SecretsManager/Commands/Secrets/Interfaces/ICreateSecretCommand.cs index 975734617..2fad75784 100644 --- a/src/Core/SecretsManager/Commands/Secrets/Interfaces/ICreateSecretCommand.cs +++ b/src/Core/SecretsManager/Commands/Secrets/Interfaces/ICreateSecretCommand.cs @@ -4,5 +4,5 @@ namespace Bit.Core.SecretsManager.Commands.Secrets.Interfaces; public interface ICreateSecretCommand { - Task CreateAsync(Secret secret); + Task CreateAsync(Secret secret, Guid userId); } diff --git a/src/Core/SecretsManager/Commands/Secrets/Interfaces/IDeleteSecretCommand.cs b/src/Core/SecretsManager/Commands/Secrets/Interfaces/IDeleteSecretCommand.cs index 493060e52..2517c70df 100644 --- a/src/Core/SecretsManager/Commands/Secrets/Interfaces/IDeleteSecretCommand.cs +++ b/src/Core/SecretsManager/Commands/Secrets/Interfaces/IDeleteSecretCommand.cs @@ -4,6 +4,6 @@ namespace Bit.Core.SecretsManager.Commands.Secrets.Interfaces; public interface IDeleteSecretCommand { - Task>> DeleteSecrets(List ids); + Task>> DeleteSecrets(List ids, Guid userId); } diff --git a/src/Core/SecretsManager/Commands/Secrets/Interfaces/IUpdateSecretCommand.cs b/src/Core/SecretsManager/Commands/Secrets/Interfaces/IUpdateSecretCommand.cs index 8c2f61abc..23e6910c8 100644 --- a/src/Core/SecretsManager/Commands/Secrets/Interfaces/IUpdateSecretCommand.cs +++ b/src/Core/SecretsManager/Commands/Secrets/Interfaces/IUpdateSecretCommand.cs @@ -4,5 +4,5 @@ namespace Bit.Core.SecretsManager.Commands.Secrets.Interfaces; public interface IUpdateSecretCommand { - Task UpdateAsync(Secret secret); + Task UpdateAsync(Secret secret, Guid userId); } diff --git a/src/Core/SecretsManager/Repositories/IProjectRepository.cs b/src/Core/SecretsManager/Repositories/IProjectRepository.cs index 3d62d571c..dddd2e21d 100644 --- a/src/Core/SecretsManager/Repositories/IProjectRepository.cs +++ b/src/Core/SecretsManager/Repositories/IProjectRepository.cs @@ -15,4 +15,6 @@ public interface IProjectRepository Task> ImportAsync(IEnumerable projects); Task UserHasReadAccessToProject(Guid id, Guid userId); Task UserHasWriteAccessToProject(Guid id, Guid userId); + Task ServiceAccountHasWriteAccessToProject(Guid id, Guid userId); + Task ServiceAccountHasReadAccessToProject(Guid id, Guid userId); } diff --git a/src/Core/SecretsManager/Repositories/ISecretRepository.cs b/src/Core/SecretsManager/Repositories/ISecretRepository.cs index 56f4dc866..8bc9f8eb6 100644 --- a/src/Core/SecretsManager/Repositories/ISecretRepository.cs +++ b/src/Core/SecretsManager/Repositories/ISecretRepository.cs @@ -1,12 +1,13 @@ -using Bit.Core.SecretsManager.Entities; +using Bit.Core.Enums; +using Bit.Core.SecretsManager.Entities; namespace Bit.Core.SecretsManager.Repositories; public interface ISecretRepository { - Task> GetManyByOrganizationIdAsync(Guid organizationId); + Task> GetManyByOrganizationIdAsync(Guid organizationId, Guid userId, AccessClientType accessType); Task> GetManyByIds(IEnumerable ids); - Task> GetManyByProjectIdAsync(Guid projectId); + Task> GetManyByProjectIdAsync(Guid projectId, Guid userId, AccessClientType accessType); Task GetByIdAsync(Guid id); Task CreateAsync(Secret secret); Task UpdateAsync(Secret secret); diff --git a/test/Api.IntegrationTest/SecretsManager/Controllers/SecretsControllerTest.cs b/test/Api.IntegrationTest/SecretsManager/Controllers/SecretsControllerTest.cs index 81ece16d3..83036a41a 100644 --- a/test/Api.IntegrationTest/SecretsManager/Controllers/SecretsControllerTest.cs +++ b/test/Api.IntegrationTest/SecretsManager/Controllers/SecretsControllerTest.cs @@ -1,9 +1,11 @@ using System.Net; using System.Net.Http.Headers; using Bit.Api.IntegrationTest.Factories; +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.Enums; using Bit.Core.SecretsManager.Entities; using Bit.Core.SecretsManager.Repositories; using Bit.Test.Common.Helpers; @@ -20,6 +22,7 @@ public class SecretsControllerTest : IClassFixture, IAsyn private readonly ApiApplicationFactory _factory; private readonly ISecretRepository _secretRepository; private readonly IProjectRepository _projectRepository; + private readonly IAccessPolicyRepository _accessPolicyRepository; private string _email = null!; private SecretsManagerOrganizationHelper _organizationHelper = null!; @@ -30,6 +33,7 @@ public class SecretsControllerTest : IClassFixture, IAsyn _client = _factory.CreateClient(); _secretRepository = _factory.GetService(); _projectRepository = _factory.GetService(); + _accessPolicyRepository = _factory.GetService(); } public async Task InitializeAsync() @@ -64,12 +68,36 @@ public class SecretsControllerTest : IClassFixture, IAsyn Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } - [Fact] - public async Task ListByOrganization_Owner_Success() + [Theory] + [InlineData(PermissionType.RunAsAdmin)] + [InlineData(PermissionType.RunAsUserWithPermission)] + public async Task ListByOrganization_Success(PermissionType permissionType) { - var (org, _) = await _organizationHelper.Initialize(true, true); + var (org, orgUserOwner) = await _organizationHelper.Initialize(true, true); await LoginAsync(_email); + var project = await _projectRepository.CreateAsync(new Project + { + Id = new Guid(), + 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 secretIds = new List(); for (var i = 0; i < 3; i++) { @@ -78,7 +106,9 @@ public class SecretsControllerTest : IClassFixture, IAsyn OrganizationId = org.Id, Key = _mockEncryptedString, Value = _mockEncryptedString, - Note = _mockEncryptedString + Note = _mockEncryptedString, + Projects = new List { project } + }); secretIds.Add(secret.Id); } @@ -113,7 +143,7 @@ public class SecretsControllerTest : IClassFixture, IAsyn } [Fact] - public async Task Create_Owner_Success() + public async Task CreateWithoutProject_RunAsAdmin_Success() { var (org, _) = await _organizationHelper.Initialize(true, true); await LoginAsync(_email); @@ -147,11 +177,33 @@ public class SecretsControllerTest : IClassFixture, IAsyn } [Fact] - public async Task CreateWithProject_Owner_Success() + public async Task CreateWithoutProject_RunAsUser_NotFound() { var (org, _) = await _organizationHelper.Initialize(true, true); + var (email, orgUser) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true); + await LoginAsync(email); + + var request = new SecretCreateRequestModel + { + Key = _mockEncryptedString, + Value = _mockEncryptedString, + Note = _mockEncryptedString + }; + + var response = await _client.PostAsJsonAsync($"/organizations/{org.Id}/secrets", request); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Theory] + [InlineData(PermissionType.RunAsAdmin)] + [InlineData(PermissionType.RunAsUserWithPermission)] + public async Task CreateWithProject_Success(PermissionType permissionType) + { + var (org, orgAdminUser) = await _organizationHelper.Initialize(true, true); await LoginAsync(_email); + AccessClientType accessType = AccessClientType.NoAccessCheck; + var project = await _projectRepository.CreateAsync(new Project() { Id = new Guid(), @@ -159,6 +211,25 @@ public class SecretsControllerTest : IClassFixture, IAsyn Name = _mockEncryptedString }); + var orgUserId = (Guid)orgAdminUser.UserId; + + if (permissionType == PermissionType.RunAsUserWithPermission) + { + var (email, orgUser) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true); + await LoginAsync(email); + accessType = AccessClientType.User; + + var accessPolicies = new List + { + new Core.SecretsManager.Entities.UserProjectAccessPolicy + { + GrantedProjectId = project.Id, OrganizationUserId = orgUser.Id , Read = true, Write = true, + }, + }; + orgUserId = (Guid)orgUser.UserId; + await _accessPolicyRepository.CreateManyAsync(accessPolicies); + } + var secretRequest = new SecretCreateRequestModel() { Key = _mockEncryptedString, @@ -170,7 +241,7 @@ public class SecretsControllerTest : IClassFixture, IAsyn secretResponse.EnsureSuccessStatusCode(); var secretResult = await secretResponse.Content.ReadFromJsonAsync(); - var secret = (await _secretRepository.GetManyByProjectIdAsync(project.Id)).First(); + var secret = (await _secretRepository.GetManyByProjectIdAsync(project.Id, orgUserId, accessType)).First(); Assert.NotNull(secretResult); Assert.Equal(secret.Id.ToString(), secretResult!.Id); @@ -203,18 +274,48 @@ public class SecretsControllerTest : IClassFixture, IAsyn Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } - [Fact] - public async Task Get_Owner_Success() + [Theory] + [InlineData(PermissionType.RunAsAdmin)] + [InlineData(PermissionType.RunAsUserWithPermission)] + public async Task Get_Success(PermissionType permissionType) { var (org, _) = await _organizationHelper.Initialize(true, true); await LoginAsync(_email); + var project = await _projectRepository.CreateAsync(new Project() + { + Id = new Guid(), + 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); + } + else + { + var (email, orgUser) = await _organizationHelper.CreateNewUser(OrganizationUserType.Admin, true); + await LoginAsync(email); + } + var secret = await _secretRepository.CreateAsync(new Secret { OrganizationId = org.Id, Key = _mockEncryptedString, Value = _mockEncryptedString, - Note = _mockEncryptedString + Note = _mockEncryptedString, + Projects = new List { project } }); var response = await _client.GetAsync($"/secrets/{secret.Id}"); @@ -255,25 +356,51 @@ public class SecretsControllerTest : IClassFixture, IAsyn Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } - [Fact] - public async Task Update_Owner_Success() + [Theory] + [InlineData(PermissionType.RunAsAdmin)] + [InlineData(PermissionType.RunAsUserWithPermission)] + public async Task Update_Success(PermissionType permissionType) { var (org, _) = await _organizationHelper.Initialize(true, true); await LoginAsync(_email); + var project = await _projectRepository.CreateAsync(new Project() + { + Id = new Guid(), + 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 secret = await _secretRepository.CreateAsync(new Secret { OrganizationId = org.Id, Key = _mockEncryptedString, Value = _mockEncryptedString, - Note = _mockEncryptedString + Note = _mockEncryptedString, + Projects = permissionType == PermissionType.RunAsUserWithPermission ? new List() { project } : null }); var request = new SecretUpdateRequestModel() { Key = _mockEncryptedString, Value = "2.3Uk+WNBIoU5xzmVFNcoWzz==|1MsPIYuRfdOHfu/0uY6H2Q==|/98xy4wb6pHP1VTZ9JcNCYgQjEUMFPlqJgCwRk1YXKg=", - Note = _mockEncryptedString + Note = _mockEncryptedString, + ProjectIds = permissionType == PermissionType.RunAsUserWithPermission ? new Guid[] { project.Id } : null }; var response = await _client.PutAsJsonAsync($"/secrets/{secret.Id}", request); @@ -316,16 +443,41 @@ public class SecretsControllerTest : IClassFixture, IAsyn }); var secretIds = new[] { secret.Id }; - var response = await _client.PostAsJsonAsync("/secrets/delete", secretIds); + var response = await _client.PostAsJsonAsync($"/secrets/{org.Id}/delete", secretIds); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } - [Fact] - public async Task Delete_Owner_Success() + [Theory] + [InlineData(PermissionType.RunAsAdmin)] + [InlineData(PermissionType.RunAsUserWithPermission)] + public async Task Delete_Success(PermissionType permissionType) { var (org, _) = await _organizationHelper.Initialize(true, true); await LoginAsync(_email); + var project = await _projectRepository.CreateAsync(new Project() + { + Id = new Guid(), + 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 secretIds = new List(); for (var i = 0; i < 3; i++) { @@ -334,12 +486,13 @@ public class SecretsControllerTest : IClassFixture, IAsyn OrganizationId = org.Id, Key = _mockEncryptedString, Value = _mockEncryptedString, - Note = _mockEncryptedString + Note = _mockEncryptedString, + Projects = new List() { project } }); secretIds.Add(secret.Id); } - var response = await _client.PostAsJsonAsync("/secrets/delete", secretIds); + var response = await _client.PostAsJsonAsync($"/secrets/delete", secretIds); response.EnsureSuccessStatusCode(); var results = await response.Content.ReadFromJsonAsync>(); diff --git a/test/Api.Test/SecretsManager/Controllers/SecretsControllerTests.cs b/test/Api.Test/SecretsManager/Controllers/SecretsControllerTests.cs index aff105abe..ab020f751 100644 --- a/test/Api.Test/SecretsManager/Controllers/SecretsControllerTests.cs +++ b/test/Api.Test/SecretsManager/Controllers/SecretsControllerTests.cs @@ -1,10 +1,13 @@ using Bit.Api.SecretsManager.Controllers; using Bit.Api.SecretsManager.Models.Request; +using Bit.Api.Test.SecretsManager.Enums; using Bit.Core.Context; +using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.SecretsManager.Commands.Secrets.Interfaces; using Bit.Core.SecretsManager.Entities; using Bit.Core.SecretsManager.Repositories; +using Bit.Core.Services; using Bit.Core.Test.SecretsManager.AutoFixture.SecretsFixture; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; @@ -22,33 +25,50 @@ public class SecretsControllerTests { [Theory] [BitAutoData] - public async void GetSecretsByOrganization_ReturnsEmptyList(SutProvider sutProvider, Guid id) + public async void GetSecretsByOrganization_ReturnsEmptyList(SutProvider sutProvider, Guid id, Guid organizationId, Guid userId, AccessClientType accessType) { sutProvider.GetDependency().AccessSecretsManager(id).Returns(true); + sutProvider.GetDependency().OrganizationAdmin(organizationId).Returns(true); + sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId); + var result = await sutProvider.Sut.ListByOrganizationAsync(id); await sutProvider.GetDependency().Received(1) - .GetManyByOrganizationIdAsync(Arg.Is(AssertHelper.AssertPropertyEqual(id))); + .GetManyByOrganizationIdAsync(Arg.Is(AssertHelper.AssertPropertyEqual(id)), userId, accessType); Assert.Empty(result.Secrets); } [Theory] - [BitAutoData] - public async void GetSecretsByOrganization_Success(SutProvider sutProvider, Secret resultSecret) + [BitAutoData(PermissionType.RunAsAdmin)] + [BitAutoData(PermissionType.RunAsUserWithPermission)] + public async void GetSecretsByOrganization_Success(PermissionType permissionType, SutProvider sutProvider, Core.SecretsManager.Entities.Secret resultSecret, Guid organizationId, Guid userId, Core.SecretsManager.Entities.Project mockProject, AccessClientType accessType) { sutProvider.GetDependency().AccessSecretsManager(default).ReturnsForAnyArgs(true); - sutProvider.GetDependency().GetManyByOrganizationIdAsync(default).ReturnsForAnyArgs(new List { resultSecret }); + sutProvider.GetDependency().GetManyByOrganizationIdAsync(default, default, default).ReturnsForAnyArgs(new List { resultSecret }); + sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId); + + if (permissionType == PermissionType.RunAsAdmin) + { + sutProvider.GetDependency().OrganizationAdmin(organizationId).Returns(true); + } + else + { + resultSecret.Projects = new List() { mockProject }; + sutProvider.GetDependency().OrganizationAdmin(organizationId).Returns(false); + sutProvider.GetDependency().UserHasReadAccessToProject(mockProject.Id, userId).Returns(true); + } + var result = await sutProvider.Sut.ListByOrganizationAsync(resultSecret.OrganizationId); await sutProvider.GetDependency().Received(1) - .GetManyByOrganizationIdAsync(Arg.Is(AssertHelper.AssertPropertyEqual(resultSecret.OrganizationId))); + .GetManyByOrganizationIdAsync(Arg.Is(AssertHelper.AssertPropertyEqual(resultSecret.OrganizationId)), userId, accessType); } [Theory] [BitAutoData] - public async void GetSecretsByOrganization_AccessDenied_Throws(SutProvider sutProvider, Secret resultSecret) + public async void GetSecretsByOrganization_AccessDenied_Throws(SutProvider sutProvider, Core.SecretsManager.Entities.Secret resultSecret) { sutProvider.GetDependency().AccessSecretsManager(default).ReturnsForAnyArgs(false); @@ -64,11 +84,29 @@ public class SecretsControllerTests } [Theory] - [BitAutoData] - public async void GetSecret_Success(SutProvider sutProvider, Secret resultSecret) + [BitAutoData(PermissionType.RunAsAdmin)] + [BitAutoData(PermissionType.RunAsUserWithPermission)] + public async void GetSecret_Success(PermissionType permissionType, SutProvider sutProvider, Secret resultSecret, Guid userId, Guid organizationId, Project mockProject) { + sutProvider.GetDependency().AccessSecretsManager(organizationId).Returns(true); + sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId); + mockProject.OrganizationId = organizationId; + resultSecret.Projects = new List() { mockProject }; + resultSecret.OrganizationId = organizationId; + sutProvider.GetDependency().GetByIdAsync(default).ReturnsForAnyArgs(resultSecret); + if (permissionType == PermissionType.RunAsAdmin) + { + resultSecret.OrganizationId = organizationId; + sutProvider.GetDependency().OrganizationAdmin(organizationId).Returns(true); + } + else + { + sutProvider.GetDependency().OrganizationAdmin(organizationId).Returns(false); + sutProvider.GetDependency().UserHasReadAccessToProject(mockProject.Id, userId).Returns(true); + } + var result = await sutProvider.Sut.GetAsync(resultSecret.Id); await sutProvider.GetDependency().Received(1) @@ -76,46 +114,89 @@ public class SecretsControllerTests } [Theory] - [BitAutoData] - public async void CreateSecret_Success(SutProvider sutProvider, SecretCreateRequestModel data, Guid organizationId) + [BitAutoData(PermissionType.RunAsAdmin)] + [BitAutoData(PermissionType.RunAsUserWithPermission)] + public async void CreateSecret_Success(PermissionType permissionType, SutProvider sutProvider, SecretCreateRequestModel data, Guid organizationId, Project mockProject, Guid userId) { var resultSecret = data.ToSecret(organizationId); + sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId); + + if (permissionType == PermissionType.RunAsAdmin) + { + sutProvider.GetDependency().OrganizationAdmin(organizationId).Returns(true); + } + else + { + resultSecret.Projects = new List() { mockProject }; + sutProvider.GetDependency().OrganizationAdmin(organizationId).Returns(false); + sutProvider.GetDependency().UserHasReadAccessToProject(mockProject.Id, userId).Returns(true); + } sutProvider.GetDependency().AccessSecretsManager(organizationId).Returns(true); - sutProvider.GetDependency().CreateAsync(default).ReturnsForAnyArgs(resultSecret); + sutProvider.GetDependency().CreateAsync(default, userId).ReturnsForAnyArgs(resultSecret); var result = await sutProvider.Sut.CreateAsync(organizationId, data); await sutProvider.GetDependency().Received(1) - .CreateAsync(Arg.Any()); + .CreateAsync(Arg.Any(), userId); } [Theory] - [BitAutoData] - public async void UpdateSecret_Success(SutProvider sutProvider, SecretUpdateRequestModel data, Guid secretId) + [BitAutoData(PermissionType.RunAsAdmin)] + [BitAutoData(PermissionType.RunAsUserWithPermission)] + public async void UpdateSecret_Success(PermissionType permissionType, SutProvider sutProvider, SecretUpdateRequestModel data, Guid secretId, Guid organizationId, Guid userId, Project mockProject) { + sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId); + + if (permissionType == PermissionType.RunAsAdmin) + { + sutProvider.GetDependency().OrganizationAdmin(organizationId).Returns(true); + } + else + { + data.ProjectIds = new Guid[] { mockProject.Id }; + sutProvider.GetDependency().OrganizationAdmin(organizationId).Returns(false); + sutProvider.GetDependency().UserHasReadAccessToProject(mockProject.Id, userId).Returns(true); + } + var resultSecret = data.ToSecret(secretId); - sutProvider.GetDependency().UpdateAsync(default).ReturnsForAnyArgs(resultSecret); + sutProvider.GetDependency().UpdateAsync(default, userId).ReturnsForAnyArgs(resultSecret); - var result = await sutProvider.Sut.UpdateAsync(secretId, data); + var result = await sutProvider.Sut.UpdateSecretAsync(secretId, data); await sutProvider.GetDependency().Received(1) - .UpdateAsync(Arg.Any()); + .UpdateAsync(Arg.Any(), userId); } [Theory] - [BitAutoData] - public async void BulkDeleteSecret_Success(SutProvider sutProvider, List data) + [BitAutoData(PermissionType.RunAsAdmin)] + [BitAutoData(PermissionType.RunAsUserWithPermission)] + public async void BulkDeleteSecret_Success(PermissionType permissionType, SutProvider sutProvider, List data, Guid organizationId, Guid userId, Project mockProject) { + sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId); + + if (permissionType == PermissionType.RunAsAdmin) + { + sutProvider.GetDependency().OrganizationAdmin(organizationId).Returns(true); + } + else + { + data.FirstOrDefault().Projects = new List() { mockProject }; + sutProvider.GetDependency().OrganizationAdmin(organizationId).Returns(false); + sutProvider.GetDependency().UserHasReadAccessToProject(mockProject.Id, userId).Returns(true); + } + + var ids = data.Select(secret => secret.Id).ToList(); var mockResult = new List>(); + foreach (var secret in data) { mockResult.Add(new Tuple(secret, "")); } - sutProvider.GetDependency().DeleteSecrets(ids).ReturnsForAnyArgs(mockResult); + sutProvider.GetDependency().DeleteSecrets(ids, userId).ReturnsForAnyArgs(mockResult); var results = await sutProvider.Sut.BulkDeleteAsync(ids); await sutProvider.GetDependency().Received(1) - .DeleteSecrets(Arg.Is(ids)); + .DeleteSecrets(Arg.Is(ids), userId); Assert.Equal(data.Count, results.Data.Count()); } @@ -123,6 +204,7 @@ public class SecretsControllerTests [BitAutoData] public async void BulkDeleteSecret_NoGuids_ThrowsArgumentNullException(SutProvider sutProvider) { + sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(new Guid()); await Assert.ThrowsAsync(() => sutProvider.Sut.BulkDeleteAsync(new List())); } }