1
0
mirror of https://github.com/bitwarden/server.git synced 2024-12-22 16:57:36 +01:00

[SM-378] Enable SM on a user basis (#2590)

* Add support for giving individual users access to secrets manager
This commit is contained in:
Oscar Hinton 2023-01-31 18:38:53 +01:00 committed by GitHub
parent 54353f8b6c
commit cf25d55090
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
57 changed files with 1419 additions and 400 deletions

View File

@ -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);

View File

@ -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);

View File

@ -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;

View File

@ -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);

View File

@ -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;

View File

@ -36,7 +36,7 @@ public class CreateServiceAccountCommandTests
sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(saData.Id).Returns(saData);
sutProvider.GetDependency<IServiceAccountRepository>().UserHasWriteAccessToServiceAccount(saData.Id, userId).Returns(false);
await Assert.ThrowsAsync<UnauthorizedAccessException>(() => sutProvider.Sut.CreateAsync(data, userId));
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.CreateAsync(data, userId));
await sutProvider.GetDependency<IApiKeyRepository>().DidNotReceiveWithAnyArgs().CreateAsync(default);
}
@ -49,6 +49,7 @@ public class CreateServiceAccountCommandTests
data.ServiceAccountId = saData.Id;
sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(saData.Id).Returns(saData);
sutProvider.GetDependency<IServiceAccountRepository>().UserHasWriteAccessToServiceAccount(saData.Id, userId).Returns(true);
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(saData.OrganizationId).Returns(true);
await sutProvider.Sut.CreateAsync(data, userId);
@ -64,6 +65,7 @@ public class CreateServiceAccountCommandTests
data.ServiceAccountId = saData.Id;
sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(saData.Id).Returns(saData);
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(saData.OrganizationId).Returns(true);
sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(saData.OrganizationId).Returns(true);
await sutProvider.Sut.CreateAsync(data, userId);

View File

