1
0
mirror of https://github.com/bitwarden/server.git synced 2025-04-05 05:00:19 -05: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); var serviceAccount = await _serviceAccountRepository.GetByIdAsync(apiKey.ServiceAccountId.Value);
if (!_currentContext.AccessSecretsManager(serviceAccount.OrganizationId))
{
throw new NotFoundException();
}
var orgAdmin = await _currentContext.OrganizationAdmin(serviceAccount.OrganizationId); var orgAdmin = await _currentContext.OrganizationAdmin(serviceAccount.OrganizationId);
var accessClient = AccessClientHelper.ToAccessClient(_currentContext.ClientType, orgAdmin); var accessClient = AccessClientHelper.ToAccessClient(_currentContext.ClientType, orgAdmin);
@ -46,7 +52,7 @@ public class CreateAccessTokenCommand : ICreateAccessTokenCommand
if (!hasAccess) if (!hasAccess)
{ {
throw new UnauthorizedAccessException(); throw new NotFoundException();
} }
apiKey.ClientSecret = CoreHelpers.SecureRandomString(_clientSecretMaxLength); apiKey.ClientSecret = CoreHelpers.SecureRandomString(_clientSecretMaxLength);

View File

@ -36,7 +36,12 @@ public class DeleteProjectCommand : IDeleteProjectCommand
var organizationId = projects.First().OrganizationId; var organizationId = projects.First().OrganizationId;
if (projects.Any(p => p.OrganizationId != 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); var orgAdmin = await _currentContext.OrganizationAdmin(organizationId);

View File

@ -26,6 +26,11 @@ public class UpdateProjectCommand : IUpdateProjectCommand
throw new NotFoundException(); throw new NotFoundException();
} }
if (!_currentContext.AccessSecretsManager(project.OrganizationId))
{
throw new NotFoundException();
}
var orgAdmin = await _currentContext.OrganizationAdmin(project.OrganizationId); var orgAdmin = await _currentContext.OrganizationAdmin(project.OrganizationId);
var accessClient = AccessClientHelper.ToAccessClient(_currentContext.ClientType, orgAdmin); var accessClient = AccessClientHelper.ToAccessClient(_currentContext.ClientType, orgAdmin);
@ -38,7 +43,7 @@ public class UpdateProjectCommand : IUpdateProjectCommand
if (!hasAccess) if (!hasAccess)
{ {
throw new UnauthorizedAccessException(); throw new NotFoundException();
} }
project.Name = updatedProject.Name; 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.Commands.Secrets.Interfaces;
using Bit.Core.SecretsManager.Entities; using Bit.Core.SecretsManager.Entities;
using Bit.Core.SecretsManager.Repositories; using Bit.Core.SecretsManager.Repositories;
@ -7,10 +8,12 @@ namespace Bit.Commercial.Core.SecretsManager.Commands.Secrets;
public class DeleteSecretCommand : IDeleteSecretCommand public class DeleteSecretCommand : IDeleteSecretCommand
{ {
private readonly ICurrentContext _currentContext;
private readonly ISecretRepository _secretRepository; private readonly ISecretRepository _secretRepository;
public DeleteSecretCommand(ISecretRepository secretRepository) public DeleteSecretCommand(ICurrentContext currentContext, ISecretRepository secretRepository)
{ {
_currentContext = currentContext;
_secretRepository = secretRepository; _secretRepository = secretRepository;
} }
@ -23,6 +26,18 @@ public class DeleteSecretCommand : IDeleteSecretCommand
throw new NotFoundException(); 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 results = ids.Select(id =>
{ {
var secret = secrets.FirstOrDefault(secret => secret.Id == id); var secret = secrets.FirstOrDefault(secret => secret.Id == id);

View File

@ -26,6 +26,11 @@ public class UpdateServiceAccountCommand : IUpdateServiceAccountCommand
throw new NotFoundException(); throw new NotFoundException();
} }
if (!_currentContext.AccessSecretsManager(serviceAccount.OrganizationId))
{
throw new NotFoundException();
}
var orgAdmin = await _currentContext.OrganizationAdmin(serviceAccount.OrganizationId); var orgAdmin = await _currentContext.OrganizationAdmin(serviceAccount.OrganizationId);
var accessClient = AccessClientHelper.ToAccessClient(_currentContext.ClientType, orgAdmin); var accessClient = AccessClientHelper.ToAccessClient(_currentContext.ClientType, orgAdmin);
@ -38,7 +43,7 @@ public class UpdateServiceAccountCommand : IUpdateServiceAccountCommand
if (!hasAccess) if (!hasAccess)
{ {
throw new UnauthorizedAccessException(); throw new NotFoundException();
} }
serviceAccount.Name = updatedServiceAccount.Name; serviceAccount.Name = updatedServiceAccount.Name;

View File

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

View File

@ -28,7 +28,7 @@ public class DeleteProjectCommandTests
[Theory] [Theory]
[BitAutoData] [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) SutProvider<DeleteProjectCommand> sutProvider)
{ {
var project = new Project() var project = new Project()
@ -49,6 +49,7 @@ public class DeleteProjectCommandTests
{ {
var projects = data.Select(id => new Project { Id = id, OrganizationId = organizationId }).ToList(); 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<ICurrentContext>().ClientType = ClientType.User;
sutProvider.GetDependency<IProjectRepository>().GetManyByIds(data).Returns(projects); sutProvider.GetDependency<IProjectRepository>().GetManyByIds(data).Returns(projects);
sutProvider.GetDependency<IProjectRepository>().UserHasWriteAccessToProject(Arg.Any<Guid>(), userId).Returns(true); sutProvider.GetDependency<IProjectRepository>().UserHasWriteAccessToProject(Arg.Any<Guid>(), userId).Returns(true);
@ -65,11 +66,12 @@ public class DeleteProjectCommandTests
[Theory] [Theory]
[BitAutoData] [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) SutProvider<DeleteProjectCommand> sutProvider)
{ {
var projects = data.Select(id => new Project { Id = id, OrganizationId = organizationId }).ToList(); 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<ICurrentContext>().ClientType = ClientType.User;
sutProvider.GetDependency<IProjectRepository>().GetManyByIds(data).Returns(projects); sutProvider.GetDependency<IProjectRepository>().GetManyByIds(data).Returns(projects);
sutProvider.GetDependency<IProjectRepository>().UserHasWriteAccessToProject(userId, userId).Returns(false); sutProvider.GetDependency<IProjectRepository>().UserHasWriteAccessToProject(userId, userId).Returns(false);
@ -86,11 +88,12 @@ public class DeleteProjectCommandTests
[Theory] [Theory]
[BitAutoData] [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) SutProvider<DeleteProjectCommand> sutProvider)
{ {
var projects = data.Select(id => new Project { Id = id, OrganizationId = organizationId }).ToList(); 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<ICurrentContext>().OrganizationAdmin(organizationId).Returns(true);
sutProvider.GetDependency<IProjectRepository>().GetManyByIds(data).Returns(projects); 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) public async Task UpdateAsync_Admin_Succeeds(Project project, Guid userId, SutProvider<UpdateProjectCommand> sutProvider)
{ {
sutProvider.GetDependency<IProjectRepository>().GetByIdAsync(project.Id).Returns(project); sutProvider.GetDependency<IProjectRepository>().GetByIdAsync(project.Id).Returns(project);
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(project.OrganizationId).Returns(true);
sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(project.OrganizationId).Returns(true); sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(project.OrganizationId).Returns(true);
var project2 = new Project { Id = project.Id, Name = "newName" }; 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>().GetByIdAsync(project.Id).Returns(project);
sutProvider.GetDependency<IProjectRepository>().UserHasWriteAccessToProject(project.Id, userId).Returns(false); 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); 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>().GetByIdAsync(project.Id).Returns(project);
sutProvider.GetDependency<IProjectRepository>().UserHasWriteAccessToProject(project.Id, userId).Returns(true); 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 project2 = new Project { Id = project.Id, Name = "newName" };
var result = await sutProvider.Sut.UpdateAsync(project2, userId); var result = await sutProvider.Sut.UpdateAsync(project2, userId);

View File

@ -1,4 +1,5 @@
using Bit.Commercial.Core.SecretsManager.Commands.Secrets; using Bit.Commercial.Core.SecretsManager.Commands.Secrets;
using Bit.Core.Context;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.SecretsManager.Entities; using Bit.Core.SecretsManager.Entities;
using Bit.Core.SecretsManager.Repositories; using Bit.Core.SecretsManager.Repositories;
@ -56,6 +57,7 @@ public class DeleteSecretCommandTests
} }
sutProvider.GetDependency<ISecretRepository>().GetManyByIds(data).Returns(secrets); sutProvider.GetDependency<ISecretRepository>().GetManyByIds(data).Returns(secrets);
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(default).ReturnsForAnyArgs(true);
var results = await sutProvider.Sut.DeleteSecrets(data); var results = await sutProvider.Sut.DeleteSecrets(data);

View File

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

View File

@ -17,6 +17,7 @@ public class OrganizationUserInviteRequestModel
[Required] [Required]
public OrganizationUserType? Type { get; set; } public OrganizationUserType? Type { get; set; }
public bool AccessAll { get; set; } public bool AccessAll { get; set; }
public bool AccessSecretsManager { get; set; }
public Permissions Permissions { get; set; } public Permissions Permissions { get; set; }
public IEnumerable<SelectionReadOnlyRequestModel> Collections { get; set; } public IEnumerable<SelectionReadOnlyRequestModel> Collections { get; set; }
public IEnumerable<Guid> Groups { get; set; } public IEnumerable<Guid> Groups { get; set; }
@ -28,6 +29,7 @@ public class OrganizationUserInviteRequestModel
Emails = Emails, Emails = Emails,
Type = Type, Type = Type,
AccessAll = AccessAll, AccessAll = AccessAll,
AccessSecretsManager = AccessSecretsManager,
Collections = Collections?.Select(c => c.ToSelectionReadOnly()), Collections = Collections?.Select(c => c.ToSelectionReadOnly()),
Groups = Groups, Groups = Groups,
Permissions = Permissions, Permissions = Permissions,
@ -73,6 +75,7 @@ public class OrganizationUserUpdateRequestModel
[Required] [Required]
public OrganizationUserType? Type { get; set; } public OrganizationUserType? Type { get; set; }
public bool AccessAll { get; set; } public bool AccessAll { get; set; }
public bool AccessSecretsManager { get; set; }
public Permissions Permissions { get; set; } public Permissions Permissions { get; set; }
public IEnumerable<SelectionReadOnlyRequestModel> Collections { get; set; } public IEnumerable<SelectionReadOnlyRequestModel> Collections { get; set; }
public IEnumerable<Guid> Groups { get; set; } public IEnumerable<Guid> Groups { get; set; }
@ -85,6 +88,7 @@ public class OrganizationUserUpdateRequestModel
PropertyNamingPolicy = JsonNamingPolicy.CamelCase, PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
}); });
existingUser.AccessAll = AccessAll; existingUser.AccessAll = AccessAll;
existingUser.AccessSecretsManager = AccessSecretsManager;
return existingUser; return existingUser;
} }
} }

