1
0
mirror of https://github.com/bitwarden/server synced 2025-12-26 05:03:18 +00:00
Files
server/src/Api/SecretsManager/Controllers/ServiceAccountsController.cs
cd-bitwarden bca1d585c5 [SM-1489] machine account events (#6187)
* Adding new logging for secrets

* fixing secrest controller tests

* fixing the tests

* Server side changes for adding ProjectId to Event table, adding Project event logging to projectsController

* Rough draft with TODO's need to work on EventRepository.cs, and ProjectRepository.cs

* Undoing changes to make projects soft delete, we want those to be fully deleted still. Adding GetManyTrashedSecretsByIds to secret repo so we can get soft deleted secrets, getSecrets in eventsController takes in orgdId, so that we can check the permission even if the secret was permanently deleted and doesn' thave the org Id set. Adding Secret Perm Deleted, and Restored to event logs

* db changes

* fixing the way we log events

* Trying to undo some manual changes that should have been migrations

* adding migration files

* fixing test

* setting up userid for project controller tests

* adding sql

* sql

* Rename file

* Trying to get it to for sure add the column before we try and update sprocs

* Adding code to refresh the view to include ProjectId I hope

* code improvements

* Suggested changes

* suggested changes

* trying to fix sql issues

* fixing swagger issue

* Update src/Core/SecretsManager/Repositories/Noop/NoopSecretRepository.cs

Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com>

* Suggested changes

* Adding event logging for machine accounts

* fixing two tests

* trying to fix all tests

* trying to fix tests

* fixing test

* Migrations

* fix

* updating eps

* adding migration

* Adding missing SQL changes

* updating sql

* fixing sql

* running migration again

* fixing sql

* adding query to add grantedSErviceAccountId to event table

* Suggested improvements

* removing more migrations

* more removal

* removing all migrations to them redo them

* redoing migration

---------

Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com>
2025-10-01 13:13:49 +00:00

272 lines
12 KiB
C#

// FIXME: Update this file to be null safe and then delete the line below
#nullable disable
using Bit.Api.Models.Response;
using Bit.Api.SecretsManager.Models.Request;
using Bit.Api.SecretsManager.Models.Response;
using Bit.Core.Billing.Pricing;
using Bit.Core.Context;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Business;
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
using Bit.Core.Repositories;
using Bit.Core.SecretsManager.AuthorizationRequirements;
using Bit.Core.SecretsManager.Commands.AccessTokens.Interfaces;
using Bit.Core.SecretsManager.Commands.ServiceAccounts.Interfaces;
using Bit.Core.SecretsManager.Entities;
using Bit.Core.SecretsManager.Queries.ServiceAccounts.Interfaces;
using Bit.Core.SecretsManager.Repositories;
using Bit.Core.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Bit.Api.SecretsManager.Controllers;
[Authorize("secrets")]
[Route("service-accounts")]
public class ServiceAccountsController : Controller
{
private readonly ICurrentContext _currentContext;
private readonly IUserService _userService;
private readonly IAuthorizationService _authorizationService;
private readonly IServiceAccountRepository _serviceAccountRepository;
private readonly IApiKeyRepository _apiKeyRepository;
private readonly IOrganizationRepository _organizationRepository;
private readonly ICountNewServiceAccountSlotsRequiredQuery _countNewServiceAccountSlotsRequiredQuery;
private readonly IUpdateSecretsManagerSubscriptionCommand _updateSecretsManagerSubscriptionCommand;
private readonly IServiceAccountSecretsDetailsQuery _serviceAccountSecretsDetailsQuery;
private readonly ICreateAccessTokenCommand _createAccessTokenCommand;
private readonly ICreateServiceAccountCommand _createServiceAccountCommand;
private readonly IUpdateServiceAccountCommand _updateServiceAccountCommand;
private readonly IDeleteServiceAccountsCommand _deleteServiceAccountsCommand;
private readonly IRevokeAccessTokensCommand _revokeAccessTokensCommand;
private readonly IPricingClient _pricingClient;
private readonly IEventService _eventService;
private readonly IOrganizationUserRepository _organizationUserRepository;
public ServiceAccountsController(
ICurrentContext currentContext,
IUserService userService,
IAuthorizationService authorizationService,
IServiceAccountRepository serviceAccountRepository,
IApiKeyRepository apiKeyRepository,
IOrganizationRepository organizationRepository,
ICountNewServiceAccountSlotsRequiredQuery countNewServiceAccountSlotsRequiredQuery,
IUpdateSecretsManagerSubscriptionCommand updateSecretsManagerSubscriptionCommand,
IServiceAccountSecretsDetailsQuery serviceAccountSecretsDetailsQuery,
ICreateAccessTokenCommand createAccessTokenCommand,
ICreateServiceAccountCommand createServiceAccountCommand,
IUpdateServiceAccountCommand updateServiceAccountCommand,
IDeleteServiceAccountsCommand deleteServiceAccountsCommand,
IRevokeAccessTokensCommand revokeAccessTokensCommand,
IPricingClient pricingClient,
IEventService eventService,
IOrganizationUserRepository organizationUserRepository)
{
_currentContext = currentContext;
_userService = userService;
_authorizationService = authorizationService;
_serviceAccountRepository = serviceAccountRepository;
_apiKeyRepository = apiKeyRepository;
_organizationRepository = organizationRepository;
_countNewServiceAccountSlotsRequiredQuery = countNewServiceAccountSlotsRequiredQuery;
_serviceAccountSecretsDetailsQuery = serviceAccountSecretsDetailsQuery;
_createServiceAccountCommand = createServiceAccountCommand;
_updateServiceAccountCommand = updateServiceAccountCommand;
_deleteServiceAccountsCommand = deleteServiceAccountsCommand;
_revokeAccessTokensCommand = revokeAccessTokensCommand;
_pricingClient = pricingClient;
_createAccessTokenCommand = createAccessTokenCommand;
_updateSecretsManagerSubscriptionCommand = updateSecretsManagerSubscriptionCommand;
_eventService = eventService;
_organizationUserRepository = organizationUserRepository;
}
[HttpGet("/organizations/{organizationId}/service-accounts")]
public async Task<ListResponseModel<ServiceAccountSecretsDetailsResponseModel>> ListByOrganizationAsync(
[FromRoute] Guid organizationId, [FromQuery] bool includeAccessToSecrets = false)
{
if (!_currentContext.AccessSecretsManager(organizationId))
{
throw new NotFoundException();
}
var userId = _userService.GetProperUserId(User).Value;
var orgAdmin = await _currentContext.OrganizationAdmin(organizationId);
var accessClient = AccessClientHelper.ToAccessClient(_currentContext.IdentityClientType, orgAdmin);
var results =
await _serviceAccountSecretsDetailsQuery.GetManyByOrganizationIdAsync(organizationId, userId, accessClient,
includeAccessToSecrets);
var responses = results.Select(r => new ServiceAccountSecretsDetailsResponseModel(r));
return new ListResponseModel<ServiceAccountSecretsDetailsResponseModel>(responses);
}
[HttpGet("{id}")]
public async Task<ServiceAccountResponseModel> GetByServiceAccountIdAsync(
[FromRoute] Guid id)
{
var serviceAccount = await _serviceAccountRepository.GetByIdAsync(id);
var authorizationResult =
await _authorizationService.AuthorizeAsync(User, serviceAccount, ServiceAccountOperations.Read);
if (!authorizationResult.Succeeded)
{
throw new NotFoundException();
}
return new ServiceAccountResponseModel(serviceAccount);
}
[HttpPost("/organizations/{organizationId}/service-accounts")]
public async Task<ServiceAccountResponseModel> CreateAsync([FromRoute] Guid organizationId,
[FromBody] ServiceAccountCreateRequestModel createRequest)
{
var serviceAccount = createRequest.ToServiceAccount(organizationId);
var authorizationResult =
await _authorizationService.AuthorizeAsync(User, serviceAccount, ServiceAccountOperations.Create);
if (!authorizationResult.Succeeded)
{
throw new NotFoundException();
}
var newServiceAccountSlotsRequired = await _countNewServiceAccountSlotsRequiredQuery
.CountNewServiceAccountSlotsRequiredAsync(organizationId, 1);
if (newServiceAccountSlotsRequired > 0)
{
var org = await _organizationRepository.GetByIdAsync(organizationId);
// TODO: https://bitwarden.atlassian.net/browse/PM-17002
var plan = await _pricingClient.GetPlanOrThrow(org!.PlanType);
var update = new SecretsManagerSubscriptionUpdate(org, plan, true)
.AdjustServiceAccounts(newServiceAccountSlotsRequired);
await _updateSecretsManagerSubscriptionCommand.UpdateSubscriptionAsync(update);
}
var userId = _userService.GetProperUserId(User).Value;
var result =
await _createServiceAccountCommand.CreateAsync(serviceAccount, userId);
if (result != null)
{
await _eventService.LogServiceAccountEventAsync(userId, [serviceAccount], EventType.ServiceAccount_Created, _currentContext.IdentityClientType);
}
return new ServiceAccountResponseModel(result);
}
[HttpPut("{id}")]
public async Task<ServiceAccountResponseModel> UpdateAsync([FromRoute] Guid id,
[FromBody] ServiceAccountUpdateRequestModel updateRequest)
{
var serviceAccount = await _serviceAccountRepository.GetByIdAsync(id);
var authorizationResult =
await _authorizationService.AuthorizeAsync(User, serviceAccount, ServiceAccountOperations.Update);
if (!authorizationResult.Succeeded)
{
throw new NotFoundException();
}
var result = await _updateServiceAccountCommand.UpdateAsync(updateRequest.ToServiceAccount(id));
return new ServiceAccountResponseModel(result);
}
[HttpPost("delete")]
public async Task<ListResponseModel<BulkDeleteResponseModel>> BulkDeleteAsync([FromBody] List<Guid> ids)
{
var serviceAccounts = (await _serviceAccountRepository.GetManyByIds(ids)).ToList();
if (!serviceAccounts.Any() || serviceAccounts.Count != ids.Count)
{
throw new NotFoundException();
}
// Ensure all service accounts belong to the same organization
var organizationId = serviceAccounts.First().OrganizationId;
if (serviceAccounts.Any(sa => sa.OrganizationId != organizationId) ||
!_currentContext.AccessSecretsManager(organizationId))
{
throw new NotFoundException();
}
var serviceAccountsToDelete = new List<ServiceAccount>();
var results = new List<(ServiceAccount ServiceAccount, string Error)>();
foreach (var serviceAccount in serviceAccounts)
{
var authorizationResult =
await _authorizationService.AuthorizeAsync(User, serviceAccount, ServiceAccountOperations.Delete);
if (authorizationResult.Succeeded)
{
serviceAccountsToDelete.Add(serviceAccount);
results.Add((serviceAccount, ""));
}
else
{
results.Add((serviceAccount, "access denied"));
}
}
await _deleteServiceAccountsCommand.DeleteServiceAccounts(serviceAccountsToDelete);
var userId = _userService.GetProperUserId(User)!.Value;
await _eventService.LogServiceAccountEventAsync(userId, serviceAccountsToDelete, EventType.ServiceAccount_Deleted, _currentContext.IdentityClientType);
var responses = results.Select(r => new BulkDeleteResponseModel(r.ServiceAccount.Id, r.Error));
return new ListResponseModel<BulkDeleteResponseModel>(responses);
}
[HttpGet("{id}/access-tokens")]
public async Task<ListResponseModel<AccessTokenResponseModel>> GetAccessTokens([FromRoute] Guid id)
{
var serviceAccount = await _serviceAccountRepository.GetByIdAsync(id);
var authorizationResult =
await _authorizationService.AuthorizeAsync(User, serviceAccount,
ServiceAccountOperations.ReadAccessTokens);
if (!authorizationResult.Succeeded)
{
throw new NotFoundException();
}
var accessTokens = await _apiKeyRepository.GetManyByServiceAccountIdAsync(id);
var responses = accessTokens.Select(token => new AccessTokenResponseModel(token));
return new ListResponseModel<AccessTokenResponseModel>(responses);
}
[HttpPost("{id}/access-tokens")]
public async Task<AccessTokenCreationResponseModel> CreateAccessTokenAsync([FromRoute] Guid id,
[FromBody] AccessTokenCreateRequestModel request)
{
var serviceAccount = await _serviceAccountRepository.GetByIdAsync(id);
var authorizationResult =
await _authorizationService.AuthorizeAsync(User, serviceAccount,
ServiceAccountOperations.CreateAccessToken);
if (!authorizationResult.Succeeded)
{
throw new NotFoundException();
}
var result = await _createAccessTokenCommand.CreateAsync(request.ToApiKey(id));
return new AccessTokenCreationResponseModel(result);
}
[HttpPost("{id}/access-tokens/revoke")]
public async Task RevokeAccessTokensAsync(Guid id, [FromBody] RevokeAccessTokensRequest request)
{
var serviceAccount = await _serviceAccountRepository.GetByIdAsync(id);
var authorizationResult =
await _authorizationService.AuthorizeAsync(User, serviceAccount,
ServiceAccountOperations.RevokeAccessTokens);
if (!authorizationResult.Succeeded)
{
throw new NotFoundException();
}
await _revokeAccessTokensCommand.RevokeAsync(serviceAccount, request.Ids);
}
}