1
0
mirror of https://github.com/bitwarden/server.git synced 2025-06-30 23:52:50 -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

@ -10,6 +10,8 @@ using Bit.Core.SecretsManager.AuthorizationRequirements;
using Bit.Core.SecretsManager.Commands.Secrets.Interfaces;
using Bit.Core.SecretsManager.Entities;
using Bit.Core.SecretsManager.Models.Data;
using Bit.Core.SecretsManager.Models.Data.AccessPolicyUpdates;
using Bit.Core.SecretsManager.Queries.AccessPolicies.Interfaces;
using Bit.Core.SecretsManager.Queries.Interfaces;
using Bit.Core.SecretsManager.Queries.Secrets.Interfaces;
using Bit.Core.SecretsManager.Repositories;
@ -34,6 +36,7 @@ public class SecretsController : Controller
private readonly IDeleteSecretCommand _deleteSecretCommand;
private readonly IAccessClientQuery _accessClientQuery;
private readonly ISecretsSyncQuery _secretsSyncQuery;
private readonly ISecretAccessPoliciesUpdatesQuery _secretAccessPoliciesUpdatesQuery;
private readonly IUserService _userService;
private readonly IEventService _eventService;
private readonly IReferenceEventService _referenceEventService;
@ -49,6 +52,7 @@ public class SecretsController : Controller
IDeleteSecretCommand deleteSecretCommand,
IAccessClientQuery accessClientQuery,
ISecretsSyncQuery secretsSyncQuery,
ISecretAccessPoliciesUpdatesQuery secretAccessPoliciesUpdatesQuery,
IUserService userService,
IEventService eventService,
IReferenceEventService referenceEventService,
@ -63,6 +67,7 @@ public class SecretsController : Controller
_deleteSecretCommand = deleteSecretCommand;
_accessClientQuery = accessClientQuery;
_secretsSyncQuery = secretsSyncQuery;
_secretAccessPoliciesUpdatesQuery = secretAccessPoliciesUpdatesQuery;
_userService = userService;
_eventService = eventService;
_referenceEventService = referenceEventService;
@ -88,7 +93,8 @@ public class SecretsController : Controller
}
[HttpPost("organizations/{organizationId}/secrets")]
public async Task<SecretResponseModel> CreateAsync([FromRoute] Guid organizationId, [FromBody] SecretCreateRequestModel createRequest)
public async Task<SecretResponseModel> CreateAsync([FromRoute] Guid organizationId,
[FromBody] SecretCreateRequestModel createRequest)
{
var secret = createRequest.ToSecret(organizationId);
var authorizationResult = await _authorizationService.AuthorizeAsync(User, secret, SecretOperations.Create);
@ -97,7 +103,22 @@ public class SecretsController : Controller
throw new NotFoundException();
}
var result = await _createSecretCommand.CreateAsync(secret);
SecretAccessPoliciesUpdates accessPoliciesUpdates = null;
if (createRequest.AccessPoliciesRequests != null)
{
secret.SetNewId();
accessPoliciesUpdates =
new SecretAccessPoliciesUpdates(
createRequest.AccessPoliciesRequests.ToSecretAccessPolicies(secret.Id, organizationId));
var accessPolicyAuthorizationResult = await _authorizationService.AuthorizeAsync(User,
accessPoliciesUpdates, SecretAccessPoliciesOperations.Create);
if (!accessPolicyAuthorizationResult.Succeeded)
{
throw new NotFoundException();
}
}
var result = await _createSecretCommand.CreateAsync(secret, accessPoliciesUpdates);
// Creating a secret means you have read & write permission.
return new SecretResponseModel(result, true, true);
@ -162,14 +183,28 @@ public class SecretsController : Controller
throw new NotFoundException();
}
var updatedSecret = updateRequest.ToSecret(id, secret.OrganizationId);
var updatedSecret = updateRequest.ToSecret(secret);
var authorizationResult = await _authorizationService.AuthorizeAsync(User, updatedSecret, SecretOperations.Update);
if (!authorizationResult.Succeeded)
{
throw new NotFoundException();
}
var result = await _updateSecretCommand.UpdateAsync(updatedSecret);
SecretAccessPoliciesUpdates accessPoliciesUpdates = null;
if (updateRequest.AccessPoliciesRequests != null)
{
var userId = _userService.GetProperUserId(User)!.Value;
accessPoliciesUpdates = await _secretAccessPoliciesUpdatesQuery.GetAsync(updateRequest.AccessPoliciesRequests.ToSecretAccessPolicies(id, secret.OrganizationId), userId);
var accessPolicyAuthorizationResult = await _authorizationService.AuthorizeAsync(User, accessPoliciesUpdates, SecretAccessPoliciesOperations.Updates);
if (!accessPolicyAuthorizationResult.Succeeded)
{
throw new NotFoundException();
}
}
var result = await _updateSecretCommand.UpdateAsync(updatedSecret, accessPoliciesUpdates);
// Updating a secret means you have read & write permission.
return new SecretResponseModel(result, true, true);

View File

