Browse Source
The purpose of this PR is to create server endpoints for creating, reading, updating, and deleting access policies for projects.pull/2605/head
24 changed files with 1133 additions and 63 deletions
@ -0,0 +1,45 @@ |
|||||||
|
using Bit.Core.Entities; |
||||||
|
using Bit.Core.Exceptions; |
||||||
|
using Bit.Core.Repositories; |
||||||
|
using Bit.Core.SecretManagerFeatures.AccessPolicies.Interfaces; |
||||||
|
|
||||||
|
namespace Bit.Commercial.Core.SecretManagerFeatures.AccessPolicies; |
||||||
|
|
||||||
|
public class CreateAccessPoliciesCommand : ICreateAccessPoliciesCommand |
||||||
|
{ |
||||||
|
private readonly IAccessPolicyRepository _accessPolicyRepository; |
||||||
|
|
||||||
|
public CreateAccessPoliciesCommand(IAccessPolicyRepository accessPolicyRepository) |
||||||
|
{ |
||||||
|
_accessPolicyRepository = accessPolicyRepository; |
||||||
|
} |
||||||
|
|
||||||
|
public async Task<List<BaseAccessPolicy>> CreateAsync(List<BaseAccessPolicy> accessPolicies) |
||||||
|
{ |
||||||
|
var distinctAccessPolicies = accessPolicies.DistinctBy(baseAccessPolicy => |
||||||
|
{ |
||||||
|
return baseAccessPolicy switch |
||||||
|
{ |
||||||
|
UserProjectAccessPolicy ap => new Tuple<Guid?, Guid?>(ap.OrganizationUserId, ap.GrantedProjectId), |
||||||
|
GroupProjectAccessPolicy ap => new Tuple<Guid?, Guid?>(ap.GroupId, ap.GrantedProjectId), |
||||||
|
ServiceAccountProjectAccessPolicy ap => new Tuple<Guid?, Guid?>(ap.ServiceAccountId, ap.GrantedProjectId), |
||||||
|
_ => throw new ArgumentException("Unsupported access policy type provided.", nameof(baseAccessPolicy)) |
||||||
|
}; |
||||||
|
}).ToList(); |
||||||
|
|
||||||
|
if (accessPolicies.Count != distinctAccessPolicies.Count) |
||||||
|
{ |
||||||
|
throw new BadRequestException("Resources must be unique"); |
||||||
|
} |
||||||
|
|
||||||
|
foreach (var accessPolicy in accessPolicies) |
||||||
|
{ |
||||||
|
if (await _accessPolicyRepository.AccessPolicyExists(accessPolicy)) |
||||||
|
{ |
||||||
|
throw new BadRequestException("Resource already exists"); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return await _accessPolicyRepository.CreateManyAsync(accessPolicies); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,27 @@ |
|||||||
|
using Bit.Core.Exceptions; |
||||||
|
using Bit.Core.Repositories; |
||||||
|
using Bit.Core.SecretManagerFeatures.AccessPolicies.Interfaces; |
||||||
|
|
||||||
|
namespace Bit.Commercial.Core.SecretManagerFeatures.AccessPolicies; |
||||||
|
|
||||||
|
public class DeleteAccessPolicyCommand : IDeleteAccessPolicyCommand |
||||||
|
{ |
||||||
|
private readonly IAccessPolicyRepository _accessPolicyRepository; |
||||||
|
|
||||||
|
public DeleteAccessPolicyCommand(IAccessPolicyRepository accessPolicyRepository) |
||||||
|
{ |
||||||
|
_accessPolicyRepository = accessPolicyRepository; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
public async Task DeleteAsync(Guid id) |
||||||
|
{ |
||||||
|
var accessPolicy = await _accessPolicyRepository.GetByIdAsync(id); |
||||||
|
if (accessPolicy == null) |
||||||
|
{ |
||||||
|
throw new NotFoundException(); |
||||||
|
} |
||||||
|
|
||||||
|
await _accessPolicyRepository.DeleteAsync(id); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,32 @@ |
|||||||
|
using Bit.Core.Entities; |
||||||
|
using Bit.Core.Exceptions; |
||||||
|
using Bit.Core.Repositories; |
||||||
|
using Bit.Core.SecretManagerFeatures.AccessPolicies.Interfaces; |
||||||
|
|
||||||
|
namespace Bit.Commercial.Core.SecretManagerFeatures.AccessPolicies; |
||||||
|
|
||||||
|
public class UpdateAccessPolicyCommand : IUpdateAccessPolicyCommand |
||||||
|
{ |
||||||
|
private readonly IAccessPolicyRepository _accessPolicyRepository; |
||||||
|
|
||||||
|
public UpdateAccessPolicyCommand(IAccessPolicyRepository accessPolicyRepository) |
||||||
|
{ |
||||||
|
_accessPolicyRepository = accessPolicyRepository; |
||||||
|
} |
||||||
|
|
||||||
|
public async Task<BaseAccessPolicy> UpdateAsync(Guid id, bool read, bool write) |
||||||
|
{ |
||||||
|
var accessPolicy = await _accessPolicyRepository.GetByIdAsync(id); |
||||||
|
if (accessPolicy == null) |
||||||
|
{ |
||||||
|
throw new NotFoundException(); |
||||||
|
} |
||||||
|
|
||||||
|
accessPolicy.Read = read; |
||||||
|
accessPolicy.Write = write; |
||||||
|
accessPolicy.RevisionDate = DateTime.UtcNow; |
||||||
|
|
||||||
|
await _accessPolicyRepository.ReplaceAsync(accessPolicy); |
||||||
|
return accessPolicy; |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,171 @@ |
|||||||
|
using AutoMapper; |
||||||
|
using Bit.Core.Repositories; |
||||||
|
using Bit.Infrastructure.EntityFramework.Models; |
||||||
|
using Bit.Infrastructure.EntityFramework.Repositories; |
||||||
|
using Microsoft.EntityFrameworkCore; |
||||||
|
using Microsoft.Extensions.DependencyInjection; |
||||||
|
|
||||||
|
namespace Bit.Commercial.Infrastructure.EntityFramework.Repositories; |
||||||
|
|
||||||
|
public class AccessPolicyRepository : BaseEntityFrameworkRepository, IAccessPolicyRepository |
||||||
|
{ |
||||||
|
public AccessPolicyRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper) : base(serviceScopeFactory, |
||||||
|
mapper) |
||||||
|
{ |
||||||
|
} |
||||||
|
|
||||||
|
public async Task<List<Core.Entities.BaseAccessPolicy>> CreateManyAsync(List<Core.Entities.BaseAccessPolicy> baseAccessPolicies) |
||||||
|
{ |
||||||
|
using (var scope = ServiceScopeFactory.CreateScope()) |
||||||
|
{ |
||||||
|
var dbContext = GetDatabaseContext(scope); |
||||||
|
foreach (var baseAccessPolicy in baseAccessPolicies) |
||||||
|
{ |
||||||
|
baseAccessPolicy.SetNewId(); |
||||||
|
switch (baseAccessPolicy) |
||||||
|
{ |
||||||
|
case Core.Entities.UserProjectAccessPolicy accessPolicy: |
||||||
|
{ |
||||||
|
var entity = |
||||||
|
Mapper.Map<UserProjectAccessPolicy>(accessPolicy); |
||||||
|
await dbContext.AddAsync(entity); |
||||||
|
break; |
||||||
|
} |
||||||
|
case Core.Entities.GroupProjectAccessPolicy accessPolicy: |
||||||
|
{ |
||||||
|
var entity = Mapper.Map<GroupProjectAccessPolicy>(accessPolicy); |
||||||
|
await dbContext.AddAsync(entity); |
||||||
|
break; |
||||||
|
} |
||||||
|
case Core.Entities.ServiceAccountProjectAccessPolicy accessPolicy: |
||||||
|
{ |
||||||
|
var entity = Mapper.Map<ServiceAccountProjectAccessPolicy>(accessPolicy); |
||||||
|
await dbContext.AddAsync(entity); |
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
await dbContext.SaveChangesAsync(); |
||||||
|
return baseAccessPolicies; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public async Task<bool> AccessPolicyExists(Core.Entities.BaseAccessPolicy baseAccessPolicy) |
||||||
|
{ |
||||||
|
using (var scope = ServiceScopeFactory.CreateScope()) |
||||||
|
{ |
||||||
|
var dbContext = GetDatabaseContext(scope); |
||||||
|
switch (baseAccessPolicy) |
||||||
|
{ |
||||||
|
case Core.Entities.UserProjectAccessPolicy accessPolicy: |
||||||
|
{ |
||||||
|
var policy = await dbContext.UserProjectAccessPolicy |
||||||
|
.Where(c => c.OrganizationUserId == accessPolicy.OrganizationUserId && |
||||||
|
c.GrantedProjectId == accessPolicy.GrantedProjectId) |
||||||
|
.FirstOrDefaultAsync(); |
||||||
|
return policy != null; |
||||||
|
} |
||||||
|
case Core.Entities.GroupProjectAccessPolicy accessPolicy: |
||||||
|
{ |
||||||
|
var policy = await dbContext.GroupProjectAccessPolicy |
||||||
|
.Where(c => c.GroupId == accessPolicy.GroupId && |
||||||
|
c.GrantedProjectId == accessPolicy.GrantedProjectId) |
||||||
|
.FirstOrDefaultAsync(); |
||||||
|
return policy != null; |
||||||
|
} |
||||||
|
case Core.Entities.ServiceAccountProjectAccessPolicy accessPolicy: |
||||||
|
{ |
||||||
|
var policy = await dbContext.ServiceAccountProjectAccessPolicy |
||||||
|
.Where(c => c.ServiceAccountId == accessPolicy.ServiceAccountId && |
||||||
|
c.GrantedProjectId == accessPolicy.GrantedProjectId) |
||||||
|
.FirstOrDefaultAsync(); |
||||||
|
return policy != null; |
||||||
|
} |
||||||
|
default: |
||||||
|
throw new ArgumentException("Unsupported access policy type provided.", nameof(baseAccessPolicy)); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public async Task<Core.Entities.BaseAccessPolicy?> GetByIdAsync(Guid id) |
||||||
|
{ |
||||||
|
using (var scope = ServiceScopeFactory.CreateScope()) |
||||||
|
{ |
||||||
|
var dbContext = GetDatabaseContext(scope); |
||||||
|
var entity = await dbContext.AccessPolicies.Where(ap => ap.Id == id) |
||||||
|
.Include(ap => ((UserProjectAccessPolicy)ap).OrganizationUser.User) |
||||||
|
.Include(ap => ((GroupProjectAccessPolicy)ap).Group) |
||||||
|
.Include(ap => ((ServiceAccountProjectAccessPolicy)ap).ServiceAccount) |
||||||
|
.FirstOrDefaultAsync(); |
||||||
|
|
||||||
|
if (entity == null) |
||||||
|
{ |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
return MapToCore(entity); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public async Task ReplaceAsync(Core.Entities.BaseAccessPolicy baseAccessPolicy) |
||||||
|
{ |
||||||
|
using (var scope = ServiceScopeFactory.CreateScope()) |
||||||
|
{ |
||||||
|
var dbContext = GetDatabaseContext(scope); |
||||||
|
var entity = await dbContext.AccessPolicies.FindAsync(baseAccessPolicy.Id); |
||||||
|
if (entity != null) |
||||||
|
{ |
||||||
|
dbContext.AccessPolicies.Attach(entity); |
||||||
|
entity.Write = baseAccessPolicy.Write; |
||||||
|
entity.Read = baseAccessPolicy.Read; |
||||||
|
entity.RevisionDate = baseAccessPolicy.RevisionDate; |
||||||
|
await dbContext.SaveChangesAsync(); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public async Task<IEnumerable<Core.Entities.BaseAccessPolicy>?> GetManyByProjectId(Guid id) |
||||||
|
{ |
||||||
|
using (var scope = ServiceScopeFactory.CreateScope()) |
||||||
|
{ |
||||||
|
var dbContext = GetDatabaseContext(scope); |
||||||
|
|
||||||
|
var entities = await dbContext.AccessPolicies.Where(ap => |
||||||
|
((UserProjectAccessPolicy)ap).GrantedProjectId == id || |
||||||
|
((GroupProjectAccessPolicy)ap).GrantedProjectId == id || |
||||||
|
((ServiceAccountProjectAccessPolicy)ap).GrantedProjectId == id) |
||||||
|
.Include(ap => ((UserProjectAccessPolicy)ap).OrganizationUser.User) |
||||||
|
.Include(ap => ((GroupProjectAccessPolicy)ap).Group) |
||||||
|
.Include(ap => ((ServiceAccountProjectAccessPolicy)ap).ServiceAccount) |
||||||
|
.ToListAsync(); |
||||||
|
|
||||||
|
return !entities.Any() ? null : entities.Select(MapToCore); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public async Task DeleteAsync(Guid id) |
||||||
|
{ |
||||||
|
using (var scope = ServiceScopeFactory.CreateScope()) |
||||||
|
{ |
||||||
|
var dbContext = GetDatabaseContext(scope); |
||||||
|
var entity = await dbContext.AccessPolicies.FindAsync(id); |
||||||
|
if (entity != null) |
||||||
|
{ |
||||||
|
dbContext.Remove(entity); |
||||||
|
await dbContext.SaveChangesAsync(); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private Core.Entities.BaseAccessPolicy MapToCore(BaseAccessPolicy baseAccessPolicyEntity) |
||||||
|
{ |
||||||
|
return baseAccessPolicyEntity switch |
||||||
|
{ |
||||||
|
UserProjectAccessPolicy ap => Mapper.Map<Core.Entities.UserProjectAccessPolicy>(ap), |
||||||
|
GroupProjectAccessPolicy ap => Mapper.Map<Core.Entities.GroupProjectAccessPolicy>(ap), |
||||||
|
ServiceAccountProjectAccessPolicy ap => Mapper.Map<Core.Entities.ServiceAccountProjectAccessPolicy>(ap), |
||||||
|
_ => throw new ArgumentException("Unsupported access policy type") |
||||||
|
}; |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,126 @@ |
|||||||
|
using Bit.Commercial.Core.SecretManagerFeatures.AccessPolicies; |
||||||
|
using Bit.Core.Entities; |
||||||
|
using Bit.Core.Exceptions; |
||||||
|
using Bit.Core.Repositories; |
||||||
|
using Bit.Test.Common.AutoFixture; |
||||||
|
using Bit.Test.Common.AutoFixture.Attributes; |
||||||
|
using Bit.Test.Common.Helpers; |
||||||
|
using NSubstitute; |
||||||
|
using Xunit; |
||||||
|
|
||||||
|
namespace Bit.Commercial.Core.Test.SecretManagerFeatures.AccessPolicies; |
||||||
|
|
||||||
|
[SutProviderCustomize] |
||||||
|
public class CreateAccessPoliciesCommandTests |
||||||
|
{ |
||||||
|
[Theory] |
||||||
|
[BitAutoData] |
||||||
|
public async Task CreateAsync_CallsCreate(List<UserProjectAccessPolicy> userProjectAccessPolicies, |
||||||
|
List<GroupProjectAccessPolicy> groupProjectAccessPolicies, |
||||||
|
List<ServiceAccountProjectAccessPolicy> serviceAccountProjectAccessPolicies, |
||||||
|
SutProvider<CreateAccessPoliciesCommand> sutProvider) |
||||||
|
{ |
||||||
|
var data = new List<BaseAccessPolicy>(); |
||||||
|
data.AddRange(userProjectAccessPolicies); |
||||||
|
data.AddRange(groupProjectAccessPolicies); |
||||||
|
data.AddRange(serviceAccountProjectAccessPolicies); |
||||||
|
|
||||||
|
await sutProvider.Sut.CreateAsync(data); |
||||||
|
|
||||||
|
await sutProvider.GetDependency<IAccessPolicyRepository>().Received(1) |
||||||
|
.CreateManyAsync(Arg.Is(AssertHelper.AssertPropertyEqual(data))); |
||||||
|
} |
||||||
|
|
||||||
|
[Theory] |
||||||
|
[BitAutoData] |
||||||
|
public async Task CreateAsync_AlreadyExists_Throws_BadRequestException( |
||||||
|
List<UserProjectAccessPolicy> userProjectAccessPolicies, |
||||||
|
List<GroupProjectAccessPolicy> groupProjectAccessPolicies, |
||||||
|
List<ServiceAccountProjectAccessPolicy> serviceAccountProjectAccessPolicies, |
||||||
|
SutProvider<CreateAccessPoliciesCommand> sutProvider) |
||||||
|
{ |
||||||
|
var data = new List<BaseAccessPolicy>(); |
||||||
|
data.AddRange(userProjectAccessPolicies); |
||||||
|
data.AddRange(groupProjectAccessPolicies); |
||||||
|
data.AddRange(serviceAccountProjectAccessPolicies); |
||||||
|
|
||||||
|
sutProvider.GetDependency<IAccessPolicyRepository>().AccessPolicyExists(Arg.Any<BaseAccessPolicy>()) |
||||||
|
.Returns(true); |
||||||
|
|
||||||
|
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.CreateAsync(data)); |
||||||
|
|
||||||
|
await sutProvider.GetDependency<IAccessPolicyRepository>().DidNotReceiveWithAnyArgs().CreateManyAsync(default); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
[Theory] |
||||||
|
[BitAutoData(true, false, false)] |
||||||
|
[BitAutoData(false, true, false)] |
||||||
|
[BitAutoData(true, true, false)] |
||||||
|
[BitAutoData(false, false, true)] |
||||||
|
[BitAutoData(true, false, true)] |
||||||
|
[BitAutoData(false, true, true)] |
||||||
|
[BitAutoData(true, true, true)] |
||||||
|
public async Task CreateAsync_NotUnique_ThrowsException( |
||||||
|
bool testUserPolicies, |
||||||
|
bool testGroupPolicies, |
||||||
|
bool testServiceAccountPolicies, |
||||||
|
List<UserProjectAccessPolicy> userProjectAccessPolicies, |
||||||
|
List<GroupProjectAccessPolicy> groupProjectAccessPolicies, |
||||||
|
List<ServiceAccountProjectAccessPolicy> serviceAccountProjectAccessPolicies, |
||||||
|
SutProvider<CreateAccessPoliciesCommand> sutProvider |
||||||
|
) |
||||||
|
{ |
||||||
|
var data = new List<BaseAccessPolicy>(); |
||||||
|
data.AddRange(userProjectAccessPolicies); |
||||||
|
data.AddRange(groupProjectAccessPolicies); |
||||||
|
data.AddRange(serviceAccountProjectAccessPolicies); |
||||||
|
|
||||||
|
if (testUserPolicies) |
||||||
|
{ |
||||||
|
var mockUserPolicy = new UserProjectAccessPolicy |
||||||
|
{ |
||||||
|
OrganizationUserId = Guid.NewGuid(), |
||||||
|
GrantedProjectId = Guid.NewGuid() |
||||||
|
}; |
||||||
|
data.Add(mockUserPolicy); |
||||||
|
|
||||||
|
// Add a duplicate policy |
||||||
|
data.Add(mockUserPolicy); |
||||||
|
} |
||||||
|
|
||||||
|
if (testGroupPolicies) |
||||||
|
{ |
||||||
|
var mockGroupPolicy = new GroupProjectAccessPolicy |
||||||
|
{ |
||||||
|
GroupId = Guid.NewGuid(), |
||||||
|
GrantedProjectId = Guid.NewGuid() |
||||||
|
}; |
||||||
|
data.Add(mockGroupPolicy); |
||||||
|
|
||||||
|
// Add a duplicate policy |
||||||
|
data.Add(mockGroupPolicy); |
||||||
|
} |
||||||
|
|
||||||
|
if (testServiceAccountPolicies) |
||||||
|
{ |
||||||
|
var mockServiceAccountPolicy = new ServiceAccountProjectAccessPolicy |
||||||
|
{ |
||||||
|
ServiceAccountId = Guid.NewGuid(), |
||||||
|
GrantedProjectId = Guid.NewGuid() |
||||||
|
}; |
||||||
|
data.Add(mockServiceAccountPolicy); |
||||||
|
|
||||||
|
// Add a duplicate policy |
||||||
|
data.Add(mockServiceAccountPolicy); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IAccessPolicyRepository>().AccessPolicyExists(Arg.Any<BaseAccessPolicy>()) |
||||||
|
.Returns(true); |
||||||
|
|
||||||
|
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.CreateAsync(data)); |
||||||
|
|
||||||
|
await sutProvider.GetDependency<IAccessPolicyRepository>().DidNotReceiveWithAnyArgs().CreateManyAsync(default); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,38 @@ |
|||||||
|
using Bit.Commercial.Core.SecretManagerFeatures.AccessPolicies; |
||||||
|
using Bit.Core.Entities; |
||||||
|
using Bit.Core.Exceptions; |
||||||
|
using Bit.Core.Repositories; |
||||||
|
using Bit.Test.Common.AutoFixture; |
||||||
|
using Bit.Test.Common.AutoFixture.Attributes; |
||||||
|
using NSubstitute; |
||||||
|
using NSubstitute.ReturnsExtensions; |
||||||
|
using Xunit; |
||||||
|
|
||||||
|
namespace Bit.Commercial.Core.Test.SecretManagerFeatures.AccessPolicies; |
||||||
|
|
||||||
|
[SutProviderCustomize] |
||||||
|
public class DeleteAccessPolicyCommandTests |
||||||
|
{ |
||||||
|
[Theory] |
||||||
|
[BitAutoData] |
||||||
|
public async Task DeleteAccessPolicy_Throws_NotFoundException(Guid data, |
||||||
|
SutProvider<DeleteAccessPolicyCommand> sutProvider) |
||||||
|
{ |
||||||
|
sutProvider.GetDependency<IAccessPolicyRepository>().GetByIdAsync(data).ReturnsNull(); |
||||||
|
var exception = await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.DeleteAsync(data)); |
||||||
|
await sutProvider.GetDependency<IAccessPolicyRepository>().DidNotReceiveWithAnyArgs().DeleteAsync(default); |
||||||
|
} |
||||||
|
|
||||||
|
[Theory] |
||||||
|
[BitAutoData] |
||||||
|
public async Task DeleteAccessPolicy_Success(Guid data, |
||||||
|
SutProvider<DeleteAccessPolicyCommand> sutProvider) |
||||||
|
{ |
||||||
|
sutProvider.GetDependency<IAccessPolicyRepository>().GetByIdAsync(data) |
||||||
|
.Returns(new UserProjectAccessPolicy { Id = data }); |
||||||
|
|
||||||
|
await sutProvider.Sut.DeleteAsync(data); |
||||||
|
|
||||||
|
await sutProvider.GetDependency<IAccessPolicyRepository>().Received(1).DeleteAsync(Arg.Is(data)); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,40 @@ |
|||||||
|
using Bit.Commercial.Core.SecretManagerFeatures.AccessPolicies; |
||||||
|
using Bit.Core.Entities; |
||||||
|
using Bit.Core.Exceptions; |
||||||
|
using Bit.Core.Repositories; |
||||||
|
using Bit.Test.Common.AutoFixture; |
||||||
|
using Bit.Test.Common.AutoFixture.Attributes; |
||||||
|
using Bit.Test.Common.Helpers; |
||||||
|
using NSubstitute; |
||||||
|
using Xunit; |
||||||
|
|
||||||
|
namespace Bit.Commercial.Core.Test.SecretManagerFeatures.AccessPolicies; |
||||||
|
|
||||||
|
[SutProviderCustomize] |
||||||
|
public class UpdateAccessPolicyCommandTests |
||||||
|
{ |
||||||
|
[Theory] |
||||||
|
[BitAutoData] |
||||||
|
public async Task UpdateAsync_Throws_NotFoundException(Guid data, bool read, bool write, |
||||||
|
SutProvider<UpdateAccessPolicyCommand> sutProvider) |
||||||
|
{ |
||||||
|
var exception = |
||||||
|
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.UpdateAsync(data, read, write)); |
||||||
|
await sutProvider.GetDependency<IAccessPolicyRepository>().DidNotReceiveWithAnyArgs().ReplaceAsync(default); |
||||||
|
} |
||||||
|
|
||||||
|
[Theory] |
||||||
|
[BitAutoData] |
||||||
|
public async Task UpdateAsync_Calls_Replace(Guid data, bool read, bool write, |
||||||
|
SutProvider<UpdateAccessPolicyCommand> sutProvider) |
||||||
|
{ |
||||||
|
var existingPolicy = new UserProjectAccessPolicy { Id = data, Read = true, Write = true }; |
||||||
|
sutProvider.GetDependency<IAccessPolicyRepository>().GetByIdAsync(data).Returns(existingPolicy); |
||||||
|
var result = await sutProvider.Sut.UpdateAsync(data, read, write); |
||||||
|
await sutProvider.GetDependency<IAccessPolicyRepository>().Received(1).ReplaceAsync(existingPolicy); |
||||||
|
|
||||||
|
AssertHelper.AssertRecent(result.RevisionDate); |
||||||
|
Assert.Equal(read, result.Read); |
||||||
|
Assert.Equal(write, result.Write); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,69 @@ |
|||||||
|
using Bit.Api.SecretManagerFeatures.Models.Request; |
||||||
|
using Bit.Api.SecretManagerFeatures.Models.Response; |
||||||
|
using Bit.Api.Utilities; |
||||||
|
using Bit.Core.Entities; |
||||||
|
using Bit.Core.Repositories; |
||||||
|
using Bit.Core.SecretManagerFeatures.AccessPolicies.Interfaces; |
||||||
|
using Microsoft.AspNetCore.Mvc; |
||||||
|
|
||||||
|
namespace Bit.Api.Controllers; |
||||||
|
|
||||||
|
[SecretsManager] |
||||||
|
[Route("access-policies")] |
||||||
|
public class AccessPoliciesController : Controller |
||||||
|
{ |
||||||
|
private readonly IAccessPolicyRepository _accessPolicyRepository; |
||||||
|
private readonly ICreateAccessPoliciesCommand _createAccessPoliciesCommand; |
||||||
|
private readonly IDeleteAccessPolicyCommand _deleteAccessPolicyCommand; |
||||||
|
private readonly IUpdateAccessPolicyCommand _updateAccessPolicyCommand; |
||||||
|
|
||||||
|
public AccessPoliciesController( |
||||||
|
IAccessPolicyRepository accessPolicyRepository, |
||||||
|
ICreateAccessPoliciesCommand createAccessPoliciesCommand, |
||||||
|
IDeleteAccessPolicyCommand deleteAccessPolicyCommand, |
||||||
|
IUpdateAccessPolicyCommand updateAccessPolicyCommand) |
||||||
|
{ |
||||||
|
_accessPolicyRepository = accessPolicyRepository; |
||||||
|
_createAccessPoliciesCommand = createAccessPoliciesCommand; |
||||||
|
_deleteAccessPolicyCommand = deleteAccessPolicyCommand; |
||||||
|
_updateAccessPolicyCommand = updateAccessPolicyCommand; |
||||||
|
} |
||||||
|
|
||||||
|
[HttpPost("/projects/{id}/access-policies")] |
||||||
|
public async Task<ProjectAccessPoliciesResponseModel> CreateProjectAccessPoliciesAsync([FromRoute] Guid id, |
||||||
|
[FromBody] AccessPoliciesCreateRequest request) |
||||||
|
{ |
||||||
|
var policies = request.ToBaseAccessPoliciesForProject(id); |
||||||
|
var results = await _createAccessPoliciesCommand.CreateAsync(policies); |
||||||
|
return new ProjectAccessPoliciesResponseModel(results); |
||||||
|
} |
||||||
|
|
||||||
|
[HttpGet("/projects/{id}/access-policies")] |
||||||
|
public async Task<ProjectAccessPoliciesResponseModel> GetProjectAccessPoliciesAsync([FromRoute] Guid id) |
||||||
|
{ |
||||||
|
var results = await _accessPolicyRepository.GetManyByProjectId(id); |
||||||
|
return new ProjectAccessPoliciesResponseModel(results); |
||||||
|
} |
||||||
|
|
||||||
|
[HttpPut("{id}")] |
||||||
|
public async Task<BaseAccessPolicyResponseModel> UpdateAccessPolicyAsync([FromRoute] Guid id, |
||||||
|
[FromBody] AccessPolicyUpdateRequest request) |
||||||
|
{ |
||||||
|
var result = await _updateAccessPolicyCommand.UpdateAsync(id, request.Read, request.Write); |
||||||
|
|
||||||
|
return result switch |
||||||
|
{ |
||||||
|
UserProjectAccessPolicy accessPolicy => new UserProjectAccessPolicyResponseModel(accessPolicy), |
||||||
|
GroupProjectAccessPolicy accessPolicy => new GroupProjectAccessPolicyResponseModel(accessPolicy), |
||||||
|
ServiceAccountProjectAccessPolicy accessPolicy => new ServiceAccountProjectAccessPolicyResponseModel( |
||||||
|
accessPolicy), |
||||||
|
_ => throw new ArgumentException("Unsupported access policy type provided.") |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
[HttpDelete("{id}")] |
||||||
|
public async Task DeleteAccessPolicyAsync([FromRoute] Guid id) |
||||||
|
{ |
||||||
|
await _deleteAccessPolicyCommand.DeleteAsync(id); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,77 @@ |
|||||||
|
#nullable enable |
||||||
|
using System.ComponentModel.DataAnnotations; |
||||||
|
using Bit.Core.Entities; |
||||||
|
using Bit.Core.Exceptions; |
||||||
|
|
||||||
|
namespace Bit.Api.SecretManagerFeatures.Models.Request; |
||||||
|
|
||||||
|
public class AccessPoliciesCreateRequest |
||||||
|
{ |
||||||
|
public IEnumerable<AccessPolicyRequest>? UserAccessPolicyRequests { get; set; } |
||||||
|
|
||||||
|
public IEnumerable<AccessPolicyRequest>? GroupAccessPolicyRequests { get; set; } |
||||||
|
|
||||||
|
public IEnumerable<AccessPolicyRequest>? ServiceAccountAccessPolicyRequests { get; set; } |
||||||
|
|
||||||
|
public List<BaseAccessPolicy> ToBaseAccessPoliciesForProject(Guid projectId) |
||||||
|
{ |
||||||
|
if (UserAccessPolicyRequests == null && GroupAccessPolicyRequests == null && ServiceAccountAccessPolicyRequests == null) |
||||||
|
{ |
||||||
|
throw new BadRequestException("No creation requests provided."); |
||||||
|
} |
||||||
|
|
||||||
|
var userAccessPolicies = UserAccessPolicyRequests? |
||||||
|
.Select(x => x.ToUserProjectAccessPolicy(projectId)).ToList(); |
||||||
|
|
||||||
|
var groupAccessPolicies = GroupAccessPolicyRequests? |
||||||
|
.Select(x => x.ToGroupProjectAccessPolicy(projectId)).ToList(); |
||||||
|
|
||||||
|
var serviceAccountAccessPolicies = ServiceAccountAccessPolicyRequests? |
||||||
|
.Select(x => x.ToServiceAccountProjectAccessPolicy(projectId)).ToList(); |
||||||
|
|
||||||
|
var policies = new List<BaseAccessPolicy>(); |
||||||
|
if (userAccessPolicies != null) { policies.AddRange(userAccessPolicies); } |
||||||
|
if (groupAccessPolicies != null) { policies.AddRange(groupAccessPolicies); } |
||||||
|
if (serviceAccountAccessPolicies != null) { policies.AddRange(serviceAccountAccessPolicies); } |
||||||
|
return policies; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public class AccessPolicyRequest |
||||||
|
{ |
||||||
|
[Required] |
||||||
|
public Guid GranteeId { get; set; } |
||||||
|
|
||||||
|
[Required] |
||||||
|
public bool Read { get; set; } |
||||||
|
|
||||||
|
[Required] |
||||||
|
public bool Write { get; set; } |
||||||
|
|
||||||
|
public UserProjectAccessPolicy ToUserProjectAccessPolicy(Guid projectId) => |
||||||
|
new() |
||||||
|
{ |
||||||
|
OrganizationUserId = GranteeId, |
||||||
|
GrantedProjectId = projectId, |
||||||
|
Read = Read, |
||||||
|
Write = Write |
||||||
|
}; |
||||||
|
|
||||||
|
public GroupProjectAccessPolicy ToGroupProjectAccessPolicy(Guid projectId) => |
||||||
|
new() |
||||||
|
{ |
||||||
|
GroupId = GranteeId, |
||||||
|
GrantedProjectId = projectId, |
||||||
|
Read = Read, |
||||||
|
Write = Write |
||||||
|
}; |
||||||
|
|
||||||
|
public ServiceAccountProjectAccessPolicy ToServiceAccountProjectAccessPolicy(Guid projectId) => |
||||||
|
new() |
||||||
|
{ |
||||||
|
ServiceAccountId = GranteeId, |
||||||
|
GrantedProjectId = projectId, |
||||||
|
Read = Read, |
||||||
|
Write = Write |
||||||
|
}; |
||||||
|
} |
||||||
@ -0,0 +1,12 @@ |
|||||||
|
using System.ComponentModel.DataAnnotations; |
||||||
|
|
||||||
|
namespace Bit.Api.SecretManagerFeatures.Models.Request; |
||||||
|
|
||||||
|
public class AccessPolicyUpdateRequest |
||||||
|
{ |
||||||
|
[Required] |
||||||
|
public bool Read { get; set; } |
||||||
|
|
||||||
|
[Required] |
||||||
|
public bool Write { get; set; } |
||||||
|
} |
||||||
@ -0,0 +1,128 @@ |
|||||||
|
#nullable enable |
||||||
|
using Bit.Core.Entities; |
||||||
|
using Bit.Core.Models.Api; |
||||||
|
|
||||||
|
namespace Bit.Api.SecretManagerFeatures.Models.Response; |
||||||
|
|
||||||
|
public abstract class BaseAccessPolicyResponseModel : ResponseModel |
||||||
|
{ |
||||||
|
protected BaseAccessPolicyResponseModel(BaseAccessPolicy baseAccessPolicy, string obj) : base(obj) |
||||||
|
{ |
||||||
|
Id = baseAccessPolicy.Id; |
||||||
|
Read = baseAccessPolicy.Read; |
||||||
|
Write = baseAccessPolicy.Write; |
||||||
|
CreationDate = baseAccessPolicy.CreationDate; |
||||||
|
RevisionDate = baseAccessPolicy.RevisionDate; |
||||||
|
} |
||||||
|
|
||||||
|
public Guid Id { get; set; } |
||||||
|
public bool Read { get; set; } |
||||||
|
public bool Write { get; set; } |
||||||
|
public DateTime CreationDate { get; set; } |
||||||
|
public DateTime RevisionDate { get; set; } |
||||||
|
} |
||||||
|
|
||||||
|
public class UserProjectAccessPolicyResponseModel : BaseAccessPolicyResponseModel |
||||||
|
{ |
||||||
|
private const string _objectName = "userProjectAccessPolicy"; |
||||||
|
|
||||||
|
public UserProjectAccessPolicyResponseModel(UserProjectAccessPolicy accessPolicy) : base(accessPolicy, _objectName) |
||||||
|
{ |
||||||
|
OrganizationUserId = accessPolicy.OrganizationUserId; |
||||||
|
GrantedProjectId = accessPolicy.GrantedProjectId; |
||||||
|
OrganizationUserName = accessPolicy.User?.Name; |
||||||
|
} |
||||||
|
|
||||||
|
public UserProjectAccessPolicyResponseModel() : base(new UserProjectAccessPolicy(), _objectName) |
||||||
|
{ |
||||||
|
} |
||||||
|
|
||||||
|
public Guid? OrganizationUserId { get; set; } |
||||||
|
public string? OrganizationUserName { get; set; } |
||||||
|
public Guid? GrantedProjectId { get; set; } |
||||||
|
} |
||||||
|
|
||||||
|
public class UserServiceAccountAccessPolicyResponseModel : BaseAccessPolicyResponseModel |
||||||
|
{ |
||||||
|
private const string _objectName = "userServiceAccountAccessPolicy"; |
||||||
|
|
||||||
|
public UserServiceAccountAccessPolicyResponseModel(UserServiceAccountAccessPolicy accessPolicy) |
||||||
|
: base(accessPolicy, _objectName) |
||||||
|
{ |
||||||
|
OrganizationUserId = accessPolicy.OrganizationUserId; |
||||||
|
GrantedServiceAccountId = accessPolicy.GrantedServiceAccountId; |
||||||
|
OrganizationUserName = accessPolicy.User?.Name; |
||||||
|
} |
||||||
|
|
||||||
|
public UserServiceAccountAccessPolicyResponseModel() : base(new UserServiceAccountAccessPolicy(), _objectName) |
||||||
|
{ |
||||||
|
} |
||||||
|
|
||||||
|
public Guid? OrganizationUserId { get; set; } |
||||||
|
public string? OrganizationUserName { get; set; } |
||||||
|
public Guid? GrantedServiceAccountId { get; set; } |
||||||
|
} |
||||||
|
|
||||||
|
public class GroupProjectAccessPolicyResponseModel : BaseAccessPolicyResponseModel |
||||||
|
{ |
||||||
|
private const string _objectName = "groupProjectAccessPolicy"; |
||||||
|
|
||||||
|
public GroupProjectAccessPolicyResponseModel(GroupProjectAccessPolicy accessPolicy) |
||||||
|
: base(accessPolicy, _objectName) |
||||||
|
{ |
||||||
|
GroupId = accessPolicy.GroupId; |
||||||
|
GrantedProjectId = accessPolicy.GrantedProjectId; |
||||||
|
GroupName = accessPolicy.Group?.Name; |
||||||
|
} |
||||||
|
|
||||||
|
public GroupProjectAccessPolicyResponseModel() : base(new GroupProjectAccessPolicy(), _objectName) |
||||||
|
{ |
||||||
|
} |
||||||
|
|
||||||
|
public Guid? GroupId { get; set; } |
||||||
|
public string? GroupName { get; set; } |
||||||
|
public Guid? GrantedProjectId { get; set; } |
||||||
|
} |
||||||
|
|
||||||
|
public class GroupServiceAccountAccessPolicyResponseModel : BaseAccessPolicyResponseModel |
||||||
|
{ |
||||||
|
private const string _objectName = "groupServiceAccountAccessPolicy"; |
||||||
|
|
||||||
|
public GroupServiceAccountAccessPolicyResponseModel(GroupServiceAccountAccessPolicy accessPolicy) |
||||||
|
: base(accessPolicy, _objectName) |
||||||
|
{ |
||||||
|
GroupId = accessPolicy.GroupId; |
||||||
|
GroupName = accessPolicy.Group?.Name; |
||||||
|
GrantedServiceAccountId = accessPolicy.GrantedServiceAccountId; |
||||||
|
} |
||||||
|
|
||||||
|
public GroupServiceAccountAccessPolicyResponseModel() : base(new GroupServiceAccountAccessPolicy(), _objectName) |
||||||
|
{ |
||||||
|
} |
||||||
|
|
||||||
|
public Guid? GroupId { get; set; } |
||||||
|
public string? GroupName { get; set; } |
||||||
|
public Guid? GrantedServiceAccountId { get; set; } |
||||||
|
} |
||||||
|
|
||||||
|
public class ServiceAccountProjectAccessPolicyResponseModel : BaseAccessPolicyResponseModel |
||||||
|
{ |
||||||
|
private const string _objectName = "serviceAccountProjectAccessPolicy"; |
||||||
|
|
||||||
|
public ServiceAccountProjectAccessPolicyResponseModel(ServiceAccountProjectAccessPolicy accessPolicy) |
||||||
|
: base(accessPolicy, _objectName) |
||||||
|
{ |
||||||
|
ServiceAccountId = accessPolicy.ServiceAccountId; |
||||||
|
GrantedProjectId = accessPolicy.GrantedProjectId; |
||||||
|
ServiceAccountName = accessPolicy.ServiceAccount?.Name; |
||||||
|
} |
||||||
|
|
||||||
|
public ServiceAccountProjectAccessPolicyResponseModel() |
||||||
|
: base(new ServiceAccountProjectAccessPolicy(), _objectName) |
||||||
|
{ |
||||||
|
} |
||||||
|
|
||||||
|
public Guid? ServiceAccountId { get; set; } |
||||||
|
public string? ServiceAccountName { get; set; } |
||||||
|
public Guid? GrantedProjectId { get; set; } |
||||||
|
} |
||||||
@ -0,0 +1,43 @@ |
|||||||
|
using Bit.Core.Entities; |
||||||
|
using Bit.Core.Models.Api; |
||||||
|
|
||||||
|
namespace Bit.Api.SecretManagerFeatures.Models.Response; |
||||||
|
|
||||||
|
public class ProjectAccessPoliciesResponseModel : ResponseModel |
||||||
|
{ |
||||||
|
private const string _objectName = "projectAccessPolicies"; |
||||||
|
|
||||||
|
public ProjectAccessPoliciesResponseModel(IEnumerable<BaseAccessPolicy> baseAccessPolicies) |
||||||
|
: base(_objectName) |
||||||
|
{ |
||||||
|
if (baseAccessPolicies == null) |
||||||
|
{ |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
foreach (var baseAccessPolicy in baseAccessPolicies) |
||||||
|
switch (baseAccessPolicy) |
||||||
|
{ |
||||||
|
case UserProjectAccessPolicy accessPolicy: |
||||||
|
UserAccessPolicies.Add(new UserProjectAccessPolicyResponseModel(accessPolicy)); |
||||||
|
break; |
||||||
|
case GroupProjectAccessPolicy accessPolicy: |
||||||
|
GroupAccessPolicies.Add(new GroupProjectAccessPolicyResponseModel(accessPolicy)); |
||||||
|
break; |
||||||
|
case ServiceAccountProjectAccessPolicy accessPolicy: |
||||||
|
ServiceAccountAccessPolicies.Add( |
||||||
|
new ServiceAccountProjectAccessPolicyResponseModel(accessPolicy)); |
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public ProjectAccessPoliciesResponseModel() : base(_objectName) |
||||||
|
{ |
||||||
|
} |
||||||
|
|
||||||
|
public List<UserProjectAccessPolicyResponseModel> UserAccessPolicies { get; set; } = new(); |
||||||
|
|
||||||
|
public List<GroupProjectAccessPolicyResponseModel> GroupAccessPolicies { get; set; } = new(); |
||||||
|
|
||||||
|
public List<ServiceAccountProjectAccessPolicyResponseModel> ServiceAccountAccessPolicies { get; set; } = new(); |
||||||
|
} |
||||||
@ -1,7 +1,14 @@ |
|||||||
using Bit.Core.Entities; |
#nullable enable |
||||||
|
using Bit.Core.Entities; |
||||||
|
|
||||||
namespace Bit.Core.Repositories; |
namespace Bit.Core.Repositories; |
||||||
|
|
||||||
public interface IAccessPolicyRepository : IRepository<AccessPolicy, Guid> |
public interface IAccessPolicyRepository |
||||||
{ |
{ |
||||||
|
Task<List<BaseAccessPolicy>> CreateManyAsync(List<BaseAccessPolicy> baseAccessPolicies); |
||||||
|
Task<bool> AccessPolicyExists(BaseAccessPolicy baseAccessPolicy); |
||||||
|
Task<BaseAccessPolicy?> GetByIdAsync(Guid id); |
||||||
|
Task<IEnumerable<BaseAccessPolicy>?> GetManyByProjectId(Guid id); |
||||||
|
Task ReplaceAsync(BaseAccessPolicy baseAccessPolicy); |
||||||
|
Task DeleteAsync(Guid id); |
||||||
} |
} |
||||||
|
|||||||
@ -0,0 +1,8 @@ |
|||||||
|
using Bit.Core.Entities; |
||||||
|
|
||||||
|
namespace Bit.Core.SecretManagerFeatures.AccessPolicies.Interfaces; |
||||||
|
|
||||||
|
public interface ICreateAccessPoliciesCommand |
||||||
|
{ |
||||||
|
Task<List<BaseAccessPolicy>> CreateAsync(List<BaseAccessPolicy> accessPolicies); |
||||||
|
} |
||||||
@ -0,0 +1,6 @@ |
|||||||
|
namespace Bit.Core.SecretManagerFeatures.AccessPolicies.Interfaces; |
||||||
|
|
||||||
|
public interface IDeleteAccessPolicyCommand |
||||||
|
{ |
||||||
|
Task DeleteAsync(Guid id); |
||||||
|
} |
||||||
@ -0,0 +1,8 @@ |
|||||||
|
using Bit.Core.Entities; |
||||||
|
|
||||||
|
namespace Bit.Core.SecretManagerFeatures.AccessPolicies.Interfaces; |
||||||
|
|
||||||
|
public interface IUpdateAccessPolicyCommand |
||||||
|
{ |
||||||
|
public Task<BaseAccessPolicy> UpdateAsync(Guid id, bool read, bool write); |
||||||
|
} |
||||||
@ -1,27 +0,0 @@ |
|||||||
using AutoMapper; |
|
||||||
using Bit.Core.Repositories; |
|
||||||
using Bit.Infrastructure.EntityFramework.Models; |
|
||||||
using Microsoft.EntityFrameworkCore; |
|
||||||
using Microsoft.Extensions.DependencyInjection; |
|
||||||
using CoreAccessPolicy = Bit.Core.Entities.AccessPolicy; |
|
||||||
|
|
||||||
namespace Bit.Infrastructure.EntityFramework.Repositories; |
|
||||||
|
|
||||||
public class AccessPolicyRepository : IAccessPolicyRepository |
|
||||||
{ |
|
||||||
public AccessPolicyRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper) |
|
||||||
{ |
|
||||||
} |
|
||||||
|
|
||||||
protected Func<DatabaseContext, DbSet<AccessPolicy>> GetDbSet { get; private set; } |
|
||||||
|
|
||||||
public Task<CoreAccessPolicy> GetByIdAsync(Guid id) => throw new NotImplementedException(); |
|
||||||
|
|
||||||
public Task<CoreAccessPolicy> CreateAsync(CoreAccessPolicy obj) => throw new NotImplementedException(); |
|
||||||
|
|
||||||
public Task ReplaceAsync(CoreAccessPolicy obj) => throw new NotImplementedException(); |
|
||||||
|
|
||||||
public Task UpsertAsync(CoreAccessPolicy obj) => throw new NotImplementedException(); |
|
||||||
|
|
||||||
public Task DeleteAsync(CoreAccessPolicy obj) => throw new NotImplementedException(); |
|
||||||
} |
|
||||||
@ -0,0 +1,185 @@ |
|||||||
|
using System.Net.Http.Headers; |
||||||
|
using Bit.Api.IntegrationTest.Factories; |
||||||
|
using Bit.Api.IntegrationTest.Helpers; |
||||||
|
using Bit.Api.SecretManagerFeatures.Models.Request; |
||||||
|
using Bit.Api.SecretManagerFeatures.Models.Response; |
||||||
|
using Bit.Core.Entities; |
||||||
|
using Bit.Core.Repositories; |
||||||
|
using Bit.Test.Common.Helpers; |
||||||
|
using Xunit; |
||||||
|
|
||||||
|
namespace Bit.Api.IntegrationTest.Controllers; |
||||||
|
|
||||||
|
public class AccessPoliciesControllerTest : IClassFixture<ApiApplicationFactory>, IAsyncLifetime |
||||||
|
{ |
||||||
|
private readonly IAccessPolicyRepository _accessPolicyRepository; |
||||||
|
|
||||||
|
private readonly HttpClient _client; |
||||||
|
private readonly ApiApplicationFactory _factory; |
||||||
|
|
||||||
|
private const string _mockEncryptedString = "2.3Uk+WNBIoU5xzmVFNcoWzz==|1MsPIYuRfdOHfu/0uY6H2Q==|/98sp4wb6pHP1VTZ9JcNCYgQjEUMFPlqJgCwRk1YXKg="; |
||||||
|
|
||||||
|
private readonly IProjectRepository _projectRepository; |
||||||
|
private readonly IServiceAccountRepository _serviceAccountRepository; |
||||||
|
private Organization _organization = null!; |
||||||
|
|
||||||
|
public AccessPoliciesControllerTest(ApiApplicationFactory factory) |
||||||
|
{ |
||||||
|
_factory = factory; |
||||||
|
_client = _factory.CreateClient(); |
||||||
|
_accessPolicyRepository = _factory.GetService<IAccessPolicyRepository>(); |
||||||
|
_serviceAccountRepository = _factory.GetService<IServiceAccountRepository>(); |
||||||
|
_projectRepository = _factory.GetService<IProjectRepository>(); |
||||||
|
} |
||||||
|
|
||||||
|
public async Task InitializeAsync() |
||||||
|
{ |
||||||
|
var ownerEmail = $"integration-test{Guid.NewGuid()}@bitwarden.com"; |
||||||
|
var tokens = await _factory.LoginWithNewAccount(ownerEmail); |
||||||
|
var (organization, _) = |
||||||
|
await OrganizationTestHelpers.SignUpAsync(_factory, ownerEmail: ownerEmail, billingEmail: ownerEmail); |
||||||
|
_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tokens.Token); |
||||||
|
_organization = organization; |
||||||
|
} |
||||||
|
|
||||||
|
public Task DisposeAsync() => Task.CompletedTask; |
||||||
|
|
||||||
|
[Fact] |
||||||
|
public async Task CreateProjectAccessPolicies() |
||||||
|
{ |
||||||
|
var initialProject = await _projectRepository.CreateAsync(new Project |
||||||
|
{ |
||||||
|
OrganizationId = _organization.Id, |
||||||
|
Name = _mockEncryptedString |
||||||
|
}); |
||||||
|
|
||||||
|
var initialServiceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount |
||||||
|
{ |
||||||
|
OrganizationId = _organization.Id, |
||||||
|
Name = _mockEncryptedString |
||||||
|
}); |
||||||
|
|
||||||
|
var request = new AccessPoliciesCreateRequest |
||||||
|
{ |
||||||
|
ServiceAccountAccessPolicyRequests = new List<AccessPolicyRequest> |
||||||
|
{ |
||||||
|
new() { GranteeId = initialServiceAccount.Id, Read = true, Write = true } |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
var response = await _client.PostAsJsonAsync($"/projects/{initialProject.Id}/access-policies", request); |
||||||
|
response.EnsureSuccessStatusCode(); |
||||||
|
|
||||||
|
var result = await response.Content.ReadFromJsonAsync<ProjectAccessPoliciesResponseModel>(); |
||||||
|
|
||||||
|
Assert.NotNull(result); |
||||||
|
Assert.Equal(initialServiceAccount.Id, result!.ServiceAccountAccessPolicies.First().ServiceAccountId); |
||||||
|
Assert.True(result.ServiceAccountAccessPolicies.First().Read); |
||||||
|
Assert.True(result.ServiceAccountAccessPolicies.First().Write); |
||||||
|
AssertHelper.AssertRecent(result.ServiceAccountAccessPolicies.First().RevisionDate); |
||||||
|
AssertHelper.AssertRecent(result.ServiceAccountAccessPolicies.First().CreationDate); |
||||||
|
|
||||||
|
var createdAccessPolicy = |
||||||
|
await _accessPolicyRepository.GetByIdAsync(result.ServiceAccountAccessPolicies.First().Id); |
||||||
|
Assert.NotNull(createdAccessPolicy); |
||||||
|
Assert.Equal(result.ServiceAccountAccessPolicies.First().Read, createdAccessPolicy!.Read); |
||||||
|
Assert.Equal(result.ServiceAccountAccessPolicies.First().Write, createdAccessPolicy.Write); |
||||||
|
Assert.Equal(result.ServiceAccountAccessPolicies.First().Id, createdAccessPolicy.Id); |
||||||
|
AssertHelper.AssertRecent(createdAccessPolicy.CreationDate); |
||||||
|
AssertHelper.AssertRecent(createdAccessPolicy.RevisionDate); |
||||||
|
} |
||||||
|
|
||||||
|
[Fact] |
||||||
|
public async Task UpdateAccessPolicy() |
||||||
|
{ |
||||||
|
var initData = await SetupAccessPolicyRequest(); |
||||||
|
|
||||||
|
const bool expectedRead = true; |
||||||
|
const bool expectedWrite = false; |
||||||
|
var request = new AccessPolicyUpdateRequest { Read = expectedRead, Write = expectedWrite }; |
||||||
|
|
||||||
|
var response = await _client.PutAsJsonAsync($"/access-policies/{initData.InitialAccessPolicyId}", request); |
||||||
|
response.EnsureSuccessStatusCode(); |
||||||
|
|
||||||
|
var result = await response.Content.ReadFromJsonAsync<ServiceAccountProjectAccessPolicyResponseModel>(); |
||||||
|
|
||||||
|
Assert.NotNull(result); |
||||||
|
Assert.Equal(expectedRead, result!.Read); |
||||||
|
Assert.Equal(expectedWrite, result.Write); |
||||||
|
AssertHelper.AssertRecent(result.RevisionDate); |
||||||
|
|
||||||
|
var updatedAccessPolicy = await _accessPolicyRepository.GetByIdAsync(result.Id); |
||||||
|
Assert.NotNull(updatedAccessPolicy); |
||||||
|
Assert.Equal(expectedRead, updatedAccessPolicy!.Read); |
||||||
|
Assert.Equal(expectedWrite, updatedAccessPolicy.Write); |
||||||
|
AssertHelper.AssertRecent(updatedAccessPolicy.RevisionDate); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
[Fact] |
||||||
|
public async Task DeleteAccessPolicy() |
||||||
|
{ |
||||||
|
var initData = await SetupAccessPolicyRequest(); |
||||||
|
|
||||||
|
var response = await _client.DeleteAsync($"/access-policies/{initData.InitialAccessPolicyId}"); |
||||||
|
response.EnsureSuccessStatusCode(); |
||||||
|
|
||||||
|
var test = await _accessPolicyRepository.GetByIdAsync(initData.InitialAccessPolicyId); |
||||||
|
Assert.Null(test); |
||||||
|
} |
||||||
|
|
||||||
|
[Fact] |
||||||
|
public async Task GetProjectAccessPolicies() |
||||||
|
{ |
||||||
|
var initData = await SetupAccessPolicyRequest(); |
||||||
|
|
||||||
|
var response = await _client.GetAsync($"/projects/{initData.InitialProjectId}/access-policies"); |
||||||
|
response.EnsureSuccessStatusCode(); |
||||||
|
|
||||||
|
var result = await response.Content.ReadFromJsonAsync<ProjectAccessPoliciesResponseModel>(); |
||||||
|
|
||||||
|
Assert.NotNull(result?.ServiceAccountAccessPolicies); |
||||||
|
Assert.Single(result!.ServiceAccountAccessPolicies); |
||||||
|
} |
||||||
|
|
||||||
|
private async Task<RequestSetupData> SetupAccessPolicyRequest() |
||||||
|
{ |
||||||
|
var initialProject = await _projectRepository.CreateAsync(new Project |
||||||
|
{ |
||||||
|
OrganizationId = _organization.Id, |
||||||
|
Name = _mockEncryptedString, |
||||||
|
}); |
||||||
|
|
||||||
|
var initialServiceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount |
||||||
|
{ |
||||||
|
OrganizationId = _organization.Id, |
||||||
|
Name = _mockEncryptedString, |
||||||
|
}); |
||||||
|
|
||||||
|
var initialAccessPolicy = await _accessPolicyRepository.CreateManyAsync( |
||||||
|
new List<BaseAccessPolicy> |
||||||
|
{ |
||||||
|
new ServiceAccountProjectAccessPolicy |
||||||
|
{ |
||||||
|
Read = true, |
||||||
|
Write = true, |
||||||
|
ServiceAccountId = initialServiceAccount.Id, |
||||||
|
GrantedProjectId = initialProject.Id, |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
return new RequestSetupData |
||||||
|
{ |
||||||
|
InitialProjectId = initialProject.Id, |
||||||
|
InitialServiceAccountId = initialServiceAccount.Id, |
||||||
|
InitialAccessPolicyId = initialAccessPolicy.First().Id, |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
private class RequestSetupData |
||||||
|
{ |
||||||
|
public Guid InitialProjectId { get; set; } |
||||||
|
public Guid InitialAccessPolicyId { get; set; } |
||||||
|
public Guid InitialServiceAccountId { get; set; } |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,87 @@ |
|||||||
|
using Bit.Api.Controllers; |
||||||
|
using Bit.Api.SecretManagerFeatures.Models.Request; |
||||||
|
using Bit.Core.Entities; |
||||||
|
using Bit.Core.Repositories; |
||||||
|
using Bit.Core.SecretManagerFeatures.AccessPolicies.Interfaces; |
||||||
|
using Bit.Test.Common.AutoFixture; |
||||||
|
using Bit.Test.Common.AutoFixture.Attributes; |
||||||
|
using Bit.Test.Common.Helpers; |
||||||
|
using NSubstitute; |
||||||
|
using NSubstitute.ReturnsExtensions; |
||||||
|
using Xunit; |
||||||
|
|
||||||
|
namespace Bit.Api.Test.Controllers; |
||||||
|
|
||||||
|
[ControllerCustomize(typeof(AccessPoliciesController))] |
||||||
|
[SutProviderCustomize] |
||||||
|
[JsonDocumentCustomize] |
||||||
|
public class AccessPoliciesControllerTests |
||||||
|
{ |
||||||
|
[Theory] |
||||||
|
[BitAutoData] |
||||||
|
public async void GetAccessPoliciesByProject_ReturnsEmptyList(SutProvider<AccessPoliciesController> sutProvider, |
||||||
|
Guid id) |
||||||
|
{ |
||||||
|
var result = await sutProvider.Sut.GetProjectAccessPoliciesAsync(id); |
||||||
|
|
||||||
|
await sutProvider.GetDependency<IAccessPolicyRepository>().Received(1) |
||||||
|
.GetManyByProjectId(Arg.Is(AssertHelper.AssertPropertyEqual(id))); |
||||||
|
|
||||||
|
Assert.Empty(result.GroupAccessPolicies); |
||||||
|
Assert.Empty(result.UserAccessPolicies); |
||||||
|
Assert.Empty(result.ServiceAccountAccessPolicies); |
||||||
|
} |
||||||
|
|
||||||
|
[Theory] |
||||||
|
[BitAutoData] |
||||||
|
public async void GetAccessPoliciesByProject_Success(SutProvider<AccessPoliciesController> sutProvider, Guid id, |
||||||
|
UserProjectAccessPolicy resultAccessPolicy) |
||||||
|
{ |
||||||
|
sutProvider.GetDependency<IAccessPolicyRepository>().GetManyByProjectId(default) |
||||||
|
.ReturnsForAnyArgs(new List<BaseAccessPolicy> { resultAccessPolicy }); |
||||||
|
|
||||||
|
var result = await sutProvider.Sut.GetProjectAccessPoliciesAsync(id); |
||||||
|
|
||||||
|
await sutProvider.GetDependency<IAccessPolicyRepository>().Received(1) |
||||||
|
.GetManyByProjectId(Arg.Is(AssertHelper.AssertPropertyEqual(id))); |
||||||
|
|
||||||
|
Assert.Empty(result.GroupAccessPolicies); |
||||||
|
Assert.NotEmpty(result.UserAccessPolicies); |
||||||
|
Assert.Empty(result.ServiceAccountAccessPolicies); |
||||||
|
} |
||||||
|
|
||||||
|
[Theory] |
||||||
|
[BitAutoData] |
||||||
|
public async void CreateAccessPolicies_Success(SutProvider<AccessPoliciesController> sutProvider, Guid id, |
||||||
|
UserProjectAccessPolicy data, AccessPoliciesCreateRequest request) |
||||||
|
{ |
||||||
|
sutProvider.GetDependency<ICreateAccessPoliciesCommand>().CreateAsync(default) |
||||||
|
.ReturnsForAnyArgs(new List<BaseAccessPolicy> { data }); |
||||||
|
var result = await sutProvider.Sut.CreateProjectAccessPoliciesAsync(id, request); |
||||||
|
await sutProvider.GetDependency<ICreateAccessPoliciesCommand>().Received(1) |
||||||
|
.CreateAsync(Arg.Any<List<BaseAccessPolicy>>()); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
[Theory] |
||||||
|
[BitAutoData] |
||||||
|
public async void UpdateAccessPolicies_Success(SutProvider<AccessPoliciesController> sutProvider, Guid id, |
||||||
|
UserProjectAccessPolicy data, AccessPolicyUpdateRequest request) |
||||||
|
{ |
||||||
|
sutProvider.GetDependency<IUpdateAccessPolicyCommand>().UpdateAsync(default, default, default) |
||||||
|
.ReturnsForAnyArgs(data); |
||||||
|
var result = await sutProvider.Sut.UpdateAccessPolicyAsync(id, request); |
||||||
|
await sutProvider.GetDependency<IUpdateAccessPolicyCommand>().Received(1) |
||||||
|
.UpdateAsync(Arg.Any<Guid>(), Arg.Is(request.Read), Arg.Is(request.Write)); |
||||||
|
} |
||||||
|
|
||||||
|
[Theory] |
||||||
|
[BitAutoData] |
||||||
|
public async void DeleteAccessPolicies_Success(SutProvider<AccessPoliciesController> sutProvider, Guid id) |
||||||
|
{ |
||||||
|
sutProvider.GetDependency<IDeleteAccessPolicyCommand>().DeleteAsync(default).ReturnsNull(); |
||||||
|
await sutProvider.Sut.DeleteAccessPolicyAsync(id); |
||||||
|
await sutProvider.GetDependency<IDeleteAccessPolicyCommand>().Received(1) |
||||||
|
.DeleteAsync(Arg.Any<Guid>()); |
||||||
|
} |
||||||
|
} |
||||||
Loading…
Reference in new issue