View File

@ -23,6 +23,7 @@ public class OrganizationUserResponseModel : ResponseModel
Type = organizationUser.Type; Type = organizationUser.Type;
Status = organizationUser.Status; Status = organizationUser.Status;
AccessAll = organizationUser.AccessAll; AccessAll = organizationUser.AccessAll;
AccessSecretsManager = organizationUser.AccessSecretsManager;
Permissions = CoreHelpers.LoadClassFromJsonData<Permissions>(organizationUser.Permissions); Permissions = CoreHelpers.LoadClassFromJsonData<Permissions>(organizationUser.Permissions);
ResetPasswordEnrolled = !string.IsNullOrEmpty(organizationUser.ResetPasswordKey); ResetPasswordEnrolled = !string.IsNullOrEmpty(organizationUser.ResetPasswordKey);
} }
@ -40,6 +41,7 @@ public class OrganizationUserResponseModel : ResponseModel
Type = organizationUser.Type; Type = organizationUser.Type;
Status = organizationUser.Status; Status = organizationUser.Status;
AccessAll = organizationUser.AccessAll; AccessAll = organizationUser.AccessAll;
AccessSecretsManager = organizationUser.AccessSecretsManager;
Permissions = CoreHelpers.LoadClassFromJsonData<Permissions>(organizationUser.Permissions); Permissions = CoreHelpers.LoadClassFromJsonData<Permissions>(organizationUser.Permissions);
ResetPasswordEnrolled = !string.IsNullOrEmpty(organizationUser.ResetPasswordKey); ResetPasswordEnrolled = !string.IsNullOrEmpty(organizationUser.ResetPasswordKey);
UsesKeyConnector = organizationUser.UsesKeyConnector; UsesKeyConnector = organizationUser.UsesKeyConnector;
@ -50,6 +52,7 @@ public class OrganizationUserResponseModel : ResponseModel
public OrganizationUserType Type { get; set; } public OrganizationUserType Type { get; set; }
public OrganizationUserStatusType Status { get; set; } public OrganizationUserStatusType Status { get; set; }
public bool AccessAll { get; set; } public bool AccessAll { get; set; }
public bool AccessSecretsManager { get; set; }
public Permissions Permissions { get; set; } public Permissions Permissions { get; set; }
public bool ResetPasswordEnrolled { get; set; } public bool ResetPasswordEnrolled { get; set; }
public bool UsesKeyConnector { get; set; } public bool UsesKeyConnector { get; set; }

View File