@ -25,6 +25,16 @@ public class AccessPolicyRequest
Write = Write
};
public UserSecretAccessPolicy ToUserSecretAccessPolicy(Guid secretId, Guid organizationId) =>
new()
{
OrganizationUserId = GranteeId,
GrantedSecretId = secretId,
GrantedSecret = new Secret { OrganizationId = organizationId, Id = secretId },
Read = Read,
Write = Write
};
public GroupProjectAccessPolicy ToGroupProjectAccessPolicy(Guid projectId, Guid organizationId) =>
new()
{
@ -35,6 +45,16 @@ public class AccessPolicyRequest
Write = Write
};
public GroupSecretAccessPolicy ToGroupSecretAccessPolicy(Guid secretId, Guid organizationId) =>
new()
{
GroupId = GranteeId,
GrantedSecretId = secretId,
GrantedSecret = new Secret { OrganizationId = organizationId, Id = secretId },
Read = Read,
Write = Write
};
public ServiceAccountProjectAccessPolicy ToServiceAccountProjectAccessPolicy(Guid projectId, Guid organizationId) =>
new()
{
@ -45,6 +65,16 @@ public class AccessPolicyRequest
Write = Write
};
public ServiceAccountSecretAccessPolicy ToServiceAccountSecretAccessPolicy(Guid secretId, Guid organizationId) =>
new()
{
ServiceAccountId = GranteeId,
GrantedSecretId = secretId,
GrantedSecret = new Secret { OrganizationId = organizationId, Id = secretId },
Read = Read,
Write = Write
};
public UserServiceAccountAccessPolicy ToUserServiceAccountAccessPolicy(Guid id, Guid organizationId) =>
new()
{

View File

@ -0,0 +1,42 @@
#nullable enable
using Bit.Api.SecretsManager.Utilities;
using Bit.Core.SecretsManager.Entities;
using Bit.Core.SecretsManager.Models.Data;
namespace Bit.Api.SecretsManager.Models.Request;
public class SecretAccessPoliciesRequestsModel
{
public required IEnumerable<AccessPolicyRequest> UserAccessPolicyRequests { get; set; }
public required IEnumerable<AccessPolicyRequest> GroupAccessPolicyRequests { get; set; }
public required IEnumerable<AccessPolicyRequest> ServiceAccountAccessPolicyRequests { get; set; }
public SecretAccessPolicies ToSecretAccessPolicies(Guid secretId, Guid organizationId)
{
var userAccessPolicies = UserAccessPolicyRequests
.Select(x => x.ToUserSecretAccessPolicy(secretId, organizationId)).ToList();
var groupAccessPolicies = GroupAccessPolicyRequests
.Select(x => x.ToGroupSecretAccessPolicy(secretId, organizationId)).ToList();
var serviceAccountAccessPolicies = ServiceAccountAccessPolicyRequests
.Select(x => x.ToServiceAccountSecretAccessPolicy(secretId, organizationId)).ToList();
var policies = new List<BaseAccessPolicy>();
policies.AddRange(userAccessPolicies);
policies.AddRange(groupAccessPolicies);
policies.AddRange(serviceAccountAccessPolicies);
AccessPolicyHelpers.CheckForDistinctAccessPolicies(policies);
AccessPolicyHelpers.CheckAccessPoliciesHaveReadPermission(policies);
return new SecretAccessPolicies
{
SecretId = secretId,
OrganizationId = organizationId,
UserAccessPolicies = userAccessPolicies,
GroupAccessPolicies = groupAccessPolicies,
ServiceAccountAccessPolicies = serviceAccountAccessPolicies
};
}
}

View File

@ -23,6 +23,8 @@ public class SecretCreateRequestModel : IValidatableObject
public Guid[] ProjectIds { get; set; }
public SecretAccessPoliciesRequestsModel AccessPoliciesRequests { get; set; }
public Secret ToSecret(Guid organizationId)
{
return new Secret()

View File

@ -23,18 +23,27 @@ public class SecretUpdateRequestModel : IValidatableObject
public Guid[] ProjectIds { get; set; }
public Secret ToSecret(Guid id, Guid organizationId)
public SecretAccessPoliciesRequestsModel AccessPoliciesRequests { get; set; }
public Secret ToSecret(Secret secret)
{
return new Secret()
secret.Key = Key;
secret.Value = Value;
secret.Note = Note;
secret.RevisionDate = DateTime.UtcNow;
if (secret.Projects?.FirstOrDefault()?.Id == ProjectIds?.FirstOrDefault())
{
Id = id,
OrganizationId = organizationId,
Key = Key,
Value = Value,
Note = Note,
DeletedDate = null,
Projects = ProjectIds != null && ProjectIds.Any() ? ProjectIds.Select(x => new Project() { Id = x }).ToList() : null,
};
secret.Projects = null;
}
else
{
secret.Projects = ProjectIds != null && ProjectIds.Length != 0
? ProjectIds.Select(x => new Project() { Id = x }).ToList()
: [];
}
return secret;
}
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)

View File

@ -13,12 +13,16 @@ public static class AccessPolicyHelpers
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),
UserSecretAccessPolicy ap => new Tuple<Guid?, Guid?>(ap.OrganizationUserId, ap.GrantedSecretId),
UserServiceAccountAccessPolicy ap => new Tuple<Guid?, Guid?>(ap.OrganizationUserId,
ap.GrantedServiceAccountId),
GroupProjectAccessPolicy ap => new Tuple<Guid?, Guid?>(ap.GroupId, ap.GrantedProjectId),
GroupSecretAccessPolicy ap => new Tuple<Guid?, Guid?>(ap.GroupId, ap.GrantedSecretId),
GroupServiceAccountAccessPolicy ap => new Tuple<Guid?, Guid?>(ap.GroupId, ap.GrantedServiceAccountId),
ServiceAccountProjectAccessPolicy ap => new Tuple<Guid?, Guid?>(ap.ServiceAccountId,
ap.GrantedProjectId),
ServiceAccountSecretAccessPolicy ap => new Tuple<Guid?, Guid?>(ap.ServiceAccountId,
ap.GrantedSecretId),
_ => throw new ArgumentException("Unsupported access policy type provided.", nameof(baseAccessPolicy)),
};
}).ToList();