1
0
mirror of https://github.com/bitwarden/server.git synced 2024-11-22 12:15:36 +01:00

[SM-381] New secrets access (#2629)

* [SM-66] Create Secret Database Table (#2144)

Objective
The purpose of this PR is to create a database table, entity, and repository for the new Secret database table.

The new Secret table will use entity framework for all database providers.

* [SM-67] Get all secrets by org ID (#2163)

Add a controller to fetch secrets associated with an organization ID.

To note, the [SecretsManager] attribute makes this controller only available for local development.

* [SM-68] Add API endpoints for getting, creating, and editing secrets (#2201)

The purpose of this PR is to add API endpoints for getting, creating, and editing secrets for the Secrets Manager project.

* Move interfaces to core (#2211)

* [SM-63] Read UTC DateTimes from databases via EF and order by revision date (#2206)

* Read UTC DateTimes from db and order by revision

* Move orderby to repo layer

* [SM-185] Add EE_Testing_env to server (#2222)

* Sm 104 project Database (#2192)

* Project DB addition and sprocs

* Adding spaces to the end of each file, fixing minor issues

* removing useless comments

* Adding soft delete proc to migration

* Project EF Scaffold

* Additional changes to use EF instead of procedures

* Adding dependency injection

* Fixing lint errors

* Bug fixes

* Adding migration scripts, removing sproc files, and setting up Entity framework code

* Adding back accidentally deleted sproc

* Removing files that shouldn't have been created

* Lint

* Small changes based on Oscar's rec (#2215)

* Migrations for making CreateDate not null

* adding space to end of file

* Making Revision date not null

* dotnet format

* Adding nonclustered indexes to SQL

* SM-104: Update PR with changes Thomas proposed

Co-authored-by: CarleyDiaz-Bitwarden <103955722+CarleyDiaz-Bitwarden@users.noreply.github.com>
Co-authored-by: Thomas Avery <tavery@bitwarden.com>
Co-authored-by: Colton Hurst <colton@coltonhurst.com>

* Removing org ID from create request body (#2243)

* SM-114: Add create & update project endpoints (#2251)

* SM-114: Initial commit with create project endpoint (for SM)

* SM-114: Add Update Project route (for SM)

* SM-114: Fix file encodings

* Fix DI issue for SM Project Create/Update commands

* Fix import ordering for linter

* SM-114: Remove unneeded lines setting DeletedDate, as it should already be null

* SM-114: Only have OrgId in route for CreateProject

* Remove unneeded using

* SM-114: Initial commit with create project endpoint (for SM)

* SM-114: Add Update Project route (for SM)

* SM-114: Fix file encodings

* Fix DI issue for SM Project Create/Update commands

* Fix import ordering for linter

* SM-114: Remove unneeded lines setting DeletedDate, as it should already be null

* SM-114: Only have OrgId in route for CreateProject

* Remove unneeded using

* Fully remove OrgId from ProjectCreateRequestModel

* [SM-64] Soft Delete Secrets (#2253)

* Bulk delete secrets with command unit tests

* Controller unit tests

* Optimize conditionals

* SM-64 bulk delete integration test

* fix test

* SM-64 code review updated

* [SM-65] Fix return empty secrets list (#2281)

* Secrets return empty list

* [SM-246] Use repository in integration test (#2285)

* [SM-190] Add integration tests to Secrets (#2292)

* Adding integration tests for the SecretsController

Co-authored-by: Hinton <hinton@users.noreply.github.com>

* Sm 95 - Adding GetProjects endpoint (#2295)

* SM-114: Initial commit with create project endpoint (for SM)

* SM-114: Add Update Project route (for SM)

* SM-114: Fix file encodings

* Fix DI issue for SM Project Create/Update commands

* Adding GetProjectsByOrg

* fixing merge conflicts

* fix

* Updating to return empty list

* removing null check

Co-authored-by: Colton Hurst <colton@coltonhurst.com>
Co-authored-by: CarleyDiaz-Bitwarden <103955722+CarleyDiaz-Bitwarden@users.noreply.github.com>

* [SM-191] Create ServiceAccount Table (#2301)

* SM-191 Create ServiceAccount Table

* [SM-207] API for listing service accounts by organization (#2307)

* SM-207 list service accounts by org

* SM-96: Add ability to get project by id (#2314)

* SM-96: Small change to allow getting project by id

* Fix whitespace issue

* Add first integration test and fix date bug

* Ensure tests are consistent

* Add more project controller integration tests

* Remove commented delete for now

* [SM-187] Create ServiceAccounts (#2323)

* SM-187 Create & Update ServiceAccounts

* Remove extra new line src/Api/Controllers/ServiceAccountsController.cs

Co-authored-by: Oscar Hinton <Hinton@users.noreply.github.com>

* [SM-218] [SM-219] SM Auth flow (#2297)

* SM-282 Delete Projects (#2335)

* SM-282 delete & bulk delete projects

* Have delete commands return tuple with object

* Fix admin project not working after secrets manager changes (#2339)

* [SM-150] proj and secrets mapping (#2286)

* Beggining of changes for Project Secrets mapping

* Beggining of changes for project and secrets mapping

* Inital changes to add Mapping table for Project Secrets

* Resolve migration not working properly

* Indent sql

* Changes to try and return projects in the GetManyByOrganizaationIDAsync on SecretRepository.

* Changes made with Oscar

* Add reversemap

* running lint and removing comments

* Lint fixes

* fixing merge issues

* Trying to fix the DB issue

* DB fixes

* fixes

* removing unused space

* fixing lint issue

* final lint fix I hope

* removing manually added sql.sqlproj

* Lint changes and fixing the sql proj issues

* adding ServiceAccount to sql proj

* Removing ON DELETE CASCADE

* remove On delete cascade

* changes for deleting project and secret inside of the Organization_DeleteById procedure.

* changes for deleting project and secret inside of the Organization_DeleteById procedure.

* migration changes

* Updating constraints

* removing void

* remove spaces

* updating cipherRepo tests to be task instead of void

* fixing

* fixing

* test

* fix

* fix

* changes to remove circular dependency

* fixes

* sending guid and string name of the project over

* Update src/Sql/dbo/Tables/Secret.sql

Co-authored-by: Oscar Hinton <Hinton@users.noreply.github.com>

* Update src/Sql/dbo/Tables/Project.sql

Co-authored-by: Oscar Hinton <Hinton@users.noreply.github.com>

* removing unused code

* Potential refactor (#2340)

* migrations

* Postgres migraiton

* Update src/Api/SecretManagerFeatures/Models/Response/SecretResponseModel.cs

Co-authored-by: Oscar Hinton <Hinton@users.noreply.github.com>

* rename file

* Update util/Migrator/DbScripts/2022-09-19_00_ProjectSecret.sql

Co-authored-by: Oscar Hinton <Hinton@users.noreply.github.com>

* Lint fixes

* removing extra semi colon

* removing circular references with projects and secrets

* adding back projects

* Add ProjectFixture

* Update util/Migrator/DbScripts/2022-09-19_00_ProjectSecret.sql

Co-authored-by: Oscar Hinton <Hinton@users.noreply.github.com>

* Update util/Migrator/DbScripts/2022-09-19_00_ProjectSecret.sql

Co-authored-by: Oscar Hinton <Hinton@users.noreply.github.com>

Co-authored-by: CarleyDiaz-Bitwarden <103955722+CarleyDiaz-Bitwarden@users.noreply.github.com>
Co-authored-by: Hinton <hinton@users.noreply.github.com>

* [SM-300] Access token endpoint (#2377)

* [SM-324] Add Organization to JWT claim (#2379)

* [SM-259] Add create access token endpoint for service accounts (#2411)

* Add create access token for service accounts

* [SM-259] Fix create access token scope initialization (#2418)

* Fix namespace for ServiceAccount command tests

* Remove "this" from SecretsManager requests

* Fix have scope be assigned a JSON list

* SM-99: Individual Project / Secrets Tab (#2399)

Co-authored-by: Oscar Hinton <Hinton@users.noreply.github.com>

* [SM-361] Add Support for never expiring ApiKeys (#2450)

* Update database to support never expiring ApiKey

* Update Api to support never expiring ApiKeys

* Fix unit test variable naming

* Remove required from model

* Fix spacing

* Add EF migrations

* Run dotnet format

* Update util/Migrator/DbScripts/2022-11-29_00_ApiKey_Never_Expire.sql

Co-authored-by: Oscar Hinton <Hinton@users.noreply.github.com>

Co-authored-by: Oscar Hinton <Hinton@users.noreply.github.com>

* [SM-359] Fix project secrets migration (#2443)

* [SM-299] Add UseSecretsManager flag (#2413)

* [SM-193] Access Policy (#2359)

* [SM-371] Fix and re-enable parallel integration tests (#2460)

* Fix and re-enable parallel integration tests

* Fix package lock files

* Move fix to ApiApplicationFactory

* Run dotnet restore --force

* Run dotnet format

* Reset packages.lock.json files

* Add project access checks for listing

* SM-99: Add CreateSecretWithProject Integration Test (#2452)

* Add GetSecretsByProjectAsync endpoint

* Add GetManyByProjectIdAsync endpoint

* Update response model for GetSecretsByProjectAsync

* Include projects when returning secrets by project id

* SM-99: Add ability to specify projectId when creating a secret

* SM-99: Update tests to accomodate for new create secret parameter

* Fix failing test

* SM-99: Handle optional projectId for new secret in ToSecret()

* SM-99: Filter out deleted secrets on GetManyByProjectIdAsync() and small refactorings

* SM-99: make CreateAsync for secret more clear

* Add CreateSecretWithProject integration test

* Fix CreateSecretWithProject integration test for SM-99

* Run dotnet format

* Undo added space

* Refactor test

* Refactor CreateSecretWithProject API Integration test again

* Change to boolean flag

* [SM-379] Add SDK device type (#2486)

* Add support for service accounts

* Improve logic for project repository

* Add remaining client types

* Experiment with separate enum for access control

* Add access checks to update project

* Rework AccessClientType

* Add access checks to fetching project

* Add checks to delete project command (untested)

* Remove some service account stuff

* Add ServiceAccount to AccessClientType

* Change CS8509 to error and 8424 to ignore

* Remove unused utcNow

* Fix delete tests

* SM-73 changes (#2422)

* testing

* test2

* testing

* trying to save the projects associated with the secret

* changes

* more changes

* Fix  EF error

* Second attempt

* Replace AddIfNotExists with Add.

* changes

* fixing await issue

* lint

* lint fixes

* suggested changes

* suggested changes

* updating tests

* fixing tests 2

* fixing tests

* fixing test

* fixing test

* fixing tests

* test

* testing

* fixing tests for the millionth time

* fixing tests

* allowing nulls for projectIds, fixing lint

* fixing tests

Co-authored-by: Hinton <hinton@users.noreply.github.com>

* fixing tests

* fixing tests

* [SM-222] [SM-357] Squash Secrets Manager migrations (#2540)

* Fix tables not being cleaned up

* Fix migration

* Squash secrets manager migrations

* Reset EF to pre SM state

* Add EF migrations

* Fix unified docker

* Add missed copy

* Fix all unit tests

* draft changes to add access checks to secrets

* updating code

* more changes

* fixing issues

* updating logic for access checks

* updating secrets controller

* changes

* changes

* merging more

* changes

* updateS

* removing unused comment

* changes requested by Thomas

* more changes suggested by Thomas

* making thomas's suggested changes

* final changes

* Run dotnet format

* fixes

* run dotnet format

* Updating tests

* Suggested changes

* lint fixes

* Test updates

* Changes

* Fixes for tests, and dotnet format

* Fixes

* test fixes

* changes

* fix

* fix

* test fix

* removing duplicate

* Removing dupe

---------

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>
Co-authored-by: Oscar Hinton <Hinton@users.noreply.github.com>
Co-authored-by: CarleyDiaz-Bitwarden <103955722+CarleyDiaz-Bitwarden@users.noreply.github.com>
Co-authored-by: Thomas Avery <tavery@bitwarden.com>
Co-authored-by: Colton Hurst <colton@coltonhurst.com>
This commit is contained in:
cd-bitwarden 2023-02-16 11:42:07 -08:00 committed by GitHub
parent bcaba6652b
commit ec8476912d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 639 additions and 131 deletions

View File

@ -1,4 +1,7 @@
using Bit.Core.SecretsManager.Commands.Secrets.Interfaces;
using Bit.Core.Context;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.SecretsManager.Commands.Secrets.Interfaces;
using Bit.Core.SecretsManager.Entities;
using Bit.Core.SecretsManager.Repositories;
@ -7,14 +10,34 @@ namespace Bit.Commercial.Core.SecretsManager.Commands.Secrets;
public class CreateSecretCommand : ICreateSecretCommand
{
private readonly ISecretRepository _secretRepository;
private readonly IProjectRepository _projectRepository;
private readonly ICurrentContext _currentContext;
public CreateSecretCommand(ISecretRepository secretRepository)
public CreateSecretCommand(ISecretRepository secretRepository, IProjectRepository projectRepository, ICurrentContext currentContext)
{
_secretRepository = secretRepository;
_projectRepository = projectRepository;
_currentContext = currentContext;
}
public async Task<Secret> CreateAsync(Secret secret)
public async Task<Secret> CreateAsync(Secret secret, Guid userId)
{
var orgAdmin = await _currentContext.OrganizationAdmin(secret.OrganizationId);
var accessClient = AccessClientHelper.ToAccessClient(_currentContext.ClientType, orgAdmin);
var project = secret.Projects?.FirstOrDefault();
var hasAccess = accessClient switch
{
AccessClientType.NoAccessCheck => true,
AccessClientType.User => project != null && await _projectRepository.UserHasWriteAccessToProject(project.Id, userId),
_ => false,
};
if (!hasAccess)
{
throw new NotFoundException();
}
return await _secretRepository.CreateAsync(secret);
}
}

View File

@ -1,4 +1,5 @@
using Bit.Core.Context;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.SecretsManager.Commands.Secrets.Interfaces;
using Bit.Core.SecretsManager.Entities;
@ -10,25 +11,27 @@ public class DeleteSecretCommand : IDeleteSecretCommand
{
private readonly ICurrentContext _currentContext;
private readonly ISecretRepository _secretRepository;
private readonly IProjectRepository _projectRepository;
public DeleteSecretCommand(ICurrentContext currentContext, ISecretRepository secretRepository)
public DeleteSecretCommand(ISecretRepository secretRepository, IProjectRepository projectRepository, ICurrentContext currentContext)
{
_currentContext = currentContext;
_secretRepository = secretRepository;
_projectRepository = projectRepository;
}
public async Task<List<Tuple<Secret, string>>> DeleteSecrets(List<Guid> ids)
public async Task<List<Tuple<Secret, string>>> DeleteSecrets(List<Guid> ids, Guid userId)
{
var secrets = await _secretRepository.GetManyByIds(ids);
var secrets = (await _secretRepository.GetManyByIds(ids)).ToList();
if (secrets?.Any() != true)
if (secrets.Any() != true)
{
throw new NotFoundException();
}
// Ensure all secrets belongs to the same organization
var organizationId = secrets.First().OrganizationId;
if (secrets.Any(p => p.OrganizationId != organizationId))
if (secrets.Any(secret => secret.OrganizationId != organizationId))
{
throw new BadRequestException();
}
@ -38,21 +41,46 @@ public class DeleteSecretCommand : IDeleteSecretCommand
throw new NotFoundException();
}
var results = ids.Select(id =>
var orgAdmin = await _currentContext.OrganizationAdmin(organizationId);
var accessClient = AccessClientHelper.ToAccessClient(_currentContext.ClientType, orgAdmin);
var results = new List<Tuple<Secret, string>>();
var deleteIds = new List<Guid>();
foreach (var secret in secrets)
{
var secret = secrets.FirstOrDefault(secret => secret.Id == id);
if (secret == null)
var hasAccess = orgAdmin;
if (secret.Projects != null && secret.Projects?.Count > 0)
{
throw new NotFoundException();
var projectId = secret.Projects.First().Id;
hasAccess = accessClient switch
{
AccessClientType.NoAccessCheck => true,
AccessClientType.User => await _projectRepository.UserHasWriteAccessToProject(projectId, userId),
_ => false,
};
}
if (!hasAccess)
{
results.Add(new Tuple<Secret, string>(secret, "access denied"));
}
// TODO Once permissions are implemented add check for each secret here.
else
{
return new Tuple<Secret, string>(secret, "");
deleteIds.Add(secret.Id);
results.Add(new Tuple<Secret, string>(secret, ""));
}
}).ToList();
}
if (deleteIds.Count > 0)
{
await _secretRepository.SoftDeleteManyByIdAsync(deleteIds);
}
await _secretRepository.SoftDeleteManyByIdAsync(ids);
return results;
}
}

View File

@ -1,4 +1,6 @@
using Bit.Core.Exceptions;
using Bit.Core.Context;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.SecretsManager.Commands.Secrets.Interfaces;
using Bit.Core.SecretsManager.Entities;
using Bit.Core.SecretsManager.Repositories;
@ -8,23 +10,45 @@ namespace Bit.Commercial.Core.SecretsManager.Commands.Secrets;
public class UpdateSecretCommand : IUpdateSecretCommand
{
private readonly ISecretRepository _secretRepository;
private readonly IProjectRepository _projectRepository;
private readonly ICurrentContext _currentContext;
public UpdateSecretCommand(ISecretRepository secretRepository)
public UpdateSecretCommand(ISecretRepository secretRepository, IProjectRepository projectRepository, ICurrentContext currentContext)
{
_secretRepository = secretRepository;
_projectRepository = projectRepository;
_currentContext = currentContext;
}
public async Task<Secret> UpdateAsync(Secret secret)
public async Task<Secret> UpdateAsync(Secret updatedSecret, Guid userId)
{
var existingSecret = await _secretRepository.GetByIdAsync(secret.Id);
if (existingSecret == null)
var secret = await _secretRepository.GetByIdAsync(updatedSecret.Id);
if (secret == null || !_currentContext.AccessSecretsManager(secret.OrganizationId))
{
throw new NotFoundException();
}
secret.OrganizationId = existingSecret.OrganizationId;
secret.CreationDate = existingSecret.CreationDate;
secret.DeletedDate = existingSecret.DeletedDate;
var orgAdmin = await _currentContext.OrganizationAdmin(secret.OrganizationId);
var accessClient = AccessClientHelper.ToAccessClient(_currentContext.ClientType, orgAdmin);
var project = updatedSecret.Projects?.FirstOrDefault();
var hasAccess = accessClient switch
{
AccessClientType.NoAccessCheck => true,
AccessClientType.User => project != null && await _projectRepository.UserHasWriteAccessToProject(project.Id, userId),
_ => false,
};
if (!hasAccess)
{
throw new NotFoundException();
}
secret.Key = updatedSecret.Key;
secret.Value = updatedSecret.Value;
secret.Note = updatedSecret.Note;
secret.Projects = updatedSecret.Projects;
secret.RevisionDate = DateTime.UtcNow;
await _secretRepository.UpdateAsync(secret);

View File

@ -74,6 +74,9 @@ public class ProjectRepository : Repository<Core.SecretsManager.Entities.Project
private static Expression<Func<Project, bool>> ServiceAccountHasReadAccessToProject(Guid serviceAccountId) => p =>
p.ServiceAccountAccessPolicies.Any(ap => ap.ServiceAccount.Id == serviceAccountId && ap.Read);
private static Expression<Func<Project, bool>> ServiceAccountHasWriteAccessToProject(Guid serviceAccountId) => p =>
p.ServiceAccountAccessPolicies.Any(ap => ap.ServiceAccount.Id == serviceAccountId && ap.Write);
public async Task DeleteManyByIdAsync(IEnumerable<Guid> ids)
{
using (var scope = ServiceScopeFactory.CreateScope())
@ -100,6 +103,28 @@ public class ProjectRepository : Repository<Core.SecretsManager.Entities.Project
}
}
public async Task<bool> ServiceAccountHasReadAccessToProject(Guid id, Guid userId)
{
using var scope = ServiceScopeFactory.CreateScope();
var dbContext = GetDatabaseContext(scope);
var query = dbContext.Project
.Where(p => p.Id == id)
.Where(ServiceAccountHasReadAccessToProject(userId));
return await query.AnyAsync();
}
public async Task<bool> ServiceAccountHasWriteAccessToProject(Guid id, Guid userId)
{
using var scope = ServiceScopeFactory.CreateScope();
var dbContext = GetDatabaseContext(scope);
var query = dbContext.Project
.Where(p => p.Id == id)
.Where(ServiceAccountHasWriteAccessToProject(userId));
return await query.AnyAsync();
}
public async Task<bool> UserHasReadAccessToProject(Guid id, Guid userId)
{
using var scope = ServiceScopeFactory.CreateScope();

View File

@ -1,4 +1,6 @@
using AutoMapper;
using System.Linq.Expressions;
using AutoMapper;
using Bit.Core.Enums;
using Bit.Core.SecretsManager.Repositories;
using Bit.Infrastructure.EntityFramework;
using Bit.Infrastructure.EntityFramework.Repositories;
@ -6,6 +8,7 @@ using Bit.Infrastructure.EntityFramework.SecretsManager.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
namespace Bit.Commercial.Infrastructure.EntityFramework.SecretsManager.Repositories;
public class SecretRepository : Repository<Core.SecretsManager.Entities.Secret, Secret, Guid>, ISecretRepository
@ -34,35 +37,58 @@ public class SecretRepository : Repository<Core.SecretsManager.Entities.Secret,
var dbContext = GetDatabaseContext(scope);
var secrets = await dbContext.Secret
.Where(c => ids.Contains(c.Id) && c.DeletedDate == null)
.Include(c => c.Projects)
.ToListAsync();
return Mapper.Map<List<Core.SecretsManager.Entities.Secret>>(secrets);
}
}
public async Task<IEnumerable<Core.SecretsManager.Entities.Secret>> GetManyByOrganizationIdAsync(Guid organizationId)
{
using (var scope = ServiceScopeFactory.CreateScope())
{
var dbContext = GetDatabaseContext(scope);
var secrets = await dbContext.Secret
.Where(c => c.OrganizationId == organizationId && c.DeletedDate == null)
.Include("Projects")
.OrderBy(c => c.RevisionDate)
.ToListAsync();
private static Expression<Func<Secret, bool>> ServiceAccountHasReadAccessToSecret(Guid serviceAccountId) => s =>
s.Projects.Any(p =>
p.ServiceAccountAccessPolicies.Any(ap => ap.ServiceAccount.Id == serviceAccountId && ap.Read));
return Mapper.Map<List<Core.SecretsManager.Entities.Secret>>(secrets);
}
private static Expression<Func<Secret, bool>> UserHasReadAccessToSecret(Guid userId) => s =>
s.Projects.Any(p =>
p.UserAccessPolicies.Any(ap => ap.OrganizationUser.UserId == userId && ap.Read) ||
p.GroupAccessPolicies.Any(ap =>
ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.UserId == userId && ap.Read)));
public async Task<IEnumerable<Core.SecretsManager.Entities.Secret>> GetManyByOrganizationIdAsync(Guid organizationId, Guid userId, AccessClientType accessType)
{
using var scope = ServiceScopeFactory.CreateScope();
var dbContext = GetDatabaseContext(scope);
var query = dbContext.Secret.Include(c => c.Projects).Where(c => c.OrganizationId == organizationId && c.DeletedDate == null);
query = accessType switch
{
AccessClientType.NoAccessCheck => query,
AccessClientType.User => query.Where(UserHasReadAccessToSecret(userId)),
AccessClientType.ServiceAccount => query.Where(ServiceAccountHasReadAccessToSecret(userId)),
_ => throw new ArgumentOutOfRangeException(nameof(accessType), accessType, null),
};
var secrets = await query.OrderBy(c => c.RevisionDate).ToListAsync();
return Mapper.Map<List<Core.SecretsManager.Entities.Secret>>(secrets);
}
public async Task<IEnumerable<Core.SecretsManager.Entities.Secret>> GetManyByProjectIdAsync(Guid projectId)
public async Task<IEnumerable<Core.SecretsManager.Entities.Secret>> GetManyByProjectIdAsync(Guid projectId, Guid userId, AccessClientType accessType)
{
using (var scope = ServiceScopeFactory.CreateScope())
{
var dbContext = GetDatabaseContext(scope);
var secrets = await dbContext.Secret
.Where(s => s.Projects.Any(p => p.Id == projectId) && s.DeletedDate == null).Include("Projects")
.OrderBy(s => s.RevisionDate).ToListAsync();
var query = dbContext.Secret.Include(s => s.Projects)
.Where(s => s.Projects.Any(p => p.Id == projectId) && s.DeletedDate == null);
query = accessType switch
{
AccessClientType.NoAccessCheck => query,
AccessClientType.User => query.Where(UserHasReadAccessToSecret(userId)),
AccessClientType.ServiceAccount => query.Where(ServiceAccountHasReadAccessToSecret(userId)),
_ => throw new ArgumentOutOfRangeException(nameof(accessType), accessType, null),
};
var secrets = await query.OrderBy(s => s.RevisionDate).ToListAsync();
return Mapper.Map<List<Core.SecretsManager.Entities.Secret>>(secrets);
}
}
@ -96,6 +122,7 @@ public class SecretRepository : Repository<Core.SecretsManager.Entities.Secret,
{
var dbContext = GetDatabaseContext(scope);
var mappedEntity = Mapper.Map<Secret>(secret);
var entity = await dbContext.Secret
.Include("Projects")
.FirstAsync(s => s.Id == secret.Id);

View File

@ -1,4 +1,7 @@
using Bit.Commercial.Core.SecretsManager.Commands.Secrets;
using Bit.Commercial.Core.Test.SecretsManager.Enums;
using Bit.Core.Context;
using Bit.Core.Exceptions;
using Bit.Core.SecretsManager.Entities;
using Bit.Core.SecretsManager.Repositories;
using Bit.Core.Test.SecretsManager.AutoFixture.SecretsFixture;
@ -14,14 +17,54 @@ namespace Bit.Commercial.Core.Test.SecretsManager.Secrets;
public class CreateSecretCommandTests
{
[Theory]
[BitAutoData]
public async Task CreateAsync_CallsCreate(Secret data,
SutProvider<CreateSecretCommand> sutProvider)
[BitAutoData(PermissionType.RunAsAdmin)]
[BitAutoData(PermissionType.RunAsUserWithPermission)]
public async Task CreateAsync_Success(PermissionType permissionType, Secret data,
SutProvider<CreateSecretCommand> sutProvider, Guid userId, Project mockProject)
{
await sutProvider.Sut.CreateAsync(data);
data.Projects = new List<Project>() { mockProject };
if (permissionType == PermissionType.RunAsAdmin)
{
sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(data.OrganizationId).Returns(true);
}
else
{
sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(data.OrganizationId).Returns(false);
sutProvider.GetDependency<IProjectRepository>().UserHasWriteAccessToProject((Guid)(data.Projects?.First().Id), userId).Returns(true);
}
await sutProvider.Sut.CreateAsync(data, userId);
await sutProvider.GetDependency<ISecretRepository>().Received(1)
.CreateAsync(data);
}
[Theory]
[BitAutoData]
public async Task CreateAsync_UserWithoutPermission_ThrowsNotFound(Secret data,
SutProvider<CreateSecretCommand> sutProvider, Guid userId, Project mockProject)
{
data.Projects = new List<Project>() { mockProject };
sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(data.OrganizationId).Returns(false);
sutProvider.GetDependency<IProjectRepository>().UserHasWriteAccessToProject((Guid)(data.Projects?.First().Id), userId).Returns(false);
await Assert.ThrowsAsync<NotFoundException>(() =>
sutProvider.Sut.CreateAsync(data, userId));
}
[Theory]
[BitAutoData]
public async Task CreateAsync_NoProjects_User_ThrowsNotFound(Secret data,
SutProvider<CreateSecretCommand> sutProvider, Guid userId)
{
data.Projects = null;
sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(data.OrganizationId).Returns(false);
await Assert.ThrowsAsync<NotFoundException>(() =>
sutProvider.Sut.CreateAsync(data, userId));
}
}

View File

@ -1,16 +1,20 @@
using Bit.Commercial.Core.SecretsManager.Commands.Secrets;
using Bit.Commercial.Core.Test.SecretsManager.Enums;
using Bit.Core.Context;
using Bit.Core.Exceptions;
using Bit.Core.SecretsManager.Entities;
using Bit.Core.SecretsManager.Repositories;
using Bit.Core.Test.SecretsManager.AutoFixture.ProjectsFixture;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Bit.Test.Common.Helpers;
using NSubstitute;
using Xunit;
namespace Bit.Commercial.Core.Test.SecretsManager.Secrets;
[SutProviderCustomize]
[ProjectCustomize]
public class DeleteSecretCommandTests
{
[Theory]
@ -20,7 +24,7 @@ public class DeleteSecretCommandTests
{
sutProvider.GetDependency<ISecretRepository>().GetManyByIds(data).Returns(new List<Secret>());
var exception = await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.DeleteSecrets(data));
var exception = await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.DeleteSecrets(data, default));
await sutProvider.GetDependency<ISecretRepository>().DidNotReceiveWithAnyArgs().SoftDeleteManyByIdAsync(default);
}
@ -36,22 +40,39 @@ public class DeleteSecretCommandTests
};
sutProvider.GetDependency<ISecretRepository>().GetManyByIds(data).Returns(new List<Secret>() { secret });
var exception = await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.DeleteSecrets(data));
var exception = await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.DeleteSecrets(data, default));
await sutProvider.GetDependency<ISecretRepository>().DidNotReceiveWithAnyArgs().SoftDeleteManyByIdAsync(default);
}
[Theory]
[BitAutoData]
public async Task DeleteSecrets_Success(List<Guid> data,
SutProvider<DeleteSecretCommand> sutProvider)
[BitAutoData(PermissionType.RunAsAdmin)]
[BitAutoData(PermissionType.RunAsUserWithPermission)]
public async Task DeleteSecrets_Success(PermissionType permissionType, List<Guid> data,
SutProvider<DeleteSecretCommand> sutProvider, Guid userId, Guid organizationId, Project mockProject)
{
List<Project> projects = null;
if (permissionType == PermissionType.RunAsAdmin)
{
sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(organizationId).Returns(true);
}
else
{
sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(organizationId).Returns(false);
sutProvider.GetDependency<IProjectRepository>().UserHasWriteAccessToProject(mockProject.Id, userId).Returns(true);
projects = new List<Project>() { mockProject };
}
var secrets = new List<Secret>();
foreach (Guid id in data)
{
var secret = new Secret()
{
Id = id
Id = id,
OrganizationId = organizationId,
Projects = projects
};
secrets.Add(secret);
}
@ -59,9 +80,9 @@ public class DeleteSecretCommandTests
sutProvider.GetDependency<ISecretRepository>().GetManyByIds(data).Returns(secrets);
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(default).ReturnsForAnyArgs(true);
var results = await sutProvider.Sut.DeleteSecrets(data);
var results = await sutProvider.Sut.DeleteSecrets(data, userId);
await sutProvider.GetDependency<ISecretRepository>().Received(1).SoftDeleteManyByIdAsync(Arg.Is(AssertHelper.AssertPropertyEqual(data)));
await sutProvider.GetDependency<ISecretRepository>().Received(1).SoftDeleteManyByIdAsync(Arg.Is(data));
foreach (var result in results)
{
Assert.Equal("", result.Item2);

View File

@ -1,7 +1,10 @@
using Bit.Commercial.Core.SecretsManager.Commands.Secrets;
using Bit.Commercial.Core.Test.SecretsManager.Enums;
using Bit.Core.Context;
using Bit.Core.Exceptions;
using Bit.Core.SecretsManager.Entities;
using Bit.Core.SecretsManager.Repositories;
using Bit.Core.Test.SecretsManager.AutoFixture.ProjectsFixture;
using Bit.Core.Test.SecretsManager.AutoFixture.SecretsFixture;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
@ -13,23 +16,38 @@ namespace Bit.Commercial.Core.Test.SecretsManager.Secrets;
[SutProviderCustomize]
[SecretCustomize]
[ProjectCustomize]
public class UpdateSecretCommandTests
{
[Theory]
[BitAutoData]
public async Task UpdateAsync_SecretDoesNotExist_ThrowsNotFound(Secret data, SutProvider<UpdateSecretCommand> sutProvider)
{
var exception = await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.UpdateAsync(data));
var exception = await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.UpdateAsync(data, default));
await sutProvider.GetDependency<ISecretRepository>().DidNotReceiveWithAnyArgs().UpdateAsync(default);
}
[Theory]
[BitAutoData]
public async Task UpdateAsync_CallsReplaceAsync(Secret data, SutProvider<UpdateSecretCommand> sutProvider)
[BitAutoData(PermissionType.RunAsAdmin)]
[BitAutoData(PermissionType.RunAsUserWithPermission)]
public async Task UpdateAsync_Success(PermissionType permissionType, Secret data, SutProvider<UpdateSecretCommand> sutProvider, Guid userId, Project mockProject)
{
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(data.OrganizationId).Returns(true);
if (permissionType == PermissionType.RunAsAdmin)
{
sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(data.OrganizationId).Returns(true);
}
else
{
data.Projects = new List<Project>() { mockProject };
sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(data.OrganizationId).Returns(false);
sutProvider.GetDependency<IProjectRepository>().UserHasWriteAccessToProject((Guid)(data.Projects?.First().Id), userId).Returns(true);
}
sutProvider.GetDependency<ISecretRepository>().GetByIdAsync(data.Id).Returns(data);
await sutProvider.Sut.UpdateAsync(data);
await sutProvider.Sut.UpdateAsync(data, userId);
await sutProvider.GetDependency<ISecretRepository>().Received(1)
.UpdateAsync(data);
@ -37,11 +55,14 @@ public class UpdateSecretCommandTests
[Theory]
[BitAutoData]
public async Task UpdateAsync_DoesNotModifyOrganizationId(Secret existingSecret, SutProvider<UpdateSecretCommand> sutProvider)
public async Task UpdateAsync_DoesNotModifyOrganizationId(Secret existingSecret, SutProvider<UpdateSecretCommand> sutProvider, Guid userId)
{
sutProvider.GetDependency<ISecretRepository>().GetByIdAsync(existingSecret.Id).Returns(existingSecret);
var updatedOrgId = Guid.NewGuid();
sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(existingSecret.OrganizationId).Returns(true);
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(existingSecret.OrganizationId).Returns(true);
sutProvider.GetDependency<ISecretRepository>().GetByIdAsync(existingSecret.Id).Returns(existingSecret);
sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(updatedOrgId).Returns(true);
var secretUpdate = new Secret()
{
OrganizationId = updatedOrgId,
@ -49,7 +70,7 @@ public class UpdateSecretCommandTests
Key = existingSecret.Key,
};
var result = await sutProvider.Sut.UpdateAsync(secretUpdate);
var result = await sutProvider.Sut.UpdateAsync(secretUpdate, userId);
Assert.Equal(existingSecret.OrganizationId, result.OrganizationId);
Assert.NotEqual(existingSecret.OrganizationId, updatedOrgId);
@ -57,9 +78,11 @@ public class UpdateSecretCommandTests
[Theory]
[BitAutoData]
public async Task UpdateAsync_DoesNotModifyCreationDate(Secret existingSecret, SutProvider<UpdateSecretCommand> sutProvider)
public async Task UpdateAsync_DoesNotModifyCreationDate(Secret existingSecret, SutProvider<UpdateSecretCommand> sutProvider, Guid userId)
{
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(existingSecret.OrganizationId).Returns(true);
sutProvider.GetDependency<ISecretRepository>().GetByIdAsync(existingSecret.Id).Returns(existingSecret);
sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(existingSecret.OrganizationId).Returns(true);
var updatedCreationDate = DateTime.UtcNow;
var secretUpdate = new Secret()
@ -67,9 +90,10 @@ public class UpdateSecretCommandTests
CreationDate = updatedCreationDate,
Id = existingSecret.Id,
Key = existingSecret.Key,
OrganizationId = existingSecret.OrganizationId
};
var result = await sutProvider.Sut.UpdateAsync(secretUpdate);
var result = await sutProvider.Sut.UpdateAsync(secretUpdate, userId);
Assert.Equal(existingSecret.CreationDate, result.CreationDate);
Assert.NotEqual(existingSecret.CreationDate, updatedCreationDate);
@ -77,9 +101,11 @@ public class UpdateSecretCommandTests
[Theory]
[BitAutoData]
public async Task UpdateAsync_DoesNotModifyDeletionDate(Secret existingSecret, SutProvider<UpdateSecretCommand> sutProvider)
public async Task UpdateAsync_DoesNotModifyDeletionDate(Secret existingSecret, SutProvider<UpdateSecretCommand> sutProvider, Guid userId)
{
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(existingSecret.OrganizationId).Returns(true);
sutProvider.GetDependency<ISecretRepository>().GetByIdAsync(existingSecret.Id).Returns(existingSecret);
sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(existingSecret.OrganizationId).Returns(true);
var updatedDeletionDate = DateTime.UtcNow;
var secretUpdate = new Secret()
@ -87,9 +113,10 @@ public class UpdateSecretCommandTests
DeletedDate = updatedDeletionDate,
Id = existingSecret.Id,
Key = existingSecret.Key,
OrganizationId = existingSecret.OrganizationId
};
var result = await sutProvider.Sut.UpdateAsync(secretUpdate);
var result = await sutProvider.Sut.UpdateAsync(secretUpdate, userId);
Assert.Equal(existingSecret.DeletedDate, result.DeletedDate);
Assert.NotEqual(existingSecret.DeletedDate, updatedDeletionDate);
@ -98,9 +125,12 @@ public class UpdateSecretCommandTests
[Theory]
[BitAutoData]
public async Task UpdateAsync_RevisionDateIsUpdatedToUtcNow(Secret existingSecret, SutProvider<UpdateSecretCommand> sutProvider)
public async Task UpdateAsync_RevisionDateIsUpdatedToUtcNow(Secret existingSecret, SutProvider<UpdateSecretCommand> sutProvider, Guid userId)
{
sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(existingSecret.OrganizationId).Returns(true);
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(existingSecret.OrganizationId).Returns(true);
sutProvider.GetDependency<ISecretRepository>().GetByIdAsync(existingSecret.Id).Returns(existingSecret);
sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(existingSecret.OrganizationId).Returns(true);
var updatedRevisionDate = DateTime.UtcNow.AddDays(10);
var secretUpdate = new Secret()
@ -108,11 +138,12 @@ public class UpdateSecretCommandTests
RevisionDate = updatedRevisionDate,
Id = existingSecret.Id,
Key = existingSecret.Key,
OrganizationId = existingSecret.OrganizationId
};
var result = await sutProvider.Sut.UpdateAsync(secretUpdate);
var result = await sutProvider.Sut.UpdateAsync(secretUpdate, userId);
Assert.NotEqual(existingSecret.RevisionDate, result.RevisionDate);
Assert.NotEqual(secretUpdate.RevisionDate, result.RevisionDate);
AssertHelper.AssertRecent(result.RevisionDate);
}
}

View File

@ -2,9 +2,12 @@
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.SecretsManager.Commands.Secrets.Interfaces;
using Bit.Core.SecretsManager.Entities;
using Bit.Core.SecretsManager.Repositories;
using Bit.Core.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
@ -16,22 +19,21 @@ public class SecretsController : Controller
{
private readonly ICurrentContext _currentContext;
private readonly ISecretRepository _secretRepository;
private readonly IProjectRepository _projectRepository;
private readonly ICreateSecretCommand _createSecretCommand;
private readonly IUpdateSecretCommand _updateSecretCommand;
private readonly IDeleteSecretCommand _deleteSecretCommand;
private readonly IUserService _userService;
public SecretsController(
ICurrentContext currentContext,
ISecretRepository secretRepository,
ICreateSecretCommand createSecretCommand,
IUpdateSecretCommand updateSecretCommand,
IDeleteSecretCommand deleteSecretCommand)
public SecretsController(ISecretRepository secretRepository, IProjectRepository projectRepository, ICreateSecretCommand createSecretCommand, IUpdateSecretCommand updateSecretCommand, IDeleteSecretCommand deleteSecretCommand, IUserService userService, ICurrentContext currentContext)
{
_currentContext = currentContext;
_secretRepository = secretRepository;
_createSecretCommand = createSecretCommand;
_updateSecretCommand = updateSecretCommand;
_deleteSecretCommand = deleteSecretCommand;
_projectRepository = projectRepository;
_userService = userService;
}
[HttpGet("organizations/{organizationId}/secrets")]
@ -42,7 +44,12 @@ public class SecretsController : Controller
throw new NotFoundException();
}
var secrets = await _secretRepository.GetManyByOrganizationIdAsync(organizationId);
var userId = _userService.GetProperUserId(User).Value;
var orgAdmin = await _currentContext.OrganizationAdmin(organizationId);
var accessClient = AccessClientHelper.ToAccessClient(_currentContext.ClientType, orgAdmin);
var secrets = await _secretRepository.GetManyByOrganizationIdAsync(organizationId, userId, accessClient);
return new SecretWithProjectsListResponseModel(secrets);
}
@ -54,7 +61,8 @@ public class SecretsController : Controller
throw new NotFoundException();
}
var result = await _createSecretCommand.CreateAsync(createRequest.ToSecret(organizationId));
var userId = _userService.GetProperUserId(User).Value;
var result = await _createSecretCommand.CreateAsync(createRequest.ToSecret(organizationId), userId);
return new SecretResponseModel(result);
}
@ -62,34 +70,74 @@ public class SecretsController : Controller
public async Task<SecretResponseModel> GetAsync([FromRoute] Guid id)
{
var secret = await _secretRepository.GetByIdAsync(id);
if (secret == null)
if (secret == null || !_currentContext.AccessSecretsManager(secret.OrganizationId))
{
throw new NotFoundException();
}
if (!await UserHasReadAccessToSecret(secret))
{
throw new NotFoundException();
}
return new SecretResponseModel(secret);
}
[HttpGet("projects/{projectId}/secrets")]
public async Task<SecretWithProjectsListResponseModel> GetSecretsByProjectAsync([FromRoute] Guid projectId)
{
var secrets = await _secretRepository.GetManyByProjectIdAsync(projectId);
var responses = secrets.Select(s => new SecretResponseModel(s));
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.GetManyByProjectIdAsync(projectId, userId, accessClient);
return new SecretWithProjectsListResponseModel(secrets);
}
[HttpPut("secrets/{id}")]
public async Task<SecretResponseModel> UpdateAsync([FromRoute] Guid id, [FromBody] SecretUpdateRequestModel updateRequest)
public async Task<SecretResponseModel> UpdateSecretAsync([FromRoute] Guid id, [FromBody] SecretUpdateRequestModel updateRequest)
{
var result = await _updateSecretCommand.UpdateAsync(updateRequest.ToSecret(id));
var userId = _userService.GetProperUserId(User).Value;
var secret = updateRequest.ToSecret(id);
var result = await _updateSecretCommand.UpdateAsync(secret, userId);
return new SecretResponseModel(result);
}
// TODO Once permissions are setup for Secrets Manager need to enforce them on delete.
[HttpPost("secrets/delete")]
public async Task<ListResponseModel<BulkDeleteResponseModel>> BulkDeleteAsync([FromBody] List<Guid> ids)
{
var results = await _deleteSecretCommand.DeleteSecrets(ids);
var userId = _userService.GetProperUserId(User).Value;
var results = await _deleteSecretCommand.DeleteSecrets(ids, userId);
var responses = results.Select(r => new BulkDeleteResponseModel(r.Item1.Id, r.Item2));
return new ListResponseModel<BulkDeleteResponseModel>(responses);
}
public async Task<bool> UserHasReadAccessToSecret(Secret secret)
{
var userId = _userService.GetProperUserId(User).Value;
var orgAdmin = await _currentContext.OrganizationAdmin(secret.OrganizationId);
var accessClient = AccessClientHelper.ToAccessClient(_currentContext.ClientType, orgAdmin);
var hasAccess = orgAdmin;
if (secret.Projects?.Count > 0)
{
Guid projectId = secret.Projects.FirstOrDefault().Id;
hasAccess = accessClient switch
{
AccessClientType.NoAccessCheck => true,
AccessClientType.User => await _projectRepository.UserHasReadAccessToProject(projectId, userId),
AccessClientType.ServiceAccount => await _projectRepository.ServiceAccountHasReadAccessToProject(projectId, userId),
_ => false,
};
}
return hasAccess;
}
}

View File

@ -38,7 +38,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);
var secrets = await _secretRepository.GetManyByOrganizationIdAsync(organizationId, userId, AccessClientType.NoAccessCheck);
if (projects == null && secrets == null)
{

View File

@ -4,5 +4,5 @@ namespace Bit.Core.SecretsManager.Commands.Secrets.Interfaces;
public interface ICreateSecretCommand
{
Task<Secret> CreateAsync(Secret secret);
Task<Secret> CreateAsync(Secret secret, Guid userId);
}

View File

@ -4,6 +4,6 @@ namespace Bit.Core.SecretsManager.Commands.Secrets.Interfaces;
public interface IDeleteSecretCommand
{
Task<List<Tuple<Secret, string>>> DeleteSecrets(List<Guid> ids);
Task<List<Tuple<Secret, string>>> DeleteSecrets(List<Guid> ids, Guid userId);
}

View File

@ -4,5 +4,5 @@ namespace Bit.Core.SecretsManager.Commands.Secrets.Interfaces;
public interface IUpdateSecretCommand
{
Task<Secret> UpdateAsync(Secret secret);
Task<Secret> UpdateAsync(Secret secret, Guid userId);
}

View File

@ -15,4 +15,6 @@ public interface IProjectRepository
Task<IEnumerable<Project>> ImportAsync(IEnumerable<Project> projects);
Task<bool> UserHasReadAccessToProject(Guid id, Guid userId);
Task<bool> UserHasWriteAccessToProject(Guid id, Guid userId);
Task<bool> ServiceAccountHasWriteAccessToProject(Guid id, Guid userId);
Task<bool> ServiceAccountHasReadAccessToProject(Guid id, Guid userId);
}

View File

@ -1,12 +1,13 @@
using Bit.Core.SecretsManager.Entities;
using Bit.Core.Enums;
using Bit.Core.SecretsManager.Entities;
namespace Bit.Core.SecretsManager.Repositories;
public interface ISecretRepository
{
Task<IEnumerable<Secret>> GetManyByOrganizationIdAsync(Guid organizationId);
Task<IEnumerable<Secret>> GetManyByOrganizationIdAsync(Guid organizationId, Guid userId, AccessClientType accessType);
Task<IEnumerable<Secret>> GetManyByIds(IEnumerable<Guid> ids);
Task<IEnumerable<Secret>> GetManyByProjectIdAsync(Guid projectId);
Task<IEnumerable<Secret>> GetManyByProjectIdAsync(Guid projectId, Guid userId, AccessClientType accessType);
Task<Secret> GetByIdAsync(Guid id);
Task<Secret> CreateAsync(Secret secret);
Task<Secret> UpdateAsync(Secret secret);

View File

@ -1,9 +1,11 @@
using System.Net;
using System.Net.Http.Headers;
using Bit.Api.IntegrationTest.Factories;
using Bit.Api.IntegrationTest.SecretsManager.Enums;
using Bit.Api.Models.Response;
using Bit.Api.SecretsManager.Models.Request;
using Bit.Api.SecretsManager.Models.Response;
using Bit.Core.Enums;
using Bit.Core.SecretsManager.Entities;
using Bit.Core.SecretsManager.Repositories;
using Bit.Test.Common.Helpers;
@ -20,6 +22,7 @@ public class SecretsControllerTest : IClassFixture<ApiApplicationFactory>, IAsyn
private readonly ApiApplicationFactory _factory;
private readonly ISecretRepository _secretRepository;
private readonly IProjectRepository _projectRepository;
private readonly IAccessPolicyRepository _accessPolicyRepository;
private string _email = null!;
private SecretsManagerOrganizationHelper _organizationHelper = null!;
@ -30,6 +33,7 @@ public class SecretsControllerTest : IClassFixture<ApiApplicationFactory>, IAsyn
_client = _factory.CreateClient();
_secretRepository = _factory.GetService<ISecretRepository>();
_projectRepository = _factory.GetService<IProjectRepository>();
_accessPolicyRepository = _factory.GetService<IAccessPolicyRepository>();
}
public async Task InitializeAsync()
@ -64,12 +68,36 @@ public class SecretsControllerTest : IClassFixture<ApiApplicationFactory>, IAsyn
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Fact]
public async Task ListByOrganization_Owner_Success()
[Theory]
[InlineData(PermissionType.RunAsAdmin)]
[InlineData(PermissionType.RunAsUserWithPermission)]
public async Task ListByOrganization_Success(PermissionType permissionType)
{
var (org, _) = await _organizationHelper.Initialize(true, true);
var (org, orgUserOwner) = await _organizationHelper.Initialize(true, true);
await LoginAsync(_email);
var project = await _projectRepository.CreateAsync(new Project
{
Id = new Guid(),
OrganizationId = org.Id,
Name = _mockEncryptedString,
});
if (permissionType == PermissionType.RunAsUserWithPermission)
{
var (email, orgUser) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);
await LoginAsync(email);
var accessPolicies = new List<BaseAccessPolicy>
{
new UserProjectAccessPolicy
{
GrantedProjectId = project.Id, OrganizationUserId = orgUser.Id, Read = true, Write = true,
},
};
await _accessPolicyRepository.CreateManyAsync(accessPolicies);
}
var secretIds = new List<Guid>();
for (var i = 0; i < 3; i++)
{
@ -78,7 +106,9 @@ public class SecretsControllerTest : IClassFixture<ApiApplicationFactory>, IAsyn
OrganizationId = org.Id,
Key = _mockEncryptedString,
Value = _mockEncryptedString,
Note = _mockEncryptedString
Note = _mockEncryptedString,
Projects = new List<Project> { project }
});
secretIds.Add(secret.Id);
}
@ -113,7 +143,7 @@ public class SecretsControllerTest : IClassFixture<ApiApplicationFactory>, IAsyn
}
[Fact]
public async Task Create_Owner_Success()
public async Task CreateWithoutProject_RunAsAdmin_Success()
{
var (org, _) = await _organizationHelper.Initialize(true, true);
await LoginAsync(_email);
@ -147,11 +177,33 @@ public class SecretsControllerTest : IClassFixture<ApiApplicationFactory>, IAsyn
}
[Fact]
public async Task CreateWithProject_Owner_Success()
public async Task CreateWithoutProject_RunAsUser_NotFound()
{
var (org, _) = await _organizationHelper.Initialize(true, true);
var (email, orgUser) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);
await LoginAsync(email);
var request = new SecretCreateRequestModel
{
Key = _mockEncryptedString,
Value = _mockEncryptedString,
Note = _mockEncryptedString
};
var response = await _client.PostAsJsonAsync($"/organizations/{org.Id}/secrets", request);
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Theory]
[InlineData(PermissionType.RunAsAdmin)]
[InlineData(PermissionType.RunAsUserWithPermission)]
public async Task CreateWithProject_Success(PermissionType permissionType)
{
var (org, orgAdminUser) = await _organizationHelper.Initialize(true, true);
await LoginAsync(_email);
AccessClientType accessType = AccessClientType.NoAccessCheck;
var project = await _projectRepository.CreateAsync(new Project()
{
Id = new Guid(),
@ -159,6 +211,25 @@ public class SecretsControllerTest : IClassFixture<ApiApplicationFactory>, IAsyn
Name = _mockEncryptedString
});
var orgUserId = (Guid)orgAdminUser.UserId;
if (permissionType == PermissionType.RunAsUserWithPermission)
{
var (email, orgUser) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);
await LoginAsync(email);
accessType = AccessClientType.User;
var accessPolicies = new List<BaseAccessPolicy>
{
new Core.SecretsManager.Entities.UserProjectAccessPolicy
{
GrantedProjectId = project.Id, OrganizationUserId = orgUser.Id , Read = true, Write = true,
},
};
orgUserId = (Guid)orgUser.UserId;
await _accessPolicyRepository.CreateManyAsync(accessPolicies);
}
var secretRequest = new SecretCreateRequestModel()
{
Key = _mockEncryptedString,
@ -170,7 +241,7 @@ public class SecretsControllerTest : IClassFixture<ApiApplicationFactory>, IAsyn
secretResponse.EnsureSuccessStatusCode();
var secretResult = await secretResponse.Content.ReadFromJsonAsync<SecretResponseModel>();
var secret = (await _secretRepository.GetManyByProjectIdAsync(project.Id)).First();
var secret = (await _secretRepository.GetManyByProjectIdAsync(project.Id, orgUserId, accessType)).First();
Assert.NotNull(secretResult);
Assert.Equal(secret.Id.ToString(), secretResult!.Id);
@ -203,18 +274,48 @@ public class SecretsControllerTest : IClassFixture<ApiApplicationFactory>, IAsyn
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Fact]
public async Task Get_Owner_Success()
[Theory]
[InlineData(PermissionType.RunAsAdmin)]
[InlineData(PermissionType.RunAsUserWithPermission)]
public async Task Get_Success(PermissionType permissionType)
{
var (org, _) = await _organizationHelper.Initialize(true, true);
await LoginAsync(_email);
var project = await _projectRepository.CreateAsync(new Project()
{
Id = new Guid(),
OrganizationId = org.Id,
Name = _mockEncryptedString
});
if (permissionType == PermissionType.RunAsUserWithPermission)
{
var (email, orgUser) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);
await LoginAsync(email);
var accessPolicies = new List<BaseAccessPolicy>
{
new UserProjectAccessPolicy
{
GrantedProjectId = project.Id, OrganizationUserId = orgUser.Id, Read = true, Write = true,
},
};
await _accessPolicyRepository.CreateManyAsync(accessPolicies);
}
else
{
var (email, orgUser) = await _organizationHelper.CreateNewUser(OrganizationUserType.Admin, true);
await LoginAsync(email);
}
var secret = await _secretRepository.CreateAsync(new Secret
{
OrganizationId = org.Id,
Key = _mockEncryptedString,
Value = _mockEncryptedString,
Note = _mockEncryptedString
Note = _mockEncryptedString,
Projects = new List<Project> { project }
});
var response = await _client.GetAsync($"/secrets/{secret.Id}");
@ -255,25 +356,51 @@ public class SecretsControllerTest : IClassFixture<ApiApplicationFactory>, IAsyn
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Fact]
public async Task Update_Owner_Success()
[Theory]
[InlineData(PermissionType.RunAsAdmin)]
[InlineData(PermissionType.RunAsUserWithPermission)]
public async Task Update_Success(PermissionType permissionType)
{
var (org, _) = await _organizationHelper.Initialize(true, true);
await LoginAsync(_email);
var project = await _projectRepository.CreateAsync(new Project()
{
Id = new Guid(),
OrganizationId = org.Id,
Name = _mockEncryptedString
});
if (permissionType == PermissionType.RunAsUserWithPermission)
{
var (email, orgUser) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);
await LoginAsync(email);
var accessPolicies = new List<BaseAccessPolicy>
{
new UserProjectAccessPolicy
{
GrantedProjectId = project.Id, OrganizationUserId = orgUser.Id, Read = true, Write = true,
},
};
await _accessPolicyRepository.CreateManyAsync(accessPolicies);
}
var secret = await _secretRepository.CreateAsync(new Secret
{
OrganizationId = org.Id,
Key = _mockEncryptedString,
Value = _mockEncryptedString,
Note = _mockEncryptedString
Note = _mockEncryptedString,
Projects = permissionType == PermissionType.RunAsUserWithPermission ? new List<Project>() { project } : null
});
var request = new SecretUpdateRequestModel()
{
Key = _mockEncryptedString,
Value = "2.3Uk+WNBIoU5xzmVFNcoWzz==|1MsPIYuRfdOHfu/0uY6H2Q==|/98xy4wb6pHP1VTZ9JcNCYgQjEUMFPlqJgCwRk1YXKg=",
Note = _mockEncryptedString
Note = _mockEncryptedString,
ProjectIds = permissionType == PermissionType.RunAsUserWithPermission ? new Guid[] { project.Id } : null
};
var response = await _client.PutAsJsonAsync($"/secrets/{secret.Id}", request);
@ -316,16 +443,41 @@ public class SecretsControllerTest : IClassFixture<ApiApplicationFactory>, IAsyn
});
var secretIds = new[] { secret.Id };
var response = await _client.PostAsJsonAsync("/secrets/delete", secretIds);
var response = await _client.PostAsJsonAsync($"/secrets/{org.Id}/delete", secretIds);
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Fact]
public async Task Delete_Owner_Success()
[Theory]
[InlineData(PermissionType.RunAsAdmin)]
[InlineData(PermissionType.RunAsUserWithPermission)]
public async Task Delete_Success(PermissionType permissionType)
{
var (org, _) = await _organizationHelper.Initialize(true, true);
await LoginAsync(_email);
var project = await _projectRepository.CreateAsync(new Project()
{
Id = new Guid(),
OrganizationId = org.Id,
Name = _mockEncryptedString
});
if (permissionType == PermissionType.RunAsUserWithPermission)
{
var (email, orgUser) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);
await LoginAsync(email);
var accessPolicies = new List<BaseAccessPolicy>
{
new UserProjectAccessPolicy
{
GrantedProjectId = project.Id, OrganizationUserId = orgUser.Id, Read = true, Write = true,
},
};
await _accessPolicyRepository.CreateManyAsync(accessPolicies);
}
var secretIds = new List<Guid>();
for (var i = 0; i < 3; i++)
{
@ -334,12 +486,13 @@ public class SecretsControllerTest : IClassFixture<ApiApplicationFactory>, IAsyn
OrganizationId = org.Id,
Key = _mockEncryptedString,
Value = _mockEncryptedString,
Note = _mockEncryptedString
Note = _mockEncryptedString,
Projects = new List<Project>() { project }
});
secretIds.Add(secret.Id);
}
var response = await _client.PostAsJsonAsync("/secrets/delete", secretIds);
var response = await _client.PostAsJsonAsync($"/secrets/delete", secretIds);
response.EnsureSuccessStatusCode();
var results = await response.Content.ReadFromJsonAsync<ListResponseModel<BulkDeleteResponseModel>>();

View File

@ -1,10 +1,13 @@
using Bit.Api.SecretsManager.Controllers;
using Bit.Api.SecretsManager.Models.Request;
using Bit.Api.Test.SecretsManager.Enums;
using Bit.Core.Context;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.SecretsManager.Commands.Secrets.Interfaces;
using Bit.Core.SecretsManager.Entities;
using Bit.Core.SecretsManager.Repositories;
using Bit.Core.Services;
using Bit.Core.Test.SecretsManager.AutoFixture.SecretsFixture;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
@ -22,33 +25,50 @@ public class SecretsControllerTests
{
[Theory]
[BitAutoData]
public async void GetSecretsByOrganization_ReturnsEmptyList(SutProvider<SecretsController> sutProvider, Guid id)
public async void GetSecretsByOrganization_ReturnsEmptyList(SutProvider<SecretsController> sutProvider, Guid id, Guid organizationId, Guid userId, AccessClientType accessType)
{
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(id).Returns(true);
sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(organizationId).Returns(true);
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
var result = await sutProvider.Sut.ListByOrganizationAsync(id);
await sutProvider.GetDependency<ISecretRepository>().Received(1)
.GetManyByOrganizationIdAsync(Arg.Is(AssertHelper.AssertPropertyEqual(id)));
.GetManyByOrganizationIdAsync(Arg.Is(AssertHelper.AssertPropertyEqual(id)), userId, accessType);
Assert.Empty(result.Secrets);
}
[Theory]
[BitAutoData]
public async void GetSecretsByOrganization_Success(SutProvider<SecretsController> sutProvider, Secret resultSecret)
[BitAutoData(PermissionType.RunAsAdmin)]
[BitAutoData(PermissionType.RunAsUserWithPermission)]
public async void GetSecretsByOrganization_Success(PermissionType permissionType, SutProvider<SecretsController> sutProvider, Core.SecretsManager.Entities.Secret resultSecret, Guid organizationId, Guid userId, Core.SecretsManager.Entities.Project mockProject, AccessClientType accessType)
{
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(default).ReturnsForAnyArgs(true);
sutProvider.GetDependency<ISecretRepository>().GetManyByOrganizationIdAsync(default).ReturnsForAnyArgs(new List<Secret> { resultSecret });
sutProvider.GetDependency<ISecretRepository>().GetManyByOrganizationIdAsync(default, default, default).ReturnsForAnyArgs(new List<Core.SecretsManager.Entities.Secret> { resultSecret });
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
if (permissionType == PermissionType.RunAsAdmin)
{
sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(organizationId).Returns(true);
}
else
{
resultSecret.Projects = new List<Core.SecretsManager.Entities.Project>() { mockProject };
sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(organizationId).Returns(false);
sutProvider.GetDependency<IProjectRepository>().UserHasReadAccessToProject(mockProject.Id, userId).Returns(true);
}
var result = await sutProvider.Sut.ListByOrganizationAsync(resultSecret.OrganizationId);
await sutProvider.GetDependency<ISecretRepository>().Received(1)
.GetManyByOrganizationIdAsync(Arg.Is(AssertHelper.AssertPropertyEqual(resultSecret.OrganizationId)));
.GetManyByOrganizationIdAsync(Arg.Is(AssertHelper.AssertPropertyEqual(resultSecret.OrganizationId)), userId, accessType);
}
[Theory]
[BitAutoData]
public async void GetSecretsByOrganization_AccessDenied_Throws(SutProvider<SecretsController> sutProvider, Secret resultSecret)
public async void GetSecretsByOrganization_AccessDenied_Throws(SutProvider<SecretsController> sutProvider, Core.SecretsManager.Entities.Secret resultSecret)
{
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(default).ReturnsForAnyArgs(false);
@ -64,11 +84,29 @@ public class SecretsControllerTests
}
[Theory]
[BitAutoData]
public async void GetSecret_Success(SutProvider<SecretsController> sutProvider, Secret resultSecret)
[BitAutoData(PermissionType.RunAsAdmin)]
[BitAutoData(PermissionType.RunAsUserWithPermission)]
public async void GetSecret_Success(PermissionType permissionType, SutProvider<SecretsController> sutProvider, Secret resultSecret, Guid userId, Guid organizationId, Project mockProject)
{
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(organizationId).Returns(true);
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
mockProject.OrganizationId = organizationId;
resultSecret.Projects = new List<Project>() { mockProject };
resultSecret.OrganizationId = organizationId;
sutProvider.GetDependency<ISecretRepository>().GetByIdAsync(default).ReturnsForAnyArgs(resultSecret);
if (permissionType == PermissionType.RunAsAdmin)
{
resultSecret.OrganizationId = organizationId;
sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(organizationId).Returns(true);
}
else
{
sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(organizationId).Returns(false);
sutProvider.GetDependency<IProjectRepository>().UserHasReadAccessToProject(mockProject.Id, userId).Returns(true);
}
var result = await sutProvider.Sut.GetAsync(resultSecret.Id);
await sutProvider.GetDependency<ISecretRepository>().Received(1)
@ -76,46 +114,89 @@ public class SecretsControllerTests
}
[Theory]
[BitAutoData]
public async void CreateSecret_Success(SutProvider<SecretsController> sutProvider, SecretCreateRequestModel data, Guid organizationId)
[BitAutoData(PermissionType.RunAsAdmin)]
[BitAutoData(PermissionType.RunAsUserWithPermission)]
public async void CreateSecret_Success(PermissionType permissionType, SutProvider<SecretsController> sutProvider, SecretCreateRequestModel data, Guid organizationId, Project mockProject, Guid userId)
{
var resultSecret = data.ToSecret(organizationId);
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
if (permissionType == PermissionType.RunAsAdmin)
{
sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(organizationId).Returns(true);
}
else
{
resultSecret.Projects = new List<Core.SecretsManager.Entities.Project>() { mockProject };
sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(organizationId).Returns(false);
sutProvider.GetDependency<IProjectRepository>().UserHasReadAccessToProject(mockProject.Id, userId).Returns(true);
}
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(organizationId).Returns(true);
sutProvider.GetDependency<ICreateSecretCommand>().CreateAsync(default).ReturnsForAnyArgs(resultSecret);
sutProvider.GetDependency<ICreateSecretCommand>().CreateAsync(default, userId).ReturnsForAnyArgs(resultSecret);
var result = await sutProvider.Sut.CreateAsync(organizationId, data);
await sutProvider.GetDependency<ICreateSecretCommand>().Received(1)
.CreateAsync(Arg.Any<Secret>());
.CreateAsync(Arg.Any<Secret>(), userId);
}
[Theory]
[BitAutoData]
public async void UpdateSecret_Success(SutProvider<SecretsController> sutProvider, SecretUpdateRequestModel data, Guid secretId)
[BitAutoData(PermissionType.RunAsAdmin)]
[BitAutoData(PermissionType.RunAsUserWithPermission)]
public async void UpdateSecret_Success(PermissionType permissionType, SutProvider<SecretsController> sutProvider, SecretUpdateRequestModel data, Guid secretId, Guid organizationId, Guid userId, Project mockProject)
{
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
if (permissionType == PermissionType.RunAsAdmin)
{
sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(organizationId).Returns(true);
}
else
{
data.ProjectIds = new Guid[] { mockProject.Id };
sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(organizationId).Returns(false);
sutProvider.GetDependency<IProjectRepository>().UserHasReadAccessToProject(mockProject.Id, userId).Returns(true);
}
var resultSecret = data.ToSecret(secretId);
sutProvider.GetDependency<IUpdateSecretCommand>().UpdateAsync(default).ReturnsForAnyArgs(resultSecret);
sutProvider.GetDependency<IUpdateSecretCommand>().UpdateAsync(default, userId).ReturnsForAnyArgs(resultSecret);
var result = await sutProvider.Sut.UpdateAsync(secretId, data);
var result = await sutProvider.Sut.UpdateSecretAsync(secretId, data);
await sutProvider.GetDependency<IUpdateSecretCommand>().Received(1)
.UpdateAsync(Arg.Any<Secret>());
.UpdateAsync(Arg.Any<Secret>(), userId);
}
[Theory]
[BitAutoData]
public async void BulkDeleteSecret_Success(SutProvider<SecretsController> sutProvider, List<Secret> data)
[BitAutoData(PermissionType.RunAsAdmin)]
[BitAutoData(PermissionType.RunAsUserWithPermission)]
public async void BulkDeleteSecret_Success(PermissionType permissionType, SutProvider<SecretsController> sutProvider, List<Secret> data, Guid organizationId, Guid userId, Project mockProject)
{
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
if (permissionType == PermissionType.RunAsAdmin)
{
sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(organizationId).Returns(true);
}
else
{
data.FirstOrDefault().Projects = new List<Project>() { mockProject };
sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(organizationId).Returns(false);
sutProvider.GetDependency<IProjectRepository>().UserHasReadAccessToProject(mockProject.Id, userId).Returns(true);
}
var ids = data.Select(secret => secret.Id).ToList();
var mockResult = new List<Tuple<Secret, string>>();
foreach (var secret in data)
{
mockResult.Add(new Tuple<Secret, string>(secret, ""));
}
sutProvider.GetDependency<IDeleteSecretCommand>().DeleteSecrets(ids).ReturnsForAnyArgs(mockResult);
sutProvider.GetDependency<IDeleteSecretCommand>().DeleteSecrets(ids, userId).ReturnsForAnyArgs(mockResult);
var results = await sutProvider.Sut.BulkDeleteAsync(ids);
await sutProvider.GetDependency<IDeleteSecretCommand>().Received(1)
.DeleteSecrets(Arg.Is(ids));
.DeleteSecrets(Arg.Is(ids), userId);
Assert.Equal(data.Count, results.Data.Count());
}
@ -123,6 +204,7 @@ public class SecretsControllerTests
[BitAutoData]
public async void BulkDeleteSecret_NoGuids_ThrowsArgumentNullException(SutProvider<SecretsController> sutProvider)
{
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(new Guid());
await Assert.ThrowsAsync<ArgumentNullException>(() => sutProvider.Sut.BulkDeleteAsync(new List<Guid>()));
}
}