mirror of
https://github.com/bitwarden/server.git
synced 2025-04-04 20:50:21 -05:00

* 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
336 lines
14 KiB
C#
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));
|
|
}
|
|
}
|
|
}
|