1
0
mirror of https://github.com/bitwarden/server.git synced 2025-04-11 16:18:14 -05:00

[SM-381] New secrets access ()

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

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 ()

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 ()

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 ()

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

* Read UTC DateTimes from db and order by revision

* Move orderby to repo layer

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

* Sm 104 project Database ()

* 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 ()

* 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 ()

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

* 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 ()

* 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 ()

* Secrets return empty list

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

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

* Adding integration tests for the SecretsController

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

* Sm 95 - Adding GetProjects endpoint ()

* 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 ()

* SM-191 Create ServiceAccount Table

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

* SM-207 list service accounts by org

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

* 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 ()

* 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 ()

* SM-282 Delete Projects ()

* SM-282 delete & bulk delete projects

* Have delete commands return tuple with object

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

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

* 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 ()

* 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 ()

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

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

* Add create access token for service accounts

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

* 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 ()

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

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

* 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 ()

* [SM-299] Add UseSecretsManager flag ()

* [SM-193] Access Policy ()

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

* 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 ()

* 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 ()

* 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 ()

* 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 ()

* 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
bitwarden_license
src
Commercial.Core/SecretsManager/Commands/Secrets
Commercial.Infrastructure.EntityFramework/SecretsManager/Repositories
test/Commercial.Core.Test/SecretsManager/Secrets
src
test
Api.IntegrationTest/SecretsManager/Controllers
Api.Test/SecretsManager/Controllers

@ -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.Entities;
using Bit.Core.SecretsManager.Repositories; using Bit.Core.SecretsManager.Repositories;
@ -7,14 +10,34 @@ namespace Bit.Commercial.Core.SecretsManager.Commands.Secrets;
public class CreateSecretCommand : ICreateSecretCommand public class CreateSecretCommand : ICreateSecretCommand
{ {
private readonly ISecretRepository _secretRepository; 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; _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); return await _secretRepository.CreateAsync(secret);
} }
} }

@ -1,4 +1,5 @@
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Enums;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.SecretsManager.Commands.Secrets.Interfaces; using Bit.Core.SecretsManager.Commands.Secrets.Interfaces;
using Bit.Core.SecretsManager.Entities; using Bit.Core.SecretsManager.Entities;
@ -10,25 +11,27 @@ public class DeleteSecretCommand : IDeleteSecretCommand
{ {
private readonly ICurrentContext _currentContext; private readonly ICurrentContext _currentContext;
private readonly ISecretRepository _secretRepository; private readonly ISecretRepository _secretRepository;
private readonly IProjectRepository _projectRepository;
public DeleteSecretCommand(ICurrentContext currentContext, ISecretRepository secretRepository) public DeleteSecretCommand(ISecretRepository secretRepository, IProjectRepository projectRepository, ICurrentContext currentContext)
{ {
_currentContext = currentContext; _currentContext = currentContext;
_secretRepository = secretRepository; _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(); throw new NotFoundException();
} }
// Ensure all secrets belongs to the same organization // Ensure all secrets belongs to the same organization
var organizationId = secrets.First().OrganizationId; var organizationId = secrets.First().OrganizationId;
if (secrets.Any(p => p.OrganizationId != organizationId)) if (secrets.Any(secret => secret.OrganizationId != organizationId))
{ {
throw new BadRequestException(); throw new BadRequestException();
} }
@ -38,21 +41,46 @@ public class DeleteSecretCommand : IDeleteSecretCommand
throw new NotFoundException(); 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); var hasAccess = orgAdmin;
if (secret == null)
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 else
{ {
return new Tuple<Secret, string>(secret, ""); deleteIds.Add(secret.Id);
results.Add(new Tuple<Secret, string>(secret, ""));
}
}
if (deleteIds.Count > 0)
{
await _secretRepository.SoftDeleteManyByIdAsync(deleteIds);
} }
}).ToList();
await _secretRepository.SoftDeleteManyByIdAsync(ids);
return results; return results;
} }
} }

