1
0
mirror of https://github.com/bitwarden/server synced 2026-02-21 03:43:44 +00:00

[SM-1150] Add secret sync endpoint (#3906)

* Add SecretsSyncQuery

* Add SecretsSync to controller

* Add unit tests

* Add integration tests

* update repo layer
This commit is contained in:
Thomas Avery
2024-04-25 10:34:08 -05:00
committed by GitHub
parent f7aa56b324
commit a7b992d424
16 changed files with 711 additions and 138 deletions

View File

@@ -9,6 +9,9 @@ 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.Queries.Interfaces;
using Bit.Core.SecretsManager.Queries.Secrets.Interfaces;
using Bit.Core.SecretsManager.Repositories;
using Bit.Core.Services;
using Bit.Core.Tools.Enums;
@@ -29,6 +32,8 @@ public class SecretsController : Controller
private readonly ICreateSecretCommand _createSecretCommand;
private readonly IUpdateSecretCommand _updateSecretCommand;
private readonly IDeleteSecretCommand _deleteSecretCommand;
private readonly IAccessClientQuery _accessClientQuery;
private readonly ISecretsSyncQuery _secretsSyncQuery;
private readonly IUserService _userService;
private readonly IEventService _eventService;
private readonly IReferenceEventService _referenceEventService;
@@ -42,6 +47,8 @@ public class SecretsController : Controller
ICreateSecretCommand createSecretCommand,
IUpdateSecretCommand updateSecretCommand,
IDeleteSecretCommand deleteSecretCommand,
IAccessClientQuery accessClientQuery,
ISecretsSyncQuery secretsSyncQuery,
IUserService userService,
IEventService eventService,
IReferenceEventService referenceEventService,
@@ -54,6 +61,8 @@ public class SecretsController : Controller
_createSecretCommand = createSecretCommand;
_updateSecretCommand = updateSecretCommand;
_deleteSecretCommand = deleteSecretCommand;
_accessClientQuery = accessClientQuery;
_secretsSyncQuery = secretsSyncQuery;
_userService = userService;
_eventService = eventService;
_referenceEventService = referenceEventService;
@@ -73,7 +82,7 @@ public class SecretsController : Controller
var orgAdmin = await _currentContext.OrganizationAdmin(organizationId);
var accessClient = AccessClientHelper.ToAccessClient(_currentContext.ClientType, orgAdmin);
var secrets = await _secretRepository.GetManyByOrganizationIdAsync(organizationId, userId, accessClient);
var secrets = await _secretRepository.GetManyDetailsByOrganizationIdAsync(organizationId, userId, accessClient);
return new SecretWithProjectsListResponseModel(secrets);
}
@@ -139,7 +148,7 @@ public class SecretsController : Controller
var orgAdmin = await _currentContext.OrganizationAdmin(project.OrganizationId);
var accessClient = AccessClientHelper.ToAccessClient(_currentContext.ClientType, orgAdmin);
var secrets = await _secretRepository.GetManyByProjectIdAsync(projectId, userId, accessClient);
var secrets = await _secretRepository.GetManyDetailsByProjectIdAsync(projectId, userId, accessClient);
return new SecretWithProjectsListResponseModel(secrets);
}
@@ -246,4 +255,35 @@ public class SecretsController : Controller
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 (hasChanges, secrets) = await _secretsSyncQuery.GetAsync(syncRequest);
return new SecretsSyncResponseModel(hasChanges, secrets);
}
}

View File

@@ -44,7 +44,7 @@ public class SecretsManagerPortingController : Controller
var userId = _userService.GetProperUserId(User).Value;
var projects = await _projectRepository.GetManyByOrganizationIdAsync(organizationId, userId, AccessClientType.NoAccessCheck);
var secrets = await _secretRepository.GetManyByOrganizationIdAsync(organizationId, userId, AccessClientType.NoAccessCheck);
var secrets = await _secretRepository.GetManyDetailsByOrganizationIdAsync(organizationId, userId, AccessClientType.NoAccessCheck);
if (projects == null && secrets == null)
{

View File

@@ -41,7 +41,7 @@ public class TrashController : Controller
throw new UnauthorizedAccessException();
}
var secrets = await _secretRepository.GetManyByOrganizationIdInTrashAsync(organizationId);
var secrets = await _secretRepository.GetManyDetailsByOrganizationIdInTrashAsync(organizationId);
return new SecretWithProjectsListResponseModel(secrets);
}

View File

@@ -0,0 +1,27 @@
#nullable enable
using Bit.Api.Models.Response;
using Bit.Core.Models.Api;
using Bit.Core.SecretsManager.Entities;
namespace Bit.Api.SecretsManager.Models.Response;
public class SecretsSyncResponseModel : ResponseModel
{
private const string _objectName = "secretsSync";
public bool HasChanges { get; set; }
public ListResponseModel<BaseSecretResponseModel>? Secrets { get; set; }
public SecretsSyncResponseModel(bool hasChanges, IEnumerable<Secret>? secrets, string obj = _objectName)
: base(obj)
{
Secrets = secrets != null
? new ListResponseModel<BaseSecretResponseModel>(secrets.Select(s => new BaseSecretResponseModel(s)))
: null;
HasChanges = hasChanges;
}
public SecretsSyncResponseModel() : base(_objectName)
{
}
}

View File