@ -28,7 +28,7 @@ public class DeleteProjectCommandTests
[Theory]
[BitAutoData]
public async Task DeleteSecrets_OneIdNotFound_Throws_NotFoundException(List<Guid> data, Guid userId,
public async Task Delete_OneIdNotFound_Throws_NotFoundException(List<Guid> data, Guid userId,
SutProvider<DeleteProjectCommand> 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<ICurrentContext>().AccessSecretsManager(organizationId).Returns(true);
sutProvider.GetDependency<ICurrentContext>().ClientType = ClientType.User;
sutProvider.GetDependency<IProjectRepository>().GetManyByIds(data).Returns(projects);
sutProvider.GetDependency<IProjectRepository>().UserHasWriteAccessToProject(Arg.Any<Guid>(), userId).Returns(true);
@ -65,11 +66,12 @@ public class DeleteProjectCommandTests
[Theory]
[BitAutoData]
public async Task DeleteSecrets_User_No_Permission(List<Guid> data, Guid userId, Guid organizationId,
public async Task Delete_User_No_Permission(List<Guid> data, Guid userId, Guid organizationId,
SutProvider<DeleteProjectCommand> sutProvider)
{
var projects = data.Select(id => new Project { Id = id, OrganizationId = organizationId }).ToList();
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(organizationId).Returns(true);
sutProvider.GetDependency<ICurrentContext>().ClientType = ClientType.User;
sutProvider.GetDependency<IProjectRepository>().GetManyByIds(data).Returns(projects);
sutProvider.GetDependency<IProjectRepository>().UserHasWriteAccessToProject(userId, userId).Returns(false);
@ -86,11 +88,12 @@ public class DeleteProjectCommandTests
[Theory]
[BitAutoData]
public async Task DeleteSecrets_OrganizationAdmin_Success(List<Guid> data, Guid userId, Guid organizationId,
public async Task Delete_OrganizationAdmin_Success(List<Guid> data, Guid userId, Guid organizationId,
SutProvider<DeleteProjectCommand> sutProvider)
{
var projects = data.Select(id => new Project { Id = id, OrganizationId = organizationId }).ToList();
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(organizationId).Returns(true);
sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(organizationId).Returns(true);
sutProvider.GetDependency<IProjectRepository>().GetManyByIds(data).Returns(projects);

View File

@ -33,6 +33,7 @@ public class UpdateProjectCommandTests
public async Task UpdateAsync_Admin_Succeeds(Project project, Guid userId, SutProvider<UpdateProjectCommand> sutProvider)
{
sutProvider.GetDependency<IProjectRepository>().GetByIdAsync(project.Id).Returns(project);
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(project.OrganizationId).Returns(true);
sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(project.OrganizationId).Returns(true);
var project2 = new Project { Id = project.Id, Name = "newName" };
@ -51,8 +52,9 @@ public class UpdateProjectCommandTests
{
sutProvider.GetDependency<IProjectRepository>().GetByIdAsync(project.Id).Returns(project);
sutProvider.GetDependency<IProjectRepository>().UserHasWriteAccessToProject(project.Id, userId).Returns(false);
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(project.OrganizationId).Returns(true);
await Assert.ThrowsAsync<UnauthorizedAccessException>(() => sutProvider.Sut.UpdateAsync(project, userId));
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.UpdateAsync(project, userId));
await sutProvider.GetDependency<IProjectRepository>().DidNotReceiveWithAnyArgs().ReplaceAsync(default);
}
@ -63,6 +65,7 @@ public class UpdateProjectCommandTests
{
sutProvider.GetDependency<IProjectRepository>().GetByIdAsync(project.Id).Returns(project);
sutProvider.GetDependency<IProjectRepository>().UserHasWriteAccessToProject(project.Id, userId).Returns(true);
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(project.OrganizationId).Returns(true);
var project2 = new Project { Id = project.Id, Name = "newName" };
var result = await sutProvider.Sut.UpdateAsync(project2, userId);

View File

@ -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<ISecretRepository>().GetManyByIds(data).Returns(secrets);
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(default).ReturnsForAnyArgs(true);
var results = await sutProvider.Sut.DeleteSecrets(data);

View File

@ -18,7 +18,7 @@ public class UpdateServiceAccountCommandTests
[BitAutoData]
public async Task UpdateAsync_ServiceAccountDoesNotExist_ThrowsNotFound(ServiceAccount data, Guid userId, SutProvider<UpdateServiceAccountCommand> sutProvider)
{
var exception = await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.UpdateAsync(data, userId));
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.UpdateAsync(data, userId));
await sutProvider.GetDependency<IServiceAccountRepository>().DidNotReceiveWithAnyArgs().ReplaceAsync(default);
}
@ -30,7 +30,7 @@ public class UpdateServiceAccountCommandTests
sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(data.Id).Returns(data);
sutProvider.GetDependency<IServiceAccountRepository>().UserHasWriteAccessToServiceAccount(data.Id, userId).Returns(false);
await Assert.ThrowsAsync<UnauthorizedAccessException>(() => sutProvider.Sut.UpdateAsync(data, userId));
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.UpdateAsync(data, userId));
await sutProvider.GetDependency<IServiceAccountRepository>().DidNotReceiveWithAnyArgs().ReplaceAsync(default);
}
@ -39,6 +39,7 @@ public class UpdateServiceAccountCommandTests
[BitAutoData]
public async Task UpdateAsync_User_Success(ServiceAccount data, Guid userId, SutProvider<UpdateServiceAccountCommand> sutProvider)
{
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(data.OrganizationId).Returns(true);
sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(data.Id).Returns(data);
sutProvider.GetDependency<IServiceAccountRepository>().UserHasWriteAccessToServiceAccount(data.Id, userId).Returns(true);
@ -54,6 +55,7 @@ public class UpdateServiceAccountCommandTests
public async Task UpdateAsync_Admin_Success(ServiceAccount data, Guid userId, SutProvider<UpdateServiceAccountCommand> sutProvider)
{
sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(data.Id).Returns(data);
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(data.OrganizationId).Returns(true);
sutProvider.GetDependency<ICurrentContext>().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<UpdateServiceAccountCommand> sutProvider)
{
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(existingServiceAccount.OrganizationId).Returns(true);
sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(existingServiceAccount.Id).Returns(existingServiceAccount);
sutProvider.GetDependency<IServiceAccountRepository>().UserHasWriteAccessToServiceAccount(existingServiceAccount.Id, userId).Returns(true);
@ -87,6 +90,7 @@ public class UpdateServiceAccountCommandTests
[BitAutoData]
public async Task UpdateAsync_DoesNotModifyCreationDate(ServiceAccount existingServiceAccount, Guid userId, SutProvider<UpdateServiceAccountCommand> sutProvider)
{
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(existingServiceAccount.OrganizationId).Returns(true);
sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(existingServiceAccount.Id).Returns(existingServiceAccount);
sutProvider.GetDependency<IServiceAccountRepository>().UserHasWriteAccessToServiceAccount(existingServiceAccount.Id, userId).Returns(true);
@ -108,6 +112,7 @@ public class UpdateServiceAccountCommandTests
[BitAutoData]
public async Task UpdateAsync_RevisionDateIsUpdatedToUtcNow(ServiceAccount existingServiceAccount, Guid userId, SutProvider<UpdateServiceAccountCommand> sutProvider)
{
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(existingServiceAccount.OrganizationId).Returns(true);
sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(existingServiceAccount.Id).Returns(existingServiceAccount);
sutProvider.GetDependency<IServiceAccountRepository>().UserHasWriteAccessToServiceAccount(existingServiceAccount.Id, userId).Returns(true);

View File

@ -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<SelectionReadOnlyRequestModel> Collections { get; set; }
public IEnumerable<Guid> 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<SelectionReadOnlyRequestModel> Collections { get; set; }
public IEnumerable<Guid> Groups { get; set; }
@ -85,6 +88,7 @@ public class OrganizationUserUpdateRequestModel
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
});
existingUser.AccessAll = AccessAll;
existingUser.AccessSecretsManager = AccessSecretsManager;
return existingUser;
}
}

View File

@ -23,6 +23,7 @@ public class OrganizationUserResponseModel : ResponseModel
Type = organizationUser.Type;
Status = organizationUser.Status;
AccessAll = organizationUser.AccessAll;
AccessSecretsManager = organizationUser.AccessSecretsManager;
Permissions = CoreHelpers.LoadClassFromJsonData<Permissions>(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<Permissions>(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; }

View File

@ -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; }
}

View File

@ -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<ProjectResponseModel> CreateAsync([FromRoute] Guid organizationId, [FromBody] ProjectCreateRequestModel createRequest)
[HttpGet("organizations/{organizationId}/projects")]
public async Task<ListResponseModel<ProjectResponseModel>> 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<ProjectResponseModel> 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<ListResponseModel<ProjectResponseModel>> 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<ProjectResponseModel>(responses);
}
[HttpPost("organizations/{organizationId}/projects")]
public async Task<ProjectResponseModel> 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<ProjectResponseModel> 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<ProjectResponseModel> GetProjectAsync([FromRoute] Guid id)
public async Task<ProjectResponseModel> 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<ListResponseModel<BulkDeleteResponseModel>> BulkDeleteProjectsAsync([FromBody] List<Guid> ids)
public async Task<ListResponseModel<BulkDeleteResponseModel>> BulkDeleteAsync([FromBody] List<Guid> ids)
{
var userId = _userService.GetProperUserId(User).Value;

View File

@ -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<SecretWithProjectsListResponseModel> GetSecretsByOrganizationAsync([FromRoute] Guid organizationId)
public async Task<SecretWithProjectsListResponseModel> 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<SecretResponseModel> 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<SecretResponseModel> GetSecretAsync([FromRoute] Guid id)
public async Task<SecretResponseModel> 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<SecretResponseModel> 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<SecretResponseModel> UpdateSecretAsync([FromRoute] Guid id, [FromBody] SecretUpdateRequestModel updateRequest)
public async Task<SecretResponseModel> UpdateAsync([FromRoute] Guid id, [FromBody] SecretUpdateRequestModel updateRequest)
{
var result = await _updateSecretCommand.UpdateAsync(updateRequest.ToSecret(id));
return new SecretResponseModel(result);

View File

@ -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<ListResponseModel<ServiceAccountResponseModel>> GetServiceAccountsByOrganizationAsync(
public async Task<ListResponseModel<ServiceAccountResponseModel>> 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<ServiceAccountResponseModel> CreateServiceAccountAsync([FromRoute] Guid organizationId,
public async Task<ServiceAccountResponseModel> 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<ServiceAccountResponseModel> UpdateServiceAccountAsync([FromRoute] Guid id,
public async Task<ServiceAccountResponseModel> 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);

View File

@ -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<Permissions>(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; }
}

View File

@ -157,6 +157,10 @@ public class CurrentContext : ICurrentContext
private List<CurrentContentOrganization> GetOrganizations(Dictionary<string, IEnumerable<Claim>> claimsDict, bool orgApi)
{
var accessSecretsManager = claimsDict.ContainsKey(Claims.SecretsManagerAccess)
? claimsDict[Claims.SecretsManagerAccess].ToDictionary(s => s.Value, _ => true)
: new Dictionary<string, bool>();
var organizations = new List<CurrentContentOrganization>();
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<ICollection<CurrentContentOrganization>> 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();
}

View File

@ -68,4 +68,5 @@ public interface ICurrentContext
IProviderUserRepository providerUserRepository, Guid userId);
Task<Guid?> ProviderIdForOrg(Guid orgId);
bool AccessSecretsManager(Guid organizationId);
}

View File

@ -22,6 +22,7 @@ public class OrganizationUser : ITableObject<Guid>, 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()
{

View File

@ -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";

View File

@ -8,6 +8,7 @@ public class OrganizationUserInvite
public IEnumerable<string> 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<CollectionAccessSelection> Collections { get; set; }
public IEnumerable<Guid> 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;

View File

@ -7,6 +7,7 @@ public class OrganizationUserInviteData
public IEnumerable<string> Emails { get; set; }
public OrganizationUserType? Type { get; set; }
public bool AccessAll { get; set; }
public bool AccessSecretsManager { get; set; }
public IEnumerable<CollectionAccessSelection> Collections { get; set; }
public IEnumerable<Guid> Groups { get; set; }
public Permissions Permissions { get; set; }

View File

@ -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; }
}

View File

@ -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; }