@ -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.Commands.Secrets.Interfaces;
using Bit.Core.SecretsManager.Entities; using Bit.Core.SecretsManager.Entities;
using Bit.Core.SecretsManager.Repositories; using Bit.Core.SecretsManager.Repositories;
@ -8,23 +10,45 @@ namespace Bit.Commercial.Core.SecretsManager.Commands.Secrets;
public class UpdateSecretCommand : IUpdateSecretCommand public class UpdateSecretCommand : IUpdateSecretCommand
{ {
private readonly ISecretRepository _secretRepository; 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; _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); var secret = await _secretRepository.GetByIdAsync(updatedSecret.Id);
if (existingSecret == null) if (secret == null || !_currentContext.AccessSecretsManager(secret.OrganizationId))
{ {
throw new NotFoundException(); throw new NotFoundException();
} }
secret.OrganizationId = existingSecret.OrganizationId; var orgAdmin = await _currentContext.OrganizationAdmin(secret.OrganizationId);
secret.CreationDate = existingSecret.CreationDate; var accessClient = AccessClientHelper.ToAccessClient(_currentContext.ClientType, orgAdmin);
secret.DeletedDate = existingSecret.DeletedDate;
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; secret.RevisionDate = DateTime.UtcNow;
await _secretRepository.UpdateAsync(secret); await _secretRepository.UpdateAsync(secret);

@ -74,6 +74,9 @@ public class ProjectRepository : Repository<Core.SecretsManager.Entities.Project
private static Expression<Func<Project, bool>> ServiceAccountHasReadAccessToProject(Guid serviceAccountId) => p => private static Expression<Func<Project, bool>> ServiceAccountHasReadAccessToProject(Guid serviceAccountId) => p =>
p.ServiceAccountAccessPolicies.Any(ap => ap.ServiceAccount.Id == serviceAccountId && ap.Read); 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) public async Task DeleteManyByIdAsync(IEnumerable<Guid> ids)
{ {
using (var scope = ServiceScopeFactory.CreateScope()) 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) public async Task<bool> UserHasReadAccessToProject(Guid id, Guid userId)
{ {
using var scope = ServiceScopeFactory.CreateScope(); using var scope = ServiceScopeFactory.CreateScope();

@ -1,4 +1,6 @@
using AutoMapper; using System.Linq.Expressions;
using AutoMapper;
using Bit.Core.Enums;
using Bit.Core.SecretsManager.Repositories; using Bit.Core.SecretsManager.Repositories;
using Bit.Infrastructure.EntityFramework; using Bit.Infrastructure.EntityFramework;
using Bit.Infrastructure.EntityFramework.Repositories; using Bit.Infrastructure.EntityFramework.Repositories;
@ -6,6 +8,7 @@ using Bit.Infrastructure.EntityFramework.SecretsManager.Models;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
namespace Bit.Commercial.Infrastructure.EntityFramework.SecretsManager.Repositories; namespace Bit.Commercial.Infrastructure.EntityFramework.SecretsManager.Repositories;
public class SecretRepository : Repository<Core.SecretsManager.Entities.Secret, Secret, Guid>, ISecretRepository 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 dbContext = GetDatabaseContext(scope);
var secrets = await dbContext.Secret var secrets = await dbContext.Secret
.Where(c => ids.Contains(c.Id) && c.DeletedDate == null) .Where(c => ids.Contains(c.Id) && c.DeletedDate == null)
.Include(c => c.Projects)
.ToListAsync(); .ToListAsync();
return Mapper.Map<List<Core.SecretsManager.Entities.Secret>>(secrets); return Mapper.Map<List<Core.SecretsManager.Entities.Secret>>(secrets);
} }
} }
public async Task<IEnumerable<Core.SecretsManager.Entities.Secret>> GetManyByOrganizationIdAsync(Guid organizationId) private static Expression<Func<Secret, bool>> ServiceAccountHasReadAccessToSecret(Guid serviceAccountId) => s =>
{ s.Projects.Any(p =>
using (var scope = ServiceScopeFactory.CreateScope()) p.ServiceAccountAccessPolicies.Any(ap => ap.ServiceAccount.Id == serviceAccountId && ap.Read));
{
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>> 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); 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()) using (var scope = ServiceScopeFactory.CreateScope())
{ {
var dbContext = GetDatabaseContext(scope); var dbContext = GetDatabaseContext(scope);
var secrets = await dbContext.Secret var query = dbContext.Secret.Include(s => s.Projects)
.Where(s => s.Projects.Any(p => p.Id == projectId) && s.DeletedDate == null).Include("Projects") .Where(s => s.Projects.Any(p => p.Id == projectId) && s.DeletedDate == null);
.OrderBy(s => s.RevisionDate).ToListAsync();
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); 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 dbContext = GetDatabaseContext(scope);
var mappedEntity = Mapper.Map<Secret>(secret); var mappedEntity = Mapper.Map<Secret>(secret);
var entity = await dbContext.Secret var entity = await dbContext.Secret
.Include("Projects") .Include("Projects")
.FirstAsync(s => s.Id == secret.Id); .FirstAsync(s => s.Id == secret.Id);

@ -1,4 +1,7 @@
using Bit.Commercial.Core.SecretsManager.Commands.Secrets; 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.Entities;
using Bit.Core.SecretsManager.Repositories; using Bit.Core.SecretsManager.Repositories;
using Bit.Core.Test.SecretsManager.AutoFixture.SecretsFixture; using Bit.Core.Test.SecretsManager.AutoFixture.SecretsFixture;
@ -14,14 +17,54 @@ namespace Bit.Commercial.Core.Test.SecretsManager.Secrets;
public class CreateSecretCommandTests public class CreateSecretCommandTests
{ {
[Theory] [Theory]
[BitAutoData] [BitAutoData(PermissionType.RunAsAdmin)]
public async Task CreateAsync_CallsCreate(Secret data, [BitAutoData(PermissionType.RunAsUserWithPermission)]
SutProvider<CreateSecretCommand> sutProvider) 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) await sutProvider.GetDependency<ISecretRepository>().Received(1)
.CreateAsync(data); .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));
}
} }

