1
0
mirror of https://github.com/bitwarden/server.git synced 2025-07-15 06:37:54 -05:00

[SM-654] Individual secret permissions (#4160)

* Add new data and request models

* Update authz handlers

* Update secret commands to handle access policy updates

* Update secret repository to handle access policy updates

* Update secrets controller to handle access policy updates

* Add tests

* Add integration tests for secret create
This commit is contained in:
Thomas Avery
2024-06-20 12:45:28 -05:00
committed by GitHub
parent 0e6e461602
commit 01d67dce48
30 changed files with 2141 additions and 342 deletions

View File

@ -0,0 +1,13 @@
using Microsoft.AspNetCore.Authorization.Infrastructure;
namespace Bit.Core.SecretsManager.AuthorizationRequirements;
public class SecretAccessPoliciesOperationRequirement : OperationAuthorizationRequirement
{
}
public static class SecretAccessPoliciesOperations
{
public static readonly SecretAccessPoliciesOperationRequirement Updates = new() { Name = nameof(Updates) };
public static readonly SecretAccessPoliciesOperationRequirement Create = new() { Name = nameof(Create) };
}

View File

@ -1,8 +1,10 @@
using Bit.Core.SecretsManager.Entities;
#nullable enable
using Bit.Core.SecretsManager.Entities;
using Bit.Core.SecretsManager.Models.Data.AccessPolicyUpdates;
namespace Bit.Core.SecretsManager.Commands.Secrets.Interfaces;
public interface ICreateSecretCommand
{
Task<Secret> CreateAsync(Secret secret);
Task<Secret> CreateAsync(Secret secret, SecretAccessPoliciesUpdates? accessPoliciesUpdates);
}

View File

@ -1,8 +1,10 @@
using Bit.Core.SecretsManager.Entities;
#nullable enable
using Bit.Core.SecretsManager.Entities;
using Bit.Core.SecretsManager.Models.Data.AccessPolicyUpdates;
namespace Bit.Core.SecretsManager.Commands.Secrets.Interfaces;
public interface IUpdateSecretCommand
{
Task<Secret> UpdateAsync(Secret secret);
Task<Secret> UpdateAsync(Secret secret, SecretAccessPoliciesUpdates? accessPolicyUpdates);
}

View File

@ -0,0 +1,23 @@
#nullable enable
using Bit.Core.SecretsManager.Entities;
using Bit.Core.SecretsManager.Enums.AccessPolicies;
namespace Bit.Core.SecretsManager.Models.Data.AccessPolicyUpdates;
public class UserSecretAccessPolicyUpdate
{
public AccessPolicyOperation Operation { get; set; }
public required UserSecretAccessPolicy AccessPolicy { get; set; }
}
public class GroupSecretAccessPolicyUpdate
{
public AccessPolicyOperation Operation { get; set; }
public required GroupSecretAccessPolicy AccessPolicy { get; set; }
}
public class ServiceAccountSecretAccessPolicyUpdate
{
public AccessPolicyOperation Operation { get; set; }
public required ServiceAccountSecretAccessPolicy AccessPolicy { get; set; }
}

View File

@ -0,0 +1,36 @@
#nullable enable
using Bit.Core.SecretsManager.Enums.AccessPolicies;
namespace Bit.Core.SecretsManager.Models.Data.AccessPolicyUpdates;
public class SecretAccessPoliciesUpdates
{
public SecretAccessPoliciesUpdates(SecretAccessPolicies accessPolicies)
{
SecretId = accessPolicies.SecretId;
OrganizationId = accessPolicies.OrganizationId;
UserAccessPolicyUpdates =
accessPolicies.UserAccessPolicies.Select(x =>
new UserSecretAccessPolicyUpdate { Operation = AccessPolicyOperation.Create, AccessPolicy = x });
GroupAccessPolicyUpdates =
accessPolicies.GroupAccessPolicies.Select(x =>
new GroupSecretAccessPolicyUpdate { Operation = AccessPolicyOperation.Create, AccessPolicy = x });
ServiceAccountAccessPolicyUpdates = accessPolicies.ServiceAccountAccessPolicies.Select(x =>
new ServiceAccountSecretAccessPolicyUpdate { Operation = AccessPolicyOperation.Create, AccessPolicy = x });
}
public SecretAccessPoliciesUpdates() { }
public Guid SecretId { get; set; }
public Guid OrganizationId { get; set; }
public IEnumerable<UserSecretAccessPolicyUpdate> UserAccessPolicyUpdates { get; set; } = [];
public IEnumerable<GroupSecretAccessPolicyUpdate> GroupAccessPolicyUpdates { get; set; } = [];
public IEnumerable<ServiceAccountSecretAccessPolicyUpdate> ServiceAccountAccessPolicyUpdates { get; set; } = [];
public bool HasUpdates() =>
UserAccessPolicyUpdates.Any() ||
GroupAccessPolicyUpdates.Any() ||
ServiceAccountAccessPolicyUpdates.Any();
}

View File

@ -1,5 +1,7 @@
#nullable enable
using Bit.Core.SecretsManager.Entities;
using Bit.Core.SecretsManager.Enums.AccessPolicies;
using Bit.Core.SecretsManager.Models.Data.AccessPolicyUpdates;
namespace Bit.Core.SecretsManager.Models.Data;
@ -32,4 +34,113 @@ public class SecretAccessPolicies
public IEnumerable<UserSecretAccessPolicy> UserAccessPolicies { get; set; } = [];
public IEnumerable<GroupSecretAccessPolicy> GroupAccessPolicies { get; set; } = [];
public IEnumerable<ServiceAccountSecretAccessPolicy> ServiceAccountAccessPolicies { get; set; } = [];
public SecretAccessPoliciesUpdates GetPolicyUpdates(SecretAccessPolicies requested) =>
new()
{
SecretId = SecretId,
OrganizationId = OrganizationId,
UserAccessPolicyUpdates = GetUserPolicyUpdates(requested.UserAccessPolicies.ToList()),
GroupAccessPolicyUpdates = GetGroupPolicyUpdates(requested.GroupAccessPolicies.ToList()),
ServiceAccountAccessPolicyUpdates =
GetServiceAccountPolicyUpdates(requested.ServiceAccountAccessPolicies.ToList())
};
private static List<TPolicyUpdate> GetPolicyUpdates<TPolicy, TPolicyUpdate>(
List<TPolicy> currentPolicies,
List<TPolicy> requestedPolicies,
Func<IEnumerable<TPolicy>, List<Guid>> getIds,
Func<IEnumerable<TPolicy>, List<Guid>> getIdsToBeUpdated,
Func<IEnumerable<TPolicy>, List<Guid>, AccessPolicyOperation, List<TPolicyUpdate>> createPolicyUpdates)
where TPolicy : class
where TPolicyUpdate : class
{
var currentIds = getIds(currentPolicies);
var requestedIds = getIds(requestedPolicies);
var idsToBeDeleted = currentIds.Except(requestedIds).ToList();
var idsToBeCreated = requestedIds.Except(currentIds).ToList();
var idsToBeUpdated = getIdsToBeUpdated(requestedPolicies);
var policiesToBeDeleted = createPolicyUpdates(currentPolicies, idsToBeDeleted, AccessPolicyOperation.Delete);
var policiesToBeCreated = createPolicyUpdates(requestedPolicies, idsToBeCreated, AccessPolicyOperation.Create);
var policiesToBeUpdated = createPolicyUpdates(requestedPolicies, idsToBeUpdated, AccessPolicyOperation.Update);
return policiesToBeDeleted.Concat(policiesToBeCreated).Concat(policiesToBeUpdated).ToList();
}
private static List<Guid> GetOrganizationUserIds(IEnumerable<UserSecretAccessPolicy> policies) =>
policies.Select(ap => ap.OrganizationUserId!.Value).ToList();
private static List<Guid> GetGroupIds(IEnumerable<GroupSecretAccessPolicy> policies) =>
policies.Select(ap => ap.GroupId!.Value).ToList();
private static List<Guid> GetServiceAccountIds(IEnumerable<ServiceAccountSecretAccessPolicy> policies) =>
policies.Select(ap => ap.ServiceAccountId!.Value).ToList();
private static List<UserSecretAccessPolicyUpdate> CreateUserPolicyUpdates(
IEnumerable<UserSecretAccessPolicy> policies, List<Guid> userIds,
AccessPolicyOperation operation) =>
policies
.Where(ap => userIds.Contains(ap.OrganizationUserId!.Value))
.Select(ap => new UserSecretAccessPolicyUpdate { Operation = operation, AccessPolicy = ap })
.ToList();
private static List<GroupSecretAccessPolicyUpdate> CreateGroupPolicyUpdates(
IEnumerable<GroupSecretAccessPolicy> policies, List<Guid> groupIds,
AccessPolicyOperation operation) =>
policies
.Where(ap => groupIds.Contains(ap.GroupId!.Value))
.Select(ap => new GroupSecretAccessPolicyUpdate { Operation = operation, AccessPolicy = ap })
.ToList();
private static List<ServiceAccountSecretAccessPolicyUpdate> CreateServiceAccountPolicyUpdates(
IEnumerable<ServiceAccountSecretAccessPolicy> policies, List<Guid> serviceAccountIds,
AccessPolicyOperation operation) =>
policies
.Where(ap => serviceAccountIds.Contains(ap.ServiceAccountId!.Value))
.Select(ap => new ServiceAccountSecretAccessPolicyUpdate { Operation = operation, AccessPolicy = ap })
.ToList();
private List<UserSecretAccessPolicyUpdate> GetUserPolicyUpdates(List<UserSecretAccessPolicy> requestedPolicies) =>
GetPolicyUpdates(UserAccessPolicies.ToList(), requestedPolicies, GetOrganizationUserIds, GetUserIdsToBeUpdated,
CreateUserPolicyUpdates);
private List<GroupSecretAccessPolicyUpdate>
GetGroupPolicyUpdates(List<GroupSecretAccessPolicy> requestedPolicies) =>
GetPolicyUpdates(GroupAccessPolicies.ToList(), requestedPolicies, GetGroupIds, GetGroupIdsToBeUpdated,
CreateGroupPolicyUpdates);
private List<ServiceAccountSecretAccessPolicyUpdate> GetServiceAccountPolicyUpdates(
List<ServiceAccountSecretAccessPolicy> requestedPolicies) =>
GetPolicyUpdates(ServiceAccountAccessPolicies.ToList(), requestedPolicies, GetServiceAccountIds,
GetServiceAccountIdsToBeUpdated, CreateServiceAccountPolicyUpdates);
private List<Guid> GetUserIdsToBeUpdated(IEnumerable<UserSecretAccessPolicy> requested) =>
UserAccessPolicies
.Where(currentAp => requested.Any(requestedAp =>
requestedAp.GrantedSecretId == currentAp.GrantedSecretId &&
requestedAp.OrganizationUserId == currentAp.OrganizationUserId &&
(requestedAp.Write != currentAp.Write || requestedAp.Read != currentAp.Read)))
.Select(ap => ap.OrganizationUserId!.Value)
.ToList();
private List<Guid> GetGroupIdsToBeUpdated(IEnumerable<GroupSecretAccessPolicy> requested) =>
GroupAccessPolicies
.Where(currentAp => requested.Any(requestedAp =>
requestedAp.GrantedSecretId == currentAp.GrantedSecretId &&
requestedAp.GroupId == currentAp.GroupId &&
(requestedAp.Write != currentAp.Write || requestedAp.Read != currentAp.Read)))
.Select(ap => ap.GroupId!.Value)
.ToList();
private List<Guid> GetServiceAccountIdsToBeUpdated(IEnumerable<ServiceAccountSecretAccessPolicy> requested) =>
ServiceAccountAccessPolicies
.Where(currentAp => requested.Any(requestedAp =>
requestedAp.GrantedSecretId == currentAp.GrantedSecretId &&
requestedAp.ServiceAccountId == currentAp.ServiceAccountId &&
(requestedAp.Write != currentAp.Write || requestedAp.Read != currentAp.Read)))
.Select(ap => ap.ServiceAccountId!.Value)
.ToList();
}