View File

@ -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,

View File

@ -692,6 +692,15 @@ public static class CoreHelpers
default:
break;
}
// Secrets Manager
foreach (var org in group)
{
if (org.AccessSecretsManager)
{
claims.Add(new KeyValuePair<string, string>(Claims.SecretsManagerAccess, org.Id.ToString()));
}
}
}
}

View File

@ -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 }),

View File

@ -59,7 +59,7 @@ public static class DapperHelpers
public static DataTable ToTvp(this IEnumerable<OrganizationUser> orgUsers)
{
var table = new DataTable();
table.SetTypeName("[dbo].[OrganizationUserType]");
table.SetTypeName("[dbo].[OrganizationUserType2]");
var columnData = new List<(string name, Type type, Func<OrganizationUser, object> 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);

View File

@ -405,7 +405,7 @@ public class OrganizationUserRepository : Repository<OrganizationUser, Guid>, 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<OrganizationUser, Guid>, IO
using (var connection = new SqlConnection(_marsConnectionString))
{
var results = await connection.ExecuteAsync(
$"[{Schema}].[{Table}_UpdateMany]",
$"[{Schema}].[{Table}_UpdateMany2]",
new { OrganizationUsersInput = orgUsersTVP },
commandType: CommandType.StoredProcedure);
}

View File

@ -37,6 +37,7 @@ public class OrganizationUserOrganizationDetailsViewQuery : IQuery<OrganizationU
Use2fa = o.Use2fa,
UseApi = o.UseApi,
UseResetPassword = o.UseResetPassword,
UseSecretsManager = o.UseSecretsManager,
SelfHost = o.SelfHost,
UsersGetPremium = o.UsersGetPremium,
UseCustomPermissions = o.UseCustomPermissions,
@ -58,7 +59,8 @@ public class OrganizationUserOrganizationDetailsViewQuery : IQuery<OrganizationU
FamilySponsorshipFriendlyName = os.FriendlyName,
FamilySponsorshipLastSyncDate = os.LastSyncDate,
FamilySponsorshipToDelete = os.ToDelete,
FamilySponsorshipValidUntil = os.ValidUntil
FamilySponsorshipValidUntil = os.ValidUntil,
AccessSecretsManager = ou.AccessSecretsManager,
};
return query;
}