@ -1,16 +1,20 @@
using Bit.Commercial.Core.SecretsManager.Commands.Secrets; using Bit.Commercial.Core.SecretsManager.Commands.Secrets;
using Bit.Commercial.Core.Test.SecretsManager.Enums;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.SecretsManager.Entities; using Bit.Core.SecretsManager.Entities;
using Bit.Core.SecretsManager.Repositories; using Bit.Core.SecretsManager.Repositories;
using Bit.Core.Test.SecretsManager.AutoFixture.ProjectsFixture;
using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.AutoFixture.Attributes;
using Bit.Test.Common.Helpers;
using NSubstitute; using NSubstitute;
using Xunit; using Xunit;
namespace Bit.Commercial.Core.Test.SecretsManager.Secrets; namespace Bit.Commercial.Core.Test.SecretsManager.Secrets;
[SutProviderCustomize] [SutProviderCustomize]
[ProjectCustomize]
public class DeleteSecretCommandTests public class DeleteSecretCommandTests
{ {
[Theory] [Theory]
@ -20,7 +24,7 @@ public class DeleteSecretCommandTests
{ {
sutProvider.GetDependency<ISecretRepository>().GetManyByIds(data).Returns(new List<Secret>()); 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); await sutProvider.GetDependency<ISecretRepository>().DidNotReceiveWithAnyArgs().SoftDeleteManyByIdAsync(default);
} }
@ -36,22 +40,39 @@ public class DeleteSecretCommandTests
}; };
sutProvider.GetDependency<ISecretRepository>().GetManyByIds(data).Returns(new List<Secret>() { secret }); 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); await sutProvider.GetDependency<ISecretRepository>().DidNotReceiveWithAnyArgs().SoftDeleteManyByIdAsync(default);
} }
[Theory] [Theory]
[BitAutoData] [BitAutoData(PermissionType.RunAsAdmin)]
public async Task DeleteSecrets_Success(List<Guid> data, [BitAutoData(PermissionType.RunAsUserWithPermission)]
SutProvider<DeleteSecretCommand> sutProvider) 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>(); var secrets = new List<Secret>();
foreach (Guid id in data) foreach (Guid id in data)
{ {
var secret = new Secret() var secret = new Secret()
{ {
Id = id Id = id,
OrganizationId = organizationId,
Projects = projects
}; };
secrets.Add(secret); secrets.Add(secret);
} }
@ -59,9 +80,9 @@ public class DeleteSecretCommandTests
sutProvider.GetDependency<ISecretRepository>().GetManyByIds(data).Returns(secrets); sutProvider.GetDependency<ISecretRepository>().GetManyByIds(data).Returns(secrets);
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(default).ReturnsForAnyArgs(true); 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) foreach (var result in results)
{ {
Assert.Equal("", result.Item2); Assert.Equal("", result.Item2);

@ -1,7 +1,10 @@
using Bit.Commercial.Core.SecretsManager.Commands.Secrets; 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.Exceptions;
using Bit.Core.SecretsManager.Entities; using Bit.Core.SecretsManager.Entities;
using Bit.Core.SecretsManager.Repositories; using Bit.Core.SecretsManager.Repositories;
using Bit.Core.Test.SecretsManager.AutoFixture.ProjectsFixture;
using Bit.Core.Test.SecretsManager.AutoFixture.SecretsFixture; using Bit.Core.Test.SecretsManager.AutoFixture.SecretsFixture;
using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.AutoFixture.Attributes;
@ -13,23 +16,38 @@ namespace Bit.Commercial.Core.Test.SecretsManager.Secrets;
[SutProviderCustomize] [SutProviderCustomize]
[SecretCustomize] [SecretCustomize]
[ProjectCustomize]
public class UpdateSecretCommandTests public class UpdateSecretCommandTests
{ {
[Theory] [Theory]
[BitAutoData] [BitAutoData]
public async Task UpdateAsync_SecretDoesNotExist_ThrowsNotFound(Secret data, SutProvider<UpdateSecretCommand> sutProvider) 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); await sutProvider.GetDependency<ISecretRepository>().DidNotReceiveWithAnyArgs().UpdateAsync(default);
} }
[Theory] [Theory]
[BitAutoData] [BitAutoData(PermissionType.RunAsAdmin)]
public async Task UpdateAsync_CallsReplaceAsync(Secret data, SutProvider<UpdateSecretCommand> sutProvider) [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); 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) await sutProvider.GetDependency<ISecretRepository>().Received(1)
.UpdateAsync(data); .UpdateAsync(data);
@ -37,11 +55,14 @@ public class UpdateSecretCommandTests
[Theory] [Theory]
[BitAutoData] [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(); 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() var secretUpdate = new Secret()
{ {
OrganizationId = updatedOrgId, OrganizationId = updatedOrgId,
@ -49,7 +70,7 @@ public class UpdateSecretCommandTests
Key = existingSecret.Key, 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.Equal(existingSecret.OrganizationId, result.OrganizationId);
Assert.NotEqual(existingSecret.OrganizationId, updatedOrgId); Assert.NotEqual(existingSecret.OrganizationId, updatedOrgId);
@ -57,9 +78,11 @@ public class UpdateSecretCommandTests
[Theory] [Theory]
[BitAutoData] [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<ISecretRepository>().GetByIdAsync(existingSecret.Id).Returns(existingSecret);
sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(existingSecret.OrganizationId).Returns(true);
var updatedCreationDate = DateTime.UtcNow; var updatedCreationDate = DateTime.UtcNow;
var secretUpdate = new Secret() var secretUpdate = new Secret()
@ -67,9 +90,10 @@ public class UpdateSecretCommandTests
CreationDate = updatedCreationDate, CreationDate = updatedCreationDate,
Id = existingSecret.Id, Id = existingSecret.Id,
Key = existingSecret.Key, 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.Equal(existingSecret.CreationDate, result.CreationDate);
Assert.NotEqual(existingSecret.CreationDate, updatedCreationDate); Assert.NotEqual(existingSecret.CreationDate, updatedCreationDate);
@ -77,9 +101,11 @@ public class UpdateSecretCommandTests
[Theory] [Theory]
[BitAutoData] [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<ISecretRepository>().GetByIdAsync(existingSecret.Id).Returns(existingSecret);
sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(existingSecret.OrganizationId).Returns(true);
var updatedDeletionDate = DateTime.UtcNow; var updatedDeletionDate = DateTime.UtcNow;
var secretUpdate = new Secret() var secretUpdate = new Secret()
@ -87,9 +113,10 @@ public class UpdateSecretCommandTests
DeletedDate = updatedDeletionDate, DeletedDate = updatedDeletionDate,
Id = existingSecret.Id, Id = existingSecret.Id,
Key = existingSecret.Key, 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.Equal(existingSecret.DeletedDate, result.DeletedDate);
Assert.NotEqual(existingSecret.DeletedDate, updatedDeletionDate); Assert.NotEqual(existingSecret.DeletedDate, updatedDeletionDate);
@ -98,9 +125,12 @@ public class UpdateSecretCommandTests
[Theory] [Theory]
[BitAutoData] [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<ISecretRepository>().GetByIdAsync(existingSecret.Id).Returns(existingSecret);
sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(existingSecret.OrganizationId).Returns(true);
var updatedRevisionDate = DateTime.UtcNow.AddDays(10); var updatedRevisionDate = DateTime.UtcNow.AddDays(10);
var secretUpdate = new Secret() var secretUpdate = new Secret()
@ -108,11 +138,12 @@ public class UpdateSecretCommandTests
RevisionDate = updatedRevisionDate, RevisionDate = updatedRevisionDate,
Id = existingSecret.Id, Id = existingSecret.Id,
Key = existingSecret.Key, 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); AssertHelper.AssertRecent(result.RevisionDate);
} }
} }

@ -2,9 +2,12 @@
using Bit.Api.SecretsManager.Models.Request; using Bit.Api.SecretsManager.Models.Request;
using Bit.Api.SecretsManager.Models.Response; using Bit.Api.SecretsManager.Models.Response;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Enums;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.SecretsManager.Commands.Secrets.Interfaces; using Bit.Core.SecretsManager.Commands.Secrets.Interfaces;
using Bit.Core.SecretsManager.Entities;
using Bit.Core.SecretsManager.Repositories; using Bit.Core.SecretsManager.Repositories;
using Bit.Core.Services;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@ -16,22 +19,21 @@ public class SecretsController : Controller
{ {
private readonly ICurrentContext _currentContext; private readonly ICurrentContext _currentContext;
private readonly ISecretRepository _secretRepository; private readonly ISecretRepository _secretRepository;
private readonly IProjectRepository _projectRepository;
private readonly ICreateSecretCommand _createSecretCommand; private readonly ICreateSecretCommand _createSecretCommand;
private readonly IUpdateSecretCommand _updateSecretCommand; private readonly IUpdateSecretCommand _updateSecretCommand;
private readonly IDeleteSecretCommand _deleteSecretCommand; private readonly IDeleteSecretCommand _deleteSecretCommand;
private readonly IUserService _userService;
public SecretsController( public SecretsController(ISecretRepository secretRepository, IProjectRepository projectRepository, ICreateSecretCommand createSecretCommand, IUpdateSecretCommand updateSecretCommand, IDeleteSecretCommand deleteSecretCommand, IUserService userService, ICurrentContext currentContext)
ICurrentContext currentContext,
ISecretRepository secretRepository,
ICreateSecretCommand createSecretCommand,
IUpdateSecretCommand updateSecretCommand,
IDeleteSecretCommand deleteSecretCommand)
{ {
_currentContext = currentContext; _currentContext = currentContext;
_secretRepository = secretRepository; _secretRepository = secretRepository;
_createSecretCommand = createSecretCommand; _createSecretCommand = createSecretCommand;
_updateSecretCommand = updateSecretCommand; _updateSecretCommand = updateSecretCommand;
_deleteSecretCommand = deleteSecretCommand; _deleteSecretCommand = deleteSecretCommand;
_projectRepository = projectRepository;
_userService = userService;
} }
[HttpGet("organizations/{organizationId}/secrets")] [HttpGet("organizations/{organizationId}/secrets")]
@ -42,7 +44,12 @@ public class SecretsController : Controller
throw new NotFoundException(); 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); return new SecretWithProjectsListResponseModel(secrets);
} }
@ -54,7 +61,8 @@ public class SecretsController : Controller
throw new NotFoundException(); 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); return new SecretResponseModel(result);
} }
@ -62,34 +70,74 @@ public class SecretsController : Controller
public async Task<SecretResponseModel> GetAsync([FromRoute] Guid id) public async Task<SecretResponseModel> GetAsync([FromRoute] Guid id)
{ {
var secret = await _secretRepository.GetByIdAsync(id); var secret = await _secretRepository.GetByIdAsync(id);
if (secret == null) if (secret == null || !_currentContext.AccessSecretsManager(secret.OrganizationId))
{ {
throw new NotFoundException(); throw new NotFoundException();
} }
if (!await UserHasReadAccessToSecret(secret))
{
throw new NotFoundException();
}
return new SecretResponseModel(secret); return new SecretResponseModel(secret);
} }
[HttpGet("projects/{projectId}/secrets")] [HttpGet("projects/{projectId}/secrets")]
public async Task<SecretWithProjectsListResponseModel> GetSecretsByProjectAsync([FromRoute] Guid projectId) public async Task<SecretWithProjectsListResponseModel> GetSecretsByProjectAsync([FromRoute] Guid projectId)
{ {
var secrets = await _secretRepository.GetManyByProjectIdAsync(projectId); var project = await _projectRepository.GetByIdAsync(projectId);
var responses = secrets.Select(s => new SecretResponseModel(s)); 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); return new SecretWithProjectsListResponseModel(secrets);
} }
[HttpPut("secrets/{id}")] [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); return new SecretResponseModel(result);
} }
// TODO Once permissions are setup for Secrets Manager need to enforce them on delete.
[HttpPost("secrets/delete")] [HttpPost("secrets/delete")]
public async Task<ListResponseModel<BulkDeleteResponseModel>> BulkDeleteAsync([FromBody] List<Guid> ids) 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)); var responses = results.Select(r => new BulkDeleteResponseModel(r.Item1.Id, r.Item2));
return new ListResponseModel<BulkDeleteResponseModel>(responses); 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;
}
} }

