diff --git a/bitwarden_license/src/Commercial.Core/SecretsManager/AuthorizationHandlers/Secrets/SecretAuthorizationHandler.cs b/bitwarden_license/src/Commercial.Core/SecretsManager/AuthorizationHandlers/Secrets/SecretAuthorizationHandler.cs index 92461e61a9..9fd94c89b6 100644 --- a/bitwarden_license/src/Commercial.Core/SecretsManager/AuthorizationHandlers/Secrets/SecretAuthorizationHandler.cs +++ b/bitwarden_license/src/Commercial.Core/SecretsManager/AuthorizationHandlers/Secrets/SecretAuthorizationHandler.cs @@ -47,6 +47,9 @@ public class SecretAuthorizationHandler : AuthorizationHandler GetAccessToUpdateSecretAsync(Secret resource, Guid userId, AccessClientType accessClient) { var newProject = resource.Projects?.FirstOrDefault(); diff --git a/bitwarden_license/src/Commercial.Infrastructure.EntityFramework/SecretsManager/Repositories/AccessPolicyRepository.cs b/bitwarden_license/src/Commercial.Infrastructure.EntityFramework/SecretsManager/Repositories/AccessPolicyRepository.cs index 8ac904bced..93e4cf7c97 100644 --- a/bitwarden_license/src/Commercial.Infrastructure.EntityFramework/SecretsManager/Repositories/AccessPolicyRepository.cs +++ b/bitwarden_license/src/Commercial.Infrastructure.EntityFramework/SecretsManager/Repositories/AccessPolicyRepository.cs @@ -20,7 +20,8 @@ public class AccessPolicyRepository : BaseEntityFrameworkRepository, IAccessPoli { } - public async Task> CreateManyAsync(List baseAccessPolicies) + public async Task> CreateManyAsync( + List baseAccessPolicies) { await using var scope = ServiceScopeFactory.CreateAsyncScope(); var dbContext = GetDatabaseContext(scope); @@ -39,6 +40,13 @@ public class AccessPolicyRepository : BaseEntityFrameworkRepository, IAccessPoli await dbContext.AddAsync(entity); break; } + case Core.SecretsManager.Entities.UserSecretAccessPolicy accessPolicy: + { + var entity = + Mapper.Map(accessPolicy); + await dbContext.AddAsync(entity); + break; + } case Core.SecretsManager.Entities.UserServiceAccountAccessPolicy accessPolicy: { var entity = @@ -52,6 +60,12 @@ public class AccessPolicyRepository : BaseEntityFrameworkRepository, IAccessPoli await dbContext.AddAsync(entity); break; } + case Core.SecretsManager.Entities.GroupSecretAccessPolicy accessPolicy: + { + var entity = Mapper.Map(accessPolicy); + await dbContext.AddAsync(entity); + break; + } case Core.SecretsManager.Entities.GroupServiceAccountAccessPolicy accessPolicy: { var entity = Mapper.Map(accessPolicy); @@ -65,6 +79,13 @@ public class AccessPolicyRepository : BaseEntityFrameworkRepository, IAccessPoli serviceAccountIds.Add(entity.ServiceAccountId!.Value); break; } + case Core.SecretsManager.Entities.ServiceAccountSecretAccessPolicy accessPolicy: + { + var entity = Mapper.Map(accessPolicy); + await dbContext.AddAsync(entity); + serviceAccountIds.Add(entity.ServiceAccountId!.Value); + break; + } } } @@ -395,6 +416,42 @@ public class AccessPolicyRepository : BaseEntityFrameworkRepository, IAccessPoli await transaction.CommitAsync(); } + public async Task GetSecretAccessPoliciesAsync( + Guid secretId, + Guid userId) + { + await using var scope = ServiceScopeFactory.CreateAsyncScope(); + var dbContext = GetDatabaseContext(scope); + + var entities = await dbContext.AccessPolicies.Where(ap => + ((UserSecretAccessPolicy)ap).GrantedSecretId == secretId || + ((GroupSecretAccessPolicy)ap).GrantedSecretId == secretId || + ((ServiceAccountSecretAccessPolicy)ap).GrantedSecretId == secretId) + .Include(ap => ((UserSecretAccessPolicy)ap).OrganizationUser.User) + .Include(ap => ((GroupSecretAccessPolicy)ap).Group) + .Include(ap => ((ServiceAccountSecretAccessPolicy)ap).ServiceAccount) + .Select(ap => new + { + ap, + CurrentUserInGroup = ap is GroupSecretAccessPolicy && + ((GroupSecretAccessPolicy)ap).Group.GroupUsers.Any(g => + g.OrganizationUser.UserId == userId) + }) + .ToListAsync(); + + if (entities.Count == 0) + { + return null; + } + + var organizationId = await dbContext.Secret.Where(s => s.Id == secretId) + .Select(s => s.OrganizationId) + .SingleAsync(); + + return new SecretAccessPolicies(secretId, organizationId, + entities.Select(e => MapToCore(e.ap, e.CurrentUserInGroup)).ToList()); + } + private static async Task UpsertPeoplePoliciesAsync(DatabaseContext dbContext, List policies, IReadOnlyCollection userPolicyEntities, IReadOnlyCollection groupPolicyEntities) @@ -466,13 +523,17 @@ public class AccessPolicyRepository : BaseEntityFrameworkRepository, IAccessPoli baseAccessPolicyEntity switch { UserProjectAccessPolicy ap => Mapper.Map(ap), - GroupProjectAccessPolicy ap => Mapper.Map(ap), - ServiceAccountProjectAccessPolicy ap => Mapper - .Map(ap), + UserSecretAccessPolicy ap => Mapper.Map(ap), UserServiceAccountAccessPolicy ap => Mapper.Map(ap), + GroupProjectAccessPolicy ap => Mapper.Map(ap), + GroupSecretAccessPolicy ap => Mapper.Map(ap), GroupServiceAccountAccessPolicy ap => Mapper .Map(ap), + ServiceAccountProjectAccessPolicy ap => Mapper + .Map(ap), + ServiceAccountSecretAccessPolicy ap => Mapper + .Map(ap), _ => throw new ArgumentException("Unsupported access policy type") }; @@ -482,20 +543,26 @@ public class AccessPolicyRepository : BaseEntityFrameworkRepository, IAccessPoli { Core.SecretsManager.Entities.UserProjectAccessPolicy accessPolicy => Mapper.Map( accessPolicy), + Core.SecretsManager.Entities.UserSecretAccessPolicy accessPolicy => Mapper.Map( + accessPolicy), Core.SecretsManager.Entities.UserServiceAccountAccessPolicy accessPolicy => Mapper .Map(accessPolicy), Core.SecretsManager.Entities.GroupProjectAccessPolicy accessPolicy => Mapper.Map( accessPolicy), + Core.SecretsManager.Entities.GroupSecretAccessPolicy accessPolicy => Mapper.Map( + accessPolicy), Core.SecretsManager.Entities.GroupServiceAccountAccessPolicy accessPolicy => Mapper .Map(accessPolicy), Core.SecretsManager.Entities.ServiceAccountProjectAccessPolicy accessPolicy => Mapper .Map(accessPolicy), + Core.SecretsManager.Entities.ServiceAccountSecretAccessPolicy accessPolicy => Mapper + .Map(accessPolicy), _ => throw new ArgumentException("Unsupported access policy type") }; } private Core.SecretsManager.Entities.BaseAccessPolicy MapToCore( - BaseAccessPolicy baseAccessPolicyEntity, bool currentUserInGroup) + BaseAccessPolicy baseAccessPolicyEntity, bool currentUserInGroup) { switch (baseAccessPolicyEntity) { @@ -505,6 +572,12 @@ public class AccessPolicyRepository : BaseEntityFrameworkRepository, IAccessPoli mapped.CurrentUserInGroup = currentUserInGroup; return mapped; } + case GroupSecretAccessPolicy ap: + { + var mapped = Mapper.Map(ap); + mapped.CurrentUserInGroup = currentUserInGroup; + return mapped; + } case GroupServiceAccountAccessPolicy ap: { var mapped = Mapper.Map(ap); diff --git a/bitwarden_license/test/Commercial.Core.Test/SecretsManager/AuthorizationHandlers/Secrets/SecretAuthorizationHandlerTests.cs b/bitwarden_license/test/Commercial.Core.Test/SecretsManager/AuthorizationHandlers/Secrets/SecretAuthorizationHandlerTests.cs index f1737e0ad4..97d6721323 100644 --- a/bitwarden_license/test/Commercial.Core.Test/SecretsManager/AuthorizationHandlers/Secrets/SecretAuthorizationHandlerTests.cs +++ b/bitwarden_license/test/Commercial.Core.Test/SecretsManager/AuthorizationHandlers/Secrets/SecretAuthorizationHandlerTests.cs @@ -517,4 +517,85 @@ public class SecretAuthorizationHandlerTests Assert.Equal(expected, authzContext.HasSucceeded); } + + [Theory] + [BitAutoData] + public async Task CanReadAccessPolicies_AccessToSecretsManagerFalse_DoesNotSucceed( + SutProvider sutProvider, Secret secret, + ClaimsPrincipal claimsPrincipal) + { + var requirement = SecretOperations.ReadAccessPolicies; + sutProvider.GetDependency().AccessSecretsManager(secret.OrganizationId) + .Returns(false); + var authzContext = new AuthorizationHandlerContext(new List { requirement }, + claimsPrincipal, secret); + + await sutProvider.Sut.HandleAsync(authzContext); + + Assert.False(authzContext.HasSucceeded); + } + + [Theory] + [BitAutoData] + public async Task CanReadAccessPolicies_NullResource_DoesNotSucceed( + SutProvider sutProvider, Secret secret, + ClaimsPrincipal claimsPrincipal, + Guid userId) + { + var requirement = SecretOperations.ReadAccessPolicies; + SetupPermission(sutProvider, PermissionType.RunAsAdmin, secret.OrganizationId, userId); + var authzContext = new AuthorizationHandlerContext(new List { requirement }, + claimsPrincipal, null); + + await sutProvider.Sut.HandleAsync(authzContext); + + Assert.False(authzContext.HasSucceeded); + } + + [Theory] + [BitAutoData(AccessClientType.ServiceAccount)] + [BitAutoData(AccessClientType.Organization)] + public async Task CanReadAccessPolicies_UnsupportedClient_DoesNotSucceed( + AccessClientType clientType, + SutProvider sutProvider, Secret secret, + ClaimsPrincipal claimsPrincipal) + { + var requirement = SecretOperations.ReadAccessPolicies; + sutProvider.GetDependency().AccessSecretsManager(secret.OrganizationId) + .Returns(true); + sutProvider.GetDependency() + .GetAccessClientAsync(Arg.Any(), secret.OrganizationId) + .Returns((clientType, Guid.NewGuid())); + var authzContext = new AuthorizationHandlerContext(new List { requirement }, + claimsPrincipal, secret); + + await sutProvider.Sut.HandleAsync(authzContext); + + Assert.False(authzContext.HasSucceeded); + } + + [Theory] + [BitAutoData(PermissionType.RunAsAdmin, true, true, true)] + [BitAutoData(PermissionType.RunAsUserWithPermission, false, false, false)] + [BitAutoData(PermissionType.RunAsUserWithPermission, false, true, true)] + [BitAutoData(PermissionType.RunAsUserWithPermission, true, false, false)] + [BitAutoData(PermissionType.RunAsUserWithPermission, true, true, true)] + public async Task CanReadAccessPolicies_AccessCheck(PermissionType permissionType, bool read, bool write, + bool expected, + SutProvider sutProvider, Secret secret, + ClaimsPrincipal claimsPrincipal, + Guid userId) + { + var requirement = SecretOperations.ReadAccessPolicies; + SetupPermission(sutProvider, permissionType, secret.OrganizationId, userId); + sutProvider.GetDependency() + .AccessToSecretAsync(secret.Id, userId, Arg.Any()) + .Returns((read, write)); + var authzContext = new AuthorizationHandlerContext(new List { requirement }, + claimsPrincipal, secret); + + await sutProvider.Sut.HandleAsync(authzContext); + + Assert.Equal(expected, authzContext.HasSucceeded); + } } diff --git a/src/Api/SecretsManager/Controllers/AccessPoliciesController.cs b/src/Api/SecretsManager/Controllers/AccessPoliciesController.cs index 5a7df04053..cd65a7cdf8 100644 --- a/src/Api/SecretsManager/Controllers/AccessPoliciesController.cs +++ b/src/Api/SecretsManager/Controllers/AccessPoliciesController.cs @@ -24,6 +24,7 @@ public class AccessPoliciesController : Controller private readonly IAuthorizationService _authorizationService; private readonly ICurrentContext _currentContext; private readonly IProjectRepository _projectRepository; + private readonly ISecretRepository _secretRepository; private readonly IServiceAccountGrantedPolicyUpdatesQuery _serviceAccountGrantedPolicyUpdatesQuery; private readonly IServiceAccountRepository _serviceAccountRepository; private readonly IUpdateServiceAccountGrantedPoliciesCommand _updateServiceAccountGrantedPoliciesCommand; @@ -41,6 +42,7 @@ public class AccessPoliciesController : Controller IAccessPolicyRepository accessPolicyRepository, IServiceAccountRepository serviceAccountRepository, IProjectRepository projectRepository, + ISecretRepository secretRepository, IAccessClientQuery accessClientQuery, IServiceAccountGrantedPolicyUpdatesQuery serviceAccountGrantedPolicyUpdatesQuery, IProjectServiceAccountsAccessPoliciesUpdatesQuery projectServiceAccountsAccessPoliciesUpdatesQuery, @@ -52,6 +54,7 @@ public class AccessPoliciesController : Controller _currentContext = currentContext; _serviceAccountRepository = serviceAccountRepository; _projectRepository = projectRepository; + _secretRepository = secretRepository; _accessPolicyRepository = accessPolicyRepository; _updateServiceAccountGrantedPoliciesCommand = updateServiceAccountGrantedPoliciesCommand; _accessClientQuery = accessClientQuery; @@ -259,6 +262,22 @@ public class AccessPoliciesController : Controller return new ProjectServiceAccountsAccessPoliciesResponseModel(results); } + [HttpGet("/secrets/{secretId}/access-policies")] + public async Task GetSecretAccessPoliciesAsync(Guid secretId) + { + var secret = await _secretRepository.GetByIdAsync(secretId); + var authorizationResult = await _authorizationService.AuthorizeAsync(User, secret, SecretOperations.ReadAccessPolicies); + + if (!authorizationResult.Succeeded) + { + throw new NotFoundException(); + } + + var userId = _userService.GetProperUserId(User)!.Value; + var accessPolicies = await _accessPolicyRepository.GetSecretAccessPoliciesAsync(secretId, userId); + return new SecretAccessPoliciesResponseModel(accessPolicies, userId); + } + private async Task<(AccessClientType AccessClientType, Guid UserId)> CheckUserHasWriteAccessToProjectAsync( Project project) { diff --git a/src/Api/SecretsManager/Models/Response/AccessPolicyResponseModel.cs b/src/Api/SecretsManager/Models/Response/AccessPolicyResponseModel.cs index 819b60e1f7..5a795fdd43 100644 --- a/src/Api/SecretsManager/Models/Response/AccessPolicyResponseModel.cs +++ b/src/Api/SecretsManager/Models/Response/AccessPolicyResponseModel.cs @@ -9,161 +9,133 @@ 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 string? GetUserDisplayName(User? user) + protected static string? GetUserDisplayName(User? user) { return string.IsNullOrWhiteSpace(user?.Name) ? user?.Email : user?.Name; } } -public class UserProjectAccessPolicyResponseModel : BaseAccessPolicyResponseModel +public class UserAccessPolicyResponseModel : BaseAccessPolicyResponseModel { - private const string _objectName = "userProjectAccessPolicy"; + private const string _objectName = "userAccessPolicy"; - public UserProjectAccessPolicyResponseModel(UserProjectAccessPolicy accessPolicy) : base(accessPolicy, _objectName) - { - SetProperties(accessPolicy); - } - - public UserProjectAccessPolicyResponseModel(UserProjectAccessPolicy accessPolicy, Guid currentUserId) : base(accessPolicy, _objectName) + public UserAccessPolicyResponseModel(UserProjectAccessPolicy accessPolicy, Guid currentUserId) : base(accessPolicy, _objectName) { CurrentUser = currentUserId == accessPolicy.User?.Id; - SetProperties(accessPolicy); + OrganizationUserId = accessPolicy.OrganizationUserId; + OrganizationUserName = GetUserDisplayName(accessPolicy.User); } - public UserProjectAccessPolicyResponseModel() : base(new UserProjectAccessPolicy(), _objectName) + public UserAccessPolicyResponseModel(UserServiceAccountAccessPolicy accessPolicy, Guid currentUserId) : base(accessPolicy, _objectName) + { + CurrentUser = currentUserId == accessPolicy.User?.Id; + OrganizationUserId = accessPolicy.OrganizationUserId; + OrganizationUserName = GetUserDisplayName(accessPolicy.User); + } + + public UserAccessPolicyResponseModel(UserSecretAccessPolicy accessPolicy, Guid currentUserId) : base(accessPolicy, _objectName) + { + CurrentUser = currentUserId == accessPolicy.User?.Id; + OrganizationUserId = accessPolicy.OrganizationUserId; + OrganizationUserName = GetUserDisplayName(accessPolicy.User); + } + + public UserAccessPolicyResponseModel() : base(new UserProjectAccessPolicy(), _objectName) { } public Guid? OrganizationUserId { get; set; } public string? OrganizationUserName { get; set; } - public Guid? UserId { get; set; } - public Guid? GrantedProjectId { get; set; } public bool? CurrentUser { get; set; } - - private void SetProperties(UserProjectAccessPolicy accessPolicy) - { - OrganizationUserId = accessPolicy.OrganizationUserId; - GrantedProjectId = accessPolicy.GrantedProjectId; - OrganizationUserName = GetUserDisplayName(accessPolicy.User); - UserId = accessPolicy.User?.Id; - } } -public class UserServiceAccountAccessPolicyResponseModel : BaseAccessPolicyResponseModel +public class GroupAccessPolicyResponseModel : BaseAccessPolicyResponseModel { - private const string _objectName = "userServiceAccountAccessPolicy"; + private const string _objectName = "groupAccessPolicy"; - public UserServiceAccountAccessPolicyResponseModel(UserServiceAccountAccessPolicy accessPolicy) - : base(accessPolicy, _objectName) - { - SetProperties(accessPolicy); - } - - public UserServiceAccountAccessPolicyResponseModel(UserServiceAccountAccessPolicy accessPolicy, Guid userId) - : base(accessPolicy, _objectName) - { - SetProperties(accessPolicy); - CurrentUser = accessPolicy.User?.Id == userId; - } - - public UserServiceAccountAccessPolicyResponseModel() : base(new UserServiceAccountAccessPolicy(), _objectName) - { - } - - public Guid? OrganizationUserId { get; set; } - public string? OrganizationUserName { get; set; } - public Guid? UserId { get; set; } - public Guid? GrantedServiceAccountId { get; set; } - public bool CurrentUser { get; set; } - - private void SetProperties(UserServiceAccountAccessPolicy accessPolicy) - { - OrganizationUserId = accessPolicy.OrganizationUserId; - GrantedServiceAccountId = accessPolicy.GrantedServiceAccountId; - OrganizationUserName = GetUserDisplayName(accessPolicy.User); - UserId = accessPolicy.User?.Id; - } -} - -public class GroupProjectAccessPolicyResponseModel : BaseAccessPolicyResponseModel -{ - private const string _objectName = "groupProjectAccessPolicy"; - - public GroupProjectAccessPolicyResponseModel(GroupProjectAccessPolicy accessPolicy) + public GroupAccessPolicyResponseModel(GroupProjectAccessPolicy accessPolicy) : base(accessPolicy, _objectName) { GroupId = accessPolicy.GroupId; - GrantedProjectId = accessPolicy.GrantedProjectId; GroupName = accessPolicy.Group?.Name; CurrentUserInGroup = accessPolicy.CurrentUserInGroup; } - public GroupProjectAccessPolicyResponseModel() : base(new GroupProjectAccessPolicy(), _objectName) + public GroupAccessPolicyResponseModel(GroupServiceAccountAccessPolicy accessPolicy) + : base(accessPolicy, _objectName) + { + GroupId = accessPolicy.GroupId; + GroupName = accessPolicy.Group?.Name; + CurrentUserInGroup = accessPolicy.CurrentUserInGroup; + } + + public GroupAccessPolicyResponseModel(GroupSecretAccessPolicy accessPolicy) + : base(accessPolicy, _objectName) + { + GroupId = accessPolicy.GroupId; + GroupName = accessPolicy.Group?.Name; + CurrentUserInGroup = accessPolicy.CurrentUserInGroup; + } + + public GroupAccessPolicyResponseModel() : base(new GroupProjectAccessPolicy(), _objectName) { } public Guid? GroupId { get; set; } public string? GroupName { get; set; } public bool? CurrentUserInGroup { 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; - CurrentUserInGroup = accessPolicy.CurrentUserInGroup; - } - - public GroupServiceAccountAccessPolicyResponseModel() : base(new GroupServiceAccountAccessPolicy(), _objectName) - { - } - - public Guid? GroupId { get; set; } - public string? GroupName { get; set; } - public Guid? GrantedServiceAccountId { get; set; } - public bool? CurrentUserInGroup { get; set; } -} - -public class ServiceAccountProjectAccessPolicyResponseModel : BaseAccessPolicyResponseModel +public class ServiceAccountAccessPolicyResponseModel : BaseAccessPolicyResponseModel { private const string _objectName = "serviceAccountProjectAccessPolicy"; - public ServiceAccountProjectAccessPolicyResponseModel(ServiceAccountProjectAccessPolicy accessPolicy) + public ServiceAccountAccessPolicyResponseModel(ServiceAccountProjectAccessPolicy accessPolicy) : base(accessPolicy, _objectName) { ServiceAccountId = accessPolicy.ServiceAccountId; - GrantedProjectId = accessPolicy.GrantedProjectId; ServiceAccountName = accessPolicy.ServiceAccount?.Name; - GrantedProjectName = accessPolicy.GrantedProject?.Name; } - public ServiceAccountProjectAccessPolicyResponseModel() + public ServiceAccountAccessPolicyResponseModel(ServiceAccountSecretAccessPolicy accessPolicy) + : base(accessPolicy, _objectName) + { + ServiceAccountId = accessPolicy.ServiceAccountId; + ServiceAccountName = accessPolicy.ServiceAccount?.Name; + } + + public ServiceAccountAccessPolicyResponseModel() : base(new ServiceAccountProjectAccessPolicy(), _objectName) { } public Guid? ServiceAccountId { get; set; } public string? ServiceAccountName { get; set; } +} + +public class GrantedProjectAccessPolicyResponseModel : BaseAccessPolicyResponseModel +{ + private const string _objectName = "grantedProjectAccessPolicy"; + + public GrantedProjectAccessPolicyResponseModel(ServiceAccountProjectAccessPolicy accessPolicy) + : base(accessPolicy, _objectName) + { + GrantedProjectId = accessPolicy.GrantedProjectId; + GrantedProjectName = accessPolicy.GrantedProject?.Name; + } + + public GrantedProjectAccessPolicyResponseModel() + : base(new ServiceAccountProjectAccessPolicy(), _objectName) + { + } + public Guid? GrantedProjectId { get; set; } public string? GrantedProjectName { get; set; } } diff --git a/src/Api/SecretsManager/Models/Response/GrantedProjectAccessPolicyPermissionDetailsResponseModel.cs b/src/Api/SecretsManager/Models/Response/GrantedProjectAccessPolicyPermissionDetailsResponseModel.cs new file mode 100644 index 0000000000..64963d2c49 --- /dev/null +++ b/src/Api/SecretsManager/Models/Response/GrantedProjectAccessPolicyPermissionDetailsResponseModel.cs @@ -0,0 +1,25 @@ +#nullable enable +using Bit.Core.Models.Api; +using Bit.Core.SecretsManager.Models.Data; + +namespace Bit.Api.SecretsManager.Models.Response; + +public class GrantedProjectAccessPolicyPermissionDetailsResponseModel : ResponseModel +{ + private const string _objectName = "grantedProjectAccessPolicyPermissionDetails"; + + public GrantedProjectAccessPolicyPermissionDetailsResponseModel( + ServiceAccountProjectAccessPolicyPermissionDetails apPermissionDetails, string obj = _objectName) : base(obj) + { + AccessPolicy = new GrantedProjectAccessPolicyResponseModel(apPermissionDetails.AccessPolicy); + HasPermission = apPermissionDetails.HasPermission; + } + + public GrantedProjectAccessPolicyPermissionDetailsResponseModel() + : base(_objectName) + { + } + + public GrantedProjectAccessPolicyResponseModel AccessPolicy { get; set; } = new(); + public bool HasPermission { get; set; } +} diff --git a/src/Api/SecretsManager/Models/Response/ProjectPeopleAccessPoliciesResponseModel.cs b/src/Api/SecretsManager/Models/Response/ProjectPeopleAccessPoliciesResponseModel.cs index b1d949d5dc..b73b2596df 100644 --- a/src/Api/SecretsManager/Models/Response/ProjectPeopleAccessPoliciesResponseModel.cs +++ b/src/Api/SecretsManager/Models/Response/ProjectPeopleAccessPoliciesResponseModel.cs @@ -15,10 +15,10 @@ public class ProjectPeopleAccessPoliciesResponseModel : ResponseModel switch (baseAccessPolicy) { case UserProjectAccessPolicy accessPolicy: - UserAccessPolicies.Add(new UserProjectAccessPolicyResponseModel(accessPolicy, userId)); + UserAccessPolicies.Add(new UserAccessPolicyResponseModel(accessPolicy, userId)); break; case GroupProjectAccessPolicy accessPolicy: - GroupAccessPolicies.Add(new GroupProjectAccessPolicyResponseModel(accessPolicy)); + GroupAccessPolicies.Add(new GroupAccessPolicyResponseModel(accessPolicy)); break; } } @@ -28,7 +28,7 @@ public class ProjectPeopleAccessPoliciesResponseModel : ResponseModel { } - public List UserAccessPolicies { get; set; } = new(); + public List UserAccessPolicies { get; set; } = new(); - public List GroupAccessPolicies { get; set; } = new(); + public List GroupAccessPolicies { get; set; } = new(); } diff --git a/src/Api/SecretsManager/Models/Response/ProjectServiceAccountsAccessPoliciesResponseModel.cs b/src/Api/SecretsManager/Models/Response/ProjectServiceAccountsAccessPoliciesResponseModel.cs index 4242eedf10..5feda59f34 100644 --- a/src/Api/SecretsManager/Models/Response/ProjectServiceAccountsAccessPoliciesResponseModel.cs +++ b/src/Api/SecretsManager/Models/Response/ProjectServiceAccountsAccessPoliciesResponseModel.cs @@ -18,12 +18,12 @@ public class ProjectServiceAccountsAccessPoliciesResponseModel : ResponseModel } ServiceAccountAccessPolicies = projectServiceAccountsAccessPolicies.ServiceAccountAccessPolicies - .Select(x => new ServiceAccountProjectAccessPolicyResponseModel(x)).ToList(); + .Select(x => new ServiceAccountAccessPolicyResponseModel(x)).ToList(); } public ProjectServiceAccountsAccessPoliciesResponseModel() : base(_objectName) { } - public List ServiceAccountAccessPolicies { get; set; } = []; + public List ServiceAccountAccessPolicies { get; set; } = []; } diff --git a/src/Api/SecretsManager/Models/Response/SecretAccessPoliciesResponseModel.cs b/src/Api/SecretsManager/Models/Response/SecretAccessPoliciesResponseModel.cs new file mode 100644 index 0000000000..1f1ea554d3 --- /dev/null +++ b/src/Api/SecretsManager/Models/Response/SecretAccessPoliciesResponseModel.cs @@ -0,0 +1,33 @@ +#nullable enable +using Bit.Core.Models.Api; +using Bit.Core.SecretsManager.Models.Data; + +namespace Bit.Api.SecretsManager.Models.Response; + +public class SecretAccessPoliciesResponseModel : ResponseModel +{ + private const string _objectName = "secretAccessPolicies"; + + public SecretAccessPoliciesResponseModel(SecretAccessPolicies? accessPolicies, Guid userId) : + base(_objectName) + { + if (accessPolicies == null) + { + return; + } + + UserAccessPolicies = accessPolicies.UserAccessPolicies.Select(x => new UserAccessPolicyResponseModel(x, userId)).ToList(); + GroupAccessPolicies = accessPolicies.GroupAccessPolicies.Select(x => new GroupAccessPolicyResponseModel(x)).ToList(); + ServiceAccountAccessPolicies = accessPolicies.ServiceAccountAccessPolicies.Select(x => new ServiceAccountAccessPolicyResponseModel(x)).ToList(); + } + + public SecretAccessPoliciesResponseModel() : base(_objectName) + { + } + + + public List UserAccessPolicies { get; set; } = []; + public List GroupAccessPolicies { get; set; } = []; + public List ServiceAccountAccessPolicies { get; set; } = []; + +} diff --git a/src/Api/SecretsManager/Models/Response/ServiceAccountGrantedPoliciesPermissionDetailsResponseModel.cs b/src/Api/SecretsManager/Models/Response/ServiceAccountGrantedPoliciesPermissionDetailsResponseModel.cs index 4cc535ab12..3310c0ad5f 100644 --- a/src/Api/SecretsManager/Models/Response/ServiceAccountGrantedPoliciesPermissionDetailsResponseModel.cs +++ b/src/Api/SecretsManager/Models/Response/ServiceAccountGrantedPoliciesPermissionDetailsResponseModel.cs @@ -18,13 +18,13 @@ public class ServiceAccountGrantedPoliciesPermissionDetailsResponseModel : Respo } GrantedProjectPolicies = grantedPoliciesPermissionDetails.ProjectGrantedPolicies - .Select(x => new ServiceAccountProjectAccessPolicyPermissionDetailsResponseModel(x)).ToList(); + .Select(x => new GrantedProjectAccessPolicyPermissionDetailsResponseModel(x)).ToList(); } public ServiceAccountGrantedPoliciesPermissionDetailsResponseModel() : base(_objectName) { } - public List GrantedProjectPolicies { get; set; } = + public List GrantedProjectPolicies { get; set; } = []; } diff --git a/src/Api/SecretsManager/Models/Response/ServiceAccountPeopleAccessPoliciesResponseModel.cs b/src/Api/SecretsManager/Models/Response/ServiceAccountPeopleAccessPoliciesResponseModel.cs index 899eb7dba5..aa2b67667e 100644 --- a/src/Api/SecretsManager/Models/Response/ServiceAccountPeopleAccessPoliciesResponseModel.cs +++ b/src/Api/SecretsManager/Models/Response/ServiceAccountPeopleAccessPoliciesResponseModel.cs @@ -20,10 +20,10 @@ public class ServiceAccountPeopleAccessPoliciesResponseModel : ResponseModel switch (baseAccessPolicy) { case UserServiceAccountAccessPolicy accessPolicy: - UserAccessPolicies.Add(new UserServiceAccountAccessPolicyResponseModel(accessPolicy, userId)); + UserAccessPolicies.Add(new UserAccessPolicyResponseModel(accessPolicy, userId)); break; case GroupServiceAccountAccessPolicy accessPolicy: - GroupAccessPolicies.Add(new GroupServiceAccountAccessPolicyResponseModel(accessPolicy)); + GroupAccessPolicies.Add(new GroupAccessPolicyResponseModel(accessPolicy)); break; } } @@ -33,7 +33,7 @@ public class ServiceAccountPeopleAccessPoliciesResponseModel : ResponseModel { } - public List UserAccessPolicies { get; set; } = new(); + public List UserAccessPolicies { get; set; } = new(); - public List GroupAccessPolicies { get; set; } = new(); + public List GroupAccessPolicies { get; set; } = new(); } diff --git a/src/Api/SecretsManager/Models/Response/ServiceAccountProjectAccessPolicyPermissionDetailsResponseModel.cs b/src/Api/SecretsManager/Models/Response/ServiceAccountProjectAccessPolicyPermissionDetailsResponseModel.cs deleted file mode 100644 index abf7466be0..0000000000 --- a/src/Api/SecretsManager/Models/Response/ServiceAccountProjectAccessPolicyPermissionDetailsResponseModel.cs +++ /dev/null @@ -1,25 +0,0 @@ -#nullable enable -using Bit.Core.Models.Api; -using Bit.Core.SecretsManager.Models.Data; - -namespace Bit.Api.SecretsManager.Models.Response; - -public class ServiceAccountProjectAccessPolicyPermissionDetailsResponseModel : ResponseModel -{ - private const string _objectName = "serviceAccountProjectAccessPolicyPermissionDetails"; - - public ServiceAccountProjectAccessPolicyPermissionDetailsResponseModel( - ServiceAccountProjectAccessPolicyPermissionDetails apPermissionDetails, string obj = _objectName) : base(obj) - { - AccessPolicy = new ServiceAccountProjectAccessPolicyResponseModel(apPermissionDetails.AccessPolicy); - HasPermission = apPermissionDetails.HasPermission; - } - - public ServiceAccountProjectAccessPolicyPermissionDetailsResponseModel() - : base(_objectName) - { - } - - public ServiceAccountProjectAccessPolicyResponseModel AccessPolicy { get; set; } = new(); - public bool HasPermission { get; set; } -} diff --git a/src/Core/SecretsManager/AuthorizationRequirements/SecretOperationRequirement.cs b/src/Core/SecretsManager/AuthorizationRequirements/SecretOperationRequirement.cs index e737960015..948611d413 100644 --- a/src/Core/SecretsManager/AuthorizationRequirements/SecretOperationRequirement.cs +++ b/src/Core/SecretsManager/AuthorizationRequirements/SecretOperationRequirement.cs @@ -12,4 +12,5 @@ public static class SecretOperations public static readonly SecretOperationRequirement Read = new() { Name = nameof(Read) }; public static readonly SecretOperationRequirement Update = new() { Name = nameof(Update) }; public static readonly SecretOperationRequirement Delete = new() { Name = nameof(Delete) }; + public static readonly SecretOperationRequirement ReadAccessPolicies = new() { Name = nameof(ReadAccessPolicies) }; } diff --git a/src/Core/SecretsManager/Models/Data/SecretAccessPolicies.cs b/src/Core/SecretsManager/Models/Data/SecretAccessPolicies.cs new file mode 100644 index 0000000000..9b7fccb63d --- /dev/null +++ b/src/Core/SecretsManager/Models/Data/SecretAccessPolicies.cs @@ -0,0 +1,35 @@ +#nullable enable +using Bit.Core.SecretsManager.Entities; + +namespace Bit.Core.SecretsManager.Models.Data; + +public class SecretAccessPolicies +{ + public SecretAccessPolicies(Guid secretId, Guid organizationId, List policies) + { + SecretId = secretId; + OrganizationId = organizationId; + + UserAccessPolicies = policies + .OfType() + .ToList(); + + GroupAccessPolicies = policies + .OfType() + .ToList(); + + ServiceAccountAccessPolicies = policies + .OfType() + .ToList(); + } + + public SecretAccessPolicies() + { + } + + public Guid SecretId { get; set; } + public Guid OrganizationId { get; set; } + public IEnumerable UserAccessPolicies { get; set; } = []; + public IEnumerable GroupAccessPolicies { get; set; } = []; + public IEnumerable ServiceAccountAccessPolicies { get; set; } = []; +} diff --git a/src/Core/SecretsManager/Repositories/IAccessPolicyRepository.cs b/src/Core/SecretsManager/Repositories/IAccessPolicyRepository.cs index 8696e90514..af474d8e6e 100644 --- a/src/Core/SecretsManager/Repositories/IAccessPolicyRepository.cs +++ b/src/Core/SecretsManager/Repositories/IAccessPolicyRepository.cs @@ -20,4 +20,5 @@ public interface IAccessPolicyRepository Task UpdateServiceAccountGrantedPoliciesAsync(ServiceAccountGrantedPoliciesUpdates policyUpdates); Task GetProjectServiceAccountsAccessPoliciesAsync(Guid projectId); Task UpdateProjectServiceAccountsAccessPoliciesAsync(ProjectServiceAccountsAccessPoliciesUpdates updates); + Task GetSecretAccessPoliciesAsync(Guid secretId, Guid userId); } diff --git a/test/Api.IntegrationTest/SecretsManager/Controllers/AccessPoliciesControllerTests.cs b/test/Api.IntegrationTest/SecretsManager/Controllers/AccessPoliciesControllerTests.cs index aacf33860f..77614574c1 100644 --- a/test/Api.IntegrationTest/SecretsManager/Controllers/AccessPoliciesControllerTests.cs +++ b/test/Api.IntegrationTest/SecretsManager/Controllers/AccessPoliciesControllerTests.cs @@ -27,6 +27,7 @@ public class AccessPoliciesControllerTests : IClassFixture(); _serviceAccountRepository = _factory.GetService(); + _secretRepository = _factory.GetService(); _projectRepository = _factory.GetService(); _groupRepository = _factory.GetService(); _loginHelper = new LoginHelper(_factory, _client); @@ -723,9 +725,8 @@ public class AccessPoliciesControllerTests : IClassFixture(); + + Assert.NotNull(result); + Assert.Empty(result.UserAccessPolicies); + Assert.Empty(result.GroupAccessPolicies); + Assert.Empty(result.ServiceAccountAccessPolicies); + } + + [Fact] + public async Task GetSecretAccessPoliciesAsync_UserDoesntHavePermission_ReturnsNotFound() + { + var (secretId, _) = await SetupSecretAccessPoliciesTest(PermissionType.RunAsUserWithPermission); + + var response = await _client.GetAsync($"/secrets/{secretId}/access-policies"); + + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Theory] + [InlineData(PermissionType.RunAsAdmin)] + public async Task GetSecretAccessPoliciesAsync_Success(PermissionType permissionType) + { + var (secretId, currentOrgUser) = await SetupSecretAccessPoliciesTest(permissionType); + + var accessPolicies = new List + { + new UserSecretAccessPolicy + { + GrantedSecretId = secretId, OrganizationUserId = currentOrgUser.Id, Read = true, Write = true + } + }; + await _accessPolicyRepository.CreateManyAsync(accessPolicies); + + var response = await _client.GetAsync($"/secrets/{secretId}/access-policies"); + response.EnsureSuccessStatusCode(); + + var result = await response.Content + .ReadFromJsonAsync(); + + Assert.NotNull(result); + Assert.NotEmpty(result.UserAccessPolicies); + Assert.Empty(result.GroupAccessPolicies); + Assert.Empty(result.ServiceAccountAccessPolicies); + Assert.NotNull(result.UserAccessPolicies.First().OrganizationUserName); + Assert.NotNull(result.UserAccessPolicies.First().OrganizationUserId); + Assert.NotNull(result.UserAccessPolicies.First().CurrentUser); + Assert.Equal(currentOrgUser.Id, result.UserAccessPolicies.First().OrganizationUserId); + } + private async Task<(Guid ProjectId, Guid ServiceAccountId)> CreateServiceAccountProjectAccessPolicyAsync( Guid organizationId) { @@ -1290,4 +1373,31 @@ public class AccessPoliciesControllerTests : IClassFixture SetupSecretAccessPoliciesTest( + PermissionType permissionType) + { + var (org, orgAdmin) = await _organizationHelper.Initialize(true, true, true); + var currentOrgUser = orgAdmin; + if (permissionType == PermissionType.RunAsUserWithPermission) + { + var (email, orgUser) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true); + await _loginHelper.LoginAsync(email); + currentOrgUser = orgUser; + } + else + { + await _loginHelper.LoginAsync(_email); + } + + var secret = await _secretRepository.CreateAsync(new Secret + { + OrganizationId = org.Id, + Key = _mockEncryptedString, + Value = _mockEncryptedString, + Note = _mockEncryptedString + }); + + return (secret.Id, currentOrgUser); + } } diff --git a/test/Api.Test/SecretsManager/Controllers/AccessPoliciesControllerTests.cs b/test/Api.Test/SecretsManager/Controllers/AccessPoliciesControllerTests.cs index 41ce62f879..6a47679580 100644 --- a/test/Api.Test/SecretsManager/Controllers/AccessPoliciesControllerTests.cs +++ b/test/Api.Test/SecretsManager/Controllers/AccessPoliciesControllerTests.cs @@ -827,7 +827,6 @@ public class AccessPoliciesControllerTests SutProvider sutProvider, Project data) { - // FIX ME SetupProjectAccessPoliciesTest(sutProvider, data, accessClientType); sutProvider.GetDependency() @@ -953,6 +952,61 @@ public class AccessPoliciesControllerTests .UpdateAsync(Arg.Any()); } + [Theory] + [BitAutoData] + public async Task GetSecretAccessPoliciesAsync_NoAccess_ThrowsNotFound( + SutProvider sutProvider, + Secret data) + { + sutProvider.GetDependency().GetByIdAsync(data.Id).Returns(data); + sutProvider.GetDependency() + .AuthorizeAsync(Arg.Any(), data, + Arg.Any>()).ReturnsForAnyArgs(AuthorizationResult.Failed()); + + await Assert.ThrowsAsync(() => + sutProvider.Sut.GetSecretAccessPoliciesAsync(data.Id)); + + await sutProvider.GetDependency().Received(0) + .GetSecretAccessPoliciesAsync(Arg.Any(), Arg.Any()); + } + + [Theory] + [BitAutoData] + public async Task GetSecretAccessPoliciesAsync_HasAccessNoPolicies_ReturnsEmptyList( + SutProvider sutProvider, + Secret data) + { + SetupSecretAccessPoliciesTest(sutProvider, data); + sutProvider.GetDependency() + .GetSecretAccessPoliciesAsync(Arg.Any(), Arg.Any()) + .ReturnsNull(); + + var result = await sutProvider.Sut.GetSecretAccessPoliciesAsync(data.Id); + + Assert.Empty(result.UserAccessPolicies); + Assert.Empty(result.GroupAccessPolicies); + Assert.Empty(result.ServiceAccountAccessPolicies); + } + + [Theory] + [BitAutoData] + public async Task GetSecretAccessPoliciesAsync_HasAccess_Success( + SutProvider sutProvider, + SecretAccessPolicies policies, + Secret data) + { + SetupSecretAccessPoliciesTest(sutProvider, data); + sutProvider.GetDependency() + .GetSecretAccessPoliciesAsync(Arg.Any(), Arg.Any()) + .Returns(policies); + + var result = await sutProvider.Sut.GetSecretAccessPoliciesAsync(data.Id); + + Assert.NotEmpty(result.UserAccessPolicies); + Assert.NotEmpty(result.GroupAccessPolicies); + Assert.NotEmpty(result.ServiceAccountAccessPolicies); + } + private static PeopleAccessPoliciesRequestModel SetRequestToCanReadWrite(PeopleAccessPoliciesRequestModel request) { foreach (var ap in request.UserAccessPolicyRequests) @@ -1005,4 +1059,13 @@ public class AccessPoliciesControllerTests .GetAccessClientAsync(Arg.Any(), Arg.Any()) .ReturnsForAnyArgs((accessClientType, Guid.NewGuid())); } + + private static void SetupSecretAccessPoliciesTest(SutProvider sutProvider, Secret data) + { + sutProvider.GetDependency().GetByIdAsync(data.Id).Returns(data); + sutProvider.GetDependency() + .AuthorizeAsync(Arg.Any(), data, + Arg.Any>()).ReturnsForAnyArgs(AuthorizationResult.Success()); + sutProvider.GetDependency().GetProperUserId(Arg.Any()).Returns(Guid.NewGuid()); + } }