mirror of
https://github.com/bitwarden/server
synced 2026-01-03 00:53:37 +00:00
[SM-1592] API for Secret Versioning, adding controller, repository and tests (#6444)
* Adding SecretVersion table to server * making the names singular not plural for new table * removing migration * fixing migration * Adding indexes for serviceacct and orguserId * indexes for sqllite * fixing migrations * adding indexes to secretVeriosn.sql * tests * removing tests * adding GO * api repository and controller additions for SecretVersion table, as well as tests * test fix sqllite * improvements * removing comments * making files nullable safe * Justin Baurs suggested changes * claude suggestions * Claude fixes * test fixes
This commit is contained in:
337
src/Api/SecretsManager/Controllers/SecretVersionsController.cs
Normal file
337
src/Api/SecretsManager/Controllers/SecretVersionsController.cs
Normal file
@@ -0,0 +1,337 @@
|
||||
using Bit.Api.Models.Response;
|
||||
using Bit.Api.SecretsManager.Models.Request;
|
||||
using Bit.Api.SecretsManager.Models.Response;
|
||||
using Bit.Core.Auth.Identity;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.SecretsManager.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Bit.Api.SecretsManager.Controllers;
|
||||
|
||||
[Authorize("secrets")]
|
||||
public class SecretVersionsController : Controller
|
||||
{
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly ISecretVersionRepository _secretVersionRepository;
|
||||
private readonly ISecretRepository _secretRepository;
|
||||
private readonly IUserService _userService;
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
|
||||
public SecretVersionsController(
|
||||
ICurrentContext currentContext,
|
||||
ISecretVersionRepository secretVersionRepository,
|
||||
ISecretRepository secretRepository,
|
||||
IUserService userService,
|
||||
IOrganizationUserRepository organizationUserRepository)
|
||||
{
|
||||
_currentContext = currentContext;
|
||||
_secretVersionRepository = secretVersionRepository;
|
||||
_secretRepository = secretRepository;
|
||||
_userService = userService;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
}
|
||||
|
||||
[HttpGet("secrets/{secretId}/versions")]
|
||||
public async Task<ListResponseModel<SecretVersionResponseModel>> GetVersionsBySecretIdAsync([FromRoute] Guid secretId)
|
||||
{
|
||||
var secret = await _secretRepository.GetByIdAsync(secretId);
|
||||
if (secret == null || !_currentContext.AccessSecretsManager(secret.OrganizationId))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
// For service accounts and organization API, skip user-level access checks
|
||||
if (_currentContext.IdentityClientType == IdentityClientType.ServiceAccount ||
|
||||
_currentContext.IdentityClientType == IdentityClientType.Organization)
|
||||
{
|
||||
// Already verified Secrets Manager access above
|
||||
var versionList = await _secretVersionRepository.GetManyBySecretIdAsync(secretId);
|
||||
var responseList = versionList.Select(v => new SecretVersionResponseModel(v));
|
||||
return new ListResponseModel<SecretVersionResponseModel>(responseList);
|
||||
}
|
||||
|
||||
var userId = _userService.GetProperUserId(User);
|
||||
if (!userId.HasValue)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var orgAdmin = await _currentContext.OrganizationAdmin(secret.OrganizationId);
|
||||
var accessClient = AccessClientHelper.ToAccessClient(_currentContext.IdentityClientType, orgAdmin);
|
||||
|
||||
var access = await _secretRepository.AccessToSecretAsync(secretId, userId.Value, accessClient);
|
||||
if (!access.Read)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var versions = await _secretVersionRepository.GetManyBySecretIdAsync(secretId);
|
||||
var responses = versions.Select(v => new SecretVersionResponseModel(v));
|
||||
|
||||
return new ListResponseModel<SecretVersionResponseModel>(responses);
|
||||
}
|
||||
|
||||
[HttpGet("secret-versions/{id}")]
|
||||
public async Task<SecretVersionResponseModel> GetByIdAsync([FromRoute] Guid id)
|
||||
{
|
||||
var secretVersion = await _secretVersionRepository.GetByIdAsync(id);
|
||||
if (secretVersion == null)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var secret = await _secretRepository.GetByIdAsync(secretVersion.SecretId);
|
||||
if (secret == null || !_currentContext.AccessSecretsManager(secret.OrganizationId))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
// For service accounts and organization API, skip user-level access checks
|
||||
if (_currentContext.IdentityClientType == IdentityClientType.ServiceAccount ||
|
||||
_currentContext.IdentityClientType == IdentityClientType.Organization)
|
||||
{
|
||||
// Already verified Secrets Manager access above
|
||||
return new SecretVersionResponseModel(secretVersion);
|
||||
}
|
||||
|
||||
var userId = _userService.GetProperUserId(User);
|
||||
if (!userId.HasValue)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var orgAdmin = await _currentContext.OrganizationAdmin(secret.OrganizationId);
|
||||
var accessClient = AccessClientHelper.ToAccessClient(_currentContext.IdentityClientType, orgAdmin);
|
||||
|
||||
var access = await _secretRepository.AccessToSecretAsync(secretVersion.SecretId, userId.Value, accessClient);
|
||||
if (!access.Read)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
return new SecretVersionResponseModel(secretVersion);
|
||||
}
|
||||
|
||||
[HttpPost("secret-versions/get-by-ids")]
|
||||
public async Task<ListResponseModel<SecretVersionResponseModel>> GetManyByIdsAsync([FromBody] List<Guid> ids)
|
||||
{
|
||||
if (!ids.Any())
|
||||
{
|
||||
throw new BadRequestException("No version IDs provided.");
|
||||
}
|
||||
|
||||
// Get all versions
|
||||
var versions = (await _secretVersionRepository.GetManyByIdsAsync(ids)).ToList();
|
||||
if (!versions.Any())
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
// Get all associated secrets and check permissions
|
||||
var secretIds = versions.Select(v => v.SecretId).Distinct().ToList();
|
||||
var secrets = (await _secretRepository.GetManyByIds(secretIds)).ToList();
|
||||
|
||||
if (!secrets.Any())
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
// Ensure all secrets belong to the same organization
|
||||
var organizationId = secrets.First().OrganizationId;
|
||||
if (secrets.Any(s => s.OrganizationId != organizationId) ||
|
||||
!_currentContext.AccessSecretsManager(organizationId))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
// For service accounts and organization API, skip user-level access checks
|
||||
if (_currentContext.IdentityClientType == IdentityClientType.ServiceAccount ||
|
||||
_currentContext.IdentityClientType == IdentityClientType.Organization)
|
||||
{
|
||||
// Already verified Secrets Manager access and organization ownership above
|
||||
var serviceAccountResponses = versions.Select(v => new SecretVersionResponseModel(v));
|
||||
return new ListResponseModel<SecretVersionResponseModel>(serviceAccountResponses);
|
||||
}
|
||||
|
||||
var userId = _userService.GetProperUserId(User);
|
||||
if (!userId.HasValue)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var isAdmin = await _currentContext.OrganizationAdmin(organizationId);
|
||||
var accessClient = AccessClientHelper.ToAccessClient(_currentContext.IdentityClientType, isAdmin);
|
||||
|
||||
// Verify read access to all associated secrets
|
||||
var accessResults = await _secretRepository.AccessToSecretsAsync(secretIds, userId.Value, accessClient);
|
||||
if (accessResults.Values.Any(access => !access.Read))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var responses = versions.Select(v => new SecretVersionResponseModel(v));
|
||||
return new ListResponseModel<SecretVersionResponseModel>(responses);
|
||||
}
|
||||
|
||||
[HttpPut("secrets/{secretId}/versions/restore")]
|
||||
public async Task<SecretResponseModel> RestoreVersionAsync([FromRoute] Guid secretId, [FromBody] RestoreSecretVersionRequestModel request)
|
||||
{
|
||||
if (!(_currentContext.IdentityClientType == IdentityClientType.User || _currentContext.IdentityClientType == IdentityClientType.ServiceAccount))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var secret = await _secretRepository.GetByIdAsync(secretId);
|
||||
if (secret == null || !_currentContext.AccessSecretsManager(secret.OrganizationId))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
// Get the version first to validate it belongs to this secret
|
||||
var version = await _secretVersionRepository.GetByIdAsync(request.VersionId);
|
||||
if (version == null || version.SecretId != secretId)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
// Store the current value before restoration
|
||||
var currentValue = secret.Value;
|
||||
|
||||
// For service accounts and organization API, skip user-level access checks
|
||||
if (_currentContext.IdentityClientType == IdentityClientType.ServiceAccount)
|
||||
{
|
||||
// Save current value as a version before restoring
|
||||
if (currentValue != version.Value)
|
||||
{
|
||||
var editorUserId = _userService.GetProperUserId(User);
|
||||
if (editorUserId.HasValue)
|
||||
{
|
||||
var currentVersionSnapshot = new Core.SecretsManager.Entities.SecretVersion
|
||||
{
|
||||
SecretId = secretId,
|
||||
Value = currentValue!,
|
||||
VersionDate = DateTime.UtcNow,
|
||||
EditorServiceAccountId = editorUserId.Value
|
||||
};
|
||||
|
||||
await _secretVersionRepository.CreateAsync(currentVersionSnapshot);
|
||||
}
|
||||
}
|
||||
|
||||
// Already verified Secrets Manager access above
|
||||
secret.Value = version.Value;
|
||||
secret.RevisionDate = DateTime.UtcNow;
|
||||
var updatedSec = await _secretRepository.UpdateAsync(secret);
|
||||
return new SecretResponseModel(updatedSec, true, true);
|
||||
}
|
||||
|
||||
var userId = _userService.GetProperUserId(User);
|
||||
if (!userId.HasValue)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var orgAdmin = await _currentContext.OrganizationAdmin(secret.OrganizationId);
|
||||
var accessClient = AccessClientHelper.ToAccessClient(_currentContext.IdentityClientType, orgAdmin);
|
||||
|
||||
var access = await _secretRepository.AccessToSecretAsync(secretId, userId.Value, accessClient);
|
||||
if (!access.Write)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
// Save current value as a version before restoring
|
||||
if (currentValue != version.Value)
|
||||
{
|
||||
var orgUser = await _organizationUserRepository.GetByOrganizationAsync(secret.OrganizationId, userId.Value);
|
||||
if (orgUser == null)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var currentVersionSnapshot = new Core.SecretsManager.Entities.SecretVersion
|
||||
{
|
||||
SecretId = secretId,
|
||||
Value = currentValue!,
|
||||
VersionDate = DateTime.UtcNow,
|
||||
EditorOrganizationUserId = orgUser.Id
|
||||
};
|
||||
|
||||
await _secretVersionRepository.CreateAsync(currentVersionSnapshot);
|
||||
}
|
||||
|
||||
// Update the secret with the version's value
|
||||
secret.Value = version.Value;
|
||||
secret.RevisionDate = DateTime.UtcNow;
|
||||
|
||||
var updatedSecret = await _secretRepository.UpdateAsync(secret);
|
||||
|
||||
return new SecretResponseModel(updatedSecret, true, true);
|
||||
}
|
||||
|
||||
[HttpPost("secret-versions/delete")]
|
||||
public async Task<IActionResult> BulkDeleteAsync([FromBody] List<Guid> ids)
|
||||
{
|
||||
if (!ids.Any())
|
||||
{
|
||||
throw new BadRequestException("No version IDs provided.");
|
||||
}
|
||||
|
||||
var secretVersions = (await _secretVersionRepository.GetManyByIdsAsync(ids)).ToList();
|
||||
if (secretVersions.Count != ids.Count)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
// Ensure all versions belong to secrets in the same organization
|
||||
var secretIds = secretVersions.Select(v => v.SecretId).Distinct().ToList();
|
||||
var secrets = await _secretRepository.GetManyByIds(secretIds);
|
||||
var secretsList = secrets.ToList();
|
||||
|
||||
if (!secretsList.Any())
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var organizationId = secretsList.First().OrganizationId;
|
||||
if (secretsList.Any(s => s.OrganizationId != organizationId) ||
|
||||
!_currentContext.AccessSecretsManager(organizationId))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
// For service accounts and organization API, skip user-level access checks
|
||||
if (_currentContext.IdentityClientType == IdentityClientType.ServiceAccount ||
|
||||
_currentContext.IdentityClientType == IdentityClientType.Organization)
|
||||
{
|
||||
// Already verified Secrets Manager access and organization ownership above
|
||||
await _secretVersionRepository.DeleteManyByIdAsync(ids);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
var userId = _userService.GetProperUserId(User);
|
||||
if (!userId.HasValue)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var orgAdmin = await _currentContext.OrganizationAdmin(organizationId);
|
||||
var accessClient = AccessClientHelper.ToAccessClient(_currentContext.IdentityClientType, orgAdmin);
|
||||
|
||||
// Verify write access to all associated secrets
|
||||
var accessResults = await _secretRepository.AccessToSecretsAsync(secretIds, userId.Value, accessClient);
|
||||
if (accessResults.Values.Any(access => !access.Write))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
await _secretVersionRepository.DeleteManyByIdAsync(ids);
|
||||
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ using Bit.Core.Auth.Identity;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.SecretsManager.AuthorizationRequirements;
|
||||
using Bit.Core.SecretsManager.Commands.Secrets.Interfaces;
|
||||
using Bit.Core.SecretsManager.Entities;
|
||||
@@ -29,6 +30,7 @@ public class SecretsController : Controller
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly IProjectRepository _projectRepository;
|
||||
private readonly ISecretRepository _secretRepository;
|
||||
private readonly ISecretVersionRepository _secretVersionRepository;
|
||||
private readonly ICreateSecretCommand _createSecretCommand;
|
||||
private readonly IUpdateSecretCommand _updateSecretCommand;
|
||||
private readonly IDeleteSecretCommand _deleteSecretCommand;
|
||||
@@ -38,11 +40,13 @@ public class SecretsController : Controller
|
||||
private readonly IUserService _userService;
|
||||
private readonly IEventService _eventService;
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
|
||||
public SecretsController(
|
||||
ICurrentContext currentContext,
|
||||
IProjectRepository projectRepository,
|
||||
ISecretRepository secretRepository,
|
||||
ISecretVersionRepository secretVersionRepository,
|
||||
ICreateSecretCommand createSecretCommand,
|
||||
IUpdateSecretCommand updateSecretCommand,
|
||||
IDeleteSecretCommand deleteSecretCommand,
|
||||
@@ -51,11 +55,13 @@ public class SecretsController : Controller
|
||||
ISecretAccessPoliciesUpdatesQuery secretAccessPoliciesUpdatesQuery,
|
||||
IUserService userService,
|
||||
IEventService eventService,
|
||||
IAuthorizationService authorizationService)
|
||||
IAuthorizationService authorizationService,
|
||||
IOrganizationUserRepository organizationUserRepository)
|
||||
{
|
||||
_currentContext = currentContext;
|
||||
_projectRepository = projectRepository;
|
||||
_secretRepository = secretRepository;
|
||||
_secretVersionRepository = secretVersionRepository;
|
||||
_createSecretCommand = createSecretCommand;
|
||||
_updateSecretCommand = updateSecretCommand;
|
||||
_deleteSecretCommand = deleteSecretCommand;
|
||||
@@ -65,6 +71,7 @@ public class SecretsController : Controller
|
||||
_userService = userService;
|
||||
_eventService = eventService;
|
||||
_authorizationService = authorizationService;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
|
||||
}
|
||||
|
||||
@@ -190,6 +197,44 @@ public class SecretsController : Controller
|
||||
}
|
||||
}
|
||||
|
||||
// Create a version record if the value changed
|
||||
if (updateRequest.ValueChanged)
|
||||
{
|
||||
// Store the old value before updating
|
||||
var oldValue = secret.Value;
|
||||
var userId = _userService.GetProperUserId(User)!.Value;
|
||||
Guid? editorServiceAccountId = null;
|
||||
Guid? editorOrganizationUserId = null;
|
||||
|
||||
if (_currentContext.IdentityClientType == IdentityClientType.ServiceAccount)
|
||||
{
|
||||
editorServiceAccountId = userId;
|
||||
}
|
||||
else if (_currentContext.IdentityClientType == IdentityClientType.User)
|
||||
{
|
||||
var orgUser = await _organizationUserRepository.GetByOrganizationAsync(secret.OrganizationId, userId);
|
||||
if (orgUser != null)
|
||||
{
|
||||
editorOrganizationUserId = orgUser.Id;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
}
|
||||
|
||||
var secretVersion = new SecretVersion
|
||||
{
|
||||
SecretId = id,
|
||||
Value = oldValue,
|
||||
VersionDate = DateTime.UtcNow,
|
||||
EditorServiceAccountId = editorServiceAccountId,
|
||||
EditorOrganizationUserId = editorOrganizationUserId
|
||||
};
|
||||
|
||||
await _secretVersionRepository.CreateAsync(secretVersion);
|
||||
}
|
||||
|
||||
var result = await _updateSecretCommand.UpdateAsync(updatedSecret, accessPoliciesUpdates);
|
||||
await LogSecretEventAsync(secret, EventType.Secret_Edited);
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Bit.Api.SecretsManager.Models.Request;
|
||||
|
||||
public class RestoreSecretVersionRequestModel
|
||||
{
|
||||
[Required]
|
||||
public Guid VersionId { get; set; }
|
||||
}
|
||||
@@ -28,6 +28,8 @@ public class SecretUpdateRequestModel : IValidatableObject
|
||||
|
||||
public SecretAccessPoliciesRequestsModel AccessPoliciesRequests { get; set; }
|
||||
|
||||
public bool ValueChanged { get; set; } = false;
|
||||
|
||||
public Secret ToSecret(Secret secret)
|
||||
{
|
||||
secret.Key = Key;
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
using Bit.Core.Models.Api;
|
||||
using Bit.Core.SecretsManager.Entities;
|
||||
|
||||
namespace Bit.Api.SecretsManager.Models.Response;
|
||||
|
||||
public class SecretVersionResponseModel : ResponseModel
|
||||
{
|
||||
private const string _objectName = "secretVersion";
|
||||
|
||||
public Guid Id { get; set; }
|
||||
public Guid SecretId { get; set; }
|
||||
public string Value { get; set; } = string.Empty;
|
||||
public DateTime VersionDate { get; set; }
|
||||
public Guid? EditorServiceAccountId { get; set; }
|
||||
public Guid? EditorOrganizationUserId { get; set; }
|
||||
|
||||
public SecretVersionResponseModel() : base(_objectName) { }
|
||||
|
||||
public SecretVersionResponseModel(SecretVersion secretVersion) : base(_objectName)
|
||||
{
|
||||
Id = secretVersion.Id;
|
||||
SecretId = secretVersion.SecretId;
|
||||
Value = secretVersion.Value;
|
||||
VersionDate = secretVersion.VersionDate;
|
||||
EditorServiceAccountId = secretVersion.EditorServiceAccountId;
|
||||
EditorOrganizationUserId = secretVersion.EditorOrganizationUserId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using Bit.Core.SecretsManager.Entities;
|
||||
|
||||
namespace Bit.Core.SecretsManager.Repositories;
|
||||
|
||||
public interface ISecretVersionRepository
|
||||
{
|
||||
Task<SecretVersion?> GetByIdAsync(Guid id);
|
||||
Task<IEnumerable<SecretVersion>> GetManyBySecretIdAsync(Guid secretId);
|
||||
Task<IEnumerable<SecretVersion>> GetManyByIdsAsync(IEnumerable<Guid> ids);
|
||||
Task<SecretVersion> CreateAsync(SecretVersion secretVersion);
|
||||
Task DeleteManyByIdAsync(IEnumerable<Guid> ids);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using Bit.Core.SecretsManager.Entities;
|
||||
|
||||
namespace Bit.Core.SecretsManager.Repositories.Noop;
|
||||
|
||||
public class NoopSecretVersionRepository : ISecretVersionRepository
|
||||
{
|
||||
public Task<SecretVersion?> GetByIdAsync(Guid id)
|
||||
{
|
||||
return Task.FromResult(null as SecretVersion);
|
||||
}
|
||||
|
||||
public Task<IEnumerable<SecretVersion>> GetManyBySecretIdAsync(Guid secretId)
|
||||
{
|
||||
return Task.FromResult(Enumerable.Empty<SecretVersion>());
|
||||
}
|
||||
|
||||
public Task<SecretVersion> CreateAsync(SecretVersion secretVersion)
|
||||
{
|
||||
return Task.FromResult(secretVersion);
|
||||
}
|
||||
|
||||
public Task DeleteManyByIdAsync(IEnumerable<Guid> ids)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task<IEnumerable<SecretVersion>> GetManyByIdsAsync(IEnumerable<Guid> ids)
|
||||
{
|
||||
return Task.FromResult(Enumerable.Empty<SecretVersion>());
|
||||
}
|
||||
}
|
||||
@@ -344,6 +344,7 @@ public static class ServiceCollectionExtensions
|
||||
services.AddScoped<IProviderService, NoopProviderService>();
|
||||
services.AddScoped<IServiceAccountRepository, NoopServiceAccountRepository>();
|
||||
services.AddScoped<ISecretRepository, NoopSecretRepository>();
|
||||
services.AddScoped<ISecretVersionRepository, NoopSecretVersionRepository>();
|
||||
services.AddScoped<IProjectRepository, NoopProjectRepository>();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user