From cf25d550909c5d07067e361bfd432e3c6b4b5e41 Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Tue, 31 Jan 2023 18:38:53 +0100 Subject: [PATCH] [SM-378] Enable SM on a user basis (#2590) * Add support for giving individual users access to secrets manager --- .../AccessTokens/CreateAccessTokenCommand.cs | 8 +- .../Commands/Projects/DeleteProjectCommand.cs | 7 +- .../Commands/Projects/UpdateProjectCommand.cs | 7 +- .../Commands/Secrets/DeleteSecretCommand.cs | 19 +- .../UpdateServiceAccountCommand.cs | 7 +- .../CreateAccessTokenCommandTests.cs | 4 +- .../Projects/DeleteProjectCommandTests.cs | 9 +- .../Projects/UpdateProjectCommandTests.cs | 5 +- .../Secrets/DeleteSecretCommandTests.cs | 2 + .../UpdateServiceAccountCommandTests.cs | 9 +- .../OrganizationUserRequestModels.cs | 4 + .../OrganizationUserResponseModel.cs | 3 + .../ProfileOrganizationResponseModel.cs | 2 + .../Controllers/ProjectsController.cs | 63 +-- .../Controllers/SecretsController.cs | 42 +- .../Controllers/ServiceAccountsController.cs | 28 +- .../Context/CurrentContentOrganization.cs | 8 +- src/Core/Context/CurrentContext.cs | 28 +- src/Core/Context/ICurrentContext.cs | 1 + src/Core/Entities/OrganizationUser.cs | 1 + src/Core/Identity/Claims.cs | 3 + .../Models/Business/OrganizationUserInvite.cs | 2 + .../OrganizationUserInviteData.cs | 1 + .../OrganizationUserOrganizationDetails.cs | 1 + .../OrganizationUserUserDetails.cs | 1 + .../Implementations/OrganizationService.cs | 1 + src/Core/Utilities/CoreHelpers.cs | 9 + src/Identity/IdentityServer/ApiResources.cs | 1 + src/Infrastructure.Dapper/DapperHelpers.cs | 3 +- .../OrganizationUserRepository.cs | 4 +- ...izationUserOrganizationDetailsViewQuery.cs | 4 +- .../Queries/OrganizationUserUserViewQuery.cs | 1 + src/Sql/Sql.sqlproj | 3 + .../OrganizationUser_Create.sql | 9 +- .../OrganizationUser_CreateMany2.sql | 42 ++ ...OrganizationUser_CreateWithCollections.sql | 5 +- .../OrganizationUser_Update.sql | 6 +- .../OrganizationUser_UpdateMany2.sql | 34 ++ ...OrganizationUser_UpdateWithCollections.sql | 5 +- src/Sql/dbo/Tables/OrganizationUser.sql | 1 + .../OrganizationUserType2.sql | 16 + ...rganizationUserOrganizationDetailsView.sql | 3 +- .../Views/OrganizationUserUserDetailsView.sql | 1 + .../OrganizationUser_CreateMany.sql | 2 + .../OrganizationUser_UpdateMany.sql | 2 + .../OrganizationUserType.sql | 2 + .../Helpers/OrganizationTestHelpers.cs | 9 +- .../Controllers/ProjectsControllerTest.cs | 221 ++++++--- .../Controllers/SecretsControllerTest.cs | 285 ++++++++---- .../ServiceAccountsControllerTests.cs | 311 ++++++------- .../SecretsManagerOrganizationHelper.cs | 55 +++ .../Controllers/ProjectsControllerTests.cs | 4 +- .../Controllers/SecretsControllerTests.cs | 48 +- .../ServiceAccountsControllerTests.cs | 24 +- .../openid-configuration.json | 1 + ...1-17_00_SecretsManagerOrganizationUser.sql | 425 ++++++++++++++++++ .../2023-02-FutureMigration.sql | 17 + 57 files changed, 1419 insertions(+), 400 deletions(-) create mode 100644 src/Sql/dbo/Stored Procedures/OrganizationUser_CreateMany2.sql create mode 100644 src/Sql/dbo/Stored Procedures/OrganizationUser_UpdateMany2.sql create mode 100644 src/Sql/dbo/User Defined Types/OrganizationUserType2.sql create mode 100644 src/Sql/dbo_future/Stored Procedures/OrganizationUser_CreateMany.sql create mode 100644 src/Sql/dbo_future/Stored Procedures/OrganizationUser_UpdateMany.sql create mode 100644 src/Sql/dbo_future/User Defined Types/OrganizationUserType.sql create mode 100644 test/Api.IntegrationTest/SecretsManager/SecretsManagerOrganizationHelper.cs create mode 100644 util/Migrator/DbScripts/2023-01-17_00_SecretsManagerOrganizationUser.sql create mode 100644 util/Migrator/DbScripts_future/2023-02-FutureMigration.sql diff --git a/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/AccessTokens/CreateAccessTokenCommand.cs b/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/AccessTokens/CreateAccessTokenCommand.cs index e8d179e799..66b525f654 100644 --- a/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/AccessTokens/CreateAccessTokenCommand.cs +++ b/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/AccessTokens/CreateAccessTokenCommand.cs @@ -33,6 +33,12 @@ public class CreateAccessTokenCommand : ICreateAccessTokenCommand } var serviceAccount = await _serviceAccountRepository.GetByIdAsync(apiKey.ServiceAccountId.Value); + + if (!_currentContext.AccessSecretsManager(serviceAccount.OrganizationId)) + { + throw new NotFoundException(); + } + var orgAdmin = await _currentContext.OrganizationAdmin(serviceAccount.OrganizationId); var accessClient = AccessClientHelper.ToAccessClient(_currentContext.ClientType, orgAdmin); @@ -46,7 +52,7 @@ public class CreateAccessTokenCommand : ICreateAccessTokenCommand if (!hasAccess) { - throw new UnauthorizedAccessException(); + throw new NotFoundException(); } apiKey.ClientSecret = CoreHelpers.SecureRandomString(_clientSecretMaxLength); diff --git a/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/Projects/DeleteProjectCommand.cs b/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/Projects/DeleteProjectCommand.cs index 8451f1d40a..e975df93f7 100644 --- a/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/Projects/DeleteProjectCommand.cs +++ b/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/Projects/DeleteProjectCommand.cs @@ -36,7 +36,12 @@ public class DeleteProjectCommand : IDeleteProjectCommand var organizationId = projects.First().OrganizationId; if (projects.Any(p => p.OrganizationId != organizationId)) { - throw new UnauthorizedAccessException(); + throw new BadRequestException(); + } + + if (!_currentContext.AccessSecretsManager(organizationId)) + { + throw new NotFoundException(); } var orgAdmin = await _currentContext.OrganizationAdmin(organizationId); diff --git a/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/Projects/UpdateProjectCommand.cs b/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/Projects/UpdateProjectCommand.cs index 74799547a0..06ed949c0f 100644 --- a/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/Projects/UpdateProjectCommand.cs +++ b/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/Projects/UpdateProjectCommand.cs @@ -26,6 +26,11 @@ public class UpdateProjectCommand : IUpdateProjectCommand throw new NotFoundException(); } + if (!_currentContext.AccessSecretsManager(project.OrganizationId)) + { + throw new NotFoundException(); + } + var orgAdmin = await _currentContext.OrganizationAdmin(project.OrganizationId); var accessClient = AccessClientHelper.ToAccessClient(_currentContext.ClientType, orgAdmin); @@ -38,7 +43,7 @@ public class UpdateProjectCommand : IUpdateProjectCommand if (!hasAccess) { - throw new UnauthorizedAccessException(); + throw new NotFoundException(); } project.Name = updatedProject.Name; 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 b04a8f911d..c1872717b7 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.Exceptions; +using Bit.Core.Context; +using Bit.Core.Exceptions; using Bit.Core.SecretsManager.Commands.Secrets.Interfaces; using Bit.Core.SecretsManager.Entities; using Bit.Core.SecretsManager.Repositories; @@ -7,10 +8,12 @@ namespace Bit.Commercial.Core.SecretsManager.Commands.Secrets; public class DeleteSecretCommand : IDeleteSecretCommand { + private readonly ICurrentContext _currentContext; private readonly ISecretRepository _secretRepository; - public DeleteSecretCommand(ISecretRepository secretRepository) + public DeleteSecretCommand(ICurrentContext currentContext, ISecretRepository secretRepository) { + _currentContext = currentContext; _secretRepository = secretRepository; } @@ -23,6 +26,18 @@ public class DeleteSecretCommand : IDeleteSecretCommand throw new NotFoundException(); } + // Ensure all secrets belongs to the same organization + var organizationId = secrets.First().OrganizationId; + if (secrets.Any(p => p.OrganizationId != organizationId)) + { + throw new BadRequestException(); + } + + if (!_currentContext.AccessSecretsManager(organizationId)) + { + throw new NotFoundException(); + } + var results = ids.Select(id => { var secret = secrets.FirstOrDefault(secret => secret.Id == id); diff --git a/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/ServiceAccounts/UpdateServiceAccountCommand.cs b/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/ServiceAccounts/UpdateServiceAccountCommand.cs index efc77fc040..378f3cbc33 100644 --- a/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/ServiceAccounts/UpdateServiceAccountCommand.cs +++ b/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/ServiceAccounts/UpdateServiceAccountCommand.cs @@ -26,6 +26,11 @@ public class UpdateServiceAccountCommand : IUpdateServiceAccountCommand throw new NotFoundException(); } + if (!_currentContext.AccessSecretsManager(serviceAccount.OrganizationId)) + { + throw new NotFoundException(); + } + var orgAdmin = await _currentContext.OrganizationAdmin(serviceAccount.OrganizationId); var accessClient = AccessClientHelper.ToAccessClient(_currentContext.ClientType, orgAdmin); @@ -38,7 +43,7 @@ public class UpdateServiceAccountCommand : IUpdateServiceAccountCommand if (!hasAccess) { - throw new UnauthorizedAccessException(); + throw new NotFoundException(); } serviceAccount.Name = updatedServiceAccount.Name; diff --git a/bitwarden_license/test/Commercial.Core.Test/SecretsManager/AccessTokens/CreateAccessTokenCommandTests.cs b/bitwarden_license/test/Commercial.Core.Test/SecretsManager/AccessTokens/CreateAccessTokenCommandTests.cs index d9b2027c36..de34e77392 100644 --- a/bitwarden_license/test/Commercial.Core.Test/SecretsManager/AccessTokens/CreateAccessTokenCommandTests.cs +++ b/bitwarden_license/test/Commercial.Core.Test/SecretsManager/AccessTokens/CreateAccessTokenCommandTests.cs @@ -36,7 +36,7 @@ public class CreateServiceAccountCommandTests sutProvider.GetDependency().GetByIdAsync(saData.Id).Returns(saData); sutProvider.GetDependency().UserHasWriteAccessToServiceAccount(saData.Id, userId).Returns(false); - await Assert.ThrowsAsync(() => sutProvider.Sut.CreateAsync(data, userId)); + await Assert.ThrowsAsync(() => sutProvider.Sut.CreateAsync(data, userId)); await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().CreateAsync(default); } @@ -49,6 +49,7 @@ public class CreateServiceAccountCommandTests data.ServiceAccountId = saData.Id; sutProvider.GetDependency().GetByIdAsync(saData.Id).Returns(saData); sutProvider.GetDependency().UserHasWriteAccessToServiceAccount(saData.Id, userId).Returns(true); + sutProvider.GetDependency().AccessSecretsManager(saData.OrganizationId).Returns(true); await sutProvider.Sut.CreateAsync(data, userId); @@ -64,6 +65,7 @@ public class CreateServiceAccountCommandTests data.ServiceAccountId = saData.Id; sutProvider.GetDependency().GetByIdAsync(saData.Id).Returns(saData); + sutProvider.GetDependency().AccessSecretsManager(saData.OrganizationId).Returns(true); sutProvider.GetDependency().OrganizationAdmin(saData.OrganizationId).Returns(true); await sutProvider.Sut.CreateAsync(data, userId); diff --git a/bitwarden_license/test/Commercial.Core.Test/SecretsManager/Projects/DeleteProjectCommandTests.cs b/bitwarden_license/test/Commercial.Core.Test/SecretsManager/Projects/DeleteProjectCommandTests.cs index 3d93d39793..e48c2cf0af 100644 --- a/bitwarden_license/test/Commercial.Core.Test/SecretsManager/Projects/DeleteProjectCommandTests.cs +++ b/bitwarden_license/test/Commercial.Core.Test/SecretsManager/Projects/DeleteProjectCommandTests.cs @@ -28,7 +28,7 @@ public class DeleteProjectCommandTests [Theory] [BitAutoData] - public async Task DeleteSecrets_OneIdNotFound_Throws_NotFoundException(List data, Guid userId, + public async Task Delete_OneIdNotFound_Throws_NotFoundException(List data, Guid userId, SutProvider sutProvider) { var project = new Project() @@ -49,6 +49,7 @@ public class DeleteProjectCommandTests { var projects = data.Select(id => new Project { Id = id, OrganizationId = organizationId }).ToList(); + sutProvider.GetDependency().AccessSecretsManager(organizationId).Returns(true); sutProvider.GetDependency().ClientType = ClientType.User; sutProvider.GetDependency().GetManyByIds(data).Returns(projects); sutProvider.GetDependency().UserHasWriteAccessToProject(Arg.Any(), userId).Returns(true); @@ -65,11 +66,12 @@ public class DeleteProjectCommandTests [Theory] [BitAutoData] - public async Task DeleteSecrets_User_No_Permission(List data, Guid userId, Guid organizationId, + public async Task Delete_User_No_Permission(List data, Guid userId, Guid organizationId, SutProvider sutProvider) { var projects = data.Select(id => new Project { Id = id, OrganizationId = organizationId }).ToList(); + sutProvider.GetDependency().AccessSecretsManager(organizationId).Returns(true); sutProvider.GetDependency().ClientType = ClientType.User; sutProvider.GetDependency().GetManyByIds(data).Returns(projects); sutProvider.GetDependency().UserHasWriteAccessToProject(userId, userId).Returns(false); @@ -86,11 +88,12 @@ public class DeleteProjectCommandTests [Theory] [BitAutoData] - public async Task DeleteSecrets_OrganizationAdmin_Success(List data, Guid userId, Guid organizationId, + public async Task Delete_OrganizationAdmin_Success(List data, Guid userId, Guid organizationId, SutProvider sutProvider) { var projects = data.Select(id => new Project { Id = id, OrganizationId = organizationId }).ToList(); + sutProvider.GetDependency().AccessSecretsManager(organizationId).Returns(true); sutProvider.GetDependency().OrganizationAdmin(organizationId).Returns(true); sutProvider.GetDependency().GetManyByIds(data).Returns(projects); diff --git a/bitwarden_license/test/Commercial.Core.Test/SecretsManager/Projects/UpdateProjectCommandTests.cs b/bitwarden_license/test/Commercial.Core.Test/SecretsManager/Projects/UpdateProjectCommandTests.cs index d85a44a911..8bacf0fc20 100644 --- a/bitwarden_license/test/Commercial.Core.Test/SecretsManager/Projects/UpdateProjectCommandTests.cs +++ b/bitwarden_license/test/Commercial.Core.Test/SecretsManager/Projects/UpdateProjectCommandTests.cs @@ -33,6 +33,7 @@ public class UpdateProjectCommandTests public async Task UpdateAsync_Admin_Succeeds(Project project, Guid userId, SutProvider sutProvider) { sutProvider.GetDependency().GetByIdAsync(project.Id).Returns(project); + sutProvider.GetDependency().AccessSecretsManager(project.OrganizationId).Returns(true); sutProvider.GetDependency().OrganizationAdmin(project.OrganizationId).Returns(true); var project2 = new Project { Id = project.Id, Name = "newName" }; @@ -51,8 +52,9 @@ public class UpdateProjectCommandTests { sutProvider.GetDependency().GetByIdAsync(project.Id).Returns(project); sutProvider.GetDependency().UserHasWriteAccessToProject(project.Id, userId).Returns(false); + sutProvider.GetDependency().AccessSecretsManager(project.OrganizationId).Returns(true); - await Assert.ThrowsAsync(() => sutProvider.Sut.UpdateAsync(project, userId)); + await Assert.ThrowsAsync(() => sutProvider.Sut.UpdateAsync(project, userId)); await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().ReplaceAsync(default); } @@ -63,6 +65,7 @@ public class UpdateProjectCommandTests { sutProvider.GetDependency().GetByIdAsync(project.Id).Returns(project); sutProvider.GetDependency().UserHasWriteAccessToProject(project.Id, userId).Returns(true); + sutProvider.GetDependency().AccessSecretsManager(project.OrganizationId).Returns(true); var project2 = new Project { Id = project.Id, Name = "newName" }; var result = await sutProvider.Sut.UpdateAsync(project2, 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 d5adec8827..cee70548e7 100644 --- a/bitwarden_license/test/Commercial.Core.Test/SecretsManager/Secrets/DeleteSecretCommandTests.cs +++ b/bitwarden_license/test/Commercial.Core.Test/SecretsManager/Secrets/DeleteSecretCommandTests.cs @@ -1,4 +1,5 @@ using Bit.Commercial.Core.SecretsManager.Commands.Secrets; +using Bit.Core.Context; using Bit.Core.Exceptions; using Bit.Core.SecretsManager.Entities; using Bit.Core.SecretsManager.Repositories; @@ -56,6 +57,7 @@ public class DeleteSecretCommandTests } sutProvider.GetDependency().GetManyByIds(data).Returns(secrets); + sutProvider.GetDependency().AccessSecretsManager(default).ReturnsForAnyArgs(true); var results = await sutProvider.Sut.DeleteSecrets(data); diff --git a/bitwarden_license/test/Commercial.Core.Test/SecretsManager/ServiceAccounts/UpdateServiceAccountCommandTests.cs b/bitwarden_license/test/Commercial.Core.Test/SecretsManager/ServiceAccounts/UpdateServiceAccountCommandTests.cs index 0b75cb2a9a..3a06eac474 100644 --- a/bitwarden_license/test/Commercial.Core.Test/SecretsManager/ServiceAccounts/UpdateServiceAccountCommandTests.cs +++ b/bitwarden_license/test/Commercial.Core.Test/SecretsManager/ServiceAccounts/UpdateServiceAccountCommandTests.cs @@ -18,7 +18,7 @@ public class UpdateServiceAccountCommandTests [BitAutoData] public async Task UpdateAsync_ServiceAccountDoesNotExist_ThrowsNotFound(ServiceAccount data, Guid userId, SutProvider sutProvider) { - var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.UpdateAsync(data, userId)); + await Assert.ThrowsAsync(() => sutProvider.Sut.UpdateAsync(data, userId)); await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().ReplaceAsync(default); } @@ -30,7 +30,7 @@ public class UpdateServiceAccountCommandTests sutProvider.GetDependency().GetByIdAsync(data.Id).Returns(data); sutProvider.GetDependency().UserHasWriteAccessToServiceAccount(data.Id, userId).Returns(false); - await Assert.ThrowsAsync(() => sutProvider.Sut.UpdateAsync(data, userId)); + await Assert.ThrowsAsync(() => sutProvider.Sut.UpdateAsync(data, userId)); await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().ReplaceAsync(default); } @@ -39,6 +39,7 @@ public class UpdateServiceAccountCommandTests [BitAutoData] public async Task UpdateAsync_User_Success(ServiceAccount data, Guid userId, SutProvider sutProvider) { + sutProvider.GetDependency().AccessSecretsManager(data.OrganizationId).Returns(true); sutProvider.GetDependency().GetByIdAsync(data.Id).Returns(data); sutProvider.GetDependency().UserHasWriteAccessToServiceAccount(data.Id, userId).Returns(true); @@ -54,6 +55,7 @@ public class UpdateServiceAccountCommandTests public async Task UpdateAsync_Admin_Success(ServiceAccount data, Guid userId, SutProvider sutProvider) { sutProvider.GetDependency().GetByIdAsync(data.Id).Returns(data); + sutProvider.GetDependency().AccessSecretsManager(data.OrganizationId).Returns(true); sutProvider.GetDependency().OrganizationAdmin(data.OrganizationId).Returns(true); await sutProvider.Sut.UpdateAsync(data, userId); @@ -66,6 +68,7 @@ public class UpdateServiceAccountCommandTests [BitAutoData] public async Task UpdateAsync_DoesNotModifyOrganizationId(ServiceAccount existingServiceAccount, Guid userId, SutProvider sutProvider) { + sutProvider.GetDependency().AccessSecretsManager(existingServiceAccount.OrganizationId).Returns(true); sutProvider.GetDependency().GetByIdAsync(existingServiceAccount.Id).Returns(existingServiceAccount); sutProvider.GetDependency().UserHasWriteAccessToServiceAccount(existingServiceAccount.Id, userId).Returns(true); @@ -87,6 +90,7 @@ public class UpdateServiceAccountCommandTests [BitAutoData] public async Task UpdateAsync_DoesNotModifyCreationDate(ServiceAccount existingServiceAccount, Guid userId, SutProvider sutProvider) { + sutProvider.GetDependency().AccessSecretsManager(existingServiceAccount.OrganizationId).Returns(true); sutProvider.GetDependency().GetByIdAsync(existingServiceAccount.Id).Returns(existingServiceAccount); sutProvider.GetDependency().UserHasWriteAccessToServiceAccount(existingServiceAccount.Id, userId).Returns(true); @@ -108,6 +112,7 @@ public class UpdateServiceAccountCommandTests [BitAutoData] public async Task UpdateAsync_RevisionDateIsUpdatedToUtcNow(ServiceAccount existingServiceAccount, Guid userId, SutProvider sutProvider) { + sutProvider.GetDependency().AccessSecretsManager(existingServiceAccount.OrganizationId).Returns(true); sutProvider.GetDependency().GetByIdAsync(existingServiceAccount.Id).Returns(existingServiceAccount); sutProvider.GetDependency().UserHasWriteAccessToServiceAccount(existingServiceAccount.Id, userId).Returns(true); diff --git a/src/Api/Models/Request/Organizations/OrganizationUserRequestModels.cs b/src/Api/Models/Request/Organizations/OrganizationUserRequestModels.cs index a09012d792..9a8ab0192c 100644 --- a/src/Api/Models/Request/Organizations/OrganizationUserRequestModels.cs +++ b/src/Api/Models/Request/Organizations/OrganizationUserRequestModels.cs @@ -17,6 +17,7 @@ public class OrganizationUserInviteRequestModel [Required] public OrganizationUserType? Type { get; set; } public bool AccessAll { get; set; } + public bool AccessSecretsManager { get; set; } public Permissions Permissions { get; set; } public IEnumerable Collections { get; set; } public IEnumerable Groups { get; set; } @@ -28,6 +29,7 @@ public class OrganizationUserInviteRequestModel Emails = Emails, Type = Type, AccessAll = AccessAll, + AccessSecretsManager = AccessSecretsManager, Collections = Collections?.Select(c => c.ToSelectionReadOnly()), Groups = Groups, Permissions = Permissions, @@ -73,6 +75,7 @@ public class OrganizationUserUpdateRequestModel [Required] public OrganizationUserType? Type { get; set; } public bool AccessAll { get; set; } + public bool AccessSecretsManager { get; set; } public Permissions Permissions { get; set; } public IEnumerable Collections { get; set; } public IEnumerable Groups { get; set; } @@ -85,6 +88,7 @@ public class OrganizationUserUpdateRequestModel PropertyNamingPolicy = JsonNamingPolicy.CamelCase, }); existingUser.AccessAll = AccessAll; + existingUser.AccessSecretsManager = AccessSecretsManager; return existingUser; } } diff --git a/src/Api/Models/Response/Organizations/OrganizationUserResponseModel.cs b/src/Api/Models/Response/Organizations/OrganizationUserResponseModel.cs index 6c83a76940..4c7424744c 100644 --- a/src/Api/Models/Response/Organizations/OrganizationUserResponseModel.cs +++ b/src/Api/Models/Response/Organizations/OrganizationUserResponseModel.cs @@ -23,6 +23,7 @@ public class OrganizationUserResponseModel : ResponseModel Type = organizationUser.Type; Status = organizationUser.Status; AccessAll = organizationUser.AccessAll; + AccessSecretsManager = organizationUser.AccessSecretsManager; Permissions = CoreHelpers.LoadClassFromJsonData(organizationUser.Permissions); ResetPasswordEnrolled = !string.IsNullOrEmpty(organizationUser.ResetPasswordKey); } @@ -40,6 +41,7 @@ public class OrganizationUserResponseModel : ResponseModel Type = organizationUser.Type; Status = organizationUser.Status; AccessAll = organizationUser.AccessAll; + AccessSecretsManager = organizationUser.AccessSecretsManager; Permissions = CoreHelpers.LoadClassFromJsonData(organizationUser.Permissions); ResetPasswordEnrolled = !string.IsNullOrEmpty(organizationUser.ResetPasswordKey); UsesKeyConnector = organizationUser.UsesKeyConnector; @@ -50,6 +52,7 @@ public class OrganizationUserResponseModel : ResponseModel public OrganizationUserType Type { get; set; } public OrganizationUserStatusType Status { get; set; } public bool AccessAll { get; set; } + public bool AccessSecretsManager { get; set; } public Permissions Permissions { get; set; } public bool ResetPasswordEnrolled { get; set; } public bool UsesKeyConnector { get; set; } diff --git a/src/Api/Models/Response/ProfileOrganizationResponseModel.cs b/src/Api/Models/Response/ProfileOrganizationResponseModel.cs index d2b98099ed..94ef68c263 100644 --- a/src/Api/Models/Response/ProfileOrganizationResponseModel.cs +++ b/src/Api/Models/Response/ProfileOrganizationResponseModel.cs @@ -52,6 +52,7 @@ public class ProfileOrganizationResponseModel : ResponseModel FamilySponsorshipLastSyncDate = organization.FamilySponsorshipLastSyncDate; FamilySponsorshipToDelete = organization.FamilySponsorshipToDelete; FamilySponsorshipValidUntil = organization.FamilySponsorshipValidUntil; + AccessSecretsManager = organization.AccessSecretsManager; if (organization.SsoConfig != null) { @@ -101,4 +102,5 @@ public class ProfileOrganizationResponseModel : ResponseModel public DateTime? FamilySponsorshipLastSyncDate { get; set; } public DateTime? FamilySponsorshipValidUntil { get; set; } public bool? FamilySponsorshipToDelete { get; set; } + public bool AccessSecretsManager { get; set; } } diff --git a/src/Api/SecretsManager/Controllers/ProjectsController.cs b/src/Api/SecretsManager/Controllers/ProjectsController.cs index d8ef5a00f7..437079d87d 100644 --- a/src/Api/SecretsManager/Controllers/ProjectsController.cs +++ b/src/Api/SecretsManager/Controllers/ProjectsController.cs @@ -7,61 +7,46 @@ using Bit.Core.Exceptions; using Bit.Core.SecretsManager.Commands.Projects.Interfaces; 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")] public class ProjectsController : Controller { + private readonly ICurrentContext _currentContext; private readonly IUserService _userService; private readonly IProjectRepository _projectRepository; private readonly ICreateProjectCommand _createProjectCommand; private readonly IUpdateProjectCommand _updateProjectCommand; private readonly IDeleteProjectCommand _deleteProjectCommand; - private readonly ICurrentContext _currentContext; public ProjectsController( + ICurrentContext currentContext, IUserService userService, IProjectRepository projectRepository, ICreateProjectCommand createProjectCommand, IUpdateProjectCommand updateProjectCommand, - IDeleteProjectCommand deleteProjectCommand, - ICurrentContext currentContext) + IDeleteProjectCommand deleteProjectCommand) { + _currentContext = currentContext; _userService = userService; _projectRepository = projectRepository; _createProjectCommand = createProjectCommand; _updateProjectCommand = updateProjectCommand; _deleteProjectCommand = deleteProjectCommand; - _currentContext = currentContext; } - [HttpPost("organizations/{organizationId}/projects")] - public async Task CreateAsync([FromRoute] Guid organizationId, [FromBody] ProjectCreateRequestModel createRequest) + [HttpGet("organizations/{organizationId}/projects")] + public async Task> ListByOrganizationAsync([FromRoute] Guid organizationId) { - if (!await _currentContext.OrganizationUser(organizationId)) + if (!_currentContext.AccessSecretsManager(organizationId)) { throw new NotFoundException(); } - var result = await _createProjectCommand.CreateAsync(createRequest.ToProject(organizationId)); - return new ProjectResponseModel(result); - } - - [HttpPut("projects/{id}")] - public async Task UpdateProjectAsync([FromRoute] Guid id, [FromBody] ProjectUpdateRequestModel updateRequest) - { - var userId = _userService.GetProperUserId(User).Value; - - var result = await _updateProjectCommand.UpdateAsync(updateRequest.ToProject(id), userId); - return new ProjectResponseModel(result); - } - - [HttpGet("organizations/{organizationId}/projects")] - public async Task> GetProjectsByOrganizationAsync( - [FromRoute] Guid organizationId) - { var userId = _userService.GetProperUserId(User).Value; var orgAdmin = await _currentContext.OrganizationAdmin(organizationId); var accessClient = AccessClientHelper.ToAccessClient(_currentContext.ClientType, orgAdmin); @@ -72,8 +57,29 @@ public class ProjectsController : Controller return new ListResponseModel(responses); } + [HttpPost("organizations/{organizationId}/projects")] + public async Task CreateAsync([FromRoute] Guid organizationId, [FromBody] ProjectCreateRequestModel createRequest) + { + if (!_currentContext.AccessSecretsManager(organizationId)) + { + throw new NotFoundException(); + } + + var result = await _createProjectCommand.CreateAsync(createRequest.ToProject(organizationId)); + return new ProjectResponseModel(result); + } + + [HttpPut("projects/{id}")] + public async Task UpdateAsync([FromRoute] Guid id, [FromBody] ProjectUpdateRequestModel updateRequest) + { + var userId = _userService.GetProperUserId(User).Value; + + var result = await _updateProjectCommand.UpdateAsync(updateRequest.ToProject(id), userId); + return new ProjectResponseModel(result); + } + [HttpGet("projects/{id}")] - public async Task GetProjectAsync([FromRoute] Guid id) + public async Task GetAsync([FromRoute] Guid id) { var project = await _projectRepository.GetByIdAsync(id); if (project == null) @@ -81,6 +87,11 @@ public class ProjectsController : Controller throw new NotFoundException(); } + if (!_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); @@ -101,7 +112,7 @@ public class ProjectsController : Controller } [HttpPost("projects/delete")] - public async Task> BulkDeleteProjectsAsync([FromBody] List ids) + public async Task> BulkDeleteAsync([FromBody] List ids) { var userId = _userService.GetProperUserId(User).Value; diff --git a/src/Api/SecretsManager/Controllers/SecretsController.cs b/src/Api/SecretsManager/Controllers/SecretsController.cs index a66680f63b..3ddf4699d7 100644 --- a/src/Api/SecretsManager/Controllers/SecretsController.cs +++ b/src/Api/SecretsManager/Controllers/SecretsController.cs @@ -1,6 +1,7 @@ using Bit.Api.Models.Response; using Bit.Api.SecretsManager.Models.Request; using Bit.Api.SecretsManager.Models.Response; +using Bit.Core.Context; using Bit.Core.Exceptions; using Bit.Core.SecretsManager.Commands.Secrets.Interfaces; using Bit.Core.SecretsManager.Repositories; @@ -13,30 +14,52 @@ namespace Bit.Api.SecretsManager.Controllers; [Authorize("secrets")] 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; - public SecretsController(ISecretRepository secretRepository, IProjectRepository projectRepository, ICreateSecretCommand createSecretCommand, IUpdateSecretCommand updateSecretCommand, IDeleteSecretCommand deleteSecretCommand) + public SecretsController( + ICurrentContext currentContext, + ISecretRepository secretRepository, + ICreateSecretCommand createSecretCommand, + IUpdateSecretCommand updateSecretCommand, + IDeleteSecretCommand deleteSecretCommand) { + _currentContext = currentContext; _secretRepository = secretRepository; - _projectRepository = projectRepository; _createSecretCommand = createSecretCommand; _updateSecretCommand = updateSecretCommand; _deleteSecretCommand = deleteSecretCommand; } [HttpGet("organizations/{organizationId}/secrets")] - public async Task GetSecretsByOrganizationAsync([FromRoute] Guid organizationId) + public async Task ListByOrganizationAsync([FromRoute] Guid organizationId) { + if (!_currentContext.AccessSecretsManager(organizationId)) + { + throw new NotFoundException(); + } + var secrets = await _secretRepository.GetManyByOrganizationIdAsync(organizationId); return new SecretWithProjectsListResponseModel(secrets); } + [HttpPost("organizations/{organizationId}/secrets")] + public async Task CreateAsync([FromRoute] Guid organizationId, [FromBody] SecretCreateRequestModel createRequest) + { + if (!_currentContext.AccessSecretsManager(organizationId)) + { + throw new NotFoundException(); + } + + var result = await _createSecretCommand.CreateAsync(createRequest.ToSecret(organizationId)); + return new SecretResponseModel(result); + } + [HttpGet("secrets/{id}")] - public async Task GetSecretAsync([FromRoute] Guid id) + public async Task GetAsync([FromRoute] Guid id) { var secret = await _secretRepository.GetByIdAsync(id); if (secret == null) @@ -54,15 +77,8 @@ public class SecretsController : Controller return new SecretWithProjectsListResponseModel(secrets); } - [HttpPost("organizations/{organizationId}/secrets")] - public async Task CreateSecretAsync([FromRoute] Guid organizationId, [FromBody] SecretCreateRequestModel createRequest) - { - var result = await _createSecretCommand.CreateAsync(createRequest.ToSecret(organizationId)); - return new SecretResponseModel(result); - } - [HttpPut("secrets/{id}")] - public async Task UpdateSecretAsync([FromRoute] Guid id, [FromBody] SecretUpdateRequestModel updateRequest) + public async Task UpdateAsync([FromRoute] Guid id, [FromBody] SecretUpdateRequestModel updateRequest) { var result = await _updateSecretCommand.UpdateAsync(updateRequest.ToSecret(id)); return new SecretResponseModel(result); diff --git a/src/Api/SecretsManager/Controllers/ServiceAccountsController.cs b/src/Api/SecretsManager/Controllers/ServiceAccountsController.cs index 6456fa21e3..f70db9d813 100644 --- a/src/Api/SecretsManager/Controllers/ServiceAccountsController.cs +++ b/src/Api/SecretsManager/Controllers/ServiceAccountsController.cs @@ -8,43 +8,50 @@ using Bit.Core.SecretsManager.Commands.AccessTokens.Interfaces; using Bit.Core.SecretsManager.Commands.ServiceAccounts.Interfaces; 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("service-accounts")] public class ServiceAccountsController : Controller { + private readonly ICurrentContext _currentContext; private readonly IApiKeyRepository _apiKeyRepository; private readonly ICreateAccessTokenCommand _createAccessTokenCommand; private readonly ICreateServiceAccountCommand _createServiceAccountCommand; - private readonly ICurrentContext _currentContext; private readonly IServiceAccountRepository _serviceAccountRepository; private readonly IUpdateServiceAccountCommand _updateServiceAccountCommand; private readonly IUserService _userService; public ServiceAccountsController( + ICurrentContext currentContext, IUserService userService, IServiceAccountRepository serviceAccountRepository, ICreateAccessTokenCommand createAccessTokenCommand, IApiKeyRepository apiKeyRepository, ICreateServiceAccountCommand createServiceAccountCommand, - IUpdateServiceAccountCommand updateServiceAccountCommand, - ICurrentContext currentContext) + IUpdateServiceAccountCommand updateServiceAccountCommand) { + _currentContext = currentContext; _userService = userService; _serviceAccountRepository = serviceAccountRepository; _apiKeyRepository = apiKeyRepository; _createServiceAccountCommand = createServiceAccountCommand; _updateServiceAccountCommand = updateServiceAccountCommand; _createAccessTokenCommand = createAccessTokenCommand; - _currentContext = currentContext; } [HttpGet("/organizations/{organizationId}/service-accounts")] - public async Task> GetServiceAccountsByOrganizationAsync( + public async Task> ListByOrganizationAsync( [FromRoute] Guid organizationId) { + if (!_currentContext.AccessSecretsManager(organizationId)) + { + throw new NotFoundException(); + } + var userId = _userService.GetProperUserId(User).Value; var orgAdmin = await _currentContext.OrganizationAdmin(organizationId); var accessClient = AccessClientHelper.ToAccessClient(_currentContext.ClientType, orgAdmin); @@ -57,10 +64,10 @@ public class ServiceAccountsController : Controller } [HttpPost("/organizations/{organizationId}/service-accounts")] - public async Task CreateServiceAccountAsync([FromRoute] Guid organizationId, + public async Task CreateAsync([FromRoute] Guid organizationId, [FromBody] ServiceAccountCreateRequestModel createRequest) { - if (!await _currentContext.OrganizationUser(organizationId)) + if (!_currentContext.AccessSecretsManager(organizationId)) { throw new NotFoundException(); } @@ -70,7 +77,7 @@ public class ServiceAccountsController : Controller } [HttpPut("{id}")] - public async Task UpdateServiceAccountAsync([FromRoute] Guid id, + public async Task UpdateAsync([FromRoute] Guid id, [FromBody] ServiceAccountUpdateRequestModel updateRequest) { var userId = _userService.GetProperUserId(User).Value; @@ -89,6 +96,11 @@ public class ServiceAccountsController : Controller throw new NotFoundException(); } + if (!_currentContext.AccessSecretsManager(serviceAccount.OrganizationId)) + { + throw new NotFoundException(); + } + var orgAdmin = await _currentContext.OrganizationAdmin(serviceAccount.OrganizationId); var accessClient = AccessClientHelper.ToAccessClient(_currentContext.ClientType, orgAdmin); diff --git a/src/Core/Context/CurrentContentOrganization.cs b/src/Core/Context/CurrentContentOrganization.cs index 040c1ece49..b21598a035 100644 --- a/src/Core/Context/CurrentContentOrganization.cs +++ b/src/Core/Context/CurrentContentOrganization.cs @@ -1,6 +1,6 @@ -using Bit.Core.Entities; -using Bit.Core.Enums; +using Bit.Core.Enums; using Bit.Core.Models.Data; +using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Utilities; namespace Bit.Core.Context; @@ -9,14 +9,16 @@ public class CurrentContentOrganization { public CurrentContentOrganization() { } - public CurrentContentOrganization(OrganizationUser orgUser) + public CurrentContentOrganization(OrganizationUserOrganizationDetails orgUser) { Id = orgUser.OrganizationId; Type = orgUser.Type; Permissions = CoreHelpers.LoadClassFromJsonData(orgUser.Permissions); + AccessSecretsManager = orgUser.AccessSecretsManager && orgUser.UseSecretsManager; } public Guid Id { get; set; } public OrganizationUserType Type { get; set; } public Permissions Permissions { get; set; } + public bool AccessSecretsManager { get; set; } } diff --git a/src/Core/Context/CurrentContext.cs b/src/Core/Context/CurrentContext.cs index 68e4246d04..4411509cd5 100644 --- a/src/Core/Context/CurrentContext.cs +++ b/src/Core/Context/CurrentContext.cs @@ -157,6 +157,10 @@ public class CurrentContext : ICurrentContext private List GetOrganizations(Dictionary> claimsDict, bool orgApi) { + var accessSecretsManager = claimsDict.ContainsKey(Claims.SecretsManagerAccess) + ? claimsDict[Claims.SecretsManagerAccess].ToDictionary(s => s.Value, _ => true) + : new Dictionary(); + var organizations = new List(); if (claimsDict.ContainsKey(Claims.OrganizationOwner)) { @@ -164,7 +168,8 @@ public class CurrentContext : ICurrentContext new CurrentContentOrganization { Id = new Guid(c.Value), - Type = OrganizationUserType.Owner + Type = OrganizationUserType.Owner, + AccessSecretsManager = accessSecretsManager.ContainsKey(c.Value), })); } else if (orgApi && OrganizationId.HasValue) @@ -172,7 +177,7 @@ public class CurrentContext : ICurrentContext organizations.Add(new CurrentContentOrganization { Id = OrganizationId.Value, - Type = OrganizationUserType.Owner + Type = OrganizationUserType.Owner, }); } @@ -182,7 +187,8 @@ public class CurrentContext : ICurrentContext new CurrentContentOrganization { Id = new Guid(c.Value), - Type = OrganizationUserType.Admin + Type = OrganizationUserType.Admin, + AccessSecretsManager = accessSecretsManager.ContainsKey(c.Value), })); } @@ -192,7 +198,8 @@ public class CurrentContext : ICurrentContext new CurrentContentOrganization { Id = new Guid(c.Value), - Type = OrganizationUserType.User + Type = OrganizationUserType.User, + AccessSecretsManager = accessSecretsManager.ContainsKey(c.Value), })); } @@ -202,7 +209,8 @@ public class CurrentContext : ICurrentContext new CurrentContentOrganization { Id = new Guid(c.Value), - Type = OrganizationUserType.Manager + Type = OrganizationUserType.Manager, + AccessSecretsManager = accessSecretsManager.ContainsKey(c.Value), })); } @@ -213,7 +221,8 @@ public class CurrentContext : ICurrentContext { Id = new Guid(c.Value), Type = OrganizationUserType.Custom, - Permissions = SetOrganizationPermissionsFromClaims(c.Value, claimsDict) + Permissions = SetOrganizationPermissionsFromClaims(c.Value, claimsDict), + AccessSecretsManager = accessSecretsManager.ContainsKey(c.Value), })); } @@ -434,12 +443,17 @@ public class CurrentContext : ICurrentContext return po?.ProviderId; } + public bool AccessSecretsManager(Guid orgId) + { + return Organizations?.Any(o => o.Id == orgId && o.AccessSecretsManager) ?? false; + } + public async Task> OrganizationMembershipAsync( IOrganizationUserRepository organizationUserRepository, Guid userId) { if (Organizations == null) { - var userOrgs = await organizationUserRepository.GetManyByUserAsync(userId); + var userOrgs = await organizationUserRepository.GetManyDetailsByUserAsync(userId); Organizations = userOrgs.Where(ou => ou.Status == OrganizationUserStatusType.Confirmed) .Select(ou => new CurrentContentOrganization(ou)).ToList(); } diff --git a/src/Core/Context/ICurrentContext.cs b/src/Core/Context/ICurrentContext.cs index d5ea350602..a78757d090 100644 --- a/src/Core/Context/ICurrentContext.cs +++ b/src/Core/Context/ICurrentContext.cs @@ -68,4 +68,5 @@ public interface ICurrentContext IProviderUserRepository providerUserRepository, Guid userId); Task ProviderIdForOrg(Guid orgId); + bool AccessSecretsManager(Guid organizationId); } diff --git a/src/Core/Entities/OrganizationUser.cs b/src/Core/Entities/OrganizationUser.cs index ee1bdc15d4..9e2efb2626 100644 --- a/src/Core/Entities/OrganizationUser.cs +++ b/src/Core/Entities/OrganizationUser.cs @@ -22,6 +22,7 @@ public class OrganizationUser : ITableObject, IExternal public DateTime CreationDate { get; internal set; } = DateTime.UtcNow; public DateTime RevisionDate { get; internal set; } = DateTime.UtcNow; public string Permissions { get; set; } + public bool AccessSecretsManager { get; set; } public void SetNewId() { diff --git a/src/Core/Identity/Claims.cs b/src/Core/Identity/Claims.cs index b56b23ad86..318f0b4009 100644 --- a/src/Core/Identity/Claims.cs +++ b/src/Core/Identity/Claims.cs @@ -6,6 +6,7 @@ public static class Claims public const string SecurityStamp = "sstamp"; public const string Premium = "premium"; public const string Device = "device"; + public const string OrganizationOwner = "orgowner"; public const string OrganizationAdmin = "orgadmin"; public const string OrganizationManager = "orgmanager"; @@ -14,6 +15,8 @@ public static class Claims public const string ProviderAdmin = "providerprovideradmin"; public const string ProviderServiceUser = "providerserviceuser"; + public const string SecretsManagerAccess = "accesssecretsmanager"; + // Service Account public const string Organization = "organization"; diff --git a/src/Core/Models/Business/OrganizationUserInvite.cs b/src/Core/Models/Business/OrganizationUserInvite.cs index 78edfb267e..7102a5f9ef 100644 --- a/src/Core/Models/Business/OrganizationUserInvite.cs +++ b/src/Core/Models/Business/OrganizationUserInvite.cs @@ -8,6 +8,7 @@ public class OrganizationUserInvite public IEnumerable Emails { get; set; } public Enums.OrganizationUserType? Type { get; set; } public bool AccessAll { get; set; } + public bool AccessSecretsManager { get; set; } public Permissions Permissions { get; set; } public IEnumerable Collections { get; set; } public IEnumerable Groups { get; set; } @@ -19,6 +20,7 @@ public class OrganizationUserInvite Emails = requestModel.Emails; Type = requestModel.Type; AccessAll = requestModel.AccessAll; + AccessSecretsManager = requestModel.AccessSecretsManager; Collections = requestModel.Collections; Groups = requestModel.Groups; Permissions = requestModel.Permissions; diff --git a/src/Core/Models/Data/Organizations/OrganizationUsers/OrganizationUserInviteData.cs b/src/Core/Models/Data/Organizations/OrganizationUsers/OrganizationUserInviteData.cs index 887b64e963..f8789fe5d5 100644 --- a/src/Core/Models/Data/Organizations/OrganizationUsers/OrganizationUserInviteData.cs +++ b/src/Core/Models/Data/Organizations/OrganizationUsers/OrganizationUserInviteData.cs @@ -7,6 +7,7 @@ public class OrganizationUserInviteData public IEnumerable Emails { get; set; } public OrganizationUserType? Type { get; set; } public bool AccessAll { get; set; } + public bool AccessSecretsManager { get; set; } public IEnumerable Collections { get; set; } public IEnumerable Groups { get; set; } public Permissions Permissions { get; set; } diff --git a/src/Core/Models/Data/Organizations/OrganizationUsers/OrganizationUserOrganizationDetails.cs b/src/Core/Models/Data/Organizations/OrganizationUsers/OrganizationUserOrganizationDetails.cs index e104dd6213..32b7003700 100644 --- a/src/Core/Models/Data/Organizations/OrganizationUsers/OrganizationUserOrganizationDetails.cs +++ b/src/Core/Models/Data/Organizations/OrganizationUsers/OrganizationUserOrganizationDetails.cs @@ -41,4 +41,5 @@ public class OrganizationUserOrganizationDetails public DateTime? FamilySponsorshipLastSyncDate { get; set; } public DateTime? FamilySponsorshipValidUntil { get; set; } public bool? FamilySponsorshipToDelete { get; set; } + public bool AccessSecretsManager { get; set; } } diff --git a/src/Core/Models/Data/Organizations/OrganizationUsers/OrganizationUserUserDetails.cs b/src/Core/Models/Data/Organizations/OrganizationUsers/OrganizationUserUserDetails.cs index b155118161..74e06182bf 100644 --- a/src/Core/Models/Data/Organizations/OrganizationUsers/OrganizationUserUserDetails.cs +++ b/src/Core/Models/Data/Organizations/OrganizationUsers/OrganizationUserUserDetails.cs @@ -17,6 +17,7 @@ public class OrganizationUserUserDetails : IExternal, ITwoFactorProvidersUser public OrganizationUserStatusType Status { get; set; } public OrganizationUserType Type { get; set; } public bool AccessAll { get; set; } + public bool AccessSecretsManager { get; set; } public string ExternalId { get; set; } public string SsoExternalId { get; set; } public string Permissions { get; set; } diff --git a/src/Core/Services/Implementations/OrganizationService.cs b/src/Core/Services/Implementations/OrganizationService.cs index 8f89e7ed6e..88370639ec 100644 --- a/src/Core/Services/Implementations/OrganizationService.cs +++ b/src/Core/Services/Implementations/OrganizationService.cs @@ -1241,6 +1241,7 @@ public class OrganizationService : IOrganizationService Type = invite.Type.Value, Status = OrganizationUserStatusType.Invited, AccessAll = invite.AccessAll, + AccessSecretsManager = invite.AccessSecretsManager, ExternalId = externalId, CreationDate = DateTime.UtcNow, RevisionDate = DateTime.UtcNow, diff --git a/src/Core/Utilities/CoreHelpers.cs b/src/Core/Utilities/CoreHelpers.cs index b925550d77..27e4551611 100644 --- a/src/Core/Utilities/CoreHelpers.cs +++ b/src/Core/Utilities/CoreHelpers.cs @@ -692,6 +692,15 @@ public static class CoreHelpers default: break; } + + // Secrets Manager + foreach (var org in group) + { + if (org.AccessSecretsManager) + { + claims.Add(new KeyValuePair(Claims.SecretsManagerAccess, org.Id.ToString())); + } + } } } diff --git a/src/Identity/IdentityServer/ApiResources.cs b/src/Identity/IdentityServer/ApiResources.cs index 5d212f99bf..d23c06d7db 100644 --- a/src/Identity/IdentityServer/ApiResources.cs +++ b/src/Identity/IdentityServer/ApiResources.cs @@ -25,6 +25,7 @@ public class ApiResources Claims.OrganizationCustom, Claims.ProviderAdmin, Claims.ProviderServiceUser, + Claims.SecretsManagerAccess, }), new(ApiScopes.Internal, new[] { JwtClaimTypes.Subject }), new(ApiScopes.ApiPush, new[] { JwtClaimTypes.Subject }), diff --git a/src/Infrastructure.Dapper/DapperHelpers.cs b/src/Infrastructure.Dapper/DapperHelpers.cs index bef1f39741..9f40e56000 100644 --- a/src/Infrastructure.Dapper/DapperHelpers.cs +++ b/src/Infrastructure.Dapper/DapperHelpers.cs @@ -59,7 +59,7 @@ public static class DapperHelpers public static DataTable ToTvp(this IEnumerable orgUsers) { var table = new DataTable(); - table.SetTypeName("[dbo].[OrganizationUserType]"); + table.SetTypeName("[dbo].[OrganizationUserType2]"); var columnData = new List<(string name, Type type, Func getter)> { @@ -76,6 +76,7 @@ public static class DapperHelpers (nameof(OrganizationUser.RevisionDate), typeof(DateTime), ou => ou.RevisionDate), (nameof(OrganizationUser.Permissions), typeof(string), ou => ou.Permissions), (nameof(OrganizationUser.ResetPasswordKey), typeof(string), ou => ou.ResetPasswordKey), + (nameof(OrganizationUser.AccessSecretsManager), typeof(bool), ou => ou.AccessSecretsManager), }; return orgUsers.BuildTable(table, columnData); diff --git a/src/Infrastructure.Dapper/Repositories/OrganizationUserRepository.cs b/src/Infrastructure.Dapper/Repositories/OrganizationUserRepository.cs index 60c6c204c7..ef3d5bfbbd 100644 --- a/src/Infrastructure.Dapper/Repositories/OrganizationUserRepository.cs +++ b/src/Infrastructure.Dapper/Repositories/OrganizationUserRepository.cs @@ -405,7 +405,7 @@ public class OrganizationUserRepository : Repository, IO using (var connection = new SqlConnection(_marsConnectionString)) { var results = await connection.ExecuteAsync( - $"[{Schema}].[{Table}_CreateMany]", + $"[{Schema}].[{Table}_CreateMany2]", new { OrganizationUsersInput = orgUsersTVP }, commandType: CommandType.StoredProcedure); } @@ -424,7 +424,7 @@ public class OrganizationUserRepository : Repository, IO using (var connection = new SqlConnection(_marsConnectionString)) { var results = await connection.ExecuteAsync( - $"[{Schema}].[{Table}_UpdateMany]", + $"[{Schema}].[{Table}_UpdateMany2]", new { OrganizationUsersInput = orgUsersTVP }, commandType: CommandType.StoredProcedure); } diff --git a/src/Infrastructure.EntityFramework/Repositories/Queries/OrganizationUserOrganizationDetailsViewQuery.cs b/src/Infrastructure.EntityFramework/Repositories/Queries/OrganizationUserOrganizationDetailsViewQuery.cs index 1e9968cffe..aecad397b5 100644 --- a/src/Infrastructure.EntityFramework/Repositories/Queries/OrganizationUserOrganizationDetailsViewQuery.cs +++ b/src/Infrastructure.EntityFramework/Repositories/Queries/OrganizationUserOrganizationDetailsViewQuery.cs @@ -37,6 +37,7 @@ public class OrganizationUserOrganizationDetailsViewQuery : IQuery + @@ -258,6 +259,7 @@ + @@ -400,6 +402,7 @@ + diff --git a/src/Sql/dbo/Stored Procedures/OrganizationUser_Create.sql b/src/Sql/dbo/Stored Procedures/OrganizationUser_Create.sql index 95795067d4..887f874b00 100644 --- a/src/Sql/dbo/Stored Procedures/OrganizationUser_Create.sql +++ b/src/Sql/dbo/Stored Procedures/OrganizationUser_Create.sql @@ -11,7 +11,8 @@ @CreationDate DATETIME2(7), @RevisionDate DATETIME2(7), @Permissions NVARCHAR(MAX), - @ResetPasswordKey VARCHAR(MAX) + @ResetPasswordKey VARCHAR(MAX), + @AccessSecretsManager BIT = 0 AS BEGIN SET NOCOUNT ON @@ -30,7 +31,8 @@ BEGIN [CreationDate], [RevisionDate], [Permissions], - [ResetPasswordKey] + [ResetPasswordKey], + [AccessSecretsManager] ) VALUES ( @@ -46,6 +48,7 @@ BEGIN @CreationDate, @RevisionDate, @Permissions, - @ResetPasswordKey + @ResetPasswordKey, + @AccessSecretsManager ) END diff --git a/src/Sql/dbo/Stored Procedures/OrganizationUser_CreateMany2.sql b/src/Sql/dbo/Stored Procedures/OrganizationUser_CreateMany2.sql new file mode 100644 index 0000000000..0a07daeb3a --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/OrganizationUser_CreateMany2.sql @@ -0,0 +1,42 @@ +CREATE PROCEDURE [dbo].[OrganizationUser_CreateMany2] + @OrganizationUsersInput [dbo].[OrganizationUserType2] READONLY +AS +BEGIN + SET NOCOUNT ON + + INSERT INTO [dbo].[OrganizationUser] + ( + [Id], + [OrganizationId], + [UserId], + [Email], + [Key], + [Status], + [Type], + [AccessAll], + [ExternalId], + [CreationDate], + [RevisionDate], + [Permissions], + [ResetPasswordKey], + [AccessSecretsManager] + ) + SELECT + OU.[Id], + OU.[OrganizationId], + OU.[UserId], + OU.[Email], + OU.[Key], + OU.[Status], + OU.[Type], + OU.[AccessAll], + OU.[ExternalId], + OU.[CreationDate], + OU.[RevisionDate], + OU.[Permissions], + OU.[ResetPasswordKey], + OU.[AccessSecretsManager] + FROM + @OrganizationUsersInput OU +END +GO diff --git a/src/Sql/dbo/Stored Procedures/OrganizationUser_CreateWithCollections.sql b/src/Sql/dbo/Stored Procedures/OrganizationUser_CreateWithCollections.sql index 55d5f9c084..98809a0ec2 100644 --- a/src/Sql/dbo/Stored Procedures/OrganizationUser_CreateWithCollections.sql +++ b/src/Sql/dbo/Stored Procedures/OrganizationUser_CreateWithCollections.sql @@ -12,12 +12,13 @@ @RevisionDate DATETIME2(7), @Permissions NVARCHAR(MAX), @ResetPasswordKey VARCHAR(MAX), - @Collections AS [dbo].[SelectionReadOnlyArray] READONLY + @Collections AS [dbo].[SelectionReadOnlyArray] READONLY, + @AccessSecretsManager BIT = 0 AS BEGIN SET NOCOUNT ON - EXEC [dbo].[OrganizationUser_Create] @Id, @OrganizationId, @UserId, @Email, @Key, @Status, @Type, @AccessAll, @ExternalId, @CreationDate, @RevisionDate, @Permissions, @ResetPasswordKey + EXEC [dbo].[OrganizationUser_Create] @Id, @OrganizationId, @UserId, @Email, @Key, @Status, @Type, @AccessAll, @ExternalId, @CreationDate, @RevisionDate, @Permissions, @ResetPasswordKey, @AccessSecretsManager ;WITH [AvailableCollectionsCTE] AS( SELECT diff --git a/src/Sql/dbo/Stored Procedures/OrganizationUser_Update.sql b/src/Sql/dbo/Stored Procedures/OrganizationUser_Update.sql index c8cfa08a46..a4565d7894 100644 --- a/src/Sql/dbo/Stored Procedures/OrganizationUser_Update.sql +++ b/src/Sql/dbo/Stored Procedures/OrganizationUser_Update.sql @@ -11,7 +11,8 @@ @CreationDate DATETIME2(7), @RevisionDate DATETIME2(7), @Permissions NVARCHAR(MAX), - @ResetPasswordKey VARCHAR(MAX) + @ResetPasswordKey VARCHAR(MAX), + @AccessSecretsManager BIT = 0 AS BEGIN SET NOCOUNT ON @@ -30,7 +31,8 @@ BEGIN [CreationDate] = @CreationDate, [RevisionDate] = @RevisionDate, [Permissions] = @Permissions, - [ResetPasswordKey] = @ResetPasswordKey + [ResetPasswordKey] = @ResetPasswordKey, + [AccessSecretsManager] = @AccessSecretsManager WHERE [Id] = @Id diff --git a/src/Sql/dbo/Stored Procedures/OrganizationUser_UpdateMany2.sql b/src/Sql/dbo/Stored Procedures/OrganizationUser_UpdateMany2.sql new file mode 100644 index 0000000000..13427e8dc4 --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/OrganizationUser_UpdateMany2.sql @@ -0,0 +1,34 @@ +CREATE PROCEDURE [dbo].[OrganizationUser_UpdateMany2] + @OrganizationUsersInput [dbo].[OrganizationUserType2] READONLY +AS +BEGIN + SET NOCOUNT ON + + UPDATE + OU + SET + [OrganizationId] = OUI.[OrganizationId], + [UserId] = OUI.[UserId], + [Email] = OUI.[Email], + [Key] = OUI.[Key], + [Status] = OUI.[Status], + [Type] = OUI.[Type], + [AccessAll] = OUI.[AccessAll], + [ExternalId] = OUI.[ExternalId], + [CreationDate] = OUI.[CreationDate], + [RevisionDate] = OUI.[RevisionDate], + [Permissions] = OUI.[Permissions], + [ResetPasswordKey] = OUI.[ResetPasswordKey], + [AccessSecretsManager] = OUI.[AccessSecretsManager] + FROM + [dbo].[OrganizationUser] OU + INNER JOIN + @OrganizationUsersInput OUI ON OU.Id = OUI.Id + + EXEC [dbo].[User_BumpManyAccountRevisionDates] + ( + SELECT UserId + FROM @OrganizationUsersInput + ) +END +GO diff --git a/src/Sql/dbo/Stored Procedures/OrganizationUser_UpdateWithCollections.sql b/src/Sql/dbo/Stored Procedures/OrganizationUser_UpdateWithCollections.sql index 20390a02cd..a4eabdaae5 100644 --- a/src/Sql/dbo/Stored Procedures/OrganizationUser_UpdateWithCollections.sql +++ b/src/Sql/dbo/Stored Procedures/OrganizationUser_UpdateWithCollections.sql @@ -12,12 +12,13 @@ @RevisionDate DATETIME2(7), @Permissions NVARCHAR(MAX), @ResetPasswordKey VARCHAR(MAX), - @Collections AS [dbo].[SelectionReadOnlyArray] READONLY + @Collections AS [dbo].[SelectionReadOnlyArray] READONLY, + @AccessSecretsManager BIT = 0 AS BEGIN SET NOCOUNT ON - EXEC [dbo].[OrganizationUser_Update] @Id, @OrganizationId, @UserId, @Email, @Key, @Status, @Type, @AccessAll, @ExternalId, @CreationDate, @RevisionDate, @Permissions, @ResetPasswordKey + EXEC [dbo].[OrganizationUser_Update] @Id, @OrganizationId, @UserId, @Email, @Key, @Status, @Type, @AccessAll, @ExternalId, @CreationDate, @RevisionDate, @Permissions, @ResetPasswordKey, @AccessSecretsManager -- Update UPDATE [Target] diff --git a/src/Sql/dbo/Tables/OrganizationUser.sql b/src/Sql/dbo/Tables/OrganizationUser.sql index fc1fa99505..37218532ce 100644 --- a/src/Sql/dbo/Tables/OrganizationUser.sql +++ b/src/Sql/dbo/Tables/OrganizationUser.sql @@ -12,6 +12,7 @@ [CreationDate] DATETIME2 (7) NOT NULL, [RevisionDate] DATETIME2 (7) NOT NULL, [Permissions] NVARCHAR (MAX) NULL, + [AccessSecretsManager] BIT NOT NULL CONSTRAINT [DF_OrganizationUser_SecretsManager] DEFAULT (0), CONSTRAINT [PK_OrganizationUser] PRIMARY KEY CLUSTERED ([Id] ASC), CONSTRAINT [FK_OrganizationUser_Organization] FOREIGN KEY ([OrganizationId]) REFERENCES [dbo].[Organization] ([Id]) ON DELETE CASCADE, CONSTRAINT [FK_OrganizationUser_User] FOREIGN KEY ([UserId]) REFERENCES [dbo].[User] ([Id]) diff --git a/src/Sql/dbo/User Defined Types/OrganizationUserType2.sql b/src/Sql/dbo/User Defined Types/OrganizationUserType2.sql new file mode 100644 index 0000000000..eb06eff980 --- /dev/null +++ b/src/Sql/dbo/User Defined Types/OrganizationUserType2.sql @@ -0,0 +1,16 @@ +CREATE TYPE [dbo].[OrganizationUserType2] AS TABLE( + [Id] UNIQUEIDENTIFIER, + [OrganizationId] UNIQUEIDENTIFIER, + [UserId] UNIQUEIDENTIFIER, + [Email] NVARCHAR(256), + [Key] VARCHAR(MAX), + [Status] SMALLINT, + [Type] TINYINT, + [AccessAll] BIT, + [ExternalId] NVARCHAR(300), + [CreationDate] DATETIME2(7), + [RevisionDate] DATETIME2(7), + [Permissions] NVARCHAR(MAX), + [ResetPasswordKey] VARCHAR(MAX), + [AccessSecretsManager] BIT +) diff --git a/src/Sql/dbo/Views/OrganizationUserOrganizationDetailsView.sql b/src/Sql/dbo/Views/OrganizationUserOrganizationDetailsView.sql index 5c93a181b5..d475ca216e 100644 --- a/src/Sql/dbo/Views/OrganizationUserOrganizationDetailsView.sql +++ b/src/Sql/dbo/Views/OrganizationUserOrganizationDetailsView.sql @@ -39,7 +39,8 @@ SELECT OS.[FriendlyName] FamilySponsorshipFriendlyName, OS.[LastSyncDate] FamilySponsorshipLastSyncDate, OS.[ToDelete] FamilySponsorshipToDelete, - OS.[ValidUntil] FamilySponsorshipValidUntil + OS.[ValidUntil] FamilySponsorshipValidUntil, + OU.[AccessSecretsManager] FROM [dbo].[OrganizationUser] OU LEFT JOIN diff --git a/src/Sql/dbo/Views/OrganizationUserUserDetailsView.sql b/src/Sql/dbo/Views/OrganizationUserUserDetailsView.sql index e13097884a..4bc43f79ae 100644 --- a/src/Sql/dbo/Views/OrganizationUserUserDetailsView.sql +++ b/src/Sql/dbo/Views/OrganizationUserUserDetailsView.sql @@ -11,6 +11,7 @@ SELECT OU.[Status], OU.[Type], OU.[AccessAll], + OU.[AccessSecretsManager], OU.[ExternalId], SU.[ExternalId] SsoExternalId, OU.[Permissions], diff --git a/src/Sql/dbo_future/Stored Procedures/OrganizationUser_CreateMany.sql b/src/Sql/dbo_future/Stored Procedures/OrganizationUser_CreateMany.sql new file mode 100644 index 0000000000..371848cf9d --- /dev/null +++ b/src/Sql/dbo_future/Stored Procedures/OrganizationUser_CreateMany.sql @@ -0,0 +1,2 @@ +-- Created 2023-01 +-- DELETE FILE diff --git a/src/Sql/dbo_future/Stored Procedures/OrganizationUser_UpdateMany.sql b/src/Sql/dbo_future/Stored Procedures/OrganizationUser_UpdateMany.sql new file mode 100644 index 0000000000..371848cf9d --- /dev/null +++ b/src/Sql/dbo_future/Stored Procedures/OrganizationUser_UpdateMany.sql @@ -0,0 +1,2 @@ +-- Created 2023-01 +-- DELETE FILE diff --git a/src/Sql/dbo_future/User Defined Types/OrganizationUserType.sql b/src/Sql/dbo_future/User Defined Types/OrganizationUserType.sql new file mode 100644 index 0000000000..371848cf9d --- /dev/null +++ b/src/Sql/dbo_future/User Defined Types/OrganizationUserType.sql @@ -0,0 +1,2 @@ +-- Created 2023-01 +-- DELETE FILE diff --git a/test/Api.IntegrationTest/Helpers/OrganizationTestHelpers.cs b/test/Api.IntegrationTest/Helpers/OrganizationTestHelpers.cs index 7163d3e93b..e84d89713e 100644 --- a/test/Api.IntegrationTest/Helpers/OrganizationTestHelpers.cs +++ b/test/Api.IntegrationTest/Helpers/OrganizationTestHelpers.cs @@ -9,8 +9,7 @@ namespace Bit.Api.IntegrationTest.Helpers; public static class OrganizationTestHelpers { - public static async Task> SignUpAsync( - WebApplicationFactoryBase factory, + public static async Task> SignUpAsync(WebApplicationFactoryBase factory, PlanType plan = PlanType.Free, string ownerEmail = "integration-test@bitwarden.com", string name = "Integration Test Org", @@ -36,7 +35,8 @@ public static class OrganizationTestHelpers WebApplicationFactoryBase factory, Guid organizationId, string userEmail, - OrganizationUserType type + OrganizationUserType type, + bool accessSecretsManager = false ) where T : class { var userRepository = factory.GetService(); @@ -50,9 +50,10 @@ public static class OrganizationTestHelpers UserId = user.Id, Key = null, Type = type, - Status = OrganizationUserStatusType.Invited, + Status = OrganizationUserStatusType.Confirmed, AccessAll = false, ExternalId = null, + AccessSecretsManager = accessSecretsManager, }; await organizationUserRepository.CreateAsync(orgUser); diff --git a/test/Api.IntegrationTest/SecretsManager/Controllers/ProjectsControllerTest.cs b/test/Api.IntegrationTest/SecretsManager/Controllers/ProjectsControllerTest.cs index 02dce96294..1b6db5c9c9 100644 --- a/test/Api.IntegrationTest/SecretsManager/Controllers/ProjectsControllerTest.cs +++ b/test/Api.IntegrationTest/SecretsManager/Controllers/ProjectsControllerTest.cs @@ -1,11 +1,9 @@ using System.Net; using System.Net.Http.Headers; using Bit.Api.IntegrationTest.Factories; -using Bit.Api.IntegrationTest.Helpers; 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; @@ -22,7 +20,9 @@ public class ProjectsControllerTest : IClassFixture, IAsy private readonly HttpClient _client; private readonly ApiApplicationFactory _factory; private readonly IProjectRepository _projectRepository; - private Organization _organization = null!; + + private string _email = null!; + private SecretsManagerOrganizationHelper _organizationHelper = null!; public ProjectsControllerTest(ApiApplicationFactory factory) { @@ -33,20 +33,9 @@ public class ProjectsControllerTest : IClassFixture, IAsy public async Task InitializeAsync() { - var ownerEmail = $"integration-test{Guid.NewGuid()}@bitwarden.com"; - await _factory.LoginWithNewAccount(ownerEmail); - (_organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, ownerEmail: ownerEmail, billingEmail: ownerEmail); - var tokens = await _factory.LoginAsync(ownerEmail); - _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tokens.Token); - } - - public async Task LoginAsNewOrgUser(OrganizationUserType type = OrganizationUserType.User) - { - var email = $"integration-test{Guid.NewGuid()}@bitwarden.com"; - await _factory.LoginWithNewAccount(email); - await OrganizationTestHelpers.CreateUserAsync(_factory, _organization.Id, email, type); - var tokens = await _factory.LoginAsync(email); - _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tokens.Token); + _email = $"integration-test{Guid.NewGuid()}@bitwarden.com"; + await _factory.LoginWithNewAccount(_email); + _organizationHelper = new SecretsManagerOrganizationHelper(_factory, _email); } public Task DisposeAsync() @@ -55,12 +44,74 @@ public class ProjectsControllerTest : IClassFixture, IAsy return Task.CompletedTask; } - [Fact] - public async Task CreateProject_Success() + 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 ListByOrganization_SmNotEnabled_NotFound(bool useSecrets, bool accessSecrets) + { + var (org, _) = await _organizationHelper.Initialize(useSecrets, accessSecrets); + await LoginAsync(_email); + + var response = await _client.GetAsync($"/organizations/{org.Id}/projects"); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Fact] + public async Task ListByOrganization_Success() + { + var (org, _) = await _organizationHelper.Initialize(true, true); + await LoginAsync(_email); + + var projectIds = new List(); + for (var i = 0; i < 3; i++) + { + var project = await _projectRepository.CreateAsync(new Project + { + OrganizationId = org.Id, + Name = _mockEncryptedString + }); + projectIds.Add(project.Id); + } + + var response = await _client.GetAsync($"/organizations/{org.Id}/projects"); + response.EnsureSuccessStatusCode(); + + var result = await response.Content.ReadFromJsonAsync>(); + Assert.NotNull(result); + Assert.NotEmpty(result!.Data); + Assert.Equal(projectIds.Count, result.Data.Count()); + } + + [Theory] + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(false, true)] + public async Task Create_SmNotEnabled_NotFound(bool useSecrets, bool accessSecrets) + { + var (org, _) = await _organizationHelper.Initialize(useSecrets, accessSecrets); + await LoginAsync(_email); + var request = new ProjectCreateRequestModel { Name = _mockEncryptedString }; - var response = await _client.PostAsJsonAsync($"/organizations/{_organization.Id}/projects", request); + var response = await _client.PostAsJsonAsync($"/organizations/{org.Id}/projects", request); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Fact] + public async Task Create_Success() + { + var (org, _) = await _organizationHelper.Initialize(true, true); + await LoginAsync(_email); + var request = new ProjectCreateRequestModel { Name = _mockEncryptedString }; + + var response = await _client.PostAsJsonAsync($"/organizations/{org.Id}/projects", request); response.EnsureSuccessStatusCode(); var result = await response.Content.ReadFromJsonAsync(); @@ -77,28 +128,43 @@ public class ProjectsControllerTest : IClassFixture, IAsy Assert.Null(createdProject.DeletedDate); } - [Fact] - public async Task CreateProject_NoPermission() + [Theory] + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(false, true)] + public async Task Update_SmNotEnabled_NotFound(bool useSecrets, bool accessSecrets) { - var request = new ProjectCreateRequestModel { Name = _mockEncryptedString }; + var (org, _) = await _organizationHelper.Initialize(useSecrets, accessSecrets); + await LoginAsync(_email); - var response = await _client.PostAsJsonAsync("/organizations/911d9106-7cf1-4d55-a3f9-f9abdeadecb3/projects", request); + var initialProject = await _projectRepository.CreateAsync(new Project + { + OrganizationId = org.Id, + Name = _mockEncryptedString + }); + var mockEncryptedString2 = "2.3Uk+WNBIoU5xzmVFNcoWzz==|1MsPIYuRfdOHfu/0uY6H2Q==|/98xy4wb6pHP1VTZ9JcNCYgQjEUMFPlqJgCwRk1YXKg="; + var request = new ProjectCreateRequestModel { Name = mockEncryptedString2 }; + + var response = await _client.PutAsJsonAsync($"/projects/{initialProject.Id}", request); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } [Fact] - public async Task UpdateProject_Success() + public async Task Update_Success() { + var (org, _) = await _organizationHelper.Initialize(true, true); + await LoginAsync(_email); + var initialProject = await _projectRepository.CreateAsync(new Project { - OrganizationId = _organization.Id, + OrganizationId = org.Id, Name = _mockEncryptedString }); var mockEncryptedString2 = "2.3Uk+WNBIoU5xzmVFNcoWzz==|1MsPIYuRfdOHfu/0uY6H2Q==|/98xy4wb6pHP1VTZ9JcNCYgQjEUMFPlqJgCwRk1YXKg="; - var request = new ProjectUpdateRequestModel() + var request = new ProjectUpdateRequestModel { Name = mockEncryptedString2 }; @@ -121,9 +187,12 @@ public class ProjectsControllerTest : IClassFixture, IAsy } [Fact] - public async Task UpdateProject_NotFound() + public async Task Update_NonExistingProject_Throws_NotFound() { - var request = new ProjectUpdateRequestModel() + await _organizationHelper.Initialize(true, true); + await LoginAsync(_email); + + var request = new ProjectUpdateRequestModel { Name = "2.3Uk+WNBIoU5xzmVFNcoWzz==|1MsPIYuRfdOHfu/0uY6H2Q==|/98xy4wb6pHP1VTZ9JcNCYgQjEUMFPlqJgCwRk1YXKg=", }; @@ -134,34 +203,59 @@ public class ProjectsControllerTest : IClassFixture, IAsy } [Fact] - public async Task UpdateProject_MissingPermission() + public async Task Update_MissingAccessPolicy_Throws_NotFound() { - // Create a new account as a user - await LoginAsNewOrgUser(); + var (org, _) = await _organizationHelper.Initialize(true, true); + var (email, _) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true); + await LoginAsync(email); var project = await _projectRepository.CreateAsync(new Project { - OrganizationId = _organization.Id, + OrganizationId = org.Id, Name = _mockEncryptedString }); - - var request = new ProjectUpdateRequestModel() + var request = new ProjectUpdateRequestModel { Name = "2.3Uk+WNBIoU5xzmVFNcoWzz==|1MsPIYuRfdOHfu/0uY6H2Q==|/98xy4wb6pHP1VTZ9JcNCYgQjEUMFPlqJgCwRk1YXKg=", }; var response = await _client.PutAsJsonAsync($"/projects/{project.Id}", request); - Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Theory] + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(false, true)] + public async Task Get_SmNotEnabled_NotFound(bool useSecrets, bool accessSecrets) + { + var (org, _) = await _organizationHelper.Initialize(useSecrets, accessSecrets); + await LoginAsync(_email); + + var project = await _projectRepository.CreateAsync(new Project + { + OrganizationId = org.Id, + Name = _mockEncryptedString + }); + + var mockEncryptedString2 = "2.3Uk+WNBIoU5xzmVFNcoWzz==|1MsPIYuRfdOHfu/0uY6H2Q==|/98xy4wb6pHP1VTZ9JcNCYgQjEUMFPlqJgCwRk1YXKg="; + var request = new ProjectCreateRequestModel { Name = mockEncryptedString2 }; + + var response = await _client.PutAsJsonAsync($"/projects/{project.Id}", request); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } [Fact] - public async Task GetProject() + public async Task Get_Success() { + var (org, _) = await _organizationHelper.Initialize(true, true); + await LoginAsync(_email); + var createdProject = await _projectRepository.CreateAsync(new Project { - OrganizationId = _organization.Id, + OrganizationId = org.Id, Name = _mockEncryptedString }); @@ -174,39 +268,58 @@ public class ProjectsControllerTest : IClassFixture, IAsy } [Fact] - public async Task GetProjectsByOrganization() + public async Task Get_MissingAccessPolicy_Throws_NotFound() { - var projectsToCreate = 3; + var (org, _) = await _organizationHelper.Initialize(true, true); + var (email, _) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true); + await LoginAsync(email); + + var createdProject = await _projectRepository.CreateAsync(new Project + { + OrganizationId = org.Id, + Name = _mockEncryptedString + }); + + var response = await _client.GetAsync($"/projects/{createdProject.Id}"); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Theory] + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(false, true)] + public async Task Delete_SmNotEnabled_NotFound(bool useSecrets, bool accessSecrets) + { + var (org, _) = await _organizationHelper.Initialize(useSecrets, accessSecrets); + await LoginAsync(_email); + var projectIds = new List(); - for (var i = 0; i < projectsToCreate; i++) + for (var i = 0; i < 3; i++) { var project = await _projectRepository.CreateAsync(new Project { - OrganizationId = _organization.Id, - Name = _mockEncryptedString + OrganizationId = org.Id, + Name = _mockEncryptedString, }); projectIds.Add(project.Id); } - var response = await _client.GetAsync($"/organizations/{_organization.Id}/projects"); - response.EnsureSuccessStatusCode(); - - var result = await response.Content.ReadFromJsonAsync>(); - Assert.NotNull(result); - Assert.NotEmpty(result!.Data); - Assert.Equal(projectIds.Count, result.Data.Count()); + var response = await _client.PostAsync("/projects/delete", JsonContent.Create(projectIds)); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } [Fact] - public async Task DeleteProjects() + public async Task Delete_Success() { - var projectsToDelete = 3; + var (org, _) = await _organizationHelper.Initialize(true, true); + await LoginAsync(_email); + var projectIds = new List(); - for (var i = 0; i < projectsToDelete; i++) + for (var i = 0; i < 3; i++) { var project = await _projectRepository.CreateAsync(new Project { - OrganizationId = _organization.Id, + OrganizationId = org.Id, Name = _mockEncryptedString, }); projectIds.Add(project.Id); diff --git a/test/Api.IntegrationTest/SecretsManager/Controllers/SecretsControllerTest.cs b/test/Api.IntegrationTest/SecretsManager/Controllers/SecretsControllerTest.cs index a93bd05fea..81ece16d3b 100644 --- a/test/Api.IntegrationTest/SecretsManager/Controllers/SecretsControllerTest.cs +++ b/test/Api.IntegrationTest/SecretsManager/Controllers/SecretsControllerTest.cs @@ -1,10 +1,9 @@ -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.Models.Response; using Bit.Api.SecretsManager.Models.Request; using Bit.Api.SecretsManager.Models.Response; -using Bit.Core.Entities; using Bit.Core.SecretsManager.Entities; using Bit.Core.SecretsManager.Repositories; using Bit.Test.Common.Helpers; @@ -21,7 +20,9 @@ public class SecretsControllerTest : IClassFixture, IAsyn private readonly ApiApplicationFactory _factory; private readonly ISecretRepository _secretRepository; private readonly IProjectRepository _projectRepository; - private Organization _organization = null!; + + private string _email = null!; + private SecretsManagerOrganizationHelper _organizationHelper = null!; public SecretsControllerTest(ApiApplicationFactory factory) { @@ -33,29 +34,98 @@ public class SecretsControllerTest : IClassFixture, IAsyn 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() { + _client.Dispose(); return Task.CompletedTask; } - [Fact] - public async Task CreateSecret() + private async Task LoginAsync(string email) { - var request = new SecretCreateRequestModel() + 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 ListByOrganization_SmNotEnabled_NotFound(bool useSecrets, bool accessSecrets) + { + var (org, _) = await _organizationHelper.Initialize(useSecrets, accessSecrets); + await LoginAsync(_email); + + var response = await _client.GetAsync($"/organizations/{org.Id}/secrets"); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Fact] + public async Task ListByOrganization_Owner_Success() + { + var (org, _) = await _organizationHelper.Initialize(true, true); + await LoginAsync(_email); + + var secretIds = new List(); + for (var i = 0; i < 3; i++) + { + var secret = await _secretRepository.CreateAsync(new Secret + { + OrganizationId = org.Id, + Key = _mockEncryptedString, + Value = _mockEncryptedString, + Note = _mockEncryptedString + }); + secretIds.Add(secret.Id); + } + + var response = await _client.GetAsync($"/organizations/{org.Id}/secrets"); + response.EnsureSuccessStatusCode(); + + var result = await response.Content.ReadFromJsonAsync(); + Assert.NotNull(result); + Assert.NotEmpty(result!.Secrets); + Assert.Equal(secretIds.Count, result.Secrets.Count()); + } + + [Theory] + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(false, true)] + public async Task Create_SmNotEnabled_NotFound(bool useSecrets, bool accessSecrets) + { + var (org, _) = await _organizationHelper.Initialize(useSecrets, accessSecrets); + await LoginAsync(_email); + + var request = new SecretCreateRequestModel { Key = _mockEncryptedString, Value = _mockEncryptedString, Note = _mockEncryptedString }; - var response = await _client.PostAsJsonAsync($"/organizations/{_organization.Id}/secrets", request); + var response = await _client.PostAsJsonAsync($"/organizations/{org.Id}/secrets", request); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Fact] + public async Task Create_Owner_Success() + { + var (org, _) = await _organizationHelper.Initialize(true, true); + await LoginAsync(_email); + + var request = new SecretCreateRequestModel + { + Key = _mockEncryptedString, + Value = _mockEncryptedString, + Note = _mockEncryptedString + }; + + var response = await _client.PostAsJsonAsync($"/organizations/{org.Id}/secrets", request); response.EnsureSuccessStatusCode(); var result = await response.Content.ReadFromJsonAsync(); @@ -77,23 +147,26 @@ public class SecretsControllerTest : IClassFixture, IAsyn } [Fact] - public async Task CreateSecretWithProject() + public async Task CreateWithProject_Owner_Success() { + var (org, _) = await _organizationHelper.Initialize(true, true); + await LoginAsync(_email); + var project = await _projectRepository.CreateAsync(new Project() { Id = new Guid(), - OrganizationId = _organization.Id, + OrganizationId = org.Id, Name = _mockEncryptedString }); - var projectIds = new[] { project.Id }; + var secretRequest = new SecretCreateRequestModel() { Key = _mockEncryptedString, Value = _mockEncryptedString, Note = _mockEncryptedString, - ProjectIds = projectIds, + ProjectIds = new[] { project.Id }, }; - var secretResponse = await _client.PostAsJsonAsync($"/organizations/{_organization.Id}/secrets", secretRequest); + var secretResponse = await _client.PostAsJsonAsync($"/organizations/{org.Id}/secrets", secretRequest); secretResponse.EnsureSuccessStatusCode(); var secretResult = await secretResponse.Content.ReadFromJsonAsync(); @@ -109,12 +182,88 @@ public class SecretsControllerTest : IClassFixture, IAsyn Assert.Equal(secret.RevisionDate, secretResult.RevisionDate); } - [Fact] - public async Task UpdateSecret() + [Theory] + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(false, true)] + public async Task Get_SmNotEnabled_NotFound(bool useSecrets, bool accessSecrets) { - var initialSecret = await _secretRepository.CreateAsync(new Secret + var (org, _) = await _organizationHelper.Initialize(useSecrets, accessSecrets); + await LoginAsync(_email); + + var secret = await _secretRepository.CreateAsync(new Secret { - OrganizationId = _organization.Id, + OrganizationId = org.Id, + Key = _mockEncryptedString, + Value = _mockEncryptedString, + Note = _mockEncryptedString + }); + + var response = await _client.GetAsync($"/organizations/secrets/{secret.Id}"); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Fact] + public async Task Get_Owner_Success() + { + var (org, _) = await _organizationHelper.Initialize(true, true); + await LoginAsync(_email); + + var secret = await _secretRepository.CreateAsync(new Secret + { + OrganizationId = org.Id, + Key = _mockEncryptedString, + Value = _mockEncryptedString, + Note = _mockEncryptedString + }); + + var response = await _client.GetAsync($"/secrets/{secret.Id}"); + response.EnsureSuccessStatusCode(); + var result = await response.Content.ReadFromJsonAsync(); + Assert.Equal(secret.Key, result!.Key); + Assert.Equal(secret.Value, result.Value); + Assert.Equal(secret.Note, result.Note); + Assert.Equal(secret.RevisionDate, result.RevisionDate); + Assert.Equal(secret.CreationDate, result.CreationDate); + } + + [Theory] + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(false, true)] + public async Task Update_SmNotEnabled_NotFound(bool useSecrets, bool accessSecrets) + { + var (org, _) = await _organizationHelper.Initialize(useSecrets, accessSecrets); + await LoginAsync(_email); + + var secret = await _secretRepository.CreateAsync(new Secret + { + OrganizationId = org.Id, + Key = _mockEncryptedString, + Value = _mockEncryptedString, + Note = _mockEncryptedString + }); + + var request = new SecretUpdateRequestModel + { + Key = _mockEncryptedString, + Value = "2.3Uk+WNBIoU5xzmVFNcoWzz==|1MsPIYuRfdOHfu/0uY6H2Q==|/98xy4wb6pHP1VTZ9JcNCYgQjEUMFPlqJgCwRk1YXKg=", + Note = _mockEncryptedString + }; + + var response = await _client.PutAsJsonAsync($"/organizations/secrets/{secret.Id}", request); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Fact] + public async Task Update_Owner_Success() + { + var (org, _) = await _organizationHelper.Initialize(true, true); + await LoginAsync(_email); + + var secret = await _secretRepository.CreateAsync(new Secret + { + OrganizationId = org.Id, Key = _mockEncryptedString, Value = _mockEncryptedString, Note = _mockEncryptedString @@ -127,15 +276,15 @@ public class SecretsControllerTest : IClassFixture, IAsyn Note = _mockEncryptedString }; - var response = await _client.PutAsJsonAsync($"/secrets/{initialSecret.Id}", request); + var response = await _client.PutAsJsonAsync($"/secrets/{secret.Id}", request); response.EnsureSuccessStatusCode(); var result = await response.Content.ReadFromJsonAsync(); Assert.Equal(request.Key, result!.Key); Assert.Equal(request.Value, result.Value); - Assert.NotEqual(initialSecret.Value, result.Value); + Assert.NotEqual(secret.Value, result.Value); Assert.Equal(request.Note, result.Note); AssertHelper.AssertRecent(result.RevisionDate); - Assert.NotEqual(initialSecret.RevisionDate, result.RevisionDate); + Assert.NotEqual(secret.RevisionDate, result.RevisionDate); var updatedSecret = await _secretRepository.GetByIdAsync(new Guid(result.Id)); Assert.NotNull(result); @@ -145,20 +294,44 @@ public class SecretsControllerTest : IClassFixture, IAsyn AssertHelper.AssertRecent(updatedSecret.RevisionDate); AssertHelper.AssertRecent(updatedSecret.CreationDate); Assert.Null(updatedSecret.DeletedDate); - Assert.NotEqual(initialSecret.Value, updatedSecret.Value); - Assert.NotEqual(initialSecret.RevisionDate, updatedSecret.RevisionDate); + Assert.NotEqual(secret.Value, updatedSecret.Value); + Assert.NotEqual(secret.RevisionDate, updatedSecret.RevisionDate); + } + + [Theory] + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(false, true)] + public async Task Delete_SmNotEnabled_NotFound(bool useSecrets, bool accessSecrets) + { + var (org, _) = await _organizationHelper.Initialize(useSecrets, accessSecrets); + await LoginAsync(_email); + + var secret = await _secretRepository.CreateAsync(new Secret + { + OrganizationId = org.Id, + Key = _mockEncryptedString, + Value = _mockEncryptedString, + Note = _mockEncryptedString + }); + var secretIds = new[] { secret.Id }; + + var response = await _client.PostAsJsonAsync("/secrets/delete", secretIds); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } [Fact] - public async Task DeleteSecrets() + public async Task Delete_Owner_Success() { - var secretsToDelete = 3; + var (org, _) = await _organizationHelper.Initialize(true, true); + await LoginAsync(_email); + var secretIds = new List(); - for (var i = 0; i < secretsToDelete; i++) + for (var i = 0; i < 3; i++) { var secret = await _secretRepository.CreateAsync(new Secret { - OrganizationId = _organization.Id, + OrganizationId = org.Id, Key = _mockEncryptedString, Value = _mockEncryptedString, Note = _mockEncryptedString @@ -166,7 +339,7 @@ public class SecretsControllerTest : IClassFixture, IAsyn secretIds.Add(secret.Id); } - var response = await _client.PostAsync("/secrets/delete", JsonContent.Create(secretIds)); + var response = await _client.PostAsJsonAsync("/secrets/delete", secretIds); response.EnsureSuccessStatusCode(); var results = await response.Content.ReadFromJsonAsync>(); @@ -183,52 +356,4 @@ public class SecretsControllerTest : IClassFixture, IAsyn var secrets = await _secretRepository.GetManyByIds(secretIds); Assert.Empty(secrets); } - - [Fact] - public async Task GetSecret() - { - var createdSecret = await _secretRepository.CreateAsync(new Secret - { - OrganizationId = _organization.Id, - Key = _mockEncryptedString, - Value = _mockEncryptedString, - Note = _mockEncryptedString - }); - - - var response = await _client.GetAsync($"/secrets/{createdSecret.Id}"); - response.EnsureSuccessStatusCode(); - var result = await response.Content.ReadFromJsonAsync(); - Assert.Equal(createdSecret.Key, result!.Key); - Assert.Equal(createdSecret.Value, result.Value); - Assert.Equal(createdSecret.Note, result.Note); - Assert.Equal(createdSecret.RevisionDate, result.RevisionDate); - Assert.Equal(createdSecret.CreationDate, result.CreationDate); - } - - [Fact] - public async Task GetSecretsByOrganization() - { - var secretsToCreate = 3; - var secretIds = new List(); - for (var i = 0; i < secretsToCreate; i++) - { - var secret = await _secretRepository.CreateAsync(new Secret - { - OrganizationId = _organization.Id, - Key = _mockEncryptedString, - Value = _mockEncryptedString, - Note = _mockEncryptedString - }); - secretIds.Add(secret.Id); - } - - var response = await _client.GetAsync($"/organizations/{_organization.Id}/secrets"); - response.EnsureSuccessStatusCode(); - - var result = await response.Content.ReadFromJsonAsync(); - Assert.NotNull(result); - Assert.NotEmpty(result!.Secrets); - Assert.Equal(secretIds.Count, result.Secrets.Count()); - } } diff --git a/test/Api.IntegrationTest/SecretsManager/Controllers/ServiceAccountsControllerTests.cs b/test/Api.IntegrationTest/SecretsManager/Controllers/ServiceAccountsControllerTests.cs index 0de403137f..48b1c433cf 100644 --- a/test/Api.IntegrationTest/SecretsManager/Controllers/ServiceAccountsControllerTests.cs +++ b/test/Api.IntegrationTest/SecretsManager/Controllers/ServiceAccountsControllerTests.cs @@ -1,7 +1,6 @@ using System.Net; using System.Net.Http.Headers; using Bit.Api.IntegrationTest.Factories; -using Bit.Api.IntegrationTest.Helpers; using Bit.Api.Models.Response; using Bit.Api.SecretsManager.Models.Request; using Bit.Api.SecretsManager.Models.Response; @@ -26,8 +25,9 @@ public class ServiceAccountsControllerTest : IClassFixture Task.CompletedTask; + [Theory] + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(false, true)] + public async Task ListByOrganization_SmNotEnabled_NotFound(bool useSecrets, bool accessSecrets) + { + var (org, _) = await _organizationHelper.Initialize(useSecrets, accessSecrets); + await LoginAsync(_email); + + var response = await _client.GetAsync($"/organizations/{org.Id}/service-accounts"); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } [Fact] - public async Task GetServiceAccountsByOrganization_Admin() + public async Task ListByOrganization_Admin_Success() { - var serviceAccountIds = await SetupGetServiceAccountsByOrganizationAsync(); + var (org, _) = await _organizationHelper.Initialize(true, true); + await LoginAsync(_email); - var response = await _client.GetAsync($"/organizations/{_organization.Id}/service-accounts"); + var serviceAccountIds = await SetupGetServiceAccountsByOrganizationAsync(org); + + var response = await _client.GetAsync($"/organizations/{org.Id}/service-accounts"); response.EnsureSuccessStatusCode(); var result = await response.Content.ReadFromJsonAsync>(); @@ -64,56 +87,59 @@ public class ServiceAccountsControllerTest : IClassFixture new UserServiceAccountAccessPolicy { - OrganizationUserId = user.Id, + OrganizationUserId = orgUser.Id, GrantedServiceAccountId = id, Read = true, Write = false, }).Cast().ToList(); - await _accessPolicyRepository.CreateManyAsync(accessPolicies); - - var response = await _client.GetAsync($"/organizations/{_organization.Id}/service-accounts"); + var response = await _client.GetAsync($"/organizations/{org.Id}/service-accounts"); response.EnsureSuccessStatusCode(); var result = await response.Content.ReadFromJsonAsync>(); Assert.NotNull(result); Assert.NotEmpty(result!.Data); - Assert.Equal(serviceAccountIds.Count, result.Data.Count()); + Assert.Equal(2, result.Data.Count()); } - [Fact] - public async Task GetServiceAccountsByOrganization_User_NoPermission() + [Theory] + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(false, true)] + public async Task Create_SmNotEnabled_NotFound(bool useSecrets, bool accessSecrets) { - // Create a new account as a user - await LoginAsNewOrgUserAsync(); - await SetupGetServiceAccountsByOrganizationAsync(); + var (org, _) = await _organizationHelper.Initialize(useSecrets, accessSecrets); + await LoginAsync(_email); - var response = await _client.GetAsync($"/organizations/{_organization.Id}/service-accounts"); - response.EnsureSuccessStatusCode(); - var result = await response.Content.ReadFromJsonAsync>(); - - Assert.NotNull(result); - Assert.Empty(result!.Data); - } - - [Fact] - public async Task CreateServiceAccount_Admin() - { var request = new ServiceAccountCreateRequestModel { Name = _mockEncryptedString }; - var response = await _client.PostAsJsonAsync($"/organizations/{_organization.Id}/service-accounts", request); + var response = await _client.PostAsJsonAsync($"/organizations/{org.Id}/service-accounts", request); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Fact] + public async Task Create_Admin_Success() + { + var (org, _) = await _organizationHelper.Initialize(true, true); + await LoginAsync(_email); + + var request = new ServiceAccountCreateRequestModel { Name = _mockEncryptedString }; + + var response = await _client.PostAsJsonAsync($"/organizations/{org.Id}/service-accounts", request); response.EnsureSuccessStatusCode(); var result = await response.Content.ReadFromJsonAsync(); @@ -129,24 +155,36 @@ public class ServiceAccountsControllerTest : IClassFixture(); - - Assert.NotNull(result); - Assert.Equal(request.Name, result!.Name); - Assert.NotNull(result.ClientSecret); - Assert.Null(result.ExpireAt); - AssertHelper.AssertRecent(result.RevisionDate); - AssertHelper.AssertRecent(result.CreationDate); + await _accessPolicyRepository.CreateManyAsync(new List { policy }); } - [Fact] - public async Task CreateServiceAccountAccessTokenExpireAtNullAsync_User_NoPermission() + private async Task> SetupGetServiceAccountsByOrganizationAsync(Organization org) { - // Create a new account as a user - await LoginAsNewOrgUserAsync(); - - var serviceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount - { - OrganizationId = _organization.Id, - Name = _mockEncryptedString, - }); - - var request = new AccessTokenCreateRequestModel - { - Name = _mockEncryptedString, - EncryptedPayload = _mockEncryptedString, - Key = _mockEncryptedString, - ExpireAt = null, - }; - - var response = await _client.PostAsJsonAsync($"/service-accounts/{serviceAccount.Id}/access-tokens", request); - Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); - } - - private async Task> SetupGetServiceAccountsByOrganizationAsync() - { - const int serviceAccountsToCreate = 3; var serviceAccountIds = new List(); - for (var i = 0; i < serviceAccountsToCreate; i++) + for (var i = 0; i < 3; i++) { var serviceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount { - OrganizationId = _organization.Id, + OrganizationId = org.Id, Name = _mockEncryptedString, }); serviceAccountIds.Add(serviceAccount.Id); @@ -415,30 +444,4 @@ public class ServiceAccountsControllerTest : IClassFixture - { - new UserServiceAccountAccessPolicy - { - OrganizationUserId = userId, - GrantedServiceAccountId = serviceAccountId, - Read = read, - Write = write, - }, - }; - await _accessPolicyRepository.CreateManyAsync(accessPolicies); - } - - private async Task LoginAsNewOrgUserAsync(OrganizationUserType type = OrganizationUserType.User) - { - var email = $"integration-test{Guid.NewGuid()}@bitwarden.com"; - await _factory.LoginWithNewAccount(email); - var orgUser = await OrganizationTestHelpers.CreateUserAsync(_factory, _organization.Id, email, type); - var tokens = await _factory.LoginAsync(email); - _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tokens.Token); - return orgUser; - } } diff --git a/test/Api.IntegrationTest/SecretsManager/SecretsManagerOrganizationHelper.cs b/test/Api.IntegrationTest/SecretsManager/SecretsManagerOrganizationHelper.cs new file mode 100644 index 0000000000..7e86386d27 --- /dev/null +++ b/test/Api.IntegrationTest/SecretsManager/SecretsManagerOrganizationHelper.cs @@ -0,0 +1,55 @@ +using Bit.Api.IntegrationTest.Factories; +using Bit.Api.IntegrationTest.Helpers; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Repositories; + +namespace Bit.Api.IntegrationTest.SecretsManager; + +public class SecretsManagerOrganizationHelper +{ + private readonly ApiApplicationFactory _factory; + private readonly string _ownerEmail; + private readonly IOrganizationRepository _organizationRepository; + private readonly IOrganizationUserRepository _organizationUserRepository; + + public Organization _organization = null!; + public OrganizationUser _owner = null!; + + public SecretsManagerOrganizationHelper(ApiApplicationFactory factory, string ownerEmail) + { + _factory = factory; + _organizationRepository = factory.GetService(); + _organizationUserRepository = factory.GetService(); + + _ownerEmail = ownerEmail; + } + + public async Task<(Organization organization, OrganizationUser owner)> Initialize(bool useSecrets, bool ownerAccessSecrets) + { + (_organization, _owner) = await OrganizationTestHelpers.SignUpAsync(_factory, ownerEmail: _ownerEmail, billingEmail: _ownerEmail); + + if (useSecrets) + { + _organization.UseSecretsManager = true; + await _organizationRepository.ReplaceAsync(_organization); + } + + if (ownerAccessSecrets) + { + _owner.AccessSecretsManager = ownerAccessSecrets; + await _organizationUserRepository.ReplaceAsync(_owner); + } + + return (_organization, _owner); + } + + public async Task<(string email, OrganizationUser orgUser)> CreateNewUser(OrganizationUserType userType, bool accessSecrets) + { + var email = $"integration-test{Guid.NewGuid()}@bitwarden.com"; + await _factory.LoginWithNewAccount(email); + var orgUser = await OrganizationTestHelpers.CreateUserAsync(_factory, _organization.Id, email, userType, accessSecrets); + + return (email, orgUser); + } +} diff --git a/test/Api.Test/SecretsManager/Controllers/ProjectsControllerTests.cs b/test/Api.Test/SecretsManager/Controllers/ProjectsControllerTests.cs index 1a83fd5c79..747d213269 100644 --- a/test/Api.Test/SecretsManager/Controllers/ProjectsControllerTests.cs +++ b/test/Api.Test/SecretsManager/Controllers/ProjectsControllerTests.cs @@ -29,7 +29,7 @@ public class ProjectsControllerTests } sutProvider.GetDependency().DeleteProjects(ids, default).ReturnsForAnyArgs(mockResult); - var results = await sutProvider.Sut.BulkDeleteProjectsAsync(ids); + var results = await sutProvider.Sut.BulkDeleteAsync(ids); await sutProvider.GetDependency().Received(1) .DeleteProjects(Arg.Is(ids), Arg.Any()); Assert.Equal(data.Count, results.Data.Count()); @@ -40,6 +40,6 @@ public class ProjectsControllerTests public async void BulkDeleteProjects_NoGuids_ThrowsArgumentNullException(SutProvider sutProvider) { sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(Guid.NewGuid()); - await Assert.ThrowsAsync(() => sutProvider.Sut.BulkDeleteProjectsAsync(new List())); + await Assert.ThrowsAsync(() => sutProvider.Sut.BulkDeleteAsync(new List())); } } diff --git a/test/Api.Test/SecretsManager/Controllers/SecretsControllerTests.cs b/test/Api.Test/SecretsManager/Controllers/SecretsControllerTests.cs index f8454dfa1b..aff105abed 100644 --- a/test/Api.Test/SecretsManager/Controllers/SecretsControllerTests.cs +++ b/test/Api.Test/SecretsManager/Controllers/SecretsControllerTests.cs @@ -1,5 +1,6 @@ using Bit.Api.SecretsManager.Controllers; using Bit.Api.SecretsManager.Models.Request; +using Bit.Core.Context; using Bit.Core.Exceptions; using Bit.Core.SecretsManager.Commands.Secrets.Interfaces; using Bit.Core.SecretsManager.Entities; @@ -23,7 +24,8 @@ public class SecretsControllerTests [BitAutoData] public async void GetSecretsByOrganization_ReturnsEmptyList(SutProvider sutProvider, Guid id) { - var result = await sutProvider.Sut.GetSecretsByOrganizationAsync(id); + sutProvider.GetDependency().AccessSecretsManager(id).Returns(true); + var result = await sutProvider.Sut.ListByOrganizationAsync(id); await sutProvider.GetDependency().Received(1) .GetManyByOrganizationIdAsync(Arg.Is(AssertHelper.AssertPropertyEqual(id))); @@ -31,11 +33,34 @@ public class SecretsControllerTests Assert.Empty(result.Secrets); } + [Theory] + [BitAutoData] + public async void GetSecretsByOrganization_Success(SutProvider sutProvider, Secret resultSecret) + { + sutProvider.GetDependency().AccessSecretsManager(default).ReturnsForAnyArgs(true); + sutProvider.GetDependency().GetManyByOrganizationIdAsync(default).ReturnsForAnyArgs(new List { resultSecret }); + + var result = await sutProvider.Sut.ListByOrganizationAsync(resultSecret.OrganizationId); + + await sutProvider.GetDependency().Received(1) + .GetManyByOrganizationIdAsync(Arg.Is(AssertHelper.AssertPropertyEqual(resultSecret.OrganizationId))); + } + + [Theory] + [BitAutoData] + public async void GetSecretsByOrganization_AccessDenied_Throws(SutProvider sutProvider, Secret resultSecret) + { + sutProvider.GetDependency().AccessSecretsManager(default).ReturnsForAnyArgs(false); + + await Assert.ThrowsAsync(() => + sutProvider.Sut.ListByOrganizationAsync(resultSecret.OrganizationId)); + } + [Theory] [BitAutoData] public async void GetSecret_NotFound(SutProvider sutProvider) { - await Assert.ThrowsAsync(() => sutProvider.Sut.GetSecretAsync(Guid.NewGuid())); + await Assert.ThrowsAsync(() => sutProvider.Sut.GetAsync(Guid.NewGuid())); } [Theory] @@ -44,33 +69,22 @@ public class SecretsControllerTests { sutProvider.GetDependency().GetByIdAsync(default).ReturnsForAnyArgs(resultSecret); - var result = await sutProvider.Sut.GetSecretAsync(resultSecret.Id); + var result = await sutProvider.Sut.GetAsync(resultSecret.Id); await sutProvider.GetDependency().Received(1) .GetByIdAsync(Arg.Is(AssertHelper.AssertPropertyEqual(resultSecret.Id))); } - [Theory] - [BitAutoData] - public async void GetSecretsByOrganization_Success(SutProvider sutProvider, Secret resultSecret) - { - sutProvider.GetDependency().GetManyByOrganizationIdAsync(default).ReturnsForAnyArgs(new List() { resultSecret }); - - var result = await sutProvider.Sut.GetSecretsByOrganizationAsync(resultSecret.OrganizationId); - - await sutProvider.GetDependency().Received(1) - .GetManyByOrganizationIdAsync(Arg.Is(AssertHelper.AssertPropertyEqual(resultSecret.OrganizationId))); - } - [Theory] [BitAutoData] public async void CreateSecret_Success(SutProvider sutProvider, SecretCreateRequestModel data, Guid organizationId) { var resultSecret = data.ToSecret(organizationId); + sutProvider.GetDependency().AccessSecretsManager(organizationId).Returns(true); sutProvider.GetDependency().CreateAsync(default).ReturnsForAnyArgs(resultSecret); - var result = await sutProvider.Sut.CreateSecretAsync(organizationId, data); + var result = await sutProvider.Sut.CreateAsync(organizationId, data); await sutProvider.GetDependency().Received(1) .CreateAsync(Arg.Any()); } @@ -82,7 +96,7 @@ public class SecretsControllerTests var resultSecret = data.ToSecret(secretId); sutProvider.GetDependency().UpdateAsync(default).ReturnsForAnyArgs(resultSecret); - var result = await sutProvider.Sut.UpdateSecretAsync(secretId, data); + var result = await sutProvider.Sut.UpdateAsync(secretId, data); await sutProvider.GetDependency().Received(1) .UpdateAsync(Arg.Any()); } diff --git a/test/Api.Test/SecretsManager/Controllers/ServiceAccountsControllerTests.cs b/test/Api.Test/SecretsManager/Controllers/ServiceAccountsControllerTests.cs index 2cbe18df7e..7a574cb169 100644 --- a/test/Api.Test/SecretsManager/Controllers/ServiceAccountsControllerTests.cs +++ b/test/Api.Test/SecretsManager/Controllers/ServiceAccountsControllerTests.cs @@ -26,8 +26,9 @@ public class ServiceAccountsControllerTests [BitAutoData] public async void GetServiceAccountsByOrganization_ReturnsEmptyList(SutProvider sutProvider, Guid id) { + sutProvider.GetDependency().AccessSecretsManager(id).Returns(true); sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(Guid.NewGuid()); - var result = await sutProvider.Sut.GetServiceAccountsByOrganizationAsync(id); + var result = await sutProvider.Sut.ListByOrganizationAsync(id); await sutProvider.GetDependency().Received(1) .GetManyByOrganizationIdAsync(Arg.Is(AssertHelper.AssertPropertyEqual(id)), Arg.Any(), Arg.Any()); @@ -39,10 +40,11 @@ public class ServiceAccountsControllerTests [BitAutoData] public async void GetServiceAccountsByOrganization_Success(SutProvider sutProvider, ServiceAccount resultServiceAccount) { + sutProvider.GetDependency().AccessSecretsManager(default).ReturnsForAnyArgs(true); sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(Guid.NewGuid()); sutProvider.GetDependency().GetManyByOrganizationIdAsync(default, default, default).ReturnsForAnyArgs(new List() { resultServiceAccount }); - var result = await sutProvider.Sut.GetServiceAccountsByOrganizationAsync(resultServiceAccount.OrganizationId); + var result = await sutProvider.Sut.ListByOrganizationAsync(resultServiceAccount.OrganizationId); await sutProvider.GetDependency().Received(1) .GetManyByOrganizationIdAsync(Arg.Is(AssertHelper.AssertPropertyEqual(resultServiceAccount.OrganizationId)), Arg.Any(), Arg.Any()); @@ -50,16 +52,26 @@ public class ServiceAccountsControllerTests Assert.Single(result.Data); } + [Theory] + [BitAutoData] + public async void GetServiceAccountsByOrganization_AccessDenied_Throws(SutProvider sutProvider, Guid orgId) + { + sutProvider.GetDependency().AccessSecretsManager(default).ReturnsForAnyArgs(false); + + await Assert.ThrowsAsync(() => + sutProvider.Sut.ListByOrganizationAsync(orgId)); + } [Theory] [BitAutoData] public async void CreateServiceAccount_Success(SutProvider sutProvider, ServiceAccountCreateRequestModel data, Guid organizationId) { + sutProvider.GetDependency().AccessSecretsManager(organizationId).Returns(true); sutProvider.GetDependency().OrganizationUser(default).ReturnsForAnyArgs(true); var resultServiceAccount = data.ToServiceAccount(organizationId); sutProvider.GetDependency().CreateAsync(default).ReturnsForAnyArgs(resultServiceAccount); - var result = await sutProvider.Sut.CreateServiceAccountAsync(organizationId, data); + await sutProvider.Sut.CreateAsync(organizationId, data); await sutProvider.GetDependency().Received(1) .CreateAsync(Arg.Any()); } @@ -72,7 +84,7 @@ public class ServiceAccountsControllerTests var resultServiceAccount = data.ToServiceAccount(organizationId); sutProvider.GetDependency().CreateAsync(default).ReturnsForAnyArgs(resultServiceAccount); - await Assert.ThrowsAsync(() => sutProvider.Sut.CreateServiceAccountAsync(organizationId, data)); + await Assert.ThrowsAsync(() => sutProvider.Sut.CreateAsync(organizationId, data)); await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().CreateAsync(Arg.Any()); } @@ -85,7 +97,7 @@ public class ServiceAccountsControllerTests var resultServiceAccount = data.ToServiceAccount(serviceAccountId); sutProvider.GetDependency().UpdateAsync(default, default).ReturnsForAnyArgs(resultServiceAccount); - var result = await sutProvider.Sut.UpdateServiceAccountAsync(serviceAccountId, data); + var result = await sutProvider.Sut.UpdateAsync(serviceAccountId, data); await sutProvider.GetDependency().Received(1) .UpdateAsync(Arg.Any(), Arg.Any()); } @@ -121,6 +133,7 @@ public class ServiceAccountsControllerTests public async void GetAccessTokens_Admin_Success(SutProvider sutProvider, ServiceAccount data, Guid userId, ICollection resultApiKeys) { sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId); + sutProvider.GetDependency().AccessSecretsManager(data.OrganizationId).Returns(true); sutProvider.GetDependency().OrganizationAdmin(data.OrganizationId).Returns(true); sutProvider.GetDependency().GetByIdAsync(default).ReturnsForAnyArgs(data); foreach (var apiKey in resultApiKeys) @@ -140,6 +153,7 @@ public class ServiceAccountsControllerTests public async void GetAccessTokens_User_Success(SutProvider sutProvider, ServiceAccount data, Guid userId, ICollection resultApiKeys) { sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId); + sutProvider.GetDependency().AccessSecretsManager(data.OrganizationId).Returns(true); sutProvider.GetDependency().OrganizationAdmin(data.OrganizationId).Returns(false); sutProvider.GetDependency().UserHasReadAccessToServiceAccount(default, default).ReturnsForAnyArgs(true); sutProvider.GetDependency().GetByIdAsync(default).ReturnsForAnyArgs(data); diff --git a/test/Identity.IntegrationTest/openid-configuration.json b/test/Identity.IntegrationTest/openid-configuration.json index 1e95c93db2..9442330da7 100644 --- a/test/Identity.IntegrationTest/openid-configuration.json +++ b/test/Identity.IntegrationTest/openid-configuration.json @@ -28,6 +28,7 @@ "orgcustom", "providerprovideradmin", "providerserviceuser", + "accesssecretsmanager", "sub", "organization" ], diff --git a/util/Migrator/DbScripts/2023-01-17_00_SecretsManagerOrganizationUser.sql b/util/Migrator/DbScripts/2023-01-17_00_SecretsManagerOrganizationUser.sql new file mode 100644 index 0000000000..f0d36287f8 --- /dev/null +++ b/util/Migrator/DbScripts/2023-01-17_00_SecretsManagerOrganizationUser.sql @@ -0,0 +1,425 @@ +IF COL_LENGTH('[dbo].[OrganizationUser]', 'AccessSecretsManager') IS NULL +BEGIN + ALTER TABLE + [dbo].[OrganizationUser] + ADD + [AccessSecretsManager] BIT NOT NULL CONSTRAINT [DF_OrganizationUser_SecretsManager] DEFAULT (0) +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[OrganizationUser_Update] + @Id UNIQUEIDENTIFIER, + @OrganizationId UNIQUEIDENTIFIER, + @UserId UNIQUEIDENTIFIER, + @Email NVARCHAR(256), + @Key VARCHAR(MAX), + @Status SMALLINT, + @Type TINYINT, + @AccessAll BIT, + @ExternalId NVARCHAR(300), + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7), + @Permissions NVARCHAR(MAX), + @ResetPasswordKey VARCHAR(MAX), + @AccessSecretsManager BIT = 0 +AS +BEGIN + SET NOCOUNT ON + + UPDATE + [dbo].[OrganizationUser] + SET + [OrganizationId] = @OrganizationId, + [UserId] = @UserId, + [Email] = @Email, + [Key] = @Key, + [Status] = @Status, + [Type] = @Type, + [AccessAll] = @AccessAll, + [ExternalId] = @ExternalId, + [CreationDate] = @CreationDate, + [RevisionDate] = @RevisionDate, + [Permissions] = @Permissions, + [ResetPasswordKey] = @ResetPasswordKey, + [AccessSecretsManager] = @AccessSecretsManager + WHERE + [Id] = @Id + + EXEC [dbo].[User_BumpAccountRevisionDate] @UserId +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[OrganizationUser_UpdateWithCollections] + @Id UNIQUEIDENTIFIER, + @OrganizationId UNIQUEIDENTIFIER, + @UserId UNIQUEIDENTIFIER, + @Email NVARCHAR(256), + @Key VARCHAR(MAX), + @Status SMALLINT, + @Type TINYINT, + @AccessAll BIT, + @ExternalId NVARCHAR(300), + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7), + @Permissions NVARCHAR(MAX), + @ResetPasswordKey VARCHAR(MAX), + @Collections AS [dbo].[SelectionReadOnlyArray] READONLY, + @AccessSecretsManager BIT = 0 +AS +BEGIN + SET NOCOUNT ON + + EXEC [dbo].[OrganizationUser_Update] @Id, @OrganizationId, @UserId, @Email, @Key, @Status, @Type, @AccessAll, @ExternalId, @CreationDate, @RevisionDate, @Permissions, @ResetPasswordKey, @AccessSecretsManager + -- Update + UPDATE + [Target] + SET + [Target].[ReadOnly] = [Source].[ReadOnly], + [Target].[HidePasswords] = [Source].[HidePasswords] + FROM + [dbo].[CollectionUser] AS [Target] + INNER JOIN + @Collections AS [Source] ON [Source].[Id] = [Target].[CollectionId] + WHERE + [Target].[OrganizationUserId] = @Id + AND ( + [Target].[ReadOnly] != [Source].[ReadOnly] + OR [Target].[HidePasswords] != [Source].[HidePasswords] + ) + + -- Insert + INSERT INTO + [dbo].[CollectionUser] + SELECT + [Source].[Id], + @Id, + [Source].[ReadOnly], + [Source].[HidePasswords] + FROM + @Collections AS [Source] + INNER JOIN + [dbo].[Collection] C ON C.[Id] = [Source].[Id] AND C.[OrganizationId] = @OrganizationId + WHERE + NOT EXISTS ( + SELECT + 1 + FROM + [dbo].[CollectionUser] + WHERE + [CollectionId] = [Source].[Id] + AND [OrganizationUserId] = @Id + ) + + -- Delete + DELETE + CU + FROM + [dbo].[CollectionUser] CU + WHERE + CU.[OrganizationUserId] = @Id + AND NOT EXISTS ( + SELECT + 1 + FROM + @Collections + WHERE + [Id] = CU.[CollectionId] + ) +END +GO + +IF OBJECT_ID('[dbo].[OrganizationUserView]') IS NOT NULL +BEGIN + EXECUTE sp_refreshsqlmodule N'[dbo].[OrganizationUserView]'; +END +GO + +CREATE OR ALTER VIEW [dbo].[OrganizationUserUserDetailsView] +AS +SELECT + OU.[Id], + OU.[UserId], + OU.[OrganizationId], + U.[Name], + ISNULL(U.[Email], OU.[Email]) Email, + U.[TwoFactorProviders], + U.[Premium], + OU.[Status], + OU.[Type], + OU.[AccessAll], + OU.[AccessSecretsManager], + OU.[ExternalId], + SU.[ExternalId] SsoExternalId, + OU.[Permissions], + OU.[ResetPasswordKey], + U.[UsesKeyConnector] +FROM + [dbo].[OrganizationUser] OU +LEFT JOIN + [dbo].[User] U ON U.[Id] = OU.[UserId] +LEFT JOIN + [dbo].[SsoUser] SU ON SU.[UserId] = OU.[UserId] AND SU.[OrganizationId] = OU.[OrganizationId] +GO + +CREATE OR ALTER VIEW [dbo].[OrganizationUserOrganizationDetailsView] +AS +SELECT + OU.[UserId], + OU.[OrganizationId], + O.[Name], + O.[Enabled], + O.[PlanType], + O.[UsePolicies], + O.[UseSso], + O.[UseKeyConnector], + O.[UseScim], + O.[UseGroups], + O.[UseDirectory], + O.[UseEvents], + O.[UseTotp], + O.[Use2fa], + O.[UseApi], + O.[UseResetPassword], + O.[SelfHost], + O.[UsersGetPremium], + O.[UseCustomPermissions], + O.[UseSecretsManager], + O.[Seats], + O.[MaxCollections], + O.[MaxStorageGb], + O.[Identifier], + OU.[Key], + OU.[ResetPasswordKey], + O.[PublicKey], + O.[PrivateKey], + OU.[Status], + OU.[Type], + SU.[ExternalId] SsoExternalId, + OU.[Permissions], + PO.[ProviderId], + P.[Name] ProviderName, + SS.[Data] SsoConfig, + OS.[FriendlyName] FamilySponsorshipFriendlyName, + OS.[LastSyncDate] FamilySponsorshipLastSyncDate, + OS.[ToDelete] FamilySponsorshipToDelete, + OS.[ValidUntil] FamilySponsorshipValidUntil, + OU.[AccessSecretsManager] +FROM + [dbo].[OrganizationUser] OU +LEFT JOIN + [dbo].[Organization] O ON O.[Id] = OU.[OrganizationId] +LEFT JOIN + [dbo].[SsoUser] SU ON SU.[UserId] = OU.[UserId] AND SU.[OrganizationId] = OU.[OrganizationId] +LEFT JOIN + [dbo].[ProviderOrganization] PO ON PO.[OrganizationId] = O.[Id] +LEFT JOIN + [dbo].[Provider] P ON P.[Id] = PO.[ProviderId] +LEFT JOIN + [dbo].[SsoConfig] SS ON SS.[OrganizationId] = OU.[OrganizationId] +LEFT JOIN + [dbo].[OrganizationSponsorship] OS ON OS.[SponsoringOrganizationUserID] = OU.[Id] +GO + +CREATE OR ALTER PROCEDURE [dbo].[OrganizationUser_Create] + @Id UNIQUEIDENTIFIER OUTPUT, + @OrganizationId UNIQUEIDENTIFIER, + @UserId UNIQUEIDENTIFIER, + @Email NVARCHAR(256), + @Key VARCHAR(MAX), + @Status SMALLINT, + @Type TINYINT, + @AccessAll BIT, + @ExternalId NVARCHAR(300), + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7), + @Permissions NVARCHAR(MAX), + @ResetPasswordKey VARCHAR(MAX), + @AccessSecretsManager BIT = 0 +AS +BEGIN + SET NOCOUNT ON + + INSERT INTO [dbo].[OrganizationUser] + ( + [Id], + [OrganizationId], + [UserId], + [Email], + [Key], + [Status], + [Type], + [AccessAll], + [ExternalId], + [CreationDate], + [RevisionDate], + [Permissions], + [ResetPasswordKey], + [AccessSecretsManager] + ) + VALUES + ( + @Id, + @OrganizationId, + @UserId, + @Email, + @Key, + @Status, + @Type, + @AccessAll, + @ExternalId, + @CreationDate, + @RevisionDate, + @Permissions, + @ResetPasswordKey, + @AccessSecretsManager + ) +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[OrganizationUser_CreateWithCollections] + @Id UNIQUEIDENTIFIER, + @OrganizationId UNIQUEIDENTIFIER, + @UserId UNIQUEIDENTIFIER, + @Email NVARCHAR(256), + @Key VARCHAR(MAX), + @Status SMALLINT, + @Type TINYINT, + @AccessAll BIT, + @ExternalId NVARCHAR(300), + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7), + @Permissions NVARCHAR(MAX), + @ResetPasswordKey VARCHAR(MAX), + @Collections AS [dbo].[SelectionReadOnlyArray] READONLY, + @AccessSecretsManager BIT = 0 +AS +BEGIN + SET NOCOUNT ON + + EXEC [dbo].[OrganizationUser_Create] @Id, @OrganizationId, @UserId, @Email, @Key, @Status, @Type, @AccessAll, @ExternalId, @CreationDate, @RevisionDate, @Permissions, @ResetPasswordKey, @AccessSecretsManager + + ;WITH [AvailableCollectionsCTE] AS( + SELECT + [Id] + FROM + [dbo].[Collection] + WHERE + [OrganizationId] = @OrganizationId + ) + INSERT INTO [dbo].[CollectionUser] + ( + [CollectionId], + [OrganizationUserId], + [ReadOnly], + [HidePasswords] + ) + SELECT + [Id], + @Id, + [ReadOnly], + [HidePasswords] + FROM + @Collections + WHERE + [Id] IN (SELECT [Id] FROM [AvailableCollectionsCTE]) +END +GO + +IF TYPE_ID(N'[dbo].[OrganizationUserType2]') IS NULL +BEGIN + CREATE TYPE [dbo].[OrganizationUserType2] AS TABLE( + [Id] UNIQUEIDENTIFIER, + [OrganizationId] UNIQUEIDENTIFIER, + [UserId] UNIQUEIDENTIFIER, + [Email] NVARCHAR(256), + [Key] VARCHAR(MAX), + [Status] SMALLINT, + [Type] TINYINT, + [AccessAll] BIT, + [ExternalId] NVARCHAR(300), + [CreationDate] DATETIME2(7), + [RevisionDate] DATETIME2(7), + [Permissions] NVARCHAR(MAX), + [ResetPasswordKey] VARCHAR(MAX), + [AccessSecretsManager] BIT + ) +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[OrganizationUser_CreateMany2] + @OrganizationUsersInput [dbo].[OrganizationUserType2] READONLY +AS +BEGIN + SET NOCOUNT ON + + INSERT INTO [dbo].[OrganizationUser] + ( + [Id], + [OrganizationId], + [UserId], + [Email], + [Key], + [Status], + [Type], + [AccessAll], + [ExternalId], + [CreationDate], + [RevisionDate], + [Permissions], + [ResetPasswordKey], + [AccessSecretsManager] + ) + SELECT + OU.[Id], + OU.[OrganizationId], + OU.[UserId], + OU.[Email], + OU.[Key], + OU.[Status], + OU.[Type], + OU.[AccessAll], + OU.[ExternalId], + OU.[CreationDate], + OU.[RevisionDate], + OU.[Permissions], + OU.[ResetPasswordKey], + OU.[AccessSecretsManager] + FROM + @OrganizationUsersInput OU +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[OrganizationUser_UpdateMany2] + @OrganizationUsersInput [dbo].[OrganizationUserType2] READONLY +AS +BEGIN + SET NOCOUNT ON + + UPDATE + OU + SET + [OrganizationId] = OUI.[OrganizationId], + [UserId] = OUI.[UserId], + [Email] = OUI.[Email], + [Key] = OUI.[Key], + [Status] = OUI.[Status], + [Type] = OUI.[Type], + [AccessAll] = OUI.[AccessAll], + [ExternalId] = OUI.[ExternalId], + [CreationDate] = OUI.[CreationDate], + [RevisionDate] = OUI.[RevisionDate], + [Permissions] = OUI.[Permissions], + [ResetPasswordKey] = OUI.[ResetPasswordKey], + [AccessSecretsManager] = OUI.[AccessSecretsManager] + FROM + [dbo].[OrganizationUser] OU + INNER JOIN + @OrganizationUsersInput OUI ON OU.Id = OUI.Id + + EXEC [dbo].[User_BumpManyAccountRevisionDates] + ( + SELECT UserId + FROM @OrganizationUsersInput + ) +END +GO diff --git a/util/Migrator/DbScripts_future/2023-02-FutureMigration.sql b/util/Migrator/DbScripts_future/2023-02-FutureMigration.sql new file mode 100644 index 0000000000..e7336b5553 --- /dev/null +++ b/util/Migrator/DbScripts_future/2023-02-FutureMigration.sql @@ -0,0 +1,17 @@ +IF TYPE_ID(N'[dbo].[OrganizationUserType]') IS NOT NULL +BEGIN + DROP TYPE [dbo].[OrganizationUserType]; +END +GO + +IF OBJECT_ID('[dbo].[OrganizationUser_CreateMany]') IS NOT NULL +BEGIN + DROP PROCEDURE [dbo].[OrganizationUser_CreateMany]; +END +GO + +IF OBJECT_ID('[dbo].[OrganizationUser_UpdateMany]') IS NOT NULL +BEGIN + DROP PROCEDURE [dbo].[OrganizationUser_UpdateMany]; +END +GO