View File

@ -29,6 +29,7 @@ public class OrganizationUserUserDetailsViewQuery : IQuery<OrganizationUserUserD
Permissions = x.ou.Permissions,
ResetPasswordKey = x.ou.ResetPasswordKey,
UsesKeyConnector = x.u != null && x.u.UsesKeyConnector,
AccessSecretsManager = x.ou.AccessSecretsManager,
});
}
}

View File

@ -238,6 +238,7 @@
<Build Include="dbo\Stored Procedures\OrganizationUser_Activate.sql" />
<Build Include="dbo\Stored Procedures\OrganizationUser_Create.sql" />
<Build Include="dbo\Stored Procedures\OrganizationUser_CreateMany.sql" />
<Build Include="dbo\Stored Procedures\OrganizationUser_CreateMany2.sql" />
<Build Include="dbo\Stored Procedures\OrganizationUser_CreateWithCollections.sql" />
<Build Include="dbo\Stored Procedures\OrganizationUser_Deactivate.sql" />
<Build Include="dbo\Stored Procedures\OrganizationUser_DeleteById.sql" />
@ -258,6 +259,7 @@
<Build Include="dbo\Stored Procedures\OrganizationUser_SelectKnownEmails.sql" />
<Build Include="dbo\Stored Procedures\OrganizationUser_Update.sql" />
<Build Include="dbo\Stored Procedures\OrganizationUser_UpdateMany.sql" />
<Build Include="dbo\Stored Procedures\OrganizationUser_UpdateMany2.sql" />
<Build Include="dbo\Stored Procedures\OrganizationUser_UpdateWithCollections.sql" />
<Build Include="dbo\Stored Procedures\Organization_Create.sql" />
<Build Include="dbo\Stored Procedures\Organization_DeleteById.sql" />
@ -400,6 +402,7 @@
<Build Include="dbo\User Defined Types\GuidIdArray.sql" />
<Build Include="dbo\User Defined Types\OrganizationSponsorshipType.sql" />
<Build Include="dbo\User Defined Types\OrganizationUserType.sql" />
<Build Include="dbo\User Defined Types\OrganizationUserType2.sql" />
<Build Include="dbo\User Defined Types\SelectionReadOnlyArray.sql" />
<Build Include="dbo\User Defined Types\TwoGuidIdArray.sql" />
<Build Include="dbo\Views\AuthRequestView.sql" />

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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]

View File

@ -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])

View File

@ -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
)

View File

@ -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

View File

@ -11,6 +11,7 @@ SELECT
OU.[Status],
OU.[Type],
OU.[AccessAll],
OU.[AccessSecretsManager],
OU.[ExternalId],
SU.[ExternalId] SsoExternalId,
OU.[Permissions],

View File

@ -0,0 +1,2 @@
-- Created 2023-01
-- DELETE FILE

View File

@ -0,0 +1,2 @@
-- Created 2023-01
-- DELETE FILE

View File

@ -0,0 +1,2 @@
-- Created 2023-01
-- DELETE FILE

View File

@ -9,8 +9,7 @@ namespace Bit.Api.IntegrationTest.Helpers;
public static class OrganizationTestHelpers
{
public static async Task<Tuple<Organization, OrganizationUser>> SignUpAsync<T>(
WebApplicationFactoryBase<T> factory,
public static async Task<Tuple<Organization, OrganizationUser>> SignUpAsync<T>(WebApplicationFactoryBase<T> 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<T> factory,
Guid organizationId,
string userEmail,
OrganizationUserType type
OrganizationUserType type,
bool accessSecretsManager = false
) where T : class
{
var userRepository = factory.GetService<IUserRepository>();
@ -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);

View File

@ -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<ApiApplicationFactory>, 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<ApiApplicationFactory>, 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<ApiApplicationFactory>, 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<Guid>();
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<ListResponseModel<ProjectResponseModel>>();
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<ProjectResponseModel>();
@ -77,28 +128,43 @@ public class ProjectsControllerTest : IClassFixture<ApiApplicationFactory>, 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<ApiApplicationFactory>, 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<ApiApplicationFactory>, 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<ApiApplicationFactory>, 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<Guid>();
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<ListResponseModel<ProjectResponseModel>>();
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<Guid>();
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);

View File

@ -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<ApiApplicationFactory>, 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<ApiApplicationFactory>, 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<Guid>();
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<SecretWithProjectsListResponseModel>();
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<SecretResponseModel>();
@ -77,23 +147,26 @@ public class SecretsControllerTest : IClassFixture<ApiApplicationFactory>, 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<SecretResponseModel>();
@ -109,12 +182,88 @@ public class SecretsControllerTest : IClassFixture<ApiApplicationFactory>, 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<SecretResponseModel>();
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<ApiApplicationFactory>, 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<SecretResponseModel>();
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<ApiApplicationFactory>, 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<Guid>();
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<ApiApplicationFactory>, 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<ListResponseModel<BulkDeleteResponseModel>>();
@ -183,52 +356,4 @@ public class SecretsControllerTest : IClassFixture<ApiApplicationFactory>, 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<SecretResponseModel>();
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<Guid>();
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<SecretWithProjectsListResponseModel>();
Assert.NotNull(result);
Assert.NotEmpty(result!.Secrets);
Assert.Equal(secretIds.Count, result.Secrets.Count());
}
}