@ -52,6 +52,7 @@ public class ProfileOrganizationResponseModel : ResponseModel
FamilySponsorshipLastSyncDate = organization.FamilySponsorshipLastSyncDate; FamilySponsorshipLastSyncDate = organization.FamilySponsorshipLastSyncDate;
FamilySponsorshipToDelete = organization.FamilySponsorshipToDelete; FamilySponsorshipToDelete = organization.FamilySponsorshipToDelete;
FamilySponsorshipValidUntil = organization.FamilySponsorshipValidUntil; FamilySponsorshipValidUntil = organization.FamilySponsorshipValidUntil;
AccessSecretsManager = organization.AccessSecretsManager;
if (organization.SsoConfig != null) if (organization.SsoConfig != null)
{ {
@ -101,4 +102,5 @@ public class ProfileOrganizationResponseModel : ResponseModel
public DateTime? FamilySponsorshipLastSyncDate { get; set; } public DateTime? FamilySponsorshipLastSyncDate { get; set; }
public DateTime? FamilySponsorshipValidUntil { get; set; } public DateTime? FamilySponsorshipValidUntil { get; set; }
public bool? FamilySponsorshipToDelete { 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.Commands.Projects.Interfaces;
using Bit.Core.SecretsManager.Repositories; using Bit.Core.SecretsManager.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
namespace Bit.Api.SecretsManager.Controllers; namespace Bit.Api.SecretsManager.Controllers;
[SecretsManager] [SecretsManager]
[Authorize("secrets")]
public class ProjectsController : Controller public class ProjectsController : Controller
{ {
private readonly ICurrentContext _currentContext;
private readonly IUserService _userService; private readonly IUserService _userService;
private readonly IProjectRepository _projectRepository; private readonly IProjectRepository _projectRepository;
private readonly ICreateProjectCommand _createProjectCommand; private readonly ICreateProjectCommand _createProjectCommand;
private readonly IUpdateProjectCommand _updateProjectCommand; private readonly IUpdateProjectCommand _updateProjectCommand;
private readonly IDeleteProjectCommand _deleteProjectCommand; private readonly IDeleteProjectCommand _deleteProjectCommand;
private readonly ICurrentContext _currentContext;
public ProjectsController( public ProjectsController(
ICurrentContext currentContext,
IUserService userService, IUserService userService,
IProjectRepository projectRepository, IProjectRepository projectRepository,
ICreateProjectCommand createProjectCommand, ICreateProjectCommand createProjectCommand,
IUpdateProjectCommand updateProjectCommand, IUpdateProjectCommand updateProjectCommand,
IDeleteProjectCommand deleteProjectCommand, IDeleteProjectCommand deleteProjectCommand)
ICurrentContext currentContext)
{ {
_currentContext = currentContext;
_userService = userService; _userService = userService;
_projectRepository = projectRepository; _projectRepository = projectRepository;
_createProjectCommand = createProjectCommand; _createProjectCommand = createProjectCommand;
_updateProjectCommand = updateProjectCommand; _updateProjectCommand = updateProjectCommand;
_deleteProjectCommand = deleteProjectCommand; _deleteProjectCommand = deleteProjectCommand;
_currentContext = currentContext;
} }
[HttpPost("organizations/{organizationId}/projects")] [HttpGet("organizations/{organizationId}/projects")]
public async Task<ProjectResponseModel> CreateAsync([FromRoute] Guid organizationId, [FromBody] ProjectCreateRequestModel createRequest) public async Task<ListResponseModel<ProjectResponseModel>> ListByOrganizationAsync([FromRoute] Guid organizationId)
{ {
if (!await _currentContext.OrganizationUser(organizationId)) if (!_currentContext.AccessSecretsManager(organizationId))
{ {
throw new NotFoundException(); 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 userId = _userService.GetProperUserId(User).Value;
var orgAdmin = await _currentContext.OrganizationAdmin(organizationId); var orgAdmin = await _currentContext.OrganizationAdmin(organizationId);
var accessClient = AccessClientHelper.ToAccessClient(_currentContext.ClientType, orgAdmin); var accessClient = AccessClientHelper.ToAccessClient(_currentContext.ClientType, orgAdmin);
@ -72,8 +57,29 @@ public class ProjectsController : Controller
return new ListResponseModel<ProjectResponseModel>(responses); 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}")] [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); var project = await _projectRepository.GetByIdAsync(id);
if (project == null) if (project == null)
@ -81,6 +87,11 @@ public class ProjectsController : Controller
throw new NotFoundException(); throw new NotFoundException();
} }
if (!_currentContext.AccessSecretsManager(project.OrganizationId))
{
throw new NotFoundException();
}
var userId = _userService.GetProperUserId(User).Value; var userId = _userService.GetProperUserId(User).Value;
var orgAdmin = await _currentContext.OrganizationAdmin(project.OrganizationId); var orgAdmin = await _currentContext.OrganizationAdmin(project.OrganizationId);
var accessClient = AccessClientHelper.ToAccessClient(_currentContext.ClientType, orgAdmin); var accessClient = AccessClientHelper.ToAccessClient(_currentContext.ClientType, orgAdmin);
@ -101,7 +112,7 @@ public class ProjectsController : Controller
} }
[HttpPost("projects/delete")] [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; var userId = _userService.GetProperUserId(User).Value;

View File

@ -1,6 +1,7 @@
using Bit.Api.Models.Response; using Bit.Api.Models.Response;
using Bit.Api.SecretsManager.Models.Request; using Bit.Api.SecretsManager.Models.Request;
using Bit.Api.SecretsManager.Models.Response; using Bit.Api.SecretsManager.Models.Response;
using Bit.Core.Context;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.SecretsManager.Commands.Secrets.Interfaces; using Bit.Core.SecretsManager.Commands.Secrets.Interfaces;
using Bit.Core.SecretsManager.Repositories; using Bit.Core.SecretsManager.Repositories;
@ -13,30 +14,52 @@ namespace Bit.Api.SecretsManager.Controllers;
[Authorize("secrets")] [Authorize("secrets")]
public class SecretsController : Controller public class SecretsController : Controller
{ {
private readonly ICurrentContext _currentContext;
private readonly ISecretRepository _secretRepository; private readonly ISecretRepository _secretRepository;
private readonly IProjectRepository _projectRepository;
private readonly ICreateSecretCommand _createSecretCommand; private readonly ICreateSecretCommand _createSecretCommand;
private readonly IUpdateSecretCommand _updateSecretCommand; private readonly IUpdateSecretCommand _updateSecretCommand;
private readonly IDeleteSecretCommand _deleteSecretCommand; 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; _secretRepository = secretRepository;
_projectRepository = projectRepository;
_createSecretCommand = createSecretCommand; _createSecretCommand = createSecretCommand;
_updateSecretCommand = updateSecretCommand; _updateSecretCommand = updateSecretCommand;
_deleteSecretCommand = deleteSecretCommand; _deleteSecretCommand = deleteSecretCommand;
} }
[HttpGet("organizations/{organizationId}/secrets")] [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); var secrets = await _secretRepository.GetManyByOrganizationIdAsync(organizationId);
return new SecretWithProjectsListResponseModel(secrets); 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}")] [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); var secret = await _secretRepository.GetByIdAsync(id);
if (secret == null) if (secret == null)
@ -54,15 +77,8 @@ public class SecretsController : Controller
return new SecretWithProjectsListResponseModel(secrets); 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}")] [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)); var result = await _updateSecretCommand.UpdateAsync(updateRequest.ToSecret(id));
return new SecretResponseModel(result); 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.Commands.ServiceAccounts.Interfaces;
using Bit.Core.SecretsManager.Repositories; using Bit.Core.SecretsManager.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
namespace Bit.Api.SecretsManager.Controllers; namespace Bit.Api.SecretsManager.Controllers;
[SecretsManager] [SecretsManager]
[Authorize("secrets")]
[Route("service-accounts")] [Route("service-accounts")]
public class ServiceAccountsController : Controller public class ServiceAccountsController : Controller
{ {
private readonly ICurrentContext _currentContext;
private readonly IApiKeyRepository _apiKeyRepository; private readonly IApiKeyRepository _apiKeyRepository;
private readonly ICreateAccessTokenCommand _createAccessTokenCommand; private readonly ICreateAccessTokenCommand _createAccessTokenCommand;
private readonly ICreateServiceAccountCommand _createServiceAccountCommand; private readonly ICreateServiceAccountCommand _createServiceAccountCommand;
private readonly ICurrentContext _currentContext;
private readonly IServiceAccountRepository _serviceAccountRepository; private readonly IServiceAccountRepository _serviceAccountRepository;
private readonly IUpdateServiceAccountCommand _updateServiceAccountCommand; private readonly IUpdateServiceAccountCommand _updateServiceAccountCommand;
private readonly IUserService _userService; private readonly IUserService _userService;
public ServiceAccountsController( public ServiceAccountsController(
ICurrentContext currentContext,
IUserService userService, IUserService userService,
IServiceAccountRepository serviceAccountRepository, IServiceAccountRepository serviceAccountRepository,
ICreateAccessTokenCommand createAccessTokenCommand, ICreateAccessTokenCommand createAccessTokenCommand,
IApiKeyRepository apiKeyRepository, ICreateServiceAccountCommand createServiceAccountCommand, IApiKeyRepository apiKeyRepository, ICreateServiceAccountCommand createServiceAccountCommand,
IUpdateServiceAccountCommand updateServiceAccountCommand, IUpdateServiceAccountCommand updateServiceAccountCommand)
ICurrentContext currentContext)
{ {
_currentContext = currentContext;
_userService = userService; _userService = userService;
_serviceAccountRepository = serviceAccountRepository; _serviceAccountRepository = serviceAccountRepository;
_apiKeyRepository = apiKeyRepository; _apiKeyRepository = apiKeyRepository;
_createServiceAccountCommand = createServiceAccountCommand; _createServiceAccountCommand = createServiceAccountCommand;
_updateServiceAccountCommand = updateServiceAccountCommand; _updateServiceAccountCommand = updateServiceAccountCommand;
_createAccessTokenCommand = createAccessTokenCommand; _createAccessTokenCommand = createAccessTokenCommand;
_currentContext = currentContext;
} }
[HttpGet("/organizations/{organizationId}/service-accounts")] [HttpGet("/organizations/{organizationId}/service-accounts")]
public async Task<ListResponseModel<ServiceAccountResponseModel>> GetServiceAccountsByOrganizationAsync( public async Task<ListResponseModel<ServiceAccountResponseModel>> ListByOrganizationAsync(
[FromRoute] Guid organizationId) [FromRoute] Guid organizationId)
{ {
if (!_currentContext.AccessSecretsManager(organizationId))
{
throw new NotFoundException();
}
var userId = _userService.GetProperUserId(User).Value; var userId = _userService.GetProperUserId(User).Value;
var orgAdmin = await _currentContext.OrganizationAdmin(organizationId); var orgAdmin = await _currentContext.OrganizationAdmin(organizationId);
var accessClient = AccessClientHelper.ToAccessClient(_currentContext.ClientType, orgAdmin); var accessClient = AccessClientHelper.ToAccessClient(_currentContext.ClientType, orgAdmin);
@ -57,10 +64,10 @@ public class ServiceAccountsController : Controller
} }
[HttpPost("/organizations/{organizationId}/service-accounts")] [HttpPost("/organizations/{organizationId}/service-accounts")]
public async Task<ServiceAccountResponseModel> CreateServiceAccountAsync([FromRoute] Guid organizationId, public async Task<ServiceAccountResponseModel> CreateAsync([FromRoute] Guid organizationId,
[FromBody] ServiceAccountCreateRequestModel createRequest) [FromBody] ServiceAccountCreateRequestModel createRequest)
{ {
if (!await _currentContext.OrganizationUser(organizationId)) if (!_currentContext.AccessSecretsManager(organizationId))
{ {
throw new NotFoundException(); throw new NotFoundException();
} }
@ -70,7 +77,7 @@ public class ServiceAccountsController : Controller
} }
[HttpPut("{id}")] [HttpPut("{id}")]
public async Task<ServiceAccountResponseModel> UpdateServiceAccountAsync([FromRoute] Guid id, public async Task<ServiceAccountResponseModel> UpdateAsync([FromRoute] Guid id,
[FromBody] ServiceAccountUpdateRequestModel updateRequest) [FromBody] ServiceAccountUpdateRequestModel updateRequest)
{ {
var userId = _userService.GetProperUserId(User).Value; var userId = _userService.GetProperUserId(User).Value;
@ -89,6 +96,11 @@ public class ServiceAccountsController : Controller
throw new NotFoundException(); throw new NotFoundException();
} }
if (!_currentContext.AccessSecretsManager(serviceAccount.OrganizationId))
{
throw new NotFoundException();
}
var orgAdmin = await _currentContext.OrganizationAdmin(serviceAccount.OrganizationId); var orgAdmin = await _currentContext.OrganizationAdmin(serviceAccount.OrganizationId);
var accessClient = AccessClientHelper.ToAccessClient(_currentContext.ClientType, orgAdmin); 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;
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.Utilities; using Bit.Core.Utilities;
namespace Bit.Core.Context; namespace Bit.Core.Context;
@ -9,14 +9,16 @@ public class CurrentContentOrganization
{ {
public CurrentContentOrganization() { } public CurrentContentOrganization() { }
public CurrentContentOrganization(OrganizationUser orgUser) public CurrentContentOrganization(OrganizationUserOrganizationDetails orgUser)
{ {
Id = orgUser.OrganizationId; Id = orgUser.OrganizationId;
Type = orgUser.Type; Type = orgUser.Type;
Permissions = CoreHelpers.LoadClassFromJsonData<Permissions>(orgUser.Permissions); Permissions = CoreHelpers.LoadClassFromJsonData<Permissions>(orgUser.Permissions);
AccessSecretsManager = orgUser.AccessSecretsManager && orgUser.UseSecretsManager;
} }
public Guid Id { get; set; } public Guid Id { get; set; }
public OrganizationUserType Type { get; set; } public OrganizationUserType Type { get; set; }
public Permissions Permissions { 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) 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>(); var organizations = new List<CurrentContentOrganization>();
if (claimsDict.ContainsKey(Claims.OrganizationOwner)) if (claimsDict.ContainsKey(Claims.OrganizationOwner))
{ {
@ -164,7 +168,8 @@ public class CurrentContext : ICurrentContext
new CurrentContentOrganization new CurrentContentOrganization
{ {
Id = new Guid(c.Value), Id = new Guid(c.Value),
Type = OrganizationUserType.Owner Type = OrganizationUserType.Owner,
AccessSecretsManager = accessSecretsManager.ContainsKey(c.Value),
})); }));
} }
else if (orgApi && OrganizationId.HasValue) else if (orgApi && OrganizationId.HasValue)
@ -172,7 +177,7 @@ public class CurrentContext : ICurrentContext
organizations.Add(new CurrentContentOrganization organizations.Add(new CurrentContentOrganization
{ {
Id = OrganizationId.Value, Id = OrganizationId.Value,
Type = OrganizationUserType.Owner Type = OrganizationUserType.Owner,
}); });
} }
@ -182,7 +187,8 @@ public class CurrentContext : ICurrentContext
new CurrentContentOrganization new CurrentContentOrganization
{ {
Id = new Guid(c.Value), 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 new CurrentContentOrganization
{ {
Id = new Guid(c.Value), 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 new CurrentContentOrganization
{ {
Id = new Guid(c.Value), 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), Id = new Guid(c.Value),
Type = OrganizationUserType.Custom, 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; return po?.ProviderId;
} }
public bool AccessSecretsManager(Guid orgId)
{
return Organizations?.Any(o => o.Id == orgId && o.AccessSecretsManager) ?? false;
}
public async Task<ICollection<CurrentContentOrganization>> OrganizationMembershipAsync( public async Task<ICollection<CurrentContentOrganization>> OrganizationMembershipAsync(
IOrganizationUserRepository organizationUserRepository, Guid userId) IOrganizationUserRepository organizationUserRepository, Guid userId)
{ {
if (Organizations == null) if (Organizations == null)
{ {
var userOrgs = await organizationUserRepository.GetManyByUserAsync(userId); var userOrgs = await organizationUserRepository.GetManyDetailsByUserAsync(userId);
Organizations = userOrgs.Where(ou => ou.Status == OrganizationUserStatusType.Confirmed) Organizations = userOrgs.Where(ou => ou.Status == OrganizationUserStatusType.Confirmed)
.Select(ou => new CurrentContentOrganization(ou)).ToList(); .Select(ou => new CurrentContentOrganization(ou)).ToList();
} }

View File

@ -68,4 +68,5 @@ public interface ICurrentContext
IProviderUserRepository providerUserRepository, Guid userId); IProviderUserRepository providerUserRepository, Guid userId);
Task<Guid?> ProviderIdForOrg(Guid orgId); 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 CreationDate { get; internal set; } = DateTime.UtcNow;
public DateTime RevisionDate { get; internal set; } = DateTime.UtcNow; public DateTime RevisionDate { get; internal set; } = DateTime.UtcNow;
public string Permissions { get; set; } public string Permissions { get; set; }
public bool AccessSecretsManager { get; set; }
public void SetNewId() public void SetNewId()
{ {

View File

@ -6,6 +6,7 @@ public static class Claims
public const string SecurityStamp = "sstamp"; public const string SecurityStamp = "sstamp";
public const string Premium = "premium"; public const string Premium = "premium";
public const string Device = "device"; public const string Device = "device";
public const string OrganizationOwner = "orgowner"; public const string OrganizationOwner = "orgowner";
public const string OrganizationAdmin = "orgadmin"; public const string OrganizationAdmin = "orgadmin";
public const string OrganizationManager = "orgmanager"; public const string OrganizationManager = "orgmanager";
@ -14,6 +15,8 @@ public static class Claims
public const string ProviderAdmin = "providerprovideradmin"; public const string ProviderAdmin = "providerprovideradmin";
public const string ProviderServiceUser = "providerserviceuser"; public const string ProviderServiceUser = "providerserviceuser";
public const string SecretsManagerAccess = "accesssecretsmanager";
// Service Account // Service Account
public const string Organization = "organization"; public const string Organization = "organization";

View File

@ -8,6 +8,7 @@ public class OrganizationUserInvite
public IEnumerable<string> Emails { get; set; } public IEnumerable<string> Emails { get; set; }
public Enums.OrganizationUserType? Type { get; set; } public Enums.OrganizationUserType? Type { get; set; }
public bool AccessAll { get; set; } public bool AccessAll { get; set; }
public bool AccessSecretsManager { get; set; }
public Permissions Permissions { get; set; } public Permissions Permissions { get; set; }
public IEnumerable<CollectionAccessSelection> Collections { get; set; } public IEnumerable<CollectionAccessSelection> Collections { get; set; }
public IEnumerable<Guid> Groups { get; set; } public IEnumerable<Guid> Groups { get; set; }
@ -19,6 +20,7 @@ public class OrganizationUserInvite
Emails = requestModel.Emails; Emails = requestModel.Emails;
Type = requestModel.Type; Type = requestModel.Type;
AccessAll = requestModel.AccessAll; AccessAll = requestModel.AccessAll;
AccessSecretsManager = requestModel.AccessSecretsManager;
Collections = requestModel.Collections; Collections = requestModel.Collections;
Groups = requestModel.Groups; Groups = requestModel.Groups;
Permissions = requestModel.Permissions; Permissions = requestModel.Permissions;

View File

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

View File

@ -41,4 +41,5 @@ public class OrganizationUserOrganizationDetails
public DateTime? FamilySponsorshipLastSyncDate { get; set; } public DateTime? FamilySponsorshipLastSyncDate { get; set; }
public DateTime? FamilySponsorshipValidUntil { get; set; } public DateTime? FamilySponsorshipValidUntil { get; set; }
public bool? FamilySponsorshipToDelete { 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 OrganizationUserStatusType Status { get; set; }
public OrganizationUserType Type { get; set; } public OrganizationUserType Type { get; set; }
public bool AccessAll { get; set; } public bool AccessAll { get; set; }
public bool AccessSecretsManager { get; set; }
public string ExternalId { get; set; } public string ExternalId { get; set; }
public string SsoExternalId { get; set; } public string SsoExternalId { get; set; }
public string Permissions { get; set; } public string Permissions { get; set; }

View File

@ -1241,6 +1241,7 @@ public class OrganizationService : IOrganizationService
Type = invite.Type.Value, Type = invite.Type.Value,
Status = OrganizationUserStatusType.Invited, Status = OrganizationUserStatusType.Invited,
AccessAll = invite.AccessAll, AccessAll = invite.AccessAll,
AccessSecretsManager = invite.AccessSecretsManager,
ExternalId = externalId, ExternalId = externalId,
CreationDate = DateTime.UtcNow, CreationDate = DateTime.UtcNow,
RevisionDate = DateTime.UtcNow, RevisionDate = DateTime.UtcNow,

View File

@ -692,6 +692,15 @@ public static class CoreHelpers
default: default:
break; 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.OrganizationCustom,
Claims.ProviderAdmin, Claims.ProviderAdmin,
Claims.ProviderServiceUser, Claims.ProviderServiceUser,
Claims.SecretsManagerAccess,
}), }),
new(ApiScopes.Internal, new[] { JwtClaimTypes.Subject }), new(ApiScopes.Internal, new[] { JwtClaimTypes.Subject }),
new(ApiScopes.ApiPush, 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) public static DataTable ToTvp(this IEnumerable<OrganizationUser> orgUsers)
{ {
var table = new DataTable(); var table = new DataTable();
table.SetTypeName("[dbo].[OrganizationUserType]"); table.SetTypeName("[dbo].[OrganizationUserType2]");
var columnData = new List<(string name, Type type, Func<OrganizationUser, object> getter)> 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.RevisionDate), typeof(DateTime), ou => ou.RevisionDate),
(nameof(OrganizationUser.Permissions), typeof(string), ou => ou.Permissions), (nameof(OrganizationUser.Permissions), typeof(string), ou => ou.Permissions),
(nameof(OrganizationUser.ResetPasswordKey), typeof(string), ou => ou.ResetPasswordKey), (nameof(OrganizationUser.ResetPasswordKey), typeof(string), ou => ou.ResetPasswordKey),
(nameof(OrganizationUser.AccessSecretsManager), typeof(bool), ou => ou.AccessSecretsManager),
}; };
return orgUsers.BuildTable(table, columnData); return orgUsers.BuildTable(table, columnData);

View File

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

View File

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

View File

@ -29,6 +29,7 @@ public class OrganizationUserUserDetailsViewQuery : IQuery<OrganizationUserUserD
Permissions = x.ou.Permissions, Permissions = x.ou.Permissions,
ResetPasswordKey = x.ou.ResetPasswordKey, ResetPasswordKey = x.ou.ResetPasswordKey,
UsesKeyConnector = x.u != null && x.u.UsesKeyConnector, 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_Activate.sql" />
<Build Include="dbo\Stored Procedures\OrganizationUser_Create.sql" /> <Build Include="dbo\Stored Procedures\OrganizationUser_Create.sql" />
<Build Include="dbo\Stored Procedures\OrganizationUser_CreateMany.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_CreateWithCollections.sql" />
<Build Include="dbo\Stored Procedures\OrganizationUser_Deactivate.sql" /> <Build Include="dbo\Stored Procedures\OrganizationUser_Deactivate.sql" />
<Build Include="dbo\Stored Procedures\OrganizationUser_DeleteById.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_SelectKnownEmails.sql" />
<Build Include="dbo\Stored Procedures\OrganizationUser_Update.sql" /> <Build Include="dbo\Stored Procedures\OrganizationUser_Update.sql" />
<Build Include="dbo\Stored Procedures\OrganizationUser_UpdateMany.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\OrganizationUser_UpdateWithCollections.sql" />
<Build Include="dbo\Stored Procedures\Organization_Create.sql" /> <Build Include="dbo\Stored Procedures\Organization_Create.sql" />
<Build Include="dbo\Stored Procedures\Organization_DeleteById.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\GuidIdArray.sql" />
<Build Include="dbo\User Defined Types\OrganizationSponsorshipType.sql" /> <Build Include="dbo\User Defined Types\OrganizationSponsorshipType.sql" />
<Build Include="dbo\User Defined Types\OrganizationUserType.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\SelectionReadOnlyArray.sql" />
<Build Include="dbo\User Defined Types\TwoGuidIdArray.sql" /> <Build Include="dbo\User Defined Types\TwoGuidIdArray.sql" />
<Build Include="dbo\Views\AuthRequestView.sql" /> <Build Include="dbo\Views\AuthRequestView.sql" />

View File

@ -11,7 +11,8 @@
@CreationDate DATETIME2(7), @CreationDate DATETIME2(7),
@RevisionDate DATETIME2(7), @RevisionDate DATETIME2(7),
@Permissions NVARCHAR(MAX), @Permissions NVARCHAR(MAX),
@ResetPasswordKey VARCHAR(MAX) @ResetPasswordKey VARCHAR(MAX),
@AccessSecretsManager BIT = 0
AS AS
BEGIN BEGIN
SET NOCOUNT ON SET NOCOUNT ON
@ -30,7 +31,8 @@ BEGIN
[CreationDate], [CreationDate],
[RevisionDate], [RevisionDate],
[Permissions], [Permissions],
[ResetPasswordKey] [ResetPasswordKey],
[AccessSecretsManager]
) )
VALUES VALUES
( (
@ -46,6 +48,7 @@ BEGIN
@CreationDate, @CreationDate,
@RevisionDate, @RevisionDate,
@Permissions, @Permissions,
@ResetPasswordKey @ResetPasswordKey,
@AccessSecretsManager
) )
END 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), @RevisionDate DATETIME2(7),
@Permissions NVARCHAR(MAX), @Permissions NVARCHAR(MAX),
@ResetPasswordKey VARCHAR(MAX), @ResetPasswordKey VARCHAR(MAX),
@Collections AS [dbo].[SelectionReadOnlyArray] READONLY @Collections AS [dbo].[SelectionReadOnlyArray] READONLY,
@AccessSecretsManager BIT = 0
AS AS
BEGIN BEGIN
SET NOCOUNT ON 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( ;WITH [AvailableCollectionsCTE] AS(
SELECT SELECT

View File

@ -11,7 +11,8 @@
@CreationDate DATETIME2(7), @CreationDate DATETIME2(7),
@RevisionDate DATETIME2(7), @RevisionDate DATETIME2(7),
@Permissions NVARCHAR(MAX), @Permissions NVARCHAR(MAX),
@ResetPasswordKey VARCHAR(MAX) @ResetPasswordKey VARCHAR(MAX),
@AccessSecretsManager BIT = 0
AS AS
BEGIN BEGIN
SET NOCOUNT ON SET NOCOUNT ON
@ -30,7 +31,8 @@ BEGIN
[CreationDate] = @CreationDate, [CreationDate] = @CreationDate,
[RevisionDate] = @RevisionDate, [RevisionDate] = @RevisionDate,
[Permissions] = @Permissions, [Permissions] = @Permissions,
[ResetPasswordKey] = @ResetPasswordKey [ResetPasswordKey] = @ResetPasswordKey,
[AccessSecretsManager] = @AccessSecretsManager
WHERE WHERE
[Id] = @Id [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), @RevisionDate DATETIME2(7),
@Permissions NVARCHAR(MAX), @Permissions NVARCHAR(MAX),
@ResetPasswordKey VARCHAR(MAX), @ResetPasswordKey VARCHAR(MAX),
@Collections AS [dbo].[SelectionReadOnlyArray] READONLY @Collections AS [dbo].[SelectionReadOnlyArray] READONLY,
@AccessSecretsManager BIT = 0
AS AS
BEGIN BEGIN
SET NOCOUNT ON 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
UPDATE UPDATE
[Target] [Target]

View File

@ -12,6 +12,7 @@
[CreationDate] DATETIME2 (7) NOT NULL, [CreationDate] DATETIME2 (7) NOT NULL,
[RevisionDate] DATETIME2 (7) NOT NULL, [RevisionDate] DATETIME2 (7) NOT NULL,
[Permissions] NVARCHAR (MAX) NULL, [Permissions] NVARCHAR (MAX) NULL,
[AccessSecretsManager] BIT NOT NULL CONSTRAINT [DF_OrganizationUser_SecretsManager] DEFAULT (0),
CONSTRAINT [PK_OrganizationUser] PRIMARY KEY CLUSTERED ([Id] ASC), 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_Organization] FOREIGN KEY ([OrganizationId]) REFERENCES [dbo].[Organization] ([Id]) ON DELETE CASCADE,
CONSTRAINT [FK_OrganizationUser_User] FOREIGN KEY ([UserId]) REFERENCES [dbo].[User] ([Id]) 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.[FriendlyName] FamilySponsorshipFriendlyName,
OS.[LastSyncDate] FamilySponsorshipLastSyncDate, OS.[LastSyncDate] FamilySponsorshipLastSyncDate,
OS.[ToDelete] FamilySponsorshipToDelete, OS.[ToDelete] FamilySponsorshipToDelete,
OS.[ValidUntil] FamilySponsorshipValidUntil OS.[ValidUntil] FamilySponsorshipValidUntil,
OU.[AccessSecretsManager]
FROM FROM
[dbo].[OrganizationUser] OU [dbo].[OrganizationUser] OU
LEFT JOIN LEFT JOIN

View File

@ -11,6 +11,7 @@ SELECT
OU.[Status], OU.[Status],
OU.[Type], OU.[Type],
OU.[AccessAll], OU.[AccessAll],
OU.[AccessSecretsManager],
OU.[ExternalId], OU.[ExternalId],
SU.[ExternalId] SsoExternalId, SU.[ExternalId] SsoExternalId,
OU.[Permissions], 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 class OrganizationTestHelpers
{ {
public static async Task<Tuple<Organization, OrganizationUser>> SignUpAsync<T>( public static async Task<Tuple<Organization, OrganizationUser>> SignUpAsync<T>(WebApplicationFactoryBase<T> factory,
WebApplicationFactoryBase<T> factory,
PlanType plan = PlanType.Free, PlanType plan = PlanType.Free,
string ownerEmail = "integration-test@bitwarden.com", string ownerEmail = "integration-test@bitwarden.com",
string name = "Integration Test Org", string name = "Integration Test Org",
@ -36,7 +35,8 @@ public static class OrganizationTestHelpers
WebApplicationFactoryBase<T> factory, WebApplicationFactoryBase<T> factory,
Guid organizationId, Guid organizationId,
string userEmail, string userEmail,
OrganizationUserType type OrganizationUserType type,
bool accessSecretsManager = false
) where T : class ) where T : class
{ {
var userRepository = factory.GetService<IUserRepository>(); var userRepository = factory.GetService<IUserRepository>();
@ -50,9 +50,10 @@ public static class OrganizationTestHelpers
UserId = user.Id, UserId = user.Id,
Key = null, Key = null,
Type = type, Type = type,
Status = OrganizationUserStatusType.Invited, Status = OrganizationUserStatusType.Confirmed,
AccessAll = false, AccessAll = false,
ExternalId = null, ExternalId = null,
AccessSecretsManager = accessSecretsManager,
}; };
await organizationUserRepository.CreateAsync(orgUser); await organizationUserRepository.CreateAsync(orgUser);

View File

@ -1,11 +1,9 @@
using System.Net; using System.Net;
using System.Net.Http.Headers; using System.Net.Http.Headers;
using Bit.Api.IntegrationTest.Factories; using Bit.Api.IntegrationTest.Factories;
using Bit.Api.IntegrationTest.Helpers;
using Bit.Api.Models.Response; using Bit.Api.Models.Response;
using Bit.Api.SecretsManager.Models.Request; using Bit.Api.SecretsManager.Models.Request;
using Bit.Api.SecretsManager.Models.Response; using Bit.Api.SecretsManager.Models.Response;
using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.SecretsManager.Entities; using Bit.Core.SecretsManager.Entities;
using Bit.Core.SecretsManager.Repositories; using Bit.Core.SecretsManager.Repositories;
@ -22,7 +20,9 @@ public class ProjectsControllerTest : IClassFixture<ApiApplicationFactory>, IAsy
private readonly HttpClient _client; private readonly HttpClient _client;
private readonly ApiApplicationFactory _factory; private readonly ApiApplicationFactory _factory;
private readonly IProjectRepository _projectRepository; private readonly IProjectRepository _projectRepository;
private Organization _organization = null!;
private string _email = null!;
private SecretsManagerOrganizationHelper _organizationHelper = null!;
public ProjectsControllerTest(ApiApplicationFactory factory) public ProjectsControllerTest(ApiApplicationFactory factory)
{ {
@ -33,20 +33,9 @@ public class ProjectsControllerTest : IClassFixture<ApiApplicationFactory>, IAsy
public async Task InitializeAsync() public async Task InitializeAsync()
{ {
var ownerEmail = $"integration-test{Guid.NewGuid()}@bitwarden.com"; _email = $"integration-test{Guid.NewGuid()}@bitwarden.com";
await _factory.LoginWithNewAccount(ownerEmail); await _factory.LoginWithNewAccount(_email);
(_organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, ownerEmail: ownerEmail, billingEmail: ownerEmail); _organizationHelper = new SecretsManagerOrganizationHelper(_factory, _email);
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);
} }
public Task DisposeAsync() public Task DisposeAsync()
@ -55,12 +44,74 @@ public class ProjectsControllerTest : IClassFixture<ApiApplicationFactory>, IAsy
return Task.CompletedTask; return Task.CompletedTask;
} }
[Fact] private async Task LoginAsync(string email)
public async Task CreateProject_Success()
{ {
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 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(); response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<ProjectResponseModel>(); var result = await response.Content.ReadFromJsonAsync<ProjectResponseModel>();
@ -77,28 +128,43 @@ public class ProjectsControllerTest : IClassFixture<ApiApplicationFactory>, IAsy
Assert.Null(createdProject.DeletedDate); Assert.Null(createdProject.DeletedDate);
} }
[Fact] [Theory]
public async Task CreateProject_NoPermission() [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); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
} }
[Fact] [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 var initialProject = await _projectRepository.CreateAsync(new Project
{ {
OrganizationId = _organization.Id, OrganizationId = org.Id,
Name = _mockEncryptedString Name = _mockEncryptedString
}); });
var mockEncryptedString2 = "2.3Uk+WNBIoU5xzmVFNcoWzz==|1MsPIYuRfdOHfu/0uY6H2Q==|/98xy4wb6pHP1VTZ9JcNCYgQjEUMFPlqJgCwRk1YXKg="; var mockEncryptedString2 = "2.3Uk+WNBIoU5xzmVFNcoWzz==|1MsPIYuRfdOHfu/0uY6H2Q==|/98xy4wb6pHP1VTZ9JcNCYgQjEUMFPlqJgCwRk1YXKg=";
var request = new ProjectUpdateRequestModel() var request = new ProjectUpdateRequestModel
{ {
Name = mockEncryptedString2 Name = mockEncryptedString2
}; };
@ -121,9 +187,12 @@ public class ProjectsControllerTest : IClassFixture<ApiApplicationFactory>, IAsy
} }
[Fact] [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=", Name = "2.3Uk+WNBIoU5xzmVFNcoWzz==|1MsPIYuRfdOHfu/0uY6H2Q==|/98xy4wb6pHP1VTZ9JcNCYgQjEUMFPlqJgCwRk1YXKg=",
}; };
@ -134,34 +203,59 @@ public class ProjectsControllerTest : IClassFixture<ApiApplicationFactory>, IAsy
} }
[Fact] [Fact]
public async Task UpdateProject_MissingPermission() public async Task Update_MissingAccessPolicy_Throws_NotFound()
{ {
// Create a new account as a user var (org, _) = await _organizationHelper.Initialize(true, true);
await LoginAsNewOrgUser(); var (email, _) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);
await LoginAsync(email);
var project = await _projectRepository.CreateAsync(new Project var project = await _projectRepository.CreateAsync(new Project
{ {
OrganizationId = _organization.Id, OrganizationId = org.Id,
Name = _mockEncryptedString Name = _mockEncryptedString
}); });
var request = new ProjectUpdateRequestModel
var request = new ProjectUpdateRequestModel()
{ {
Name = "2.3Uk+WNBIoU5xzmVFNcoWzz==|1MsPIYuRfdOHfu/0uY6H2Q==|/98xy4wb6pHP1VTZ9JcNCYgQjEUMFPlqJgCwRk1YXKg=", Name = "2.3Uk+WNBIoU5xzmVFNcoWzz==|1MsPIYuRfdOHfu/0uY6H2Q==|/98xy4wb6pHP1VTZ9JcNCYgQjEUMFPlqJgCwRk1YXKg=",
}; };
var response = await _client.PutAsJsonAsync($"/projects/{project.Id}", request); 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] [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 var createdProject = await _projectRepository.CreateAsync(new Project
{ {
OrganizationId = _organization.Id, OrganizationId = org.Id,
Name = _mockEncryptedString Name = _mockEncryptedString
}); });
@ -174,39 +268,58 @@ public class ProjectsControllerTest : IClassFixture<ApiApplicationFactory>, IAsy
} }
[Fact] [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>(); 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 var project = await _projectRepository.CreateAsync(new Project
{ {
OrganizationId = _organization.Id, OrganizationId = org.Id,
Name = _mockEncryptedString Name = _mockEncryptedString,
}); });
projectIds.Add(project.Id); projectIds.Add(project.Id);
} }
var response = await _client.GetAsync($"/organizations/{_organization.Id}/projects"); var response = await _client.PostAsync("/projects/delete", JsonContent.Create(projectIds));
response.EnsureSuccessStatusCode(); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
var result = await response.Content.ReadFromJsonAsync<ListResponseModel<ProjectResponseModel>>();
Assert.NotNull(result);
Assert.NotEmpty(result!.Data);
Assert.Equal(projectIds.Count, result.Data.Count());
} }
[Fact] [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>(); 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 var project = await _projectRepository.CreateAsync(new Project
{ {
OrganizationId = _organization.Id, OrganizationId = org.Id,
Name = _mockEncryptedString, Name = _mockEncryptedString,
}); });
projectIds.Add(project.Id); 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.Factories;
using Bit.Api.IntegrationTest.Helpers;
using Bit.Api.Models.Response; using Bit.Api.Models.Response;
using Bit.Api.SecretsManager.Models.Request; using Bit.Api.SecretsManager.Models.Request;
using Bit.Api.SecretsManager.Models.Response; using Bit.Api.SecretsManager.Models.Response;
using Bit.Core.Entities;
using Bit.Core.SecretsManager.Entities; using Bit.Core.SecretsManager.Entities;
using Bit.Core.SecretsManager.Repositories; using Bit.Core.SecretsManager.Repositories;
using Bit.Test.Common.Helpers; using Bit.Test.Common.Helpers;
@ -21,7 +20,9 @@ public class SecretsControllerTest : IClassFixture<ApiApplicationFactory>, IAsyn
private readonly ApiApplicationFactory _factory; private readonly ApiApplicationFactory _factory;
private readonly ISecretRepository _secretRepository; private readonly ISecretRepository _secretRepository;
private readonly IProjectRepository _projectRepository; private readonly IProjectRepository _projectRepository;
private Organization _organization = null!;
private string _email = null!;
private SecretsManagerOrganizationHelper _organizationHelper = null!;
public SecretsControllerTest(ApiApplicationFactory factory) public SecretsControllerTest(ApiApplicationFactory factory)
{ {
@ -33,29 +34,98 @@ public class SecretsControllerTest : IClassFixture<ApiApplicationFactory>, IAsyn
public async Task InitializeAsync() public async Task InitializeAsync()
{ {
var ownerEmail = $"integration-test{Guid.NewGuid()}@bitwarden.com"; _email = $"integration-test{Guid.NewGuid()}@bitwarden.com";
var tokens = await _factory.LoginWithNewAccount(ownerEmail); await _factory.LoginWithNewAccount(_email);
var (organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, ownerEmail: ownerEmail, billingEmail: ownerEmail); _organizationHelper = new SecretsManagerOrganizationHelper(_factory, _email);
_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tokens.Token);
_organization = organization;
} }
public Task DisposeAsync() public Task DisposeAsync()
{ {
_client.Dispose();
return Task.CompletedTask; return Task.CompletedTask;
} }
[Fact] private async Task LoginAsync(string email)
public async Task CreateSecret()
{ {
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, Key = _mockEncryptedString,
Value = _mockEncryptedString, Value = _mockEncryptedString,
Note = _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(); response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<SecretResponseModel>(); var result = await response.Content.ReadFromJsonAsync<SecretResponseModel>();
@ -77,23 +147,26 @@ public class SecretsControllerTest : IClassFixture<ApiApplicationFactory>, IAsyn
} }
[Fact] [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() var project = await _projectRepository.CreateAsync(new Project()
{ {
Id = new Guid(), Id = new Guid(),
OrganizationId = _organization.Id, OrganizationId = org.Id,
Name = _mockEncryptedString Name = _mockEncryptedString
}); });
var projectIds = new[] { project.Id };
var secretRequest = new SecretCreateRequestModel() var secretRequest = new SecretCreateRequestModel()
{ {
Key = _mockEncryptedString, Key = _mockEncryptedString,
Value = _mockEncryptedString, Value = _mockEncryptedString,
Note = _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(); secretResponse.EnsureSuccessStatusCode();
var secretResult = await secretResponse.Content.ReadFromJsonAsync<SecretResponseModel>(); var secretResult = await secretResponse.Content.ReadFromJsonAsync<SecretResponseModel>();
@ -109,12 +182,88 @@ public class SecretsControllerTest : IClassFixture<ApiApplicationFactory>, IAsyn
Assert.Equal(secret.RevisionDate, secretResult.RevisionDate); Assert.Equal(secret.RevisionDate, secretResult.RevisionDate);
} }
[Fact] [Theory]
public async Task UpdateSecret() [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, Key = _mockEncryptedString,
Value = _mockEncryptedString, Value = _mockEncryptedString,
Note = _mockEncryptedString Note = _mockEncryptedString
@ -127,15 +276,15 @@ public class SecretsControllerTest : IClassFixture<ApiApplicationFactory>, IAsyn
Note = _mockEncryptedString Note = _mockEncryptedString
}; };
var response = await _client.PutAsJsonAsync($"/secrets/{initialSecret.Id}", request); var response = await _client.PutAsJsonAsync($"/secrets/{secret.Id}", request);
response.EnsureSuccessStatusCode(); response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<SecretResponseModel>(); var result = await response.Content.ReadFromJsonAsync<SecretResponseModel>();
Assert.Equal(request.Key, result!.Key); Assert.Equal(request.Key, result!.Key);
Assert.Equal(request.Value, result.Value); Assert.Equal(request.Value, result.Value);
Assert.NotEqual(initialSecret.Value, result.Value); Assert.NotEqual(secret.Value, result.Value);
Assert.Equal(request.Note, result.Note); Assert.Equal(request.Note, result.Note);
AssertHelper.AssertRecent(result.RevisionDate); 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)); var updatedSecret = await _secretRepository.GetByIdAsync(new Guid(result.Id));
Assert.NotNull(result); Assert.NotNull(result);
@ -145,20 +294,44 @@ public class SecretsControllerTest : IClassFixture<ApiApplicationFactory>, IAsyn
AssertHelper.AssertRecent(updatedSecret.RevisionDate); AssertHelper.AssertRecent(updatedSecret.RevisionDate);
AssertHelper.AssertRecent(updatedSecret.CreationDate); AssertHelper.AssertRecent(updatedSecret.CreationDate);
Assert.Null(updatedSecret.DeletedDate); Assert.Null(updatedSecret.DeletedDate);
Assert.NotEqual(initialSecret.Value, updatedSecret.Value); Assert.NotEqual(secret.Value, updatedSecret.Value);
Assert.NotEqual(initialSecret.RevisionDate, updatedSecret.RevisionDate); 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] [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>(); 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 var secret = await _secretRepository.CreateAsync(new Secret
{ {
OrganizationId = _organization.Id, OrganizationId = org.Id,
Key = _mockEncryptedString, Key = _mockEncryptedString,
Value = _mockEncryptedString, Value = _mockEncryptedString,
Note = _mockEncryptedString Note = _mockEncryptedString
@ -166,7 +339,7 @@ public class SecretsControllerTest : IClassFixture<ApiApplicationFactory>, IAsyn
secretIds.Add(secret.Id); secretIds.Add(secret.Id);
} }
var response = await _client.PostAsync("/secrets/delete", JsonContent.Create(secretIds)); var response = await _client.PostAsJsonAsync("/secrets/delete", secretIds);
response.EnsureSuccessStatusCode(); response.EnsureSuccessStatusCode();
var results = await response.Content.ReadFromJsonAsync<ListResponseModel<BulkDeleteResponseModel>>(); var results = await response.Content.ReadFromJsonAsync<ListResponseModel<BulkDeleteResponseModel>>();
@ -183,52 +356,4 @@ public class SecretsControllerTest : IClassFixture<ApiApplicationFactory>, IAsyn
var secrets = await _secretRepository.GetManyByIds(secretIds); var secrets = await _secretRepository.GetManyByIds(secretIds);
Assert.Empty(secrets); 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;
using System.Net.Http.Headers; using System.Net.Http.Headers;
using Bit.Api.IntegrationTest.Factories; using Bit.Api.IntegrationTest.Factories;
using Bit.Api.IntegrationTest.Helpers;
using Bit.Api.Models.Response; using Bit.Api.Models.Response;
using Bit.Api.SecretsManager.Models.Request; using Bit.Api.SecretsManager.Models.Request;
using Bit.Api.SecretsManager.Models.Response; using Bit.Api.SecretsManager.Models.Response;
@ -26,8 +25,9 @@ public class ServiceAccountsControllerTest : IClassFixture<ApiApplicationFactory
private readonly HttpClient _client; private readonly HttpClient _client;
private readonly ApiApplicationFactory _factory; private readonly ApiApplicationFactory _factory;
private readonly IServiceAccountRepository _serviceAccountRepository; private readonly IServiceAccountRepository _serviceAccountRepository;
private Organization _organization = null!;
private string _email = null!;
private SecretsManagerOrganizationHelper _organizationHelper = null!;
public ServiceAccountsControllerTest(ApiApplicationFactory factory) public ServiceAccountsControllerTest(ApiApplicationFactory factory)
{ {
@ -39,22 +39,45 @@ public class ServiceAccountsControllerTest : IClassFixture<ApiApplicationFactory
public async Task InitializeAsync() public async Task InitializeAsync()
{ {
var ownerEmail = $"integration-test{Guid.NewGuid()}@bitwarden.com"; _email = $"integration-test{Guid.NewGuid()}@bitwarden.com";
await _factory.LoginWithNewAccount(ownerEmail); await _factory.LoginWithNewAccount(_email);
(_organization, _) = _organizationHelper = new SecretsManagerOrganizationHelper(_factory, _email);
await OrganizationTestHelpers.SignUpAsync(_factory, ownerEmail: ownerEmail, billingEmail: ownerEmail); }
var tokens = await _factory.LoginAsync(ownerEmail);
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); _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] [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(); response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<ListResponseModel<ServiceAccountResponseModel>>(); var result = await response.Content.ReadFromJsonAsync<ListResponseModel<ServiceAccountResponseModel>>();
@ -64,56 +87,59 @@ public class ServiceAccountsControllerTest : IClassFixture<ApiApplicationFactory
} }
[Fact] [Fact]
public async Task GetServiceAccountsByOrganization_User_Success() public async Task ListByOrganization_User_Success()
{ {
// Create a new account as a user var (org, _) = await _organizationHelper.Initialize(true, true);
var user = await LoginAsNewOrgUserAsync(); 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 id => new UserServiceAccountAccessPolicy
{ {
OrganizationUserId = user.Id, OrganizationUserId = orgUser.Id,
GrantedServiceAccountId = id, GrantedServiceAccountId = id,
Read = true, Read = true,
Write = false, Write = false,
}).Cast<BaseAccessPolicy>().ToList(); }).Cast<BaseAccessPolicy>().ToList();
await _accessPolicyRepository.CreateManyAsync(accessPolicies); await _accessPolicyRepository.CreateManyAsync(accessPolicies);
var response = await _client.GetAsync($"/organizations/{org.Id}/service-accounts");
var response = await _client.GetAsync($"/organizations/{_organization.Id}/service-accounts");
response.EnsureSuccessStatusCode(); response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<ListResponseModel<ServiceAccountResponseModel>>(); var result = await response.Content.ReadFromJsonAsync<ListResponseModel<ServiceAccountResponseModel>>();
Assert.NotNull(result); Assert.NotNull(result);
Assert.NotEmpty(result!.Data); Assert.NotEmpty(result!.Data);
Assert.Equal(serviceAccountIds.Count, result.Data.Count()); Assert.Equal(2, result.Data.Count());
} }
[Fact] [Theory]
public async Task GetServiceAccountsByOrganization_User_NoPermission() [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 var (org, _) = await _organizationHelper.Initialize(useSecrets, accessSecrets);
await LoginAsNewOrgUserAsync(); await LoginAsync(_email);
await SetupGetServiceAccountsByOrganizationAsync();
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 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(); response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<ServiceAccountResponseModel>(); var result = await response.Content.ReadFromJsonAsync<ServiceAccountResponseModel>();
@ -129,24 +155,36 @@ public class ServiceAccountsControllerTest : IClassFixture<ApiApplicationFactory
AssertHelper.AssertRecent(createdServiceAccount.CreationDate); AssertHelper.AssertRecent(createdServiceAccount.CreationDate);
} }
[Fact] [Theory]
public async Task CreateServiceAccount_User_NoPermissions() [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 var (org, _) = await _organizationHelper.Initialize(useSecrets, accessSecrets);
await LoginAsNewOrgUserAsync(); 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); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
} }
[Fact] [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 var initialServiceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount
{ {
OrganizationId = _organization.Id, OrganizationId = org.Id,
Name = _mockEncryptedString, Name = _mockEncryptedString,
}); });
@ -171,18 +209,19 @@ public class ServiceAccountsControllerTest : IClassFixture<ApiApplicationFactory
} }
[Fact] [Fact]
public async Task UpdateServiceAccount_User_WithPermission() public async Task Update_User_WithPermission()
{ {
// Create a new account as a user var (org, _) = await _organizationHelper.Initialize(true, true);
var user = await LoginAsNewOrgUserAsync(); var (email, orgUser) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);
await LoginAsync(email);
var initialServiceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount var initialServiceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount
{ {
OrganizationId = _organization.Id, OrganizationId = org.Id,
Name = _mockEncryptedString, Name = _mockEncryptedString,
}); });
await CreateUserServiceAccountAccessPolicyAsync(user.Id, initialServiceAccount.Id, true, true); await CreateUserPolicyAsync(orgUser.Id, initialServiceAccount.Id, true, true);
var request = new ServiceAccountUpdateRequestModel { Name = _mockNewName }; var request = new ServiceAccountUpdateRequestModel { Name = _mockNewName };
@ -205,29 +244,61 @@ public class ServiceAccountsControllerTest : IClassFixture<ApiApplicationFactory
} }
[Fact] [Fact]
public async Task UpdateServiceAccount_User_NoPermissions() public async Task Update_User_NoPermissions()
{ {
// Create a new account as a user var (org, _) = await _organizationHelper.Initialize(true, true);
await LoginAsNewOrgUserAsync(); var (email, _) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);
await LoginAsync(email);
var initialServiceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount var initialServiceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount
{ {
OrganizationId = _organization.Id, OrganizationId = org.Id,
Name = _mockEncryptedString, Name = _mockEncryptedString,
}); });
var request = new ServiceAccountUpdateRequestModel { Name = _mockNewName }; var request = new ServiceAccountUpdateRequestModel { Name = _mockNewName };
var response = await _client.PutAsJsonAsync($"/service-accounts/{initialServiceAccount.Id}", request); var response = await _client.PutAsJsonAsync($"/service-accounts/{initialServiceAccount.Id}", request);
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
} }
[Fact] [Theory]
public async Task CreateServiceAccountAccessToken_Admin() [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 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, Name = _mockEncryptedString,
}); });
@ -253,18 +324,19 @@ public class ServiceAccountsControllerTest : IClassFixture<ApiApplicationFactory
} }
[Fact] [Fact]
public async Task CreateServiceAccountAccessToken_User_WithPermission() public async Task CreateAccessToken_User_WithPermission()
{ {
// Create a new account as a user var (org, _) = await _organizationHelper.Initialize(true, true);
var user = await LoginAsNewOrgUserAsync(); var (email, orgUser) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);
await LoginAsync(email);
var serviceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount var serviceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount
{ {
OrganizationId = _organization.Id, OrganizationId = org.Id,
Name = _mockEncryptedString, 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 mockExpiresAt = DateTime.UtcNow.AddDays(30);
var request = new AccessTokenCreateRequestModel var request = new AccessTokenCreateRequestModel
@ -288,14 +360,15 @@ public class ServiceAccountsControllerTest : IClassFixture<ApiApplicationFactory
} }
[Fact] [Fact]
public async Task CreateServiceAccountAccessToken_User_NoPermission() public async Task CreateAccessToken_User_NoPermission()
{ {
// Create a new account as a user var (org, _) = await _organizationHelper.Initialize(true, true);
await LoginAsNewOrgUserAsync(); var (email, _) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);
await LoginAsync(email);
var serviceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount var serviceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount
{ {
OrganizationId = _organization.Id, OrganizationId = org.Id,
Name = _mockEncryptedString, Name = _mockEncryptedString,
}); });
@ -309,15 +382,18 @@ public class ServiceAccountsControllerTest : IClassFixture<ApiApplicationFactory
}; };
var response = await _client.PostAsJsonAsync($"/service-accounts/{serviceAccount.Id}/access-tokens", request); 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] [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 var serviceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount
{ {
OrganizationId = _organization.Id, OrganizationId = org.Id,
Name = _mockEncryptedString, Name = _mockEncryptedString,
}); });
@ -341,73 +417,26 @@ public class ServiceAccountsControllerTest : IClassFixture<ApiApplicationFactory
AssertHelper.AssertRecent(result.CreationDate); AssertHelper.AssertRecent(result.CreationDate);
} }
[Fact] private async Task CreateUserPolicyAsync(Guid userId, Guid serviceAccountId, bool read, bool write)
public async Task CreateServiceAccountAccessTokenExpireAtNullAsync_User_WithPermission()
{ {
// Create a new account as a user var policy = new UserServiceAccountAccessPolicy
var user = await LoginAsNewOrgUserAsync();
var serviceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount
{ {
OrganizationId = _organization.Id, OrganizationUserId = userId,
Name = _mockEncryptedString, GrantedServiceAccountId = serviceAccountId,
}); Read = read,
Write = write,
await CreateUserServiceAccountAccessPolicyAsync(user.Id, serviceAccount.Id, true, true);
var request = new AccessTokenCreateRequestModel
{
Name = _mockEncryptedString,
EncryptedPayload = _mockEncryptedString,
Key = _mockEncryptedString,
ExpireAt = null,
}; };
await _accessPolicyRepository.CreateManyAsync(new List<BaseAccessPolicy> { policy });
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);
} }
[Fact] private async Task<List<Guid>> SetupGetServiceAccountsByOrganizationAsync(Organization org)
public async Task CreateServiceAccountAccessTokenExpireAtNullAsync_User_NoPermission()
{ {
// 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>(); 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 var serviceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount
{ {
OrganizationId = _organization.Id, OrganizationId = org.Id,
Name = _mockEncryptedString, Name = _mockEncryptedString,
}); });
serviceAccountIds.Add(serviceAccount.Id); serviceAccountIds.Add(serviceAccount.Id);
@ -415,30 +444,4 @@ public class ServiceAccountsControllerTest : IClassFixture<ApiApplicationFactory
return serviceAccountIds; 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); 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) await sutProvider.GetDependency<IDeleteProjectCommand>().Received(1)
.DeleteProjects(Arg.Is(ids), Arg.Any<Guid>()); .DeleteProjects(Arg.Is(ids), Arg.Any<Guid>());
Assert.Equal(data.Count, results.Data.Count()); Assert.Equal(data.Count, results.Data.Count());
@ -40,6 +40,6 @@ public class ProjectsControllerTests
public async void BulkDeleteProjects_NoGuids_ThrowsArgumentNullException(SutProvider<ProjectsController> sutProvider) public async void BulkDeleteProjects_NoGuids_ThrowsArgumentNullException(SutProvider<ProjectsController> sutProvider)
{ {
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(Guid.NewGuid()); 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.Controllers;
using Bit.Api.SecretsManager.Models.Request; using Bit.Api.SecretsManager.Models.Request;
using Bit.Core.Context;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.SecretsManager.Commands.Secrets.Interfaces; using Bit.Core.SecretsManager.Commands.Secrets.Interfaces;
using Bit.Core.SecretsManager.Entities; using Bit.Core.SecretsManager.Entities;
@ -23,7 +24,8 @@ public class SecretsControllerTests
[BitAutoData] [BitAutoData]
public async void GetSecretsByOrganization_ReturnsEmptyList(SutProvider<SecretsController> sutProvider, Guid id) 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) await sutProvider.GetDependency<ISecretRepository>().Received(1)
.GetManyByOrganizationIdAsync(Arg.Is(AssertHelper.AssertPropertyEqual(id))); .GetManyByOrganizationIdAsync(Arg.Is(AssertHelper.AssertPropertyEqual(id)));
@ -31,11 +33,34 @@ public class SecretsControllerTests
Assert.Empty(result.Secrets); 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] [Theory]
[BitAutoData] [BitAutoData]
public async void GetSecret_NotFound(SutProvider<SecretsController> sutProvider) 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] [Theory]
@ -44,33 +69,22 @@ public class SecretsControllerTests
{ {
sutProvider.GetDependency<ISecretRepository>().GetByIdAsync(default).ReturnsForAnyArgs(resultSecret); 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) await sutProvider.GetDependency<ISecretRepository>().Received(1)
.GetByIdAsync(Arg.Is(AssertHelper.AssertPropertyEqual(resultSecret.Id))); .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] [Theory]
[BitAutoData] [BitAutoData]
public async void CreateSecret_Success(SutProvider<SecretsController> sutProvider, SecretCreateRequestModel data, Guid organizationId) public async void CreateSecret_Success(SutProvider<SecretsController> sutProvider, SecretCreateRequestModel data, Guid organizationId)
{ {
var resultSecret = data.ToSecret(organizationId); var resultSecret = data.ToSecret(organizationId);
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(organizationId).Returns(true);
sutProvider.GetDependency<ICreateSecretCommand>().CreateAsync(default).ReturnsForAnyArgs(resultSecret); 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) await sutProvider.GetDependency<ICreateSecretCommand>().Received(1)
.CreateAsync(Arg.Any<Secret>()); .CreateAsync(Arg.Any<Secret>());
} }
@ -82,7 +96,7 @@ public class SecretsControllerTests
var resultSecret = data.ToSecret(secretId); var resultSecret = data.ToSecret(secretId);
sutProvider.GetDependency<IUpdateSecretCommand>().UpdateAsync(default).ReturnsForAnyArgs(resultSecret); 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) await sutProvider.GetDependency<IUpdateSecretCommand>().Received(1)
.UpdateAsync(Arg.Any<Secret>()); .UpdateAsync(Arg.Any<Secret>());
} }

View File

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

View File

@ -28,6 +28,7 @@
"orgcustom", "orgcustom",
"providerprovideradmin", "providerprovideradmin",
"providerserviceuser", "providerserviceuser",
"accesssecretsmanager",
"sub", "sub",
"organization" "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