mirror of
https://github.com/bitwarden/server.git
synced 2025-06-30 07:36:14 -05: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:
@ -22,6 +22,7 @@ public class SecretsControllerTests : IClassFixture<ApiApplicationFactory>, IAsy
|
||||
private readonly ApiApplicationFactory _factory;
|
||||
private readonly ISecretRepository _secretRepository;
|
||||
private readonly IProjectRepository _projectRepository;
|
||||
private readonly IServiceAccountRepository _serviceAccountRepository;
|
||||
private readonly IAccessPolicyRepository _accessPolicyRepository;
|
||||
private readonly LoginHelper _loginHelper;
|
||||
|
||||
@ -35,6 +36,7 @@ public class SecretsControllerTests : IClassFixture<ApiApplicationFactory>, IAsy
|
||||
_secretRepository = _factory.GetService<ISecretRepository>();
|
||||
_projectRepository = _factory.GetService<IProjectRepository>();
|
||||
_accessPolicyRepository = _factory.GetService<IAccessPolicyRepository>();
|
||||
_serviceAccountRepository = _factory.GetService<IServiceAccountRepository>();
|
||||
_loginHelper = new LoginHelper(_factory, _client);
|
||||
}
|
||||
|
||||
@ -264,6 +266,7 @@ public class SecretsControllerTests : IClassFixture<ApiApplicationFactory>, IAsy
|
||||
{
|
||||
var (email, orgUser) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);
|
||||
await _loginHelper.LoginAsync(email);
|
||||
await _loginHelper.LoginAsync(email);
|
||||
accessType = AccessClientType.User;
|
||||
|
||||
var accessPolicies = new List<BaseAccessPolicy>
|
||||
@ -288,7 +291,7 @@ public class SecretsControllerTests : IClassFixture<ApiApplicationFactory>, IAsy
|
||||
secretResponse.EnsureSuccessStatusCode();
|
||||
var secretResult = await secretResponse.Content.ReadFromJsonAsync<SecretResponseModel>();
|
||||
|
||||
var result = (await _secretRepository.GetManyByProjectIdAsync(project.Id, orgUserId, accessType)).First();
|
||||
var result = (await _secretRepository.GetManyDetailsByProjectIdAsync(project.Id, orgUserId, accessType)).First();
|
||||
var secret = result.Secret;
|
||||
|
||||
Assert.NotNull(secretResult);
|
||||
@ -392,6 +395,7 @@ public class SecretsControllerTests : IClassFixture<ApiApplicationFactory>, IAsy
|
||||
{
|
||||
var (org, _) = await _organizationHelper.Initialize(useSecrets, accessSecrets, organizationEnabled);
|
||||
await _loginHelper.LoginAsync(_email);
|
||||
await _loginHelper.LoginAsync(_email);
|
||||
|
||||
var project = await _projectRepository.CreateAsync(new Project
|
||||
{
|
||||
@ -784,6 +788,106 @@ public class SecretsControllerTests : IClassFixture<ApiApplicationFactory>, IAsy
|
||||
Assert.Equal(secretIds.Count, result.Data.Count());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false, false, false)]
|
||||
[InlineData(false, false, true)]
|
||||
[InlineData(false, true, false)]
|
||||
[InlineData(false, true, true)]
|
||||
[InlineData(true, false, false)]
|
||||
[InlineData(true, false, true)]
|
||||
[InlineData(true, true, false)]
|
||||
public async Task GetSecretsSyncAsync_SmAccessDenied_NotFound(bool useSecrets, bool accessSecrets,
|
||||
bool organizationEnabled)
|
||||
{
|
||||
var (org, _) = await _organizationHelper.Initialize(useSecrets, accessSecrets, organizationEnabled);
|
||||
await _loginHelper.LoginAsync(_email);
|
||||
|
||||
var response = await _client.GetAsync($"/organizations/{org.Id}/secrets/sync");
|
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetSecretsSyncAsync_UserClient_BadRequest()
|
||||
{
|
||||
var (org, _) = await _organizationHelper.Initialize(true, true, true);
|
||||
await _loginHelper.LoginAsync(_email);
|
||||
|
||||
var response = await _client.GetAsync($"/organizations/{org.Id}/secrets/sync");
|
||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(true)]
|
||||
[InlineData(false)]
|
||||
public async Task GetSecretsSyncAsync_NoSecrets_ReturnsEmptyList(bool useLastSyncedDate)
|
||||
{
|
||||
var (org, _) = await _organizationHelper.Initialize(true, true, true);
|
||||
var apiKeyDetails = await _organizationHelper.CreateNewServiceAccountApiKeyAsync();
|
||||
await _loginHelper.LoginWithApiKeyAsync(apiKeyDetails);
|
||||
|
||||
var requestUrl = $"/organizations/{org.Id}/secrets/sync";
|
||||
if (useLastSyncedDate)
|
||||
{
|
||||
requestUrl = $"/organizations/{org.Id}/secrets/sync?lastSyncedDate={DateTime.UtcNow.AddDays(-1)}";
|
||||
}
|
||||
|
||||
var response = await _client.GetAsync(requestUrl);
|
||||
response.EnsureSuccessStatusCode();
|
||||
var result = await response.Content.ReadFromJsonAsync<SecretsSyncResponseModel>();
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.True(result.HasChanges);
|
||||
Assert.NotNull(result.Secrets);
|
||||
Assert.Empty(result.Secrets.Data);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(true)]
|
||||
[InlineData(false)]
|
||||
public async Task GetSecretsSyncAsync_HasSecrets_ReturnsAll(bool useLastSyncedDate)
|
||||
{
|
||||
var (org, _) = await _organizationHelper.Initialize(true, true, true);
|
||||
var apiKeyDetails = await _organizationHelper.CreateNewServiceAccountApiKeyAsync();
|
||||
await _loginHelper.LoginWithApiKeyAsync(apiKeyDetails);
|
||||
var secretIds = await SetupSecretsSyncRequestAsync(org.Id, apiKeyDetails.ApiKey.ServiceAccountId!.Value);
|
||||
|
||||
var requestUrl = $"/organizations/{org.Id}/secrets/sync";
|
||||
if (useLastSyncedDate)
|
||||
{
|
||||
requestUrl = $"/organizations/{org.Id}/secrets/sync?lastSyncedDate={DateTime.UtcNow.AddDays(-1)}";
|
||||
}
|
||||
|
||||
var response = await _client.GetAsync(requestUrl);
|
||||
response.EnsureSuccessStatusCode();
|
||||
var result = await response.Content.ReadFromJsonAsync<SecretsSyncResponseModel>();
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.True(result.HasChanges);
|
||||
Assert.NotNull(result.Secrets);
|
||||
Assert.NotEmpty(result.Secrets.Data);
|
||||
Assert.Equal(secretIds.Count, result.Secrets.Data.Count());
|
||||
Assert.All(result.Secrets.Data, item => Assert.Contains(item.Id, secretIds));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetSecretsSyncAsync_ServiceAccountNotRevised_ReturnsNoChanges()
|
||||
{
|
||||
var (org, _) = await _organizationHelper.Initialize(true, true, true);
|
||||
var apiKeyDetails = await _organizationHelper.CreateNewServiceAccountApiKeyAsync();
|
||||
var serviceAccountId = apiKeyDetails.ApiKey.ServiceAccountId!.Value;
|
||||
await _loginHelper.LoginWithApiKeyAsync(apiKeyDetails);
|
||||
await SetupSecretsSyncRequestAsync(org.Id, serviceAccountId);
|
||||
await UpdateServiceAccountRevisionAsync(serviceAccountId, DateTime.UtcNow.AddDays(-1));
|
||||
|
||||
var response = await _client.GetAsync($"/organizations/{org.Id}/secrets/sync?lastSyncedDate={DateTime.UtcNow}");
|
||||
response.EnsureSuccessStatusCode();
|
||||
var result = await response.Content.ReadFromJsonAsync<SecretsSyncResponseModel>();
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.False(result.HasChanges);
|
||||
Assert.Null(result.Secrets);
|
||||
}
|
||||
|
||||
private async Task<(Project Project, List<Guid> secretIds)> CreateSecretsAsync(Guid orgId, int numberToCreate = 3)
|
||||
{
|
||||
var project = await _projectRepository.CreateAsync(new Project
|
||||
@ -853,4 +957,25 @@ public class SecretsControllerTests : IClassFixture<ApiApplicationFactory>, IAsy
|
||||
throw new ArgumentOutOfRangeException(nameof(permissionType), permissionType, null);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<List<Guid>> SetupSecretsSyncRequestAsync(Guid organizationId, Guid serviceAccountId)
|
||||
{
|
||||
var (project, secretIds) = await CreateSecretsAsync(organizationId);
|
||||
var accessPolicies = new List<BaseAccessPolicy>
|
||||
{
|
||||
new ServiceAccountProjectAccessPolicy
|
||||
{
|
||||
GrantedProjectId = project.Id, ServiceAccountId = serviceAccountId, Read = true, Write = true
|
||||
}
|
||||
};
|
||||
await _accessPolicyRepository.CreateManyAsync(accessPolicies);
|
||||
return secretIds;
|
||||
}
|
||||
|
||||
private async Task UpdateServiceAccountRevisionAsync(Guid serviceAccountId, DateTime revisionDate)
|
||||
{
|
||||
var sa = await _serviceAccountRepository.GetByIdAsync(serviceAccountId);
|
||||
sa.RevisionDate = revisionDate;
|
||||
await _serviceAccountRepository.ReplaceAsync(sa);
|
||||
}
|
||||
}
|
||||
|
@ -8,6 +8,8 @@ using Bit.Core.Exceptions;
|
||||
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.Test.SecretsManager.AutoFixture.SecretsFixture;
|
||||
@ -37,7 +39,7 @@ public class SecretsControllerTests
|
||||
var result = await sutProvider.Sut.ListByOrganizationAsync(id);
|
||||
|
||||
await sutProvider.GetDependency<ISecretRepository>().Received(1)
|
||||
.GetManyByOrganizationIdAsync(Arg.Is(AssertHelper.AssertPropertyEqual(id)), userId, accessType);
|
||||
.GetManyDetailsByOrganizationIdAsync(Arg.Is(AssertHelper.AssertPropertyEqual(id)), userId, accessType);
|
||||
|
||||
Assert.Empty(result.Secrets);
|
||||
}
|
||||
@ -45,10 +47,10 @@ public class SecretsControllerTests
|
||||
[Theory]
|
||||
[BitAutoData(PermissionType.RunAsAdmin)]
|
||||
[BitAutoData(PermissionType.RunAsUserWithPermission)]
|
||||
public async Task GetSecretsByOrganization_Success(PermissionType permissionType, SutProvider<SecretsController> sutProvider, Core.SecretsManager.Entities.Secret resultSecret, Guid organizationId, Guid userId, Core.SecretsManager.Entities.Project mockProject, AccessClientType accessType)
|
||||
public async Task GetSecretsByOrganization_Success(PermissionType permissionType, SutProvider<SecretsController> sutProvider, Secret resultSecret, Guid organizationId, Guid userId, Project mockProject, AccessClientType accessType)
|
||||
{
|
||||
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(default).ReturnsForAnyArgs(true);
|
||||
sutProvider.GetDependency<ISecretRepository>().GetManyByOrganizationIdAsync(default, default, default)
|
||||
sutProvider.GetDependency<ISecretRepository>().GetManyDetailsByOrganizationIdAsync(default, default, default)
|
||||
.ReturnsForAnyArgs(new List<SecretPermissionDetails>
|
||||
{
|
||||
new() { Secret = resultSecret, Read = true, Write = true },
|
||||
@ -61,22 +63,22 @@ public class SecretsControllerTests
|
||||
}
|
||||
else
|
||||
{
|
||||
resultSecret.Projects = new List<Core.SecretsManager.Entities.Project>() { mockProject };
|
||||
resultSecret.Projects = new List<Project>() { mockProject };
|
||||
sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(organizationId).Returns(false);
|
||||
sutProvider.GetDependency<IProjectRepository>().AccessToProjectAsync(default, default, default)
|
||||
.Returns((true, true));
|
||||
}
|
||||
|
||||
|
||||
var result = await sutProvider.Sut.ListByOrganizationAsync(resultSecret.OrganizationId);
|
||||
await sutProvider.Sut.ListByOrganizationAsync(resultSecret.OrganizationId);
|
||||
|
||||
await sutProvider.GetDependency<ISecretRepository>().Received(1)
|
||||
.GetManyByOrganizationIdAsync(Arg.Is(AssertHelper.AssertPropertyEqual(resultSecret.OrganizationId)), userId, accessType);
|
||||
.GetManyDetailsByOrganizationIdAsync(Arg.Is(AssertHelper.AssertPropertyEqual(resultSecret.OrganizationId)), userId, accessType);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetSecretsByOrganization_AccessDenied_Throws(SutProvider<SecretsController> sutProvider, Core.SecretsManager.Entities.Secret resultSecret)
|
||||
public async Task GetSecretsByOrganization_AccessDenied_Throws(SutProvider<SecretsController> sutProvider, Secret resultSecret)
|
||||
{
|
||||
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(default).ReturnsForAnyArgs(false);
|
||||
|
||||
@ -429,6 +431,76 @@ public class SecretsControllerTests
|
||||
Assert.Equal(data.Count, results.Data.Count());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(true)]
|
||||
[BitAutoData(false)]
|
||||
public async Task GetSecretsSyncAsync_AccessSecretsManagerFalse_ThrowsNotFound(
|
||||
bool nullLastSyncedDate,
|
||||
SutProvider<SecretsController> sutProvider, Guid organizationId)
|
||||
{
|
||||
var lastSyncedDate = GetLastSyncedDate(nullLastSyncedDate);
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(Arg.Is(organizationId))
|
||||
.ReturnsForAnyArgs(false);
|
||||
|
||||
await Assert.ThrowsAsync<NotFoundException>(() =>
|
||||
sutProvider.Sut.GetSecretsSyncAsync(organizationId, lastSyncedDate));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(true, AccessClientType.NoAccessCheck)]
|
||||
[BitAutoData(true, AccessClientType.User)]
|
||||
[BitAutoData(true, AccessClientType.Organization)]
|
||||
[BitAutoData(false, AccessClientType.NoAccessCheck)]
|
||||
[BitAutoData(false, AccessClientType.User)]
|
||||
[BitAutoData(false, AccessClientType.Organization)]
|
||||
public async Task GetSecretsSyncAsync_AccessClientIsNotAServiceAccount_ThrowsBadRequest(
|
||||
bool nullLastSyncedDate,
|
||||
AccessClientType accessClientType,
|
||||
SutProvider<SecretsController> sutProvider, Guid organizationId)
|
||||
{
|
||||
var lastSyncedDate = GetLastSyncedDate(nullLastSyncedDate);
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(Arg.Is(organizationId))
|
||||
.ReturnsForAnyArgs(true);
|
||||
sutProvider.GetDependency<IAccessClientQuery>()
|
||||
.GetAccessClientAsync(Arg.Any<ClaimsPrincipal>(), Arg.Any<Guid>())
|
||||
.Returns((accessClientType, new Guid()));
|
||||
|
||||
await Assert.ThrowsAsync<BadRequestException>(() =>
|
||||
sutProvider.Sut.GetSecretsSyncAsync(organizationId, lastSyncedDate));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetSecretsSyncAsync_LastSyncedInFuture_ThrowsBadRequest(
|
||||
List<Secret> secrets,
|
||||
SutProvider<SecretsController> sutProvider, Guid organizationId)
|
||||
{
|
||||
DateTime? lastSyncedDate = DateTime.UtcNow.AddDays(3);
|
||||
|
||||
SetupSecretsSyncRequest(false, secrets, sutProvider, organizationId);
|
||||
|
||||
await Assert.ThrowsAsync<BadRequestException>(() =>
|
||||
sutProvider.Sut.GetSecretsSyncAsync(organizationId, lastSyncedDate));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(true)]
|
||||
[BitAutoData(false)]
|
||||
public async Task GetSecretsSyncAsync_AccessClientIsAServiceAccount_Success(
|
||||
bool nullLastSyncedDate,
|
||||
List<Secret> secrets,
|
||||
SutProvider<SecretsController> sutProvider, Guid organizationId)
|
||||
{
|
||||
var lastSyncedDate = SetupSecretsSyncRequest(nullLastSyncedDate, secrets, sutProvider, organizationId);
|
||||
|
||||
var result = await sutProvider.Sut.GetSecretsSyncAsync(organizationId, lastSyncedDate);
|
||||
Assert.True(result.HasChanges);
|
||||
Assert.NotNull(result.Secrets);
|
||||
Assert.NotEmpty(result.Secrets.Data);
|
||||
}
|
||||
|
||||
private static (List<Guid> Ids, GetSecretsRequestModel request) BuildGetSecretsRequestModel(
|
||||
IEnumerable<Secret> data)
|
||||
{
|
||||
@ -447,4 +519,24 @@ public class SecretsControllerTests
|
||||
|
||||
return organizationId;
|
||||
}
|
||||
|
||||
private static DateTime? SetupSecretsSyncRequest(bool nullLastSyncedDate, List<Secret> secrets,
|
||||
SutProvider<SecretsController> sutProvider, Guid organizationId)
|
||||
{
|
||||
var lastSyncedDate = GetLastSyncedDate(nullLastSyncedDate);
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(Arg.Is(organizationId))
|
||||
.ReturnsForAnyArgs(true);
|
||||
sutProvider.GetDependency<IAccessClientQuery>()
|
||||
.GetAccessClientAsync(Arg.Any<ClaimsPrincipal>(), Arg.Any<Guid>())
|
||||
.Returns((AccessClientType.ServiceAccount, new Guid()));
|
||||
sutProvider.GetDependency<ISecretsSyncQuery>().GetAsync(Arg.Any<SecretsSyncRequest>())
|
||||
.Returns((true, secrets));
|
||||
return lastSyncedDate;
|
||||
}
|
||||
|
||||
private static DateTime? GetLastSyncedDate(bool nullLastSyncedDate)
|
||||
{
|
||||
return nullLastSyncedDate ? null : DateTime.UtcNow.AddDays(-1);
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user