View File

@ -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<ApiApplicationFactory
private readonly HttpClient _client;
private readonly ApiApplicationFactory _factory;
private readonly IServiceAccountRepository _serviceAccountRepository;
private Organization _organization = null!;
private string _email = null!;
private SecretsManagerOrganizationHelper _organizationHelper = null!;
public ServiceAccountsControllerTest(ApiApplicationFactory factory)
{
@ -39,22 +39,45 @@ public class ServiceAccountsControllerTest : IClassFixture<ApiApplicationFactory
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);
_email = $"integration-test{Guid.NewGuid()}@bitwarden.com";
await _factory.LoginWithNewAccount(_email);
_organizationHelper = new SecretsManagerOrganizationHelper(_factory, _email);
}
public Task DisposeAsync()
{
_client.Dispose();
return Task.CompletedTask;
}
private async Task LoginAsync(string email)
{
var tokens = await _factory.LoginAsync(email);
_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tokens.Token);
}
public Task DisposeAsync() => 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<ListResponseModel<ServiceAccountResponseModel>>();
@ -64,56 +87,59 @@ public class ServiceAccountsControllerTest : IClassFixture<ApiApplicationFactory
}
[Fact]
public async Task GetServiceAccountsByOrganization_User_Success()
public async Task ListByOrganization_User_Success()
{
// Create a new account as a user
var user = await LoginAsNewOrgUserAsync();
var (org, _) = await _organizationHelper.Initialize(true, true);
var (email, orgUser) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);
await LoginAsync(email);
var serviceAccountIds = await SetupGetServiceAccountsByOrganizationAsync();
var serviceAccountIds = await SetupGetServiceAccountsByOrganizationAsync(org);
var accessPolicies = serviceAccountIds.Select(
// Setup access for two
var accessPolicies = serviceAccountIds.Take(2).Select(
id => new UserServiceAccountAccessPolicy
{
OrganizationUserId = user.Id,
OrganizationUserId = orgUser.Id,
GrantedServiceAccountId = id,
Read = true,
Write = false,
}).Cast<BaseAccessPolicy>().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<ListResponseModel<ServiceAccountResponseModel>>();
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<ListResponseModel<ServiceAccountResponseModel>>();
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<ServiceAccountResponseModel>();
@ -129,24 +155,36 @@ public class ServiceAccountsControllerTest : IClassFixture<ApiApplicationFactory
AssertHelper.AssertRecent(createdServiceAccount.CreationDate);
}
[Fact]
public async Task CreateServiceAccount_User_NoPermissions()
[Theory]
[InlineData(false, false)]
[InlineData(true, false)]
[InlineData(false, true)]
public async Task Update_SmNotEnabled_NotFound(bool useSecrets, bool accessSecrets)
{
// Create a new account as a user
await LoginAsNewOrgUserAsync();
var (org, _) = await _organizationHelper.Initialize(useSecrets, accessSecrets);
await LoginAsync(_email);
var request = new ServiceAccountCreateRequestModel { Name = _mockEncryptedString };
var initialServiceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount
{
OrganizationId = org.Id,
Name = _mockEncryptedString,
});
var response = await _client.PostAsJsonAsync($"/organizations/{_organization.Id}/service-accounts", request);
var request = new ServiceAccountUpdateRequestModel { Name = _mockNewName };
var response = await _client.PutAsJsonAsync($"/service-accounts/{initialServiceAccount.Id}", request);
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Fact]
public async Task UpdateServiceAccount_Admin()
public async Task Update_Admin()
{
var (org, _) = await _organizationHelper.Initialize(true, true);
await LoginAsync(_email);
var initialServiceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount
{
OrganizationId = _organization.Id,
OrganizationId = org.Id,
Name = _mockEncryptedString,
});
@ -171,18 +209,19 @@ public class ServiceAccountsControllerTest : IClassFixture<ApiApplicationFactory
}
[Fact]
public async Task UpdateServiceAccount_User_WithPermission()
public async Task Update_User_WithPermission()
{
// Create a new account as a user
var user = await LoginAsNewOrgUserAsync();
var (org, _) = await _organizationHelper.Initialize(true, true);
var (email, orgUser) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);
await LoginAsync(email);
var initialServiceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount
{
OrganizationId = _organization.Id,
OrganizationId = org.Id,
Name = _mockEncryptedString,
});
await CreateUserServiceAccountAccessPolicyAsync(user.Id, initialServiceAccount.Id, true, true);
await CreateUserPolicyAsync(orgUser.Id, initialServiceAccount.Id, true, true);
var request = new ServiceAccountUpdateRequestModel { Name = _mockNewName };
@ -205,29 +244,61 @@ public class ServiceAccountsControllerTest : IClassFixture<ApiApplicationFactory
}
[Fact]
public async Task UpdateServiceAccount_User_NoPermissions()
public async Task Update_User_NoPermissions()
{
// Create a new account as a user
await LoginAsNewOrgUserAsync();
var (org, _) = await _organizationHelper.Initialize(true, true);
var (email, _) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);
await LoginAsync(email);
var initialServiceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount
{
OrganizationId = _organization.Id,
OrganizationId = org.Id,
Name = _mockEncryptedString,
});
var request = new ServiceAccountUpdateRequestModel { Name = _mockNewName };
var response = await _client.PutAsJsonAsync($"/service-accounts/{initialServiceAccount.Id}", request);
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Fact]
public async Task CreateServiceAccountAccessToken_Admin()
[Theory]
[InlineData(false, false)]
[InlineData(true, false)]
[InlineData(false, true)]
public async Task CreateAccessToken_SmNotEnabled_NotFound(bool useSecrets, bool accessSecrets)
{
var (org, _) = await _organizationHelper.Initialize(useSecrets, accessSecrets);
await LoginAsync(_email);
var serviceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount
{
OrganizationId = _organization.Id,
OrganizationId = org.Id,
Name = _mockEncryptedString,
});
var mockExpiresAt = DateTime.UtcNow.AddDays(30);
var request = new AccessTokenCreateRequestModel
{
Name = _mockEncryptedString,
EncryptedPayload = _mockEncryptedString,
Key = _mockEncryptedString,
ExpireAt = mockExpiresAt,
};
var response = await _client.PostAsJsonAsync($"/service-accounts/{serviceAccount.Id}/access-tokens", request);
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Fact]
public async Task CreateAccessToken_Admin()
{
var (org, _) = await _organizationHelper.Initialize(true, true);
await LoginAsync(_email);
var serviceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount
{
OrganizationId = org.Id,
Name = _mockEncryptedString,
});
@ -253,18 +324,19 @@ public class ServiceAccountsControllerTest : IClassFixture<ApiApplicationFactory
}
[Fact]
public async Task CreateServiceAccountAccessToken_User_WithPermission()
public async Task CreateAccessToken_User_WithPermission()
{
// Create a new account as a user
var user = await LoginAsNewOrgUserAsync();
var (org, _) = await _organizationHelper.Initialize(true, true);
var (email, orgUser) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);
await LoginAsync(email);
var serviceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount
{
OrganizationId = _organization.Id,
OrganizationId = org.Id,
Name = _mockEncryptedString,
});
await CreateUserServiceAccountAccessPolicyAsync(user.Id, serviceAccount.Id, true, true);
await CreateUserPolicyAsync(orgUser.Id, serviceAccount.Id, true, true);
var mockExpiresAt = DateTime.UtcNow.AddDays(30);
var request = new AccessTokenCreateRequestModel
@ -288,14 +360,15 @@ public class ServiceAccountsControllerTest : IClassFixture<ApiApplicationFactory
}
[Fact]
public async Task CreateServiceAccountAccessToken_User_NoPermission()
public async Task CreateAccessToken_User_NoPermission()
{
// Create a new account as a user
await LoginAsNewOrgUserAsync();
var (org, _) = await _organizationHelper.Initialize(true, true);
var (email, _) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);
await LoginAsync(email);
var serviceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount
{
OrganizationId = _organization.Id,
OrganizationId = org.Id,
Name = _mockEncryptedString,
});
@ -309,15 +382,18 @@ public class ServiceAccountsControllerTest : IClassFixture<ApiApplicationFactory
};
var response = await _client.PostAsJsonAsync($"/service-accounts/{serviceAccount.Id}/access-tokens", request);
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Fact]
public async Task CreateServiceAccountAccessTokenExpireAtNullAsync_Admin()
public async Task CreateAccessToken_ExpireAtNull_Admin()
{
var (org, _) = await _organizationHelper.Initialize(true, true);
await LoginAsync(_email);
var serviceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount
{
OrganizationId = _organization.Id,
OrganizationId = org.Id,
Name = _mockEncryptedString,
});
@ -341,73 +417,26 @@ public class ServiceAccountsControllerTest : IClassFixture<ApiApplicationFactory
AssertHelper.AssertRecent(result.CreationDate);
}
[Fact]
public async Task CreateServiceAccountAccessTokenExpireAtNullAsync_User_WithPermission()
private async Task CreateUserPolicyAsync(Guid userId, Guid serviceAccountId, bool read, bool write)
{
// Create a new account as a user
var user = await LoginAsNewOrgUserAsync();
var serviceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount
var policy = new UserServiceAccountAccessPolicy
{
OrganizationId = _organization.Id,
Name = _mockEncryptedString,
});
await CreateUserServiceAccountAccessPolicyAsync(user.Id, serviceAccount.Id, true, true);
var request = new AccessTokenCreateRequestModel
{
Name = _mockEncryptedString,
EncryptedPayload = _mockEncryptedString,
Key = _mockEncryptedString,
ExpireAt = null,
OrganizationUserId = userId,
GrantedServiceAccountId = serviceAccountId,
Read = read,
Write = write,
};
var response = await _client.PostAsJsonAsync($"/service-accounts/{serviceAccount.Id}/access-tokens", request);
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<AccessTokenCreationResponseModel>();
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<BaseAccessPolicy> { policy });
}
[Fact]
public async Task CreateServiceAccountAccessTokenExpireAtNullAsync_User_NoPermission()
private async Task<List<Guid>> 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<List<Guid>> SetupGetServiceAccountsByOrganizationAsync()
{
const int serviceAccountsToCreate = 3;
var serviceAccountIds = new List<Guid>();
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<ApiApplicationFactory
return serviceAccountIds;
}
private async Task CreateUserServiceAccountAccessPolicyAsync(Guid userId, Guid serviceAccountId, bool read,
bool write)
{
var accessPolicies = new List<BaseAccessPolicy>
{
new UserServiceAccountAccessPolicy
{
OrganizationUserId = userId,
GrantedServiceAccountId = serviceAccountId,
Read = read,
Write = write,
},
};
await _accessPolicyRepository.CreateManyAsync(accessPolicies);
}
private async Task<OrganizationUser> 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;
}
}

