mirror of
https://github.com/bitwarden/server
synced 2026-01-06 02:23:51 +00:00
[SM-1293] Add endpoint to fetch secret's access policies (#4146)
* Add authz handling for secret access policy reads * Add the ability to fetch secret access polices from the repository * refactor response models * Add new endpoint
This commit is contained in:
@@ -47,6 +47,9 @@ public class SecretAuthorizationHandler : AuthorizationHandler<SecretOperationRe
|
||||
case not null when requirement == SecretOperations.Delete:
|
||||
await CanDeleteSecretAsync(context, requirement, resource);
|
||||
break;
|
||||
case not null when requirement == SecretOperations.ReadAccessPolicies:
|
||||
await CanReadAccessPoliciesAsync(context, requirement, resource);
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentException("Unsupported operation requirement type provided.", nameof(requirement));
|
||||
}
|
||||
@@ -152,6 +155,26 @@ public class SecretAuthorizationHandler : AuthorizationHandler<SecretOperationRe
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CanReadAccessPoliciesAsync(AuthorizationHandlerContext context,
|
||||
SecretOperationRequirement requirement, Secret resource)
|
||||
{
|
||||
var (accessClient, userId) = await _accessClientQuery.GetAccessClientAsync(context.User, resource.OrganizationId);
|
||||
|
||||
// Only users and admins can read access policies
|
||||
if (accessClient != AccessClientType.User && accessClient != AccessClientType.NoAccessCheck)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var access = await _secretRepository.AccessToSecretAsync(resource.Id, userId, accessClient);
|
||||
|
||||
if (access.Write)
|
||||
{
|
||||
context.Succeed(requirement);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private async Task<bool> GetAccessToUpdateSecretAsync(Secret resource, Guid userId, AccessClientType accessClient)
|
||||
{
|
||||
var newProject = resource.Projects?.FirstOrDefault();
|
||||
|
||||
@@ -20,7 +20,8 @@ public class AccessPolicyRepository : BaseEntityFrameworkRepository, IAccessPoli
|
||||
{
|
||||
}
|
||||
|
||||
public async Task<List<Core.SecretsManager.Entities.BaseAccessPolicy>> CreateManyAsync(List<Core.SecretsManager.Entities.BaseAccessPolicy> baseAccessPolicies)
|
||||
public async Task<List<Core.SecretsManager.Entities.BaseAccessPolicy>> CreateManyAsync(
|
||||
List<Core.SecretsManager.Entities.BaseAccessPolicy> 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<UserSecretAccessPolicy>(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<GroupSecretAccessPolicy>(accessPolicy);
|
||||
await dbContext.AddAsync(entity);
|
||||
break;
|
||||
}
|
||||
case Core.SecretsManager.Entities.GroupServiceAccountAccessPolicy accessPolicy:
|
||||
{
|
||||
var entity = Mapper.Map<GroupServiceAccountAccessPolicy>(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<ServiceAccountSecretAccessPolicy>(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<SecretAccessPolicies?> 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<BaseAccessPolicy> policies, IReadOnlyCollection<AccessPolicy> userPolicyEntities,
|
||||
IReadOnlyCollection<AccessPolicy> groupPolicyEntities)
|
||||
@@ -466,13 +523,17 @@ public class AccessPolicyRepository : BaseEntityFrameworkRepository, IAccessPoli
|
||||
baseAccessPolicyEntity switch
|
||||
{
|
||||
UserProjectAccessPolicy ap => Mapper.Map<Core.SecretsManager.Entities.UserProjectAccessPolicy>(ap),
|
||||
GroupProjectAccessPolicy ap => Mapper.Map<Core.SecretsManager.Entities.GroupProjectAccessPolicy>(ap),
|
||||
ServiceAccountProjectAccessPolicy ap => Mapper
|
||||
.Map<Core.SecretsManager.Entities.ServiceAccountProjectAccessPolicy>(ap),
|
||||
UserSecretAccessPolicy ap => Mapper.Map<Core.SecretsManager.Entities.UserSecretAccessPolicy>(ap),
|
||||
UserServiceAccountAccessPolicy ap =>
|
||||
Mapper.Map<Core.SecretsManager.Entities.UserServiceAccountAccessPolicy>(ap),
|
||||
GroupProjectAccessPolicy ap => Mapper.Map<Core.SecretsManager.Entities.GroupProjectAccessPolicy>(ap),
|
||||
GroupSecretAccessPolicy ap => Mapper.Map<Core.SecretsManager.Entities.GroupSecretAccessPolicy>(ap),
|
||||
GroupServiceAccountAccessPolicy ap => Mapper
|
||||
.Map<Core.SecretsManager.Entities.GroupServiceAccountAccessPolicy>(ap),
|
||||
ServiceAccountProjectAccessPolicy ap => Mapper
|
||||
.Map<Core.SecretsManager.Entities.ServiceAccountProjectAccessPolicy>(ap),
|
||||
ServiceAccountSecretAccessPolicy ap => Mapper
|
||||
.Map<Core.SecretsManager.Entities.ServiceAccountSecretAccessPolicy>(ap),
|
||||
_ => throw new ArgumentException("Unsupported access policy type")
|
||||
};
|
||||
|
||||
@@ -482,20 +543,26 @@ public class AccessPolicyRepository : BaseEntityFrameworkRepository, IAccessPoli
|
||||
{
|
||||
Core.SecretsManager.Entities.UserProjectAccessPolicy accessPolicy => Mapper.Map<UserProjectAccessPolicy>(
|
||||
accessPolicy),
|
||||
Core.SecretsManager.Entities.UserSecretAccessPolicy accessPolicy => Mapper.Map<UserSecretAccessPolicy>(
|
||||
accessPolicy),
|
||||
Core.SecretsManager.Entities.UserServiceAccountAccessPolicy accessPolicy => Mapper
|
||||
.Map<UserServiceAccountAccessPolicy>(accessPolicy),
|
||||
Core.SecretsManager.Entities.GroupProjectAccessPolicy accessPolicy => Mapper.Map<GroupProjectAccessPolicy>(
|
||||
accessPolicy),
|
||||
Core.SecretsManager.Entities.GroupSecretAccessPolicy accessPolicy => Mapper.Map<GroupSecretAccessPolicy>(
|
||||
accessPolicy),
|
||||
Core.SecretsManager.Entities.GroupServiceAccountAccessPolicy accessPolicy => Mapper
|
||||
.Map<GroupServiceAccountAccessPolicy>(accessPolicy),
|
||||
Core.SecretsManager.Entities.ServiceAccountProjectAccessPolicy accessPolicy => Mapper
|
||||
.Map<ServiceAccountProjectAccessPolicy>(accessPolicy),
|
||||
Core.SecretsManager.Entities.ServiceAccountSecretAccessPolicy accessPolicy => Mapper
|
||||
.Map<ServiceAccountSecretAccessPolicy>(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<Core.SecretsManager.Entities.GroupSecretAccessPolicy>(ap);
|
||||
mapped.CurrentUserInGroup = currentUserInGroup;
|
||||
return mapped;
|
||||
}
|
||||
case GroupServiceAccountAccessPolicy ap:
|
||||
{
|
||||
var mapped = Mapper.Map<Core.SecretsManager.Entities.GroupServiceAccountAccessPolicy>(ap);
|
||||
|
||||
@@ -517,4 +517,85 @@ public class SecretAuthorizationHandlerTests
|
||||
|
||||
Assert.Equal(expected, authzContext.HasSucceeded);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task CanReadAccessPolicies_AccessToSecretsManagerFalse_DoesNotSucceed(
|
||||
SutProvider<SecretAuthorizationHandler> sutProvider, Secret secret,
|
||||
ClaimsPrincipal claimsPrincipal)
|
||||
{
|
||||
var requirement = SecretOperations.ReadAccessPolicies;
|
||||
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(secret.OrganizationId)
|
||||
.Returns(false);
|
||||
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
|
||||
claimsPrincipal, secret);
|
||||
|
||||
await sutProvider.Sut.HandleAsync(authzContext);
|
||||
|
||||
Assert.False(authzContext.HasSucceeded);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task CanReadAccessPolicies_NullResource_DoesNotSucceed(
|
||||
SutProvider<SecretAuthorizationHandler> sutProvider, Secret secret,
|
||||
ClaimsPrincipal claimsPrincipal,
|
||||
Guid userId)
|
||||
{
|
||||
var requirement = SecretOperations.ReadAccessPolicies;
|
||||
SetupPermission(sutProvider, PermissionType.RunAsAdmin, secret.OrganizationId, userId);
|
||||
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { 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<SecretAuthorizationHandler> sutProvider, Secret secret,
|
||||
ClaimsPrincipal claimsPrincipal)
|
||||
{
|
||||
var requirement = SecretOperations.ReadAccessPolicies;
|
||||
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(secret.OrganizationId)
|
||||
.Returns(true);
|
||||
sutProvider.GetDependency<IAccessClientQuery>()
|
||||
.GetAccessClientAsync(Arg.Any<ClaimsPrincipal>(), secret.OrganizationId)
|
||||
.Returns((clientType, Guid.NewGuid()));
|
||||
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { 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<SecretAuthorizationHandler> sutProvider, Secret secret,
|
||||
ClaimsPrincipal claimsPrincipal,
|
||||
Guid userId)
|
||||
{
|
||||
var requirement = SecretOperations.ReadAccessPolicies;
|
||||
SetupPermission(sutProvider, permissionType, secret.OrganizationId, userId);
|
||||
sutProvider.GetDependency<ISecretRepository>()
|
||||
.AccessToSecretAsync(secret.Id, userId, Arg.Any<AccessClientType>())
|
||||
.Returns((read, write));
|
||||
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
|
||||
claimsPrincipal, secret);
|
||||
|
||||
await sutProvider.Sut.HandleAsync(authzContext);
|
||||
|
||||
Assert.Equal(expected, authzContext.HasSucceeded);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user