@ -38,7 +38,7 @@ public class SecretsManagerPortingController : Controller
var userId = _userService.GetProperUserId(User).Value; var userId = _userService.GetProperUserId(User).Value;
var projects = await _projectRepository.GetManyByOrganizationIdAsync(organizationId, userId, AccessClientType.NoAccessCheck); 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) if (projects == null && secrets == null)
{ {

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

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

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

@ -15,4 +15,6 @@ public interface IProjectRepository
Task<IEnumerable<Project>> ImportAsync(IEnumerable<Project> projects); Task<IEnumerable<Project>> ImportAsync(IEnumerable<Project> projects);
Task<bool> UserHasReadAccessToProject(Guid id, Guid userId); Task<bool> UserHasReadAccessToProject(Guid id, Guid userId);
Task<bool> UserHasWriteAccessToProject(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);
} }

@ -1,12 +1,13 @@
using Bit.Core.SecretsManager.Entities; using Bit.Core.Enums;
using Bit.Core.SecretsManager.Entities;
namespace Bit.Core.SecretsManager.Repositories; namespace Bit.Core.SecretsManager.Repositories;
public interface ISecretRepository 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>> 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> GetByIdAsync(Guid id);
Task<Secret> CreateAsync(Secret secret); Task<Secret> CreateAsync(Secret secret);
Task<Secret> UpdateAsync(Secret secret); Task<Secret> UpdateAsync(Secret secret);

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

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