View File

@ -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<IOrganizationRepository>();
_organizationUserRepository = factory.GetService<IOrganizationUserRepository>();
_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);
}
}

View File

@ -29,7 +29,7 @@ public class ProjectsControllerTests
}
sutProvider.GetDependency<IDeleteProjectCommand>().DeleteProjects(ids, default).ReturnsForAnyArgs(mockResult);
var results = await sutProvider.Sut.BulkDeleteProjectsAsync(ids);
var results = await sutProvider.Sut.BulkDeleteAsync(ids);
await sutProvider.GetDependency<IDeleteProjectCommand>().Received(1)
.DeleteProjects(Arg.Is(ids), Arg.Any<Guid>());
Assert.Equal(data.Count, results.Data.Count());
@ -40,6 +40,6 @@ public class ProjectsControllerTests
public async void BulkDeleteProjects_NoGuids_ThrowsArgumentNullException(SutProvider<ProjectsController> sutProvider)
{
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(Guid.NewGuid());
await Assert.ThrowsAsync<ArgumentNullException>(() => sutProvider.Sut.BulkDeleteProjectsAsync(new List<Guid>()));
await Assert.ThrowsAsync<ArgumentNullException>(() => sutProvider.Sut.BulkDeleteAsync(new List<Guid>()));
}
}