View File

@ -0,0 +1,10 @@
#nullable enable
using Bit.Core.SecretsManager.Models.Data;
using Bit.Core.SecretsManager.Models.Data.AccessPolicyUpdates;
namespace Bit.Core.SecretsManager.Queries.AccessPolicies.Interfaces;
public interface ISecretAccessPoliciesUpdatesQuery
{
Task<SecretAccessPoliciesUpdates> GetAsync(SecretAccessPolicies accessPolicies, Guid userId);
}

View File

@ -1,6 +1,7 @@
using Bit.Core.Enums;
using Bit.Core.SecretsManager.Entities;
using Bit.Core.SecretsManager.Models.Data;
using Bit.Core.SecretsManager.Models.Data.AccessPolicyUpdates;
namespace Bit.Core.SecretsManager.Repositories;
@ -13,8 +14,8 @@ public interface ISecretRepository
Task<IEnumerable<Secret>> GetManyByOrganizationIdInTrashByIdsAsync(Guid organizationId, IEnumerable<Guid> ids);
Task<IEnumerable<Secret>> GetManyByIds(IEnumerable<Guid> ids);
Task<Secret> GetByIdAsync(Guid id);
Task<Secret> CreateAsync(Secret secret);
Task<Secret> UpdateAsync(Secret secret);
Task<Secret> CreateAsync(Secret secret, SecretAccessPoliciesUpdates accessPoliciesUpdates = null);
Task<Secret> UpdateAsync(Secret secret, SecretAccessPoliciesUpdates accessPoliciesUpdates = null);
Task SoftDeleteManyByIdAsync(IEnumerable<Guid> ids);
Task HardDeleteManyByIdAsync(IEnumerable<Guid> ids);
Task RestoreManyByIdAsync(IEnumerable<Guid> ids);

View File

@ -1,6 +1,7 @@
using Bit.Core.Enums;
using Bit.Core.SecretsManager.Entities;
using Bit.Core.SecretsManager.Models.Data;
using Bit.Core.SecretsManager.Models.Data.AccessPolicyUpdates;
namespace Bit.Core.SecretsManager.Repositories.Noop;
@ -45,12 +46,12 @@ public class NoopSecretRepository : ISecretRepository
return Task.FromResult(null as Secret);
}
public Task<Secret> CreateAsync(Secret secret)
public Task<Secret> CreateAsync(Secret secret, SecretAccessPoliciesUpdates accessPoliciesUpdates)
{
return Task.FromResult(null as Secret);
}
public Task<Secret> UpdateAsync(Secret secret)
public Task<Secret> UpdateAsync(Secret secret, SecretAccessPoliciesUpdates accessPoliciesUpdates)
{
return Task.FromResult(null as Secret);
}