@@ -0,0 +1,12 @@
#nullable enable
using Bit.Core.Enums;
namespace Bit.Core.SecretsManager.Models.Data;
public class SecretsSyncRequest
{
public AccessClientType AccessClientType { get; set; }
public Guid OrganizationId { get; set; }
public Guid ServiceAccountId { get; set; }
public DateTime? LastSyncedDate { get; set; }
}

View File

@@ -0,0 +1,10 @@
#nullable enable
using Bit.Core.SecretsManager.Entities;
using Bit.Core.SecretsManager.Models.Data;
namespace Bit.Core.SecretsManager.Queries.Secrets.Interfaces;
public interface ISecretsSyncQuery
{
Task<(bool HasChanges, IEnumerable<Secret>? Secrets)> GetAsync(SecretsSyncRequest syncRequest);
}

View File

@@ -6,11 +6,12 @@ namespace Bit.Core.SecretsManager.Repositories;
public interface ISecretRepository
{
Task<IEnumerable<SecretPermissionDetails>> GetManyByOrganizationIdAsync(Guid organizationId, Guid userId, AccessClientType accessType);
Task<IEnumerable<SecretPermissionDetails>> GetManyByOrganizationIdInTrashAsync(Guid organizationId);
Task<IEnumerable<SecretPermissionDetails>> GetManyDetailsByOrganizationIdAsync(Guid organizationId, Guid userId, AccessClientType accessType);
Task<IEnumerable<SecretPermissionDetails>> GetManyDetailsByOrganizationIdInTrashAsync(Guid organizationId);
Task<IEnumerable<SecretPermissionDetails>> GetManyDetailsByProjectIdAsync(Guid projectId, Guid userId, AccessClientType accessType);
Task<IEnumerable<Secret>> GetManyByOrganizationIdAsync(Guid organizationId, Guid userId, AccessClientType accessType);
Task<IEnumerable<Secret>> GetManyByOrganizationIdInTrashByIdsAsync(Guid organizationId, IEnumerable<Guid> ids);
Task<IEnumerable<Secret>> GetManyByIds(IEnumerable<Guid> ids);
Task<IEnumerable<SecretPermissionDetails>> GetManyByProjectIdAsync(Guid projectId, Guid userId, AccessClientType accessType);
Task<Secret> GetByIdAsync(Guid id);
Task<Secret> CreateAsync(Secret secret);
Task<Secret> UpdateAsync(Secret secret);
@@ -18,7 +19,6 @@ public interface ISecretRepository
Task HardDeleteManyByIdAsync(IEnumerable<Guid> ids);
Task RestoreManyByIdAsync(IEnumerable<Guid> ids);
Task<IEnumerable<Secret>> ImportAsync(IEnumerable<Secret> secrets);
Task UpdateRevisionDates(IEnumerable<Guid> ids);
Task<(bool Read, bool Write)> AccessToSecretAsync(Guid id, Guid userId, AccessClientType accessType);
Task EmptyTrash(DateTime nowTime, uint deleteAfterThisNumberOfDays);
Task<int> GetSecretsCountByOrganizationIdAsync(Guid organizationId);

View File

@@ -6,17 +6,23 @@ namespace Bit.Core.SecretsManager.Repositories.Noop;
public class NoopSecretRepository : ISecretRepository
{
public Task<IEnumerable<SecretPermissionDetails>> GetManyByOrganizationIdAsync(Guid organizationId, Guid userId,
public Task<IEnumerable<SecretPermissionDetails>> GetManyDetailsByOrganizationIdAsync(Guid organizationId, Guid userId,
AccessClientType accessType)
{
return Task.FromResult(null as IEnumerable<SecretPermissionDetails>);
}
public Task<IEnumerable<SecretPermissionDetails>> GetManyByOrganizationIdInTrashAsync(Guid organizationId)
public Task<IEnumerable<SecretPermissionDetails>> GetManyDetailsByOrganizationIdInTrashAsync(Guid organizationId)
{
return Task.FromResult(null as IEnumerable<SecretPermissionDetails>);
}
public Task<IEnumerable<Secret>> GetManyByOrganizationIdAsync(Guid organizationId, Guid userId,
AccessClientType accessType)
{
return Task.FromResult(null as IEnumerable<Secret>);
}
public Task<IEnumerable<Secret>> GetManyByOrganizationIdInTrashByIdsAsync(Guid organizationId,
IEnumerable<Guid> ids)
{
@@ -28,7 +34,7 @@ public class NoopSecretRepository : ISecretRepository
return Task.FromResult(null as IEnumerable<Secret>);
}
public Task<IEnumerable<SecretPermissionDetails>> GetManyByProjectIdAsync(Guid projectId, Guid userId,
public Task<IEnumerable<SecretPermissionDetails>> GetManyDetailsByProjectIdAsync(Guid projectId, Guid userId,
AccessClientType accessType)
{
return Task.FromResult(null as IEnumerable<SecretPermissionDetails>);
@@ -69,11 +75,6 @@ public class NoopSecretRepository : ISecretRepository
return Task.FromResult(null as IEnumerable<Secret>);
}
public Task UpdateRevisionDates(IEnumerable<Guid> ids)
{
return Task.FromResult(0);
}
public Task<(bool Read, bool Write)> AccessToSecretAsync(Guid id, Guid userId, AccessClientType accessType)
{
return Task.FromResult((false, false));