View File

@ -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<SecretsController> sutProvider, Guid id)
{
var result = await sutProvider.Sut.GetSecretsByOrganizationAsync(id);
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(id).Returns(true);
var result = await sutProvider.Sut.ListByOrganizationAsync(id);
await sutProvider.GetDependency<ISecretRepository>().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<SecretsController> sutProvider, Secret resultSecret)
{
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(default).ReturnsForAnyArgs(true);
sutProvider.GetDependency<ISecretRepository>().GetManyByOrganizationIdAsync(default).ReturnsForAnyArgs(new List<Secret> { resultSecret });
var result = await sutProvider.Sut.ListByOrganizationAsync(resultSecret.OrganizationId);
await sutProvider.GetDependency<ISecretRepository>().Received(1)
.GetManyByOrganizationIdAsync(Arg.Is(AssertHelper.AssertPropertyEqual(resultSecret.OrganizationId)));
}
[Theory]
[BitAutoData]
public async void GetSecretsByOrganization_AccessDenied_Throws(SutProvider<SecretsController> sutProvider, Secret resultSecret)
{
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(default).ReturnsForAnyArgs(false);
await Assert.ThrowsAsync<NotFoundException>(() =>
sutProvider.Sut.ListByOrganizationAsync(resultSecret.OrganizationId));
}
[Theory]
[BitAutoData]
public async void GetSecret_NotFound(SutProvider<SecretsController> sutProvider)
{
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.GetSecretAsync(Guid.NewGuid()));
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.GetAsync(Guid.NewGuid()));
}
[Theory]
@ -44,33 +69,22 @@ public class SecretsControllerTests
{
sutProvider.GetDependency<ISecretRepository>().GetByIdAsync(default).ReturnsForAnyArgs(resultSecret);
var result = await sutProvider.Sut.GetSecretAsync(resultSecret.Id);
var result = await sutProvider.Sut.GetAsync(resultSecret.Id);
await sutProvider.GetDependency<ISecretRepository>().Received(1)
.GetByIdAsync(Arg.Is(AssertHelper.AssertPropertyEqual(resultSecret.Id)));
}
[Theory]
[BitAutoData]
public async void GetSecretsByOrganization_Success(SutProvider<SecretsController> sutProvider, Secret resultSecret)
{
sutProvider.GetDependency<ISecretRepository>().GetManyByOrganizationIdAsync(default).ReturnsForAnyArgs(new List<Secret>() { resultSecret });
var result = await sutProvider.Sut.GetSecretsByOrganizationAsync(resultSecret.OrganizationId);
await sutProvider.GetDependency<ISecretRepository>().Received(1)
.GetManyByOrganizationIdAsync(Arg.Is(AssertHelper.AssertPropertyEqual(resultSecret.OrganizationId)));
}
[Theory]
[BitAutoData]
public async void CreateSecret_Success(SutProvider<SecretsController> sutProvider, SecretCreateRequestModel data, Guid organizationId)
{
var resultSecret = data.ToSecret(organizationId);
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(organizationId).Returns(true);
sutProvider.GetDependency<ICreateSecretCommand>().CreateAsync(default).ReturnsForAnyArgs(resultSecret);
var result = await sutProvider.Sut.CreateSecretAsync(organizationId, data);
var result = await sutProvider.Sut.CreateAsync(organizationId, data);
await sutProvider.GetDependency<ICreateSecretCommand>().Received(1)
.CreateAsync(Arg.Any<Secret>());
}
@ -82,7 +96,7 @@ public class SecretsControllerTests
var resultSecret = data.ToSecret(secretId);
sutProvider.GetDependency<IUpdateSecretCommand>().UpdateAsync(default).ReturnsForAnyArgs(resultSecret);
var result = await sutProvider.Sut.UpdateSecretAsync(secretId, data);
var result = await sutProvider.Sut.UpdateAsync(secretId, data);
await sutProvider.GetDependency<IUpdateSecretCommand>().Received(1)
.UpdateAsync(Arg.Any<Secret>());
}

View File

