1
0
mirror of https://github.com/bitwarden/server.git synced 2025-04-04 20:50:21 -05:00
bitwarden/src/Api/SecretsManager/Controllers/SecretsController.cs
Thomas Avery 01d67dce48
[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
2024-06-20 12:45:28 -05:00

336 lines
14 KiB
C#

using Bit.Api.Models.Response;
using Bit.Api.SecretsManager.Models.Request;
using Bit.Api.SecretsManager.Models.Response;
using Bit.Core.Context;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Identity;
using Bit.Core.Repositories;
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;
using Bit.Core.Services;
using Bit.Core.Tools.Enums;
using Bit.Core.Tools.Models.Business;
using Bit.Core.Tools.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Bit.Api.SecretsManager.Controllers;
[Authorize("secrets")]
public class SecretsController : Controller
{
private readonly ICurrentContext _currentContext;
private readonly IProjectRepository _projectRepository;
private readonly ISecretRepository _secretRepository;
private readonly IOrganizationRepository _organizationRepository;
private readonly ICreateSecretCommand _createSecretCommand;
private readonly IUpdateSecretCommand _updateSecretCommand;
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;
private readonly IAuthorizationService _authorizationService;
public SecretsController(
ICurrentContext currentContext,
IProjectRepository projectRepository,
ISecretRepository secretRepository,
IOrganizationRepository organizationRepository,
ICreateSecretCommand createSecretCommand,
IUpdateSecretCommand updateSecretCommand,
IDeleteSecretCommand deleteSecretCommand,
IAccessClientQuery accessClientQuery,
ISecretsSyncQuery secretsSyncQuery,
ISecretAccessPoliciesUpdatesQuery secretAccessPoliciesUpdatesQuery,
IUserService userService,
IEventService eventService,
IReferenceEventService referenceEventService,
IAuthorizationService authorizationService)
{
_currentContext = currentContext;
_projectRepository = projectRepository;
_secretRepository = secretRepository;
_organizationRepository = organizationRepository;
_createSecretCommand = createSecretCommand;
_updateSecretCommand = updateSecretCommand;
_deleteSecretCommand = deleteSecretCommand;
_accessClientQuery = accessClientQuery;
_secretsSyncQuery = secretsSyncQuery;
_secretAccessPoliciesUpdatesQuery = secretAccessPoliciesUpdatesQuery;
_userService = userService;
_eventService = eventService;
_referenceEventService = referenceEventService;
_authorizationService = authorizationService;
}
[HttpGet("organizations/{organizationId}/secrets")]
public async Task<SecretWithProjectsListResponseModel> ListByOrganizationAsync([FromRoute] Guid organizationId)
{
if (!_currentContext.AccessSecretsManager(organizationId))
{
throw new NotFoundException();
}
var userId = _userService.GetProperUserId(User).Value;
var orgAdmin = await _currentContext.OrganizationAdmin(organizationId);
var accessClient = AccessClientHelper.ToAccessClient(_currentContext.ClientType, orgAdmin);
var secrets = await _secretRepository.GetManyDetailsByOrganizationIdAsync(organizationId, userId, accessClient);
return new SecretWithProjectsListResponseModel(secrets);
}
[HttpPost("organizations/{organizationId}/secrets")]
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);
if (!authorizationResult.Succeeded)
{
throw new NotFoundException();
}
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);
}
[HttpGet("secrets/{id}")]
public async Task<SecretResponseModel> GetAsync([FromRoute] Guid id)
{
var secret = await _secretRepository.GetByIdAsync(id);
if (secret == null || !_currentContext.AccessSecretsManager(secret.OrganizationId))
{
throw new NotFoundException();
}
var userId = _userService.GetProperUserId(User).Value;
var orgAdmin = await _currentContext.OrganizationAdmin(secret.OrganizationId);
var accessClient = AccessClientHelper.ToAccessClient(_currentContext.ClientType, orgAdmin);
var access = await _secretRepository.AccessToSecretAsync(id, userId, accessClient);
if (!access.Read)
{
throw new NotFoundException();
}
if (_currentContext.ClientType == ClientType.ServiceAccount)
{
await _eventService.LogServiceAccountSecretEventAsync(userId, secret, EventType.Secret_Retrieved);
var org = await _organizationRepository.GetByIdAsync(secret.OrganizationId);
await _referenceEventService.RaiseEventAsync(new ReferenceEvent(ReferenceEventType.SmServiceAccountAccessedSecret, org, _currentContext));
}
return new SecretResponseModel(secret, access.Read, access.Write);
}
[HttpGet("projects/{projectId}/secrets")]
public async Task<SecretWithProjectsListResponseModel> GetSecretsByProjectAsync([FromRoute] Guid projectId)
{
var project = await _projectRepository.GetByIdAsync(projectId);
if (project == null || !_currentContext.AccessSecretsManager(project.OrganizationId))
{
throw new NotFoundException();
}
var userId = _userService.GetProperUserId(User).Value;
var orgAdmin = await _currentContext.OrganizationAdmin(project.OrganizationId);
var accessClient = AccessClientHelper.ToAccessClient(_currentContext.ClientType, orgAdmin);
var secrets = await _secretRepository.GetManyDetailsByProjectIdAsync(projectId, userId, accessClient);
return new SecretWithProjectsListResponseModel(secrets);
}
[HttpPut("secrets/{id}")]
public async Task<SecretResponseModel> UpdateSecretAsync([FromRoute] Guid id, [FromBody] SecretUpdateRequestModel updateRequest)
{
var secret = await _secretRepository.GetByIdAsync(id);
if (secret == null)
{
throw new NotFoundException();
}
var updatedSecret = updateRequest.ToSecret(secret);
var authorizationResult = await _authorizationService.AuthorizeAsync(User, updatedSecret, SecretOperations.Update);
if (!authorizationResult.Succeeded)
{
throw new NotFoundException();
}
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);
}
[HttpPost("secrets/delete")]
public async Task<ListResponseModel<BulkDeleteResponseModel>> BulkDeleteAsync([FromBody] List<Guid> ids)
{
var secrets = (await _secretRepository.GetManyByIds(ids)).ToList();
if (!secrets.Any() || secrets.Count != ids.Count)
{
throw new NotFoundException();
}
// Ensure all secrets belong to the same organization.
var organizationId = secrets.First().OrganizationId;
if (secrets.Any(secret => secret.OrganizationId != organizationId) ||
!_currentContext.AccessSecretsManager(organizationId))
{
throw new NotFoundException();
}
var secretsToDelete = new List<Secret>();
var results = new List<(Secret Secret, string Error)>();
foreach (var secret in secrets)
{
var authorizationResult =
await _authorizationService.AuthorizeAsync(User, secret, SecretOperations.Delete);
if (authorizationResult.Succeeded)
{
secretsToDelete.Add(secret);
results.Add((secret, ""));
}
else
{
results.Add((secret, "access denied"));
}
}
await _deleteSecretCommand.DeleteSecrets(secretsToDelete);
var responses = results.Select(r => new BulkDeleteResponseModel(r.Secret.Id, r.Error));
return new ListResponseModel<BulkDeleteResponseModel>(responses);
}
[HttpPost("secrets/get-by-ids")]
public async Task<ListResponseModel<BaseSecretResponseModel>> GetSecretsByIdsAsync(
[FromBody] GetSecretsRequestModel request)
{
var secrets = (await _secretRepository.GetManyByIds(request.Ids)).ToList();
if (!secrets.Any() || secrets.Count != request.Ids.Count())
{
throw new NotFoundException();
}
// Ensure all secrets belong to the same organization.
var organizationId = secrets.First().OrganizationId;
if (secrets.Any(secret => secret.OrganizationId != organizationId) ||
!_currentContext.AccessSecretsManager(organizationId))
{
throw new NotFoundException();
}
foreach (var secret in secrets)
{
var authorizationResult = await _authorizationService.AuthorizeAsync(User, secret, SecretOperations.Read);
if (!authorizationResult.Succeeded)
{
throw new NotFoundException();
}
}
await LogSecretsRetrievalAsync(organizationId, secrets);
var responses = secrets.Select(s => new BaseSecretResponseModel(s));
return new ListResponseModel<BaseSecretResponseModel>(responses);
}
[HttpGet("/organizations/{organizationId}/secrets/sync")]
public async Task<SecretsSyncResponseModel> GetSecretsSyncAsync([FromRoute] Guid organizationId,
[FromQuery] DateTime? lastSyncedDate = null)
{
if (lastSyncedDate.HasValue && lastSyncedDate.Value > DateTime.UtcNow)
{
throw new BadRequestException("Last synced date must be in the past.");
}
if (!_currentContext.AccessSecretsManager(organizationId))
{
throw new NotFoundException();
}
var (accessClient, serviceAccountId) = await _accessClientQuery.GetAccessClientAsync(User, organizationId);
if (accessClient != AccessClientType.ServiceAccount)
{
throw new BadRequestException("Only service accounts can sync secrets.");
}
var syncRequest = new SecretsSyncRequest
{
AccessClientType = accessClient,
OrganizationId = organizationId,
ServiceAccountId = serviceAccountId,
LastSyncedDate = lastSyncedDate
};
var syncResult = await _secretsSyncQuery.GetAsync(syncRequest);
if (syncResult.HasChanges)
{
await LogSecretsRetrievalAsync(organizationId, syncResult.Secrets);
}
return new SecretsSyncResponseModel(syncResult.HasChanges, syncResult.Secrets);
}
private async Task LogSecretsRetrievalAsync(Guid organizationId, IEnumerable<Secret> secrets)
{
if (_currentContext.ClientType == ClientType.ServiceAccount)
{
var userId = _userService.GetProperUserId(User)!.Value;
var org = await _organizationRepository.GetByIdAsync(organizationId);
await _eventService.LogServiceAccountSecretsEventAsync(userId, secrets, EventType.Secret_Retrieved);
await _referenceEventService.RaiseEventAsync(
new ReferenceEvent(ReferenceEventType.SmServiceAccountAccessedSecret, org, _currentContext));
}
}
}