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)); } } }