@ -26,8 +26,9 @@ public class ServiceAccountsControllerTests
[BitAutoData]
public async void GetServiceAccountsByOrganization_ReturnsEmptyList(SutProvider<ServiceAccountsController> sutProvider, Guid id)
{
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(id).Returns(true);
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(Guid.NewGuid());
var result = await sutProvider.Sut.GetServiceAccountsByOrganizationAsync(id);
var result = await sutProvider.Sut.ListByOrganizationAsync(id);
await sutProvider.GetDependency<IServiceAccountRepository>().Received(1)
.GetManyByOrganizationIdAsync(Arg.Is(AssertHelper.AssertPropertyEqual(id)), Arg.Any<Guid>(), Arg.Any<AccessClientType>());
@ -39,10 +40,11 @@ public class ServiceAccountsControllerTests
[BitAutoData]
public async void GetServiceAccountsByOrganization_Success(SutProvider<ServiceAccountsController> sutProvider, ServiceAccount resultServiceAccount)
{
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(default).ReturnsForAnyArgs(true);
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(Guid.NewGuid());
sutProvider.GetDependency<IServiceAccountRepository>().GetManyByOrganizationIdAsync(default, default, default).ReturnsForAnyArgs(new List<ServiceAccount>() { resultServiceAccount });
var result = await sutProvider.Sut.GetServiceAccountsByOrganizationAsync(resultServiceAccount.OrganizationId);
var result = await sutProvider.Sut.ListByOrganizationAsync(resultServiceAccount.OrganizationId);
await sutProvider.GetDependency<IServiceAccountRepository>().Received(1)
.GetManyByOrganizationIdAsync(Arg.Is(AssertHelper.AssertPropertyEqual(resultServiceAccount.OrganizationId)), Arg.Any<Guid>(), Arg.Any<AccessClientType>());
@ -50,16 +52,26 @@ public class ServiceAccountsControllerTests
Assert.Single(result.Data);
}
[Theory]
[BitAutoData]
public async void GetServiceAccountsByOrganization_AccessDenied_Throws(SutProvider<ServiceAccountsController> sutProvider, Guid orgId)
{
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(default).ReturnsForAnyArgs(false);
await Assert.ThrowsAsync<NotFoundException>(() =>
sutProvider.Sut.ListByOrganizationAsync(orgId));
}
[Theory]
[BitAutoData]
public async void CreateServiceAccount_Success(SutProvider<ServiceAccountsController> sutProvider, ServiceAccountCreateRequestModel data, Guid organizationId)
{
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(organizationId).Returns(true);
sutProvider.GetDependency<ICurrentContext>().OrganizationUser(default).ReturnsForAnyArgs(true);
var resultServiceAccount = data.ToServiceAccount(organizationId);
sutProvider.GetDependency<ICreateServiceAccountCommand>().CreateAsync(default).ReturnsForAnyArgs(resultServiceAccount);
var result = await sutProvider.Sut.CreateServiceAccountAsync(organizationId, data);
await sutProvider.Sut.CreateAsync(organizationId, data);
await sutProvider.GetDependency<ICreateServiceAccountCommand>().Received(1)
.CreateAsync(Arg.Any<ServiceAccount>());
}
@ -72,7 +84,7 @@ public class ServiceAccountsControllerTests
var resultServiceAccount = data.ToServiceAccount(organizationId);
sutProvider.GetDependency<ICreateServiceAccountCommand>().CreateAsync(default).ReturnsForAnyArgs(resultServiceAccount);
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.CreateServiceAccountAsync(organizationId, data));
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.CreateAsync(organizationId, data));
await sutProvider.GetDependency<ICreateServiceAccountCommand>().DidNotReceiveWithAnyArgs().CreateAsync(Arg.Any<ServiceAccount>());
}
@ -85,7 +97,7 @@ public class ServiceAccountsControllerTests
var resultServiceAccount = data.ToServiceAccount(serviceAccountId);
sutProvider.GetDependency<IUpdateServiceAccountCommand>().UpdateAsync(default, default).ReturnsForAnyArgs(resultServiceAccount);
var result = await sutProvider.Sut.UpdateServiceAccountAsync(serviceAccountId, data);
var result = await sutProvider.Sut.UpdateAsync(serviceAccountId, data);
await sutProvider.GetDependency<IUpdateServiceAccountCommand>().Received(1)
.UpdateAsync(Arg.Any<ServiceAccount>(), Arg.Any<Guid>());
}
@ -121,6 +133,7 @@ public class ServiceAccountsControllerTests
public async void GetAccessTokens_Admin_Success(SutProvider<ServiceAccountsController> sutProvider, ServiceAccount data, Guid userId, ICollection<ApiKey> resultApiKeys)
{
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(data.OrganizationId).Returns(true);
sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(data.OrganizationId).Returns(true);
sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(default).ReturnsForAnyArgs(data);
foreach (var apiKey in resultApiKeys)
@ -140,6 +153,7 @@ public class ServiceAccountsControllerTests
public async void GetAccessTokens_User_Success(SutProvider<ServiceAccountsController> sutProvider, ServiceAccount data, Guid userId, ICollection<ApiKey> resultApiKeys)
{
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(data.OrganizationId).Returns(true);
sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(data.OrganizationId).Returns(false);
sutProvider.GetDependency<IServiceAccountRepository>().UserHasReadAccessToServiceAccount(default, default).ReturnsForAnyArgs(true);
sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(default).ReturnsForAnyArgs(data);

View File

@ -28,6 +28,7 @@
"orgcustom",
"providerprovideradmin",
"providerserviceuser",
"accesssecretsmanager",
"sub",
"organization"
],

View File

@ -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

View File

@ -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