mirror of
https://github.com/bitwarden/server.git
synced 2024-11-21 12:05:42 +01:00
[SM-394] Secrets Manager (#2164)
Long lived feature branch for Secrets Manager Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> Co-authored-by: cd-bitwarden <106776772+cd-bitwarden@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:
parent
09e524c9a2
commit
1f0fc43278
1
.github/workflows/build.yml
vendored
1
.github/workflows/build.yml
vendored
@ -455,6 +455,7 @@ jobs:
|
||||
ASPNETCORE_ENVIRONMENT: Production
|
||||
swaggerGen: "True"
|
||||
DOTNET_ROLL_FORWARD_ON_NO_CANDIDATE_FX: 2
|
||||
GLOBALSETTINGS__SQLSERVER__CONNECTIONSTRING: "placeholder"
|
||||
|
||||
- name: Upload Swagger artifact
|
||||
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535
|
||||
|
@ -92,6 +92,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IntegrationTestCommon", "te
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Scim", "bitwarden_license\src\Scim\Scim.csproj", "{BC3B3F8C-621A-4CB8-9563-6EC0A2C8C747}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SqlServerEFScaffold", "util\SqlServerEFScaffold\SqlServerEFScaffold.csproj", "{2F2E8BB0-6838-48DA-B581-71B9F13DE364}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Commercial.Infrastructure.EntityFramework", "bitwarden_license\src\Commercial.Infrastructure.EntityFramework\Commercial.Infrastructure.EntityFramework.csproj", "{5AB3BBFB-9D98-4EF8-BFCD-462D50A16EB1}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Infrastructure.EFIntegration.Test", "test\Infrastructure.EFIntegration.Test\Infrastructure.EFIntegration.Test.csproj", "{7EFB1124-F40A-40EB-9EDA-94FD540AA8FD}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Api.IntegrationTest", "test\Api.IntegrationTest\Api.IntegrationTest.csproj", "{CBE96C6D-A4D6-46E1-94C5-42D6CAD8531C}"
|
||||
@ -236,6 +240,14 @@ Global
|
||||
{BC3B3F8C-621A-4CB8-9563-6EC0A2C8C747}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{BC3B3F8C-621A-4CB8-9563-6EC0A2C8C747}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{BC3B3F8C-621A-4CB8-9563-6EC0A2C8C747}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{2F2E8BB0-6838-48DA-B581-71B9F13DE364}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{2F2E8BB0-6838-48DA-B581-71B9F13DE364}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{2F2E8BB0-6838-48DA-B581-71B9F13DE364}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{2F2E8BB0-6838-48DA-B581-71B9F13DE364}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{5AB3BBFB-9D98-4EF8-BFCD-462D50A16EB1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{5AB3BBFB-9D98-4EF8-BFCD-462D50A16EB1}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{5AB3BBFB-9D98-4EF8-BFCD-462D50A16EB1}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{5AB3BBFB-9D98-4EF8-BFCD-462D50A16EB1}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{7EFB1124-F40A-40EB-9EDA-94FD540AA8FD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{7EFB1124-F40A-40EB-9EDA-94FD540AA8FD}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{7EFB1124-F40A-40EB-9EDA-94FD540AA8FD}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
@ -299,6 +311,8 @@ Global
|
||||
{0D3B2BD2-53F3-421D-AD8F-C19B954C796B} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
|
||||
{0923DE59-5FB1-44F2-9302-A09D2236B470} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
|
||||
{BC3B3F8C-621A-4CB8-9563-6EC0A2C8C747} = {4FDB6543-F68B-4202-9EA6-7FEA984D2D0A}
|
||||
{2F2E8BB0-6838-48DA-B581-71B9F13DE364} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84E}
|
||||
{5AB3BBFB-9D98-4EF8-BFCD-462D50A16EB1} = {4FDB6543-F68B-4202-9EA6-7FEA984D2D0A}
|
||||
{7EFB1124-F40A-40EB-9EDA-94FD540AA8FD} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
|
||||
{CBE96C6D-A4D6-46E1-94C5-42D6CAD8531C} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
|
||||
{FE998849-5FC8-41A2-B7C9-9227901471A0} = {287CFF34-BBDB-4BC4-AF88-1E19A5A4679B}
|
||||
|
@ -0,0 +1,23 @@
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.SecretManagerFeatures.AccessTokens.Interfaces;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Commercial.Core.SecretManagerFeatures.AccessTokens;
|
||||
|
||||
public class CreateAccessTokenCommand : ICreateAccessTokenCommand
|
||||
{
|
||||
private readonly int _clientSecretMaxLength = 30;
|
||||
private readonly IApiKeyRepository _apiKeyRepository;
|
||||
|
||||
public CreateAccessTokenCommand(IApiKeyRepository apiKeyRepository)
|
||||
{
|
||||
_apiKeyRepository = apiKeyRepository;
|
||||
}
|
||||
|
||||
public async Task<ApiKey> CreateAsync(ApiKey apiKey)
|
||||
{
|
||||
apiKey.ClientSecret = CoreHelpers.SecureRandomString(_clientSecretMaxLength);
|
||||
return await _apiKeyRepository.CreateAsync(apiKey);
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.SecretManagerFeatures.Projects.Interfaces;
|
||||
|
||||
namespace Bit.Commercial.Core.SecretManagerFeatures.Projects;
|
||||
|
||||
public class CreateProjectCommand : ICreateProjectCommand
|
||||
{
|
||||
private readonly IProjectRepository _projectRepository;
|
||||
|
||||
public CreateProjectCommand(IProjectRepository projectRepository)
|
||||
{
|
||||
_projectRepository = projectRepository;
|
||||
}
|
||||
|
||||
public async Task<Project> CreateAsync(Project project)
|
||||
{
|
||||
return await _projectRepository.CreateAsync(project);
|
||||
}
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.SecretManagerFeatures.Projects.Interfaces;
|
||||
|
||||
namespace Bit.Commercial.Core.SecretManagerFeatures.Projects;
|
||||
|
||||
public class DeleteProjectCommand : IDeleteProjectCommand
|
||||
{
|
||||
private readonly IProjectRepository _projectRepository;
|
||||
|
||||
public DeleteProjectCommand(IProjectRepository projectRepository)
|
||||
{
|
||||
_projectRepository = projectRepository;
|
||||
}
|
||||
|
||||
public async Task<List<Tuple<Project, string>>> DeleteProjects(List<Guid> ids)
|
||||
{
|
||||
var projects = await _projectRepository.GetManyByIds(ids);
|
||||
|
||||
if (projects?.Any() != true)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var results = ids.Select(id =>
|
||||
{
|
||||
var project = projects.FirstOrDefault(project => project.Id == id);
|
||||
if (project == null)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
// TODO Once permissions are implemented add check for each project here.
|
||||
else
|
||||
{
|
||||
return new Tuple<Project, string>(project, "");
|
||||
}
|
||||
}).ToList();
|
||||
|
||||
await _projectRepository.DeleteManyByIdAsync(ids);
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,33 @@
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.SecretManagerFeatures.Projects.Interfaces;
|
||||
|
||||
namespace Bit.Commercial.Core.SecretManagerFeatures.Projects;
|
||||
|
||||
public class UpdateProjectCommand : IUpdateProjectCommand
|
||||
{
|
||||
private readonly IProjectRepository _projectRepository;
|
||||
|
||||
public UpdateProjectCommand(IProjectRepository projectRepository)
|
||||
{
|
||||
_projectRepository = projectRepository;
|
||||
}
|
||||
|
||||
public async Task<Project> UpdateAsync(Project project)
|
||||
{
|
||||
var existingProject = await _projectRepository.GetByIdAsync(project.Id);
|
||||
if (existingProject == null)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
project.OrganizationId = existingProject.OrganizationId;
|
||||
project.CreationDate = existingProject.CreationDate;
|
||||
project.DeletedDate = existingProject.DeletedDate;
|
||||
project.RevisionDate = DateTime.UtcNow;
|
||||
|
||||
await _projectRepository.ReplaceAsync(project);
|
||||
return project;
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
using Bit.Commercial.Core.SecretManagerFeatures.AccessTokens;
|
||||
using Bit.Commercial.Core.SecretManagerFeatures.Projects;
|
||||
using Bit.Commercial.Core.SecretManagerFeatures.Secrets;
|
||||
using Bit.Commercial.Core.SecretManagerFeatures.ServiceAccounts;
|
||||
using Bit.Core.SecretManagerFeatures.AccessTokens.Interfaces;
|
||||
using Bit.Core.SecretManagerFeatures.Projects.Interfaces;
|
||||
using Bit.Core.SecretManagerFeatures.Secrets.Interfaces;
|
||||
using Bit.Core.SecretManagerFeatures.ServiceAccounts.Interfaces;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Bit.Commercial.Core.SecretManagerFeatures;
|
||||
|
||||
public static class SecretManagerCollectionExtensions
|
||||
{
|
||||
public static void AddSecretManagerServices(this IServiceCollection services)
|
||||
{
|
||||
services.AddScoped<ICreateSecretCommand, CreateSecretCommand>();
|
||||
services.AddScoped<IUpdateSecretCommand, UpdateSecretCommand>();
|
||||
services.AddScoped<IDeleteSecretCommand, DeleteSecretCommand>();
|
||||
services.AddScoped<ICreateProjectCommand, CreateProjectCommand>();
|
||||
services.AddScoped<IUpdateProjectCommand, UpdateProjectCommand>();
|
||||
services.AddScoped<IDeleteProjectCommand, DeleteProjectCommand>();
|
||||
services.AddScoped<ICreateServiceAccountCommand, CreateServiceAccountCommand>();
|
||||
services.AddScoped<IUpdateServiceAccountCommand, UpdateServiceAccountCommand>();
|
||||
services.AddScoped<ICreateAccessTokenCommand, CreateAccessTokenCommand>();
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,20 @@
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.SecretManagerFeatures.Secrets.Interfaces;
|
||||
|
||||
namespace Bit.Commercial.Core.SecretManagerFeatures.Secrets;
|
||||
|
||||
public class CreateSecretCommand : ICreateSecretCommand
|
||||
{
|
||||
private readonly ISecretRepository _secretRepository;
|
||||
|
||||
public CreateSecretCommand(ISecretRepository secretRepository)
|
||||
{
|
||||
_secretRepository = secretRepository;
|
||||
}
|
||||
|
||||
public async Task<Secret> CreateAsync(Secret secret)
|
||||
{
|
||||
return await _secretRepository.CreateAsync(secret);
|
||||
}
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.SecretManagerFeatures.Secrets.Interfaces;
|
||||
|
||||
namespace Bit.Commercial.Core.SecretManagerFeatures.Secrets;
|
||||
|
||||
public class DeleteSecretCommand : IDeleteSecretCommand
|
||||
{
|
||||
private readonly ISecretRepository _secretRepository;
|
||||
|
||||
public DeleteSecretCommand(ISecretRepository secretRepository)
|
||||
{
|
||||
_secretRepository = secretRepository;
|
||||
}
|
||||
|
||||
public async Task<List<Tuple<Secret, string>>> DeleteSecrets(List<Guid> ids)
|
||||
{
|
||||
var secrets = await _secretRepository.GetManyByIds(ids);
|
||||
|
||||
if (secrets?.Any() != true)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var results = ids.Select(id =>
|
||||
{
|
||||
var secret = secrets.FirstOrDefault(secret => secret.Id == id);
|
||||
if (secret == null)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
// TODO Once permissions are implemented add check for each secret here.
|
||||
else
|
||||
{
|
||||
return new Tuple<Secret, string>(secret, "");
|
||||
}
|
||||
}).ToList();
|
||||
|
||||
await _secretRepository.SoftDeleteManyByIdAsync(ids);
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,33 @@
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.SecretManagerFeatures.Secrets.Interfaces;
|
||||
|
||||
namespace Bit.Commercial.Core.SecretManagerFeatures.Secrets;
|
||||
|
||||
public class UpdateSecretCommand : IUpdateSecretCommand
|
||||
{
|
||||
private readonly ISecretRepository _secretRepository;
|
||||
|
||||
public UpdateSecretCommand(ISecretRepository secretRepository)
|
||||
{
|
||||
_secretRepository = secretRepository;
|
||||
}
|
||||
|
||||
public async Task<Secret> UpdateAsync(Secret secret)
|
||||
{
|
||||
var existingSecret = await _secretRepository.GetByIdAsync(secret.Id);
|
||||
if (existingSecret == null)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
secret.OrganizationId = existingSecret.OrganizationId;
|
||||
secret.CreationDate = existingSecret.CreationDate;
|
||||
secret.DeletedDate = existingSecret.DeletedDate;
|
||||
secret.RevisionDate = DateTime.UtcNow;
|
||||
|
||||
await _secretRepository.UpdateAsync(secret);
|
||||
return secret;
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.SecretManagerFeatures.ServiceAccounts.Interfaces;
|
||||
|
||||
namespace Bit.Commercial.Core.SecretManagerFeatures.ServiceAccounts;
|
||||
|
||||
public class CreateServiceAccountCommand : ICreateServiceAccountCommand
|
||||
{
|
||||
private readonly IServiceAccountRepository _serviceAccountRepository;
|
||||
|
||||
public CreateServiceAccountCommand(IServiceAccountRepository serviceAccountRepository)
|
||||
{
|
||||
_serviceAccountRepository = serviceAccountRepository;
|
||||
}
|
||||
|
||||
public async Task<ServiceAccount> CreateAsync(ServiceAccount serviceAccount)
|
||||
{
|
||||
return await _serviceAccountRepository.CreateAsync(serviceAccount);
|
||||
}
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.SecretManagerFeatures.ServiceAccounts.Interfaces;
|
||||
|
||||
namespace Bit.Commercial.Core.SecretManagerFeatures.ServiceAccounts;
|
||||
|
||||
public class UpdateServiceAccountCommand : IUpdateServiceAccountCommand
|
||||
{
|
||||
private readonly IServiceAccountRepository _serviceAccountRepository;
|
||||
|
||||
public UpdateServiceAccountCommand(IServiceAccountRepository serviceAccountRepository)
|
||||
{
|
||||
_serviceAccountRepository = serviceAccountRepository;
|
||||
}
|
||||
|
||||
public async Task<ServiceAccount> UpdateAsync(ServiceAccount serviceAccount)
|
||||
{
|
||||
var existingServiceAccount = await _serviceAccountRepository.GetByIdAsync(serviceAccount.Id);
|
||||
if (existingServiceAccount == null)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
serviceAccount.OrganizationId = existingServiceAccount.OrganizationId;
|
||||
serviceAccount.CreationDate = existingServiceAccount.CreationDate;
|
||||
serviceAccount.RevisionDate = DateTime.UtcNow;
|
||||
|
||||
await _serviceAccountRepository.ReplaceAsync(serviceAccount);
|
||||
return serviceAccount;
|
||||
}
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
using Bit.Commercial.Core.Services;
|
||||
using Bit.Commercial.Core.SecretManagerFeatures;
|
||||
using Bit.Commercial.Core.Services;
|
||||
using Bit.Core.Services;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
@ -6,8 +7,13 @@ namespace Bit.Commercial.Core.Utilities;
|
||||
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
public static void AddCommCoreServices(this IServiceCollection services)
|
||||
public static void AddCommercialCoreServices(this IServiceCollection services)
|
||||
{
|
||||
services.AddScoped<IProviderService, ProviderService>();
|
||||
}
|
||||
|
||||
public static void AddCommercialSecretsManagerServices(this IServiceCollection services)
|
||||
{
|
||||
services.AddSecretManagerServices();
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,13 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="11.0.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\src\Core\Core.csproj" />
|
||||
<ProjectReference Include="..\..\..\src\Infrastructure.EntityFramework\Infrastructure.EntityFramework.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
@ -0,0 +1,18 @@
|
||||
using Bit.Commercial.Infrastructure.EntityFramework.Repositories;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Infrastructure.EntityFramework.Repositories;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Bit.Commercial.Infrastructure.EntityFramework;
|
||||
|
||||
public static class CommercialEFServiceCollectionExtensions
|
||||
{
|
||||
public static void AddCommercialEFRepositories(this IServiceCollection services)
|
||||
{
|
||||
services.AddSingleton<IAccessPolicyRepository, AccessPolicyRepository>();
|
||||
services.AddSingleton<ISecretRepository, SecretRepository>();
|
||||
services.AddSingleton<IProjectRepository, ProjectRepository>();
|
||||
services.AddSingleton<IServiceAccountRepository, ServiceAccountRepository>();
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,73 @@
|
||||
using System.Linq.Expressions;
|
||||
using AutoMapper;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Infrastructure.EntityFramework.Models;
|
||||
using Bit.Infrastructure.EntityFramework.Repositories;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Bit.Commercial.Infrastructure.EntityFramework.Repositories;
|
||||
|
||||
public class ProjectRepository : Repository<Core.Entities.Project, Project, Guid>, IProjectRepository
|
||||
{
|
||||
public ProjectRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper)
|
||||
: base(serviceScopeFactory, mapper, db => db.Project)
|
||||
{ }
|
||||
|
||||
public override async Task<Core.Entities.Project> GetByIdAsync(Guid id)
|
||||
{
|
||||
using (var scope = ServiceScopeFactory.CreateScope())
|
||||
{
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
var project = await dbContext.Project
|
||||
.Where(c => c.Id == id && c.DeletedDate == null)
|
||||
.FirstOrDefaultAsync();
|
||||
return Mapper.Map<Core.Entities.Project>(project);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Core.Entities.Project>> GetManyByOrganizationIdAsync(Guid organizationId, Guid userId)
|
||||
{
|
||||
using var scope = ServiceScopeFactory.CreateScope();
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
var project = await dbContext.Project
|
||||
.Where(p => p.OrganizationId == organizationId && p.DeletedDate == null)
|
||||
// TODO: Enable this + Handle Admins
|
||||
//.Where(UserHasAccessToProject(userId))
|
||||
.OrderBy(p => p.RevisionDate)
|
||||
.ToListAsync();
|
||||
return Mapper.Map<List<Core.Entities.Project>>(project);
|
||||
}
|
||||
|
||||
private static Expression<Func<Project, bool>> UserHasAccessToProject(Guid userId) => p =>
|
||||
p.UserAccessPolicies.Any(ap => ap.OrganizationUser.User.Id == userId && ap.Read) ||
|
||||
p.GroupAccessPolicies.Any(ap => ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.User.Id == userId && ap.Read));
|
||||
|
||||
public async Task DeleteManyByIdAsync(IEnumerable<Guid> ids)
|
||||
{
|
||||
using (var scope = ServiceScopeFactory.CreateScope())
|
||||
{
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
var utcNow = DateTime.UtcNow;
|
||||
var projects = dbContext.Project.Where(c => ids.Contains(c.Id));
|
||||
await projects.ForEachAsync(project =>
|
||||
{
|
||||
dbContext.Remove(project);
|
||||
});
|
||||
await dbContext.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Core.Entities.Project>> GetManyByIds(IEnumerable<Guid> ids)
|
||||
{
|
||||
using (var scope = ServiceScopeFactory.CreateScope())
|
||||
{
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
var projects = await dbContext.Project
|
||||
.Where(c => ids.Contains(c.Id) && c.DeletedDate == null)
|
||||
.ToListAsync();
|
||||
return Mapper.Map<List<Core.Entities.Project>>(projects);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,139 @@
|
||||
using AutoMapper;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Infrastructure.EntityFramework;
|
||||
using Bit.Infrastructure.EntityFramework.Models;
|
||||
using Bit.Infrastructure.EntityFramework.Repositories;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Bit.Commercial.Infrastructure.EntityFramework.Repositories;
|
||||
|
||||
public class SecretRepository : Repository<Core.Entities.Secret, Secret, Guid>, ISecretRepository
|
||||
{
|
||||
public SecretRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper)
|
||||
: base(serviceScopeFactory, mapper, db => db.Secret)
|
||||
{ }
|
||||
|
||||
public override async Task<Core.Entities.Secret> GetByIdAsync(Guid id)
|
||||
{
|
||||
using (var scope = ServiceScopeFactory.CreateScope())
|
||||
{
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
var secret = await dbContext.Secret
|
||||
.Include("Projects")
|
||||
.Where(c => c.Id == id && c.DeletedDate == null)
|
||||
.FirstOrDefaultAsync();
|
||||
return Mapper.Map<Core.Entities.Secret>(secret);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Core.Entities.Secret>> GetManyByIds(IEnumerable<Guid> ids)
|
||||
{
|
||||
using (var scope = ServiceScopeFactory.CreateScope())
|
||||
{
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
var secrets = await dbContext.Secret
|
||||
.Where(c => ids.Contains(c.Id) && c.DeletedDate == null)
|
||||
.ToListAsync();
|
||||
return Mapper.Map<List<Core.Entities.Secret>>(secrets);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Core.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();
|
||||
|
||||
return Mapper.Map<List<Core.Entities.Secret>>(secrets);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Core.Entities.Secret>> GetManyByProjectIdAsync(Guid projectId)
|
||||
{
|
||||
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();
|
||||
|
||||
return Mapper.Map<List<Core.Entities.Secret>>(secrets);
|
||||
}
|
||||
}
|
||||
|
||||
public override async Task<Core.Entities.Secret> CreateAsync(Core.Entities.Secret secret)
|
||||
{
|
||||
using (var scope = ServiceScopeFactory.CreateScope())
|
||||
{
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
secret.SetNewId();
|
||||
var entity = Mapper.Map<Secret>(secret);
|
||||
|
||||
if (secret.Projects?.Count > 0)
|
||||
{
|
||||
foreach (var p in entity.Projects)
|
||||
{
|
||||
dbContext.Attach(p);
|
||||
}
|
||||
}
|
||||
|
||||
await dbContext.AddAsync(entity);
|
||||
await dbContext.SaveChangesAsync();
|
||||
secret.Id = entity.Id;
|
||||
return secret;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Core.Entities.Secret> UpdateAsync(Core.Entities.Secret secret)
|
||||
{
|
||||
|
||||
using (var scope = ServiceScopeFactory.CreateScope())
|
||||
{
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
var mappedEntity = Mapper.Map<Secret>(secret);
|
||||
var entity = await dbContext.Secret
|
||||
.Include("Projects")
|
||||
.FirstAsync(s => s.Id == secret.Id);
|
||||
|
||||
foreach (var p in entity.Projects?.Where(p => mappedEntity.Projects.All(mp => mp.Id != p.Id)))
|
||||
{
|
||||
entity.Projects.Remove(p);
|
||||
}
|
||||
|
||||
// Add new relationships
|
||||
foreach (var project in mappedEntity.Projects?.Where(p => entity.Projects.All(ep => ep.Id != p.Id)))
|
||||
{
|
||||
var p = dbContext.AttachToOrGet<Project>(_ => _.Id == project.Id, () => project);
|
||||
entity.Projects.Add(p);
|
||||
}
|
||||
|
||||
dbContext.Entry(entity).CurrentValues.SetValues(mappedEntity);
|
||||
await dbContext.SaveChangesAsync();
|
||||
}
|
||||
|
||||
return secret;
|
||||
}
|
||||
|
||||
public async Task SoftDeleteManyByIdAsync(IEnumerable<Guid> ids)
|
||||
{
|
||||
using (var scope = ServiceScopeFactory.CreateScope())
|
||||
{
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
var utcNow = DateTime.UtcNow;
|
||||
var secrets = dbContext.Secret.Where(c => ids.Contains(c.Id));
|
||||
await secrets.ForEachAsync(secret =>
|
||||
{
|
||||
dbContext.Attach(secret);
|
||||
secret.DeletedDate = utcNow;
|
||||
secret.RevisionDate = utcNow;
|
||||
});
|
||||
await dbContext.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
using AutoMapper;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Infrastructure.EntityFramework.Models;
|
||||
using Bit.Infrastructure.EntityFramework.Repositories;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Bit.Commercial.Infrastructure.EntityFramework.Repositories;
|
||||
|
||||
public class ServiceAccountRepository : Repository<Core.Entities.ServiceAccount, ServiceAccount, Guid>, IServiceAccountRepository
|
||||
{
|
||||
public ServiceAccountRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper)
|
||||
: base(serviceScopeFactory, mapper, db => db.ServiceAccount)
|
||||
{ }
|
||||
|
||||
public async Task<IEnumerable<Core.Entities.ServiceAccount>> GetManyByOrganizationIdAsync(Guid organizationId)
|
||||
{
|
||||
using (var scope = ServiceScopeFactory.CreateScope())
|
||||
{
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
var serviceAccounts = await dbContext.ServiceAccount
|
||||
.Where(c => c.OrganizationId == organizationId)
|
||||
.OrderBy(c => c.RevisionDate)
|
||||
.ToListAsync();
|
||||
return Mapper.Map<List<Core.Entities.ServiceAccount>>(serviceAccounts);
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -644,6 +644,15 @@
|
||||
"Microsoft.Extensions.DependencyModel": "6.0.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.EntityFrameworkCore.SqlServer": {
|
||||
"type": "Transitive",
|
||||
"resolved": "6.0.12",
|
||||
"contentHash": "bdKnSz1w+WZz9QYWhs3wwGuMn4YssjdR+HOBpzChQ6C3+dblq4Pammm5fzugcPOhTgCiWftOT2jPOT5hEy4bYg==",
|
||||
"dependencies": {
|
||||
"Microsoft.Data.SqlClient": "2.1.4",
|
||||
"Microsoft.EntityFrameworkCore.Relational": "6.0.12"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Caching.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "6.0.0",
|
||||
@ -3026,6 +3035,7 @@
|
||||
"AutoMapper.Extensions.Microsoft.DependencyInjection": "[11.0.0, )",
|
||||
"Core": "[2022.12.0, )",
|
||||
"Microsoft.EntityFrameworkCore.Relational": "[6.0.12, )",
|
||||
"Microsoft.EntityFrameworkCore.SqlServer": "[6.0.12, )",
|
||||
"Microsoft.EntityFrameworkCore.Sqlite": "[6.0.12, )",
|
||||
"Npgsql.EntityFrameworkCore.PostgreSQL": "[6.0.8, )",
|
||||
"Pomelo.EntityFrameworkCore.MySql": "[6.0.2, )",
|
||||
|
@ -625,6 +625,15 @@
|
||||
"Microsoft.Extensions.DependencyModel": "6.0.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.EntityFrameworkCore.SqlServer": {
|
||||
"type": "Transitive",
|
||||
"resolved": "6.0.12",
|
||||
"contentHash": "bdKnSz1w+WZz9QYWhs3wwGuMn4YssjdR+HOBpzChQ6C3+dblq4Pammm5fzugcPOhTgCiWftOT2jPOT5hEy4bYg==",
|
||||
"dependencies": {
|
||||
"Microsoft.Data.SqlClient": "2.1.4",
|
||||
"Microsoft.EntityFrameworkCore.Relational": "6.0.12"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Caching.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "6.0.0",
|
||||
@ -2896,6 +2905,7 @@
|
||||
"AutoMapper.Extensions.Microsoft.DependencyInjection": "[11.0.0, )",
|
||||
"Core": "[2022.12.0, )",
|
||||
"Microsoft.EntityFrameworkCore.Relational": "[6.0.12, )",
|
||||
"Microsoft.EntityFrameworkCore.SqlServer": "[6.0.12, )",
|
||||
"Microsoft.EntityFrameworkCore.Sqlite": "[6.0.12, )",
|
||||
"Npgsql.EntityFrameworkCore.PostgreSQL": "[6.0.8, )",
|
||||
"Pomelo.EntityFrameworkCore.MySql": "[6.0.2, )",
|
||||
|
@ -0,0 +1,25 @@
|
||||
using Bit.Commercial.Core.SecretManagerFeatures.AccessTokens;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Repositories;
|
||||
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.SecretManagerFeatures.AccessTokens;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class CreateServiceAccountCommandTests
|
||||
{
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task CreateAsync_CallsCreate(ApiKey data,
|
||||
SutProvider<CreateAccessTokenCommand> sutProvider)
|
||||
{
|
||||
await sutProvider.Sut.CreateAsync(data);
|
||||
|
||||
await sutProvider.GetDependency<IApiKeyRepository>().Received(1)
|
||||
.CreateAsync(Arg.Is(AssertHelper.AssertPropertyEqual(data)));
|
||||
}
|
||||
}
|
@ -0,0 +1,69 @@
|
||||
using Bit.Commercial.Core.SecretManagerFeatures.Projects;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Commercial.Core.Test.SecretManagerFeatures.Projects;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class DeleteProjectCommandTests
|
||||
{
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task DeleteProjects_Throws_NotFoundException(List<Guid> data,
|
||||
SutProvider<DeleteProjectCommand> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<IProjectRepository>().GetManyByIds(data).Returns(new List<Project>());
|
||||
|
||||
var exception = await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.DeleteProjects(data));
|
||||
|
||||
await sutProvider.GetDependency<IProjectRepository>().DidNotReceiveWithAnyArgs().DeleteManyByIdAsync(default);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task DeleteSecrets_OneIdNotFound_Throws_NotFoundException(List<Guid> data,
|
||||
SutProvider<DeleteProjectCommand> sutProvider)
|
||||
{
|
||||
var project = new Project()
|
||||
{
|
||||
Id = Guid.NewGuid()
|
||||
};
|
||||
sutProvider.GetDependency<IProjectRepository>().GetManyByIds(data).Returns(new List<Project>() { project });
|
||||
|
||||
var exception = await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.DeleteProjects(data));
|
||||
|
||||
await sutProvider.GetDependency<IProjectRepository>().DidNotReceiveWithAnyArgs().DeleteManyByIdAsync(default);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task DeleteSecrets_Success(List<Guid> data,
|
||||
SutProvider<DeleteProjectCommand> sutProvider)
|
||||
{
|
||||
var projects = new List<Project>();
|
||||
foreach (Guid id in data)
|
||||
{
|
||||
var project = new Project()
|
||||
{
|
||||
Id = id
|
||||
};
|
||||
projects.Add(project);
|
||||
}
|
||||
|
||||
sutProvider.GetDependency<IProjectRepository>().GetManyByIds(data).Returns(projects);
|
||||
|
||||
var results = await sutProvider.Sut.DeleteProjects(data);
|
||||
|
||||
await sutProvider.GetDependency<IProjectRepository>().Received(1).DeleteManyByIdAsync(Arg.Is(data));
|
||||
foreach (var result in results)
|
||||
{
|
||||
Assert.Equal("", result.Item2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,27 @@
|
||||
using Bit.Commercial.Core.SecretManagerFeatures.Secrets;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Test.AutoFixture.SecretsFixture;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Commercial.Core.Test.SecretManagerFeatures.Secrets;
|
||||
|
||||
[SutProviderCustomize]
|
||||
[SecretCustomize]
|
||||
public class CreateSecretCommandTests
|
||||
{
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task CreateAsync_CallsCreate(Secret data,
|
||||
SutProvider<CreateSecretCommand> sutProvider)
|
||||
{
|
||||
await sutProvider.Sut.CreateAsync(data);
|
||||
|
||||
await sutProvider.GetDependency<ISecretRepository>().Received(1)
|
||||
.CreateAsync(data);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,69 @@
|
||||
using Bit.Commercial.Core.SecretManagerFeatures.Secrets;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Commercial.Core.Test.SecretManagerFeatures.Secrets;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class DeleteSecretCommandTests
|
||||
{
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task DeleteSecrets_Throws_NotFoundException(List<Guid> data,
|
||||
SutProvider<DeleteSecretCommand> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<ISecretRepository>().GetManyByIds(data).Returns(new List<Secret>());
|
||||
|
||||
var exception = await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.DeleteSecrets(data));
|
||||
|
||||
await sutProvider.GetDependency<ISecretRepository>().DidNotReceiveWithAnyArgs().SoftDeleteManyByIdAsync(default);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task DeleteSecrets_OneIdNotFound_Throws_NotFoundException(List<Guid> data,
|
||||
SutProvider<DeleteSecretCommand> sutProvider)
|
||||
{
|
||||
var secret = new Secret()
|
||||
{
|
||||
Id = Guid.NewGuid()
|
||||
};
|
||||
sutProvider.GetDependency<ISecretRepository>().GetManyByIds(data).Returns(new List<Secret>() { secret });
|
||||
|
||||
var exception = await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.DeleteSecrets(data));
|
||||
|
||||
await sutProvider.GetDependency<ISecretRepository>().DidNotReceiveWithAnyArgs().SoftDeleteManyByIdAsync(default);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task DeleteSecrets_Success(List<Guid> data,
|
||||
SutProvider<DeleteSecretCommand> sutProvider)
|
||||
{
|
||||
var secrets = new List<Secret>();
|
||||
foreach (Guid id in data)
|
||||
{
|
||||
var secret = new Secret()
|
||||
{
|
||||
Id = id
|
||||
};
|
||||
secrets.Add(secret);
|
||||
}
|
||||
|
||||
sutProvider.GetDependency<ISecretRepository>().GetManyByIds(data).Returns(secrets);
|
||||
|
||||
var results = await sutProvider.Sut.DeleteSecrets(data);
|
||||
|
||||
await sutProvider.GetDependency<ISecretRepository>().Received(1).SoftDeleteManyByIdAsync(Arg.Is(data));
|
||||
foreach (var result in results)
|
||||
{
|
||||
Assert.Equal("", result.Item2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,119 @@
|
||||
using Bit.Commercial.Core.SecretManagerFeatures.Secrets;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Test.AutoFixture.SecretsFixture;
|
||||
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.SecretManagerFeatures.Secrets;
|
||||
|
||||
[SutProviderCustomize]
|
||||
[SecretCustomize]
|
||||
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));
|
||||
|
||||
await sutProvider.GetDependency<ISecretRepository>().DidNotReceiveWithAnyArgs().UpdateAsync(default);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task UpdateAsync_CallsReplaceAsync(Secret data, SutProvider<UpdateSecretCommand> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<ISecretRepository>().GetByIdAsync(data.Id).Returns(data);
|
||||
await sutProvider.Sut.UpdateAsync(data);
|
||||
|
||||
await sutProvider.GetDependency<ISecretRepository>().Received(1)
|
||||
.UpdateAsync(data);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task UpdateAsync_DoesNotModifyOrganizationId(Secret existingSecret, SutProvider<UpdateSecretCommand> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<ISecretRepository>().GetByIdAsync(existingSecret.Id).Returns(existingSecret);
|
||||
|
||||
var updatedOrgId = Guid.NewGuid();
|
||||
var secretUpdate = new Secret()
|
||||
{
|
||||
OrganizationId = updatedOrgId,
|
||||
Id = existingSecret.Id,
|
||||
Key = existingSecret.Key,
|
||||
};
|
||||
|
||||
var result = await sutProvider.Sut.UpdateAsync(secretUpdate);
|
||||
|
||||
Assert.Equal(existingSecret.OrganizationId, result.OrganizationId);
|
||||
Assert.NotEqual(existingSecret.OrganizationId, updatedOrgId);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task UpdateAsync_DoesNotModifyCreationDate(Secret existingSecret, SutProvider<UpdateSecretCommand> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<ISecretRepository>().GetByIdAsync(existingSecret.Id).Returns(existingSecret);
|
||||
|
||||
var updatedCreationDate = DateTime.UtcNow;
|
||||
var secretUpdate = new Secret()
|
||||
{
|
||||
CreationDate = updatedCreationDate,
|
||||
Id = existingSecret.Id,
|
||||
Key = existingSecret.Key,
|
||||
};
|
||||
|
||||
var result = await sutProvider.Sut.UpdateAsync(secretUpdate);
|
||||
|
||||
Assert.Equal(existingSecret.CreationDate, result.CreationDate);
|
||||
Assert.NotEqual(existingSecret.CreationDate, updatedCreationDate);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task UpdateAsync_DoesNotModifyDeletionDate(Secret existingSecret, SutProvider<UpdateSecretCommand> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<ISecretRepository>().GetByIdAsync(existingSecret.Id).Returns(existingSecret);
|
||||
|
||||
var updatedDeletionDate = DateTime.UtcNow;
|
||||
var secretUpdate = new Secret()
|
||||
{
|
||||
DeletedDate = updatedDeletionDate,
|
||||
Id = existingSecret.Id,
|
||||
Key = existingSecret.Key,
|
||||
};
|
||||
|
||||
var result = await sutProvider.Sut.UpdateAsync(secretUpdate);
|
||||
|
||||
Assert.Equal(existingSecret.DeletedDate, result.DeletedDate);
|
||||
Assert.NotEqual(existingSecret.DeletedDate, updatedDeletionDate);
|
||||
}
|
||||
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task UpdateAsync_RevisionDateIsUpdatedToUtcNow(Secret existingSecret, SutProvider<UpdateSecretCommand> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<ISecretRepository>().GetByIdAsync(existingSecret.Id).Returns(existingSecret);
|
||||
|
||||
var updatedRevisionDate = DateTime.UtcNow.AddDays(10);
|
||||
var secretUpdate = new Secret()
|
||||
{
|
||||
RevisionDate = updatedRevisionDate,
|
||||
Id = existingSecret.Id,
|
||||
Key = existingSecret.Key,
|
||||
};
|
||||
|
||||
var result = await sutProvider.Sut.UpdateAsync(secretUpdate);
|
||||
|
||||
Assert.NotEqual(existingSecret.RevisionDate, result.RevisionDate);
|
||||
AssertHelper.AssertRecent(result.RevisionDate);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,25 @@
|
||||
using Bit.Commercial.Core.SecretManagerFeatures.ServiceAccounts;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Repositories;
|
||||
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.SecretManagerFeatures.ServiceAccounts;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class CreateServiceAccountCommandTests
|
||||
{
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task CreateAsync_CallsCreate(ServiceAccount data,
|
||||
SutProvider<CreateServiceAccountCommand> sutProvider)
|
||||
{
|
||||
await sutProvider.Sut.CreateAsync(data);
|
||||
|
||||
await sutProvider.GetDependency<IServiceAccountRepository>().Received(1)
|
||||
.CreateAsync(Arg.Is(AssertHelper.AssertPropertyEqual(data)));
|
||||
}
|
||||
}
|
@ -0,0 +1,95 @@
|
||||
using Bit.Commercial.Core.SecretManagerFeatures.ServiceAccounts;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
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.SecretManagerFeatures.ServiceAccounts;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class UpdateServiceAccountCommandTests
|
||||
{
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task UpdateAsync_ServiceAccountDoesNotExist_ThrowsNotFound(ServiceAccount data, SutProvider<UpdateServiceAccountCommand> sutProvider)
|
||||
{
|
||||
var exception = await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.UpdateAsync(data));
|
||||
|
||||
await sutProvider.GetDependency<IServiceAccountRepository>().DidNotReceiveWithAnyArgs().ReplaceAsync(default);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task UpdateAsync_CallsReplaceAsync(ServiceAccount data, SutProvider<UpdateServiceAccountCommand> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(data.Id).Returns(data);
|
||||
await sutProvider.Sut.UpdateAsync(data);
|
||||
|
||||
await sutProvider.GetDependency<IServiceAccountRepository>().Received(1)
|
||||
.ReplaceAsync(Arg.Is(AssertHelper.AssertPropertyEqual(data)));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task UpdateAsync_DoesNotModifyOrganizationId(ServiceAccount existingServiceAccount, SutProvider<UpdateServiceAccountCommand> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(existingServiceAccount.Id).Returns(existingServiceAccount);
|
||||
|
||||
var updatedOrgId = Guid.NewGuid();
|
||||
var serviceAccountUpdate = new ServiceAccount()
|
||||
{
|
||||
OrganizationId = updatedOrgId,
|
||||
Id = existingServiceAccount.Id,
|
||||
Name = existingServiceAccount.Name,
|
||||
};
|
||||
|
||||
var result = await sutProvider.Sut.UpdateAsync(serviceAccountUpdate);
|
||||
|
||||
Assert.Equal(existingServiceAccount.OrganizationId, result.OrganizationId);
|
||||
Assert.NotEqual(existingServiceAccount.OrganizationId, updatedOrgId);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task UpdateAsync_DoesNotModifyCreationDate(ServiceAccount existingServiceAccount, SutProvider<UpdateServiceAccountCommand> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(existingServiceAccount.Id).Returns(existingServiceAccount);
|
||||
|
||||
var updatedCreationDate = DateTime.UtcNow;
|
||||
var serviceAccountUpdate = new ServiceAccount()
|
||||
{
|
||||
CreationDate = updatedCreationDate,
|
||||
Id = existingServiceAccount.Id,
|
||||
Name = existingServiceAccount.Name,
|
||||
};
|
||||
|
||||
var result = await sutProvider.Sut.UpdateAsync(serviceAccountUpdate);
|
||||
|
||||
Assert.Equal(existingServiceAccount.CreationDate, result.CreationDate);
|
||||
Assert.NotEqual(existingServiceAccount.CreationDate, updatedCreationDate);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task UpdateAsync_RevisionDateIsUpdatedToUtcNow(ServiceAccount existingServiceAccount, SutProvider<UpdateServiceAccountCommand> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(existingServiceAccount.Id).Returns(existingServiceAccount);
|
||||
|
||||
var updatedRevisionDate = DateTime.UtcNow.AddDays(10);
|
||||
var serviceAccountUpdate = new ServiceAccount()
|
||||
{
|
||||
RevisionDate = updatedRevisionDate,
|
||||
Id = existingServiceAccount.Id,
|
||||
Name = existingServiceAccount.Name,
|
||||
};
|
||||
|
||||
var result = await sutProvider.Sut.UpdateAsync(serviceAccountUpdate);
|
||||
|
||||
Assert.NotEqual(existingServiceAccount.RevisionDate, result.RevisionDate);
|
||||
AssertHelper.AssertRecent(result.RevisionDate);
|
||||
}
|
||||
}
|
@ -771,6 +771,15 @@
|
||||
"Microsoft.Extensions.DependencyModel": "6.0.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.EntityFrameworkCore.SqlServer": {
|
||||
"type": "Transitive",
|
||||
"resolved": "6.0.12",
|
||||
"contentHash": "bdKnSz1w+WZz9QYWhs3wwGuMn4YssjdR+HOBpzChQ6C3+dblq4Pammm5fzugcPOhTgCiWftOT2jPOT5hEy4bYg==",
|
||||
"dependencies": {
|
||||
"Microsoft.Data.SqlClient": "2.1.4",
|
||||
"Microsoft.EntityFrameworkCore.Relational": "6.0.12"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Caching.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "6.0.0",
|
||||
@ -3438,6 +3447,7 @@
|
||||
"AutoMapper.Extensions.Microsoft.DependencyInjection": "[11.0.0, )",
|
||||
"Core": "[2022.12.0, )",
|
||||
"Microsoft.EntityFrameworkCore.Relational": "[6.0.12, )",
|
||||
"Microsoft.EntityFrameworkCore.SqlServer": "[6.0.12, )",
|
||||
"Microsoft.EntityFrameworkCore.Sqlite": "[6.0.12, )",
|
||||
"Npgsql.EntityFrameworkCore.PostgreSQL": "[6.0.8, )",
|
||||
"Pomelo.EntityFrameworkCore.MySql": "[6.0.2, )",
|
||||
|
@ -743,6 +743,15 @@
|
||||
"Microsoft.Extensions.DependencyModel": "6.0.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.EntityFrameworkCore.SqlServer": {
|
||||
"type": "Transitive",
|
||||
"resolved": "6.0.12",
|
||||
"contentHash": "bdKnSz1w+WZz9QYWhs3wwGuMn4YssjdR+HOBpzChQ6C3+dblq4Pammm5fzugcPOhTgCiWftOT2jPOT5hEy4bYg==",
|
||||
"dependencies": {
|
||||
"Microsoft.Data.SqlClient": "2.1.4",
|
||||
"Microsoft.EntityFrameworkCore.Relational": "6.0.12"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Caching.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "6.0.0",
|
||||
@ -3275,6 +3284,7 @@
|
||||
"AutoMapper.Extensions.Microsoft.DependencyInjection": "[11.0.0, )",
|
||||
"Core": "[2022.12.0, )",
|
||||
"Microsoft.EntityFrameworkCore.Relational": "[6.0.12, )",
|
||||
"Microsoft.EntityFrameworkCore.SqlServer": "[6.0.12, )",
|
||||
"Microsoft.EntityFrameworkCore.Sqlite": "[6.0.12, )",
|
||||
"Npgsql.EntityFrameworkCore.PostgreSQL": "[6.0.8, )",
|
||||
"Pomelo.EntityFrameworkCore.MySql": "[6.0.2, )",
|
||||
|
@ -75,6 +75,7 @@ COPY util/MySqlMigrations/*.csproj ./util/MySqlMigrations/
|
||||
COPY util/PostgresMigrations/*.csproj ./util/PostgresMigrations/
|
||||
COPY util/SqliteMigrations/*.csproj ./util/SqliteMigrations/
|
||||
COPY bitwarden_license/src/Commercial.Core/*.csproj ./bitwarden_license/src/Commercial.Core/
|
||||
COPY bitwarden_license/src/Commercial.Infrastructure.EntityFramework/*.csproj ./bitwarden_license/src/Commercial.Infrastructure.EntityFramework/
|
||||
COPY Directory.Build.props .
|
||||
|
||||
# Restore Admin project dependencies and tools
|
||||
@ -129,6 +130,7 @@ COPY util/PostgresMigrations/. ./util/PostgresMigrations/
|
||||
COPY util/SqliteMigrations/. ./util/SqliteMigrations/
|
||||
COPY util/EfShared/. ./util/EfShared/
|
||||
COPY bitwarden_license/src/Commercial.Core/. ./bitwarden_license/src/Commercial.Core/
|
||||
COPY bitwarden_license/src/Commercial.Infrastructure.EntityFramework/. ./bitwarden_license/src/Commercial.Infrastructure.EntityFramework/
|
||||
COPY .git/. ./.git/
|
||||
|
||||
# Build Admin app
|
||||
|
@ -39,6 +39,7 @@ public class OrganizationEditModel : OrganizationViewModel
|
||||
UseTotp = org.UseTotp;
|
||||
Use2fa = org.Use2fa;
|
||||
UseApi = org.UseApi;
|
||||
UseSecretsManager = org.UseSecretsManager;
|
||||
UseResetPassword = org.UseResetPassword;
|
||||
SelfHost = org.SelfHost;
|
||||
UsersGetPremium = org.UsersGetPremium;
|
||||
@ -98,6 +99,8 @@ public class OrganizationEditModel : OrganizationViewModel
|
||||
public bool UseResetPassword { get; set; }
|
||||
[Display(Name = "SCIM")]
|
||||
public bool UseScim { get; set; }
|
||||
[Display(Name = "Secrets Manager")]
|
||||
public bool UseSecretsManager { get; set; }
|
||||
[Display(Name = "Self Host")]
|
||||
public bool SelfHost { get; set; }
|
||||
[Display(Name = "Users Get Premium")]
|
||||
@ -139,6 +142,7 @@ public class OrganizationEditModel : OrganizationViewModel
|
||||
existingOrganization.UseTotp = UseTotp;
|
||||
existingOrganization.Use2fa = Use2fa;
|
||||
existingOrganization.UseApi = UseApi;
|
||||
existingOrganization.UseSecretsManager = UseSecretsManager;
|
||||
existingOrganization.UseResetPassword = UseResetPassword;
|
||||
existingOrganization.SelfHost = SelfHost;
|
||||
existingOrganization.UsersGetPremium = UsersGetPremium;
|
||||
|
@ -85,7 +85,7 @@ public class Startup
|
||||
#if OSS
|
||||
services.AddOosServices();
|
||||
#else
|
||||
services.AddCommCoreServices();
|
||||
services.AddCommercialCoreServices();
|
||||
#endif
|
||||
|
||||
// Mvc
|
||||
|
@ -191,10 +191,9 @@
|
||||
</div>
|
||||
</div>
|
||||
<h2>Features</h2>
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" asp-for="UseTotp">
|
||||
<label class="form-check-label" asp-for="UseTotp"></label>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-4">
|
||||
<h3>General</h3>
|
||||
<div class="form-check mb-2">
|
||||
<input type="checkbox" class="form-check-input" asp-for="SelfHost">
|
||||
<label class="form-check-label" asp-for="SelfHost"></label>
|
||||
@ -240,13 +239,30 @@
|
||||
<label class="form-check-label" asp-for="UseResetPassword"></label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" asp-for="UsersGetPremium">
|
||||
<label class="form-check-label" asp-for="UsersGetPremium"></label>
|
||||
</div>
|
||||
<div class="form-check mb-3">
|
||||
<input type="checkbox" class="form-check-input" asp-for="UseCustomPermissions">
|
||||
<label class="form-check-label" asp-for="UseCustomPermissions"></label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<h3>Password Manager</h3>
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" asp-for="UseTotp">
|
||||
<label class="form-check-label" asp-for="UseTotp"></label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" asp-for="UsersGetPremium">
|
||||
<label class="form-check-label" asp-for="UsersGetPremium"></label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<h3>Secrets Manager</h3>
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" asp-for="UseSecretsManager">
|
||||
<label class="form-check-label" asp-for="UseSecretsManager"></label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>Licensing</h2>
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
|
@ -815,6 +815,15 @@
|
||||
"Microsoft.Extensions.DependencyModel": "6.0.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.EntityFrameworkCore.SqlServer": {
|
||||
"type": "Transitive",
|
||||
"resolved": "6.0.12",
|
||||
"contentHash": "bdKnSz1w+WZz9QYWhs3wwGuMn4YssjdR+HOBpzChQ6C3+dblq4Pammm5fzugcPOhTgCiWftOT2jPOT5hEy4bYg==",
|
||||
"dependencies": {
|
||||
"Microsoft.Data.SqlClient": "2.1.4",
|
||||
"Microsoft.EntityFrameworkCore.Relational": "6.0.12"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Caching.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "6.0.0",
|
||||
@ -3338,6 +3347,7 @@
|
||||
"AutoMapper.Extensions.Microsoft.DependencyInjection": "[11.0.0, )",
|
||||
"Core": "[2022.12.0, )",
|
||||
"Microsoft.EntityFrameworkCore.Relational": "[6.0.12, )",
|
||||
"Microsoft.EntityFrameworkCore.SqlServer": "[6.0.12, )",
|
||||
"Microsoft.EntityFrameworkCore.Sqlite": "[6.0.12, )",
|
||||
"Npgsql.EntityFrameworkCore.PostgreSQL": "[6.0.8, )",
|
||||
"Pomelo.EntityFrameworkCore.MySql": "[6.0.2, )",
|
||||
|
@ -24,6 +24,7 @@
|
||||
<When Condition="!$(DefineConstants.Contains('OSS'))">
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\bitwarden_license\src\Commercial.Core\Commercial.Core.csproj" />
|
||||
<ProjectReference Include="..\..\bitwarden_license\src\Commercial.Infrastructure.EntityFramework\Commercial.Infrastructure.EntityFramework.csproj" />
|
||||
</ItemGroup>
|
||||
</When>
|
||||
</Choose>
|
||||
|
77
src/Api/Controllers/ProjectsController.cs
Normal file
77
src/Api/Controllers/ProjectsController.cs
Normal file
@ -0,0 +1,77 @@
|
||||
using Bit.Api.Models.Response;
|
||||
using Bit.Api.SecretManagerFeatures.Models.Request;
|
||||
using Bit.Api.SecretManagerFeatures.Models.Response;
|
||||
using Bit.Api.Utilities;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.SecretManagerFeatures.Projects.Interfaces;
|
||||
using Bit.Core.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Bit.Api.Controllers;
|
||||
|
||||
[SecretsManager]
|
||||
public class ProjectsController : Controller
|
||||
{
|
||||
private readonly IUserService _userService;
|
||||
private readonly IProjectRepository _projectRepository;
|
||||
private readonly ICreateProjectCommand _createProjectCommand;
|
||||
private readonly IUpdateProjectCommand _updateProjectCommand;
|
||||
private readonly IDeleteProjectCommand _deleteProjectCommand;
|
||||
|
||||
public ProjectsController(
|
||||
IUserService userService,
|
||||
IProjectRepository projectRepository,
|
||||
ICreateProjectCommand createProjectCommand,
|
||||
IUpdateProjectCommand updateProjectCommand,
|
||||
IDeleteProjectCommand deleteProjectCommand)
|
||||
{
|
||||
_userService = userService;
|
||||
_projectRepository = projectRepository;
|
||||
_createProjectCommand = createProjectCommand;
|
||||
_updateProjectCommand = updateProjectCommand;
|
||||
_deleteProjectCommand = deleteProjectCommand;
|
||||
}
|
||||
|
||||
[HttpPost("organizations/{organizationId}/projects")]
|
||||
public async Task<ProjectResponseModel> CreateAsync([FromRoute] Guid organizationId, [FromBody] ProjectCreateRequestModel createRequest)
|
||||
{
|
||||
var result = await _createProjectCommand.CreateAsync(createRequest.ToProject(organizationId));
|
||||
return new ProjectResponseModel(result);
|
||||
}
|
||||
|
||||
[HttpPut("projects/{id}")]
|
||||
public async Task<ProjectResponseModel> UpdateProjectAsync([FromRoute] Guid id, [FromBody] ProjectUpdateRequestModel updateRequest)
|
||||
{
|
||||
var result = await _updateProjectCommand.UpdateAsync(updateRequest.ToProject(id));
|
||||
return new ProjectResponseModel(result);
|
||||
}
|
||||
|
||||
[HttpGet("organizations/{organizationId}/projects")]
|
||||
public async Task<ListResponseModel<ProjectResponseModel>> GetProjectsByOrganizationAsync([FromRoute] Guid organizationId)
|
||||
{
|
||||
var userId = _userService.GetProperUserId(User).Value;
|
||||
var projects = await _projectRepository.GetManyByOrganizationIdAsync(organizationId, userId);
|
||||
var responses = projects.Select(project => new ProjectResponseModel(project));
|
||||
return new ListResponseModel<ProjectResponseModel>(responses);
|
||||
}
|
||||
|
||||
[HttpGet("projects/{id}")]
|
||||
public async Task<ProjectResponseModel> GetProjectAsync([FromRoute] Guid id)
|
||||
{
|
||||
var project = await _projectRepository.GetByIdAsync(id);
|
||||
if (project == null)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
return new ProjectResponseModel(project);
|
||||
}
|
||||
|
||||
[HttpPost("projects/delete")]
|
||||
public async Task<ListResponseModel<BulkDeleteResponseModel>> BulkDeleteProjectsAsync([FromBody] List<Guid> ids)
|
||||
{
|
||||
var results = await _deleteProjectCommand.DeleteProjects(ids);
|
||||
var responses = results.Select(r => new BulkDeleteResponseModel(r.Item1.Id, r.Item2));
|
||||
return new ListResponseModel<BulkDeleteResponseModel>(responses);
|
||||
}
|
||||
}
|
80
src/Api/Controllers/SecretsController.cs
Normal file
80
src/Api/Controllers/SecretsController.cs
Normal file
@ -0,0 +1,80 @@
|
||||
using Bit.Api.Models.Response;
|
||||
using Bit.Api.SecretManagerFeatures.Models.Request;
|
||||
using Bit.Api.SecretManagerFeatures.Models.Response;
|
||||
using Bit.Api.Utilities;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.SecretManagerFeatures.Secrets.Interfaces;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Bit.Api.Controllers;
|
||||
|
||||
[SecretsManager]
|
||||
[Authorize("secrets")]
|
||||
public class SecretsController : Controller
|
||||
{
|
||||
private readonly ISecretRepository _secretRepository;
|
||||
private readonly IProjectRepository _projectRepository;
|
||||
private readonly ICreateSecretCommand _createSecretCommand;
|
||||
private readonly IUpdateSecretCommand _updateSecretCommand;
|
||||
private readonly IDeleteSecretCommand _deleteSecretCommand;
|
||||
|
||||
public SecretsController(ISecretRepository secretRepository, IProjectRepository projectRepository, ICreateSecretCommand createSecretCommand, IUpdateSecretCommand updateSecretCommand, IDeleteSecretCommand deleteSecretCommand)
|
||||
{
|
||||
_secretRepository = secretRepository;
|
||||
_projectRepository = projectRepository;
|
||||
_createSecretCommand = createSecretCommand;
|
||||
_updateSecretCommand = updateSecretCommand;
|
||||
_deleteSecretCommand = deleteSecretCommand;
|
||||
}
|
||||
|
||||
[HttpGet("organizations/{organizationId}/secrets")]
|
||||
public async Task<SecretWithProjectsListResponseModel> GetSecretsByOrganizationAsync([FromRoute] Guid organizationId)
|
||||
{
|
||||
var secrets = await _secretRepository.GetManyByOrganizationIdAsync(organizationId);
|
||||
return new SecretWithProjectsListResponseModel(secrets);
|
||||
}
|
||||
|
||||
[HttpGet("secrets/{id}")]
|
||||
public async Task<SecretResponseModel> GetSecretAsync([FromRoute] Guid id)
|
||||
{
|
||||
var secret = await _secretRepository.GetByIdAsync(id);
|
||||
if (secret == null)
|
||||
{
|
||||
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));
|
||||
return new SecretWithProjectsListResponseModel(secrets);
|
||||
}
|
||||
|
||||
[HttpPost("organizations/{organizationId}/secrets")]
|
||||
public async Task<SecretResponseModel> CreateSecretAsync([FromRoute] Guid organizationId, [FromBody] SecretCreateRequestModel createRequest)
|
||||
{
|
||||
var result = await _createSecretCommand.CreateAsync(createRequest.ToSecret(organizationId));
|
||||
return new SecretResponseModel(result);
|
||||
}
|
||||
|
||||
[HttpPut("secrets/{id}")]
|
||||
public async Task<SecretResponseModel> UpdateSecretAsync([FromRoute] Guid id, [FromBody] SecretUpdateRequestModel updateRequest)
|
||||
{
|
||||
var result = await _updateSecretCommand.UpdateAsync(updateRequest.ToSecret(id));
|
||||
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 responses = results.Select(r => new BulkDeleteResponseModel(r.Item1.Id, r.Item2));
|
||||
return new ListResponseModel<BulkDeleteResponseModel>(responses);
|
||||
}
|
||||
}
|
72
src/Api/Controllers/ServiceAccountsController.cs
Normal file
72
src/Api/Controllers/ServiceAccountsController.cs
Normal file
@ -0,0 +1,72 @@
|
||||
using Bit.Api.Models.Response;
|
||||
using Bit.Api.Models.Response.SecretsManager;
|
||||
using Bit.Api.SecretManagerFeatures.Models.Request;
|
||||
using Bit.Api.SecretManagerFeatures.Models.Response;
|
||||
using Bit.Api.Utilities;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.SecretManagerFeatures.AccessTokens.Interfaces;
|
||||
using Bit.Core.SecretManagerFeatures.ServiceAccounts.Interfaces;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Bit.Api.Controllers;
|
||||
|
||||
[SecretsManager]
|
||||
[Route("service-accounts")]
|
||||
public class ServiceAccountsController : Controller
|
||||
{
|
||||
private readonly IServiceAccountRepository _serviceAccountRepository;
|
||||
private readonly IApiKeyRepository _apiKeyRepository;
|
||||
private readonly ICreateServiceAccountCommand _createServiceAccountCommand;
|
||||
private readonly ICreateAccessTokenCommand _createAccessTokenCommand;
|
||||
private readonly IUpdateServiceAccountCommand _updateServiceAccountCommand;
|
||||
|
||||
public ServiceAccountsController(
|
||||
IServiceAccountRepository serviceAccountRepository,
|
||||
ICreateAccessTokenCommand createAccessTokenCommand,
|
||||
IApiKeyRepository apiKeyRepository, ICreateServiceAccountCommand createServiceAccountCommand,
|
||||
IUpdateServiceAccountCommand updateServiceAccountCommand)
|
||||
{
|
||||
_serviceAccountRepository = serviceAccountRepository;
|
||||
_apiKeyRepository = apiKeyRepository;
|
||||
_createServiceAccountCommand = createServiceAccountCommand;
|
||||
_updateServiceAccountCommand = updateServiceAccountCommand;
|
||||
_createAccessTokenCommand = createAccessTokenCommand;
|
||||
}
|
||||
|
||||
[HttpGet("/organizations/{organizationId}/service-accounts")]
|
||||
public async Task<ListResponseModel<ServiceAccountResponseModel>> GetServiceAccountsByOrganizationAsync([FromRoute] Guid organizationId)
|
||||
{
|
||||
var serviceAccounts = await _serviceAccountRepository.GetManyByOrganizationIdAsync(organizationId);
|
||||
var responses = serviceAccounts.Select(serviceAccount => new ServiceAccountResponseModel(serviceAccount));
|
||||
return new ListResponseModel<ServiceAccountResponseModel>(responses);
|
||||
}
|
||||
|
||||
[HttpPost("/organizations/{organizationId}/service-accounts")]
|
||||
public async Task<ServiceAccountResponseModel> CreateServiceAccountAsync([FromRoute] Guid organizationId, [FromBody] ServiceAccountCreateRequestModel createRequest)
|
||||
{
|
||||
var result = await _createServiceAccountCommand.CreateAsync(createRequest.ToServiceAccount(organizationId));
|
||||
return new ServiceAccountResponseModel(result);
|
||||
}
|
||||
|
||||
[HttpPut("{id}")]
|
||||
public async Task<ServiceAccountResponseModel> UpdateServiceAccountAsync([FromRoute] Guid id, [FromBody] ServiceAccountUpdateRequestModel updateRequest)
|
||||
{
|
||||
var result = await _updateServiceAccountCommand.UpdateAsync(updateRequest.ToServiceAccount(id));
|
||||
return new ServiceAccountResponseModel(result);
|
||||
}
|
||||
|
||||
[HttpGet("{id}/access-tokens")]
|
||||
public async Task<ListResponseModel<AccessTokenResponseModel>> GetAccessTokens([FromRoute] Guid id)
|
||||
{
|
||||
var accessTokens = await _apiKeyRepository.GetManyByServiceAccountIdAsync(id);
|
||||
var responses = accessTokens.Select(token => new AccessTokenResponseModel(token));
|
||||
return new ListResponseModel<AccessTokenResponseModel>(responses);
|
||||
}
|
||||
|
||||
[HttpPost("{id}/access-tokens")]
|
||||
public async Task<AccessTokenCreationResponseModel> CreateAccessTokenAsync([FromRoute] Guid id, [FromBody] AccessTokenCreateRequestModel request)
|
||||
{
|
||||
var result = await _createAccessTokenCommand.CreateAsync(request.ToApiKey(id));
|
||||
return new AccessTokenCreationResponseModel(result);
|
||||
}
|
||||
}
|
@ -43,6 +43,7 @@ public class OrganizationResponseModel : ResponseModel
|
||||
Use2fa = organization.Use2fa;
|
||||
UseApi = organization.UseApi;
|
||||
UseResetPassword = organization.UseResetPassword;
|
||||
UseSecretsManager = organization.UseSecretsManager;
|
||||
UsersGetPremium = organization.UsersGetPremium;
|
||||
UseCustomPermissions = organization.UseCustomPermissions;
|
||||
SelfHost = organization.SelfHost;
|
||||
@ -75,6 +76,7 @@ public class OrganizationResponseModel : ResponseModel
|
||||
public bool UseTotp { get; set; }
|
||||
public bool Use2fa { get; set; }
|
||||
public bool UseApi { get; set; }
|
||||
public bool UseSecretsManager { get; set; }
|
||||
public bool UseResetPassword { get; set; }
|
||||
public bool UsersGetPremium { get; set; }
|
||||
public bool UseCustomPermissions { get; set; }
|
||||
|
@ -25,6 +25,7 @@ public class ProfileOrganizationResponseModel : ResponseModel
|
||||
Use2fa = organization.Use2fa;
|
||||
UseApi = organization.UseApi;
|
||||
UseResetPassword = organization.UseResetPassword;
|
||||
UseSecretsManager = organization.UseSecretsManager;
|
||||
UsersGetPremium = organization.UsersGetPremium;
|
||||
UseCustomPermissions = organization.UseCustomPermissions;
|
||||
SelfHost = organization.SelfHost;
|
||||
@ -73,6 +74,7 @@ public class ProfileOrganizationResponseModel : ResponseModel
|
||||
public bool Use2fa { get; set; }
|
||||
public bool UseApi { get; set; }
|
||||
public bool UseResetPassword { get; set; }
|
||||
public bool UseSecretsManager { get; set; }
|
||||
public bool UsersGetPremium { get; set; }
|
||||
public bool UseCustomPermissions { get; set; }
|
||||
public bool SelfHost { get; set; }
|
||||
|
@ -0,0 +1,27 @@
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Models.Api;
|
||||
|
||||
namespace Bit.Api.Models.Response.SecretsManager;
|
||||
|
||||
public class AccessTokenResponseModel : ResponseModel
|
||||
{
|
||||
public AccessTokenResponseModel(ApiKey apiKey, string obj = "accessToken")
|
||||
: base(obj)
|
||||
{
|
||||
Id = apiKey.Id;
|
||||
Name = apiKey.Name;
|
||||
Scopes = apiKey.GetScopes();
|
||||
|
||||
ExpireAt = apiKey.ExpireAt;
|
||||
CreationDate = apiKey.CreationDate;
|
||||
RevisionDate = apiKey.RevisionDate;
|
||||
}
|
||||
|
||||
public Guid Id { get; }
|
||||
public string Name { get; }
|
||||
public ICollection<string> Scopes { get; }
|
||||
|
||||
public DateTime? ExpireAt { get; }
|
||||
public DateTime CreationDate { get; }
|
||||
public DateTime RevisionDate { get; }
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Api.SecretManagerFeatures.Models.Request;
|
||||
|
||||
public class AccessTokenCreateRequestModel
|
||||
{
|
||||
[Required]
|
||||
[EncryptedString]
|
||||
[EncryptedStringLength(200)]
|
||||
public string Name { get; set; }
|
||||
|
||||
[Required]
|
||||
[EncryptedString]
|
||||
[EncryptedStringLength(4000)]
|
||||
public string EncryptedPayload { get; set; }
|
||||
|
||||
[Required]
|
||||
[EncryptedString]
|
||||
public string Key { get; set; }
|
||||
|
||||
public DateTime? ExpireAt { get; set; }
|
||||
|
||||
public ApiKey ToApiKey(Guid serviceAccountId)
|
||||
{
|
||||
return new ApiKey()
|
||||
{
|
||||
ServiceAccountId = serviceAccountId,
|
||||
Name = Name,
|
||||
Key = Key,
|
||||
ExpireAt = ExpireAt,
|
||||
Scope = "[\"api.secrets\"]",
|
||||
EncryptedPayload = EncryptedPayload,
|
||||
};
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Api.SecretManagerFeatures.Models.Request;
|
||||
|
||||
public class ProjectCreateRequestModel
|
||||
{
|
||||
[Required]
|
||||
[EncryptedString]
|
||||
public string Name { get; set; }
|
||||
|
||||
public Project ToProject(Guid organizationId)
|
||||
{
|
||||
return new Project()
|
||||
{
|
||||
OrganizationId = organizationId,
|
||||
Name = Name,
|
||||
};
|
||||
}
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Api.SecretManagerFeatures.Models.Request;
|
||||
|
||||
public class ProjectUpdateRequestModel
|
||||
{
|
||||
[Required]
|
||||
[EncryptedString]
|
||||
public string Name { get; set; }
|
||||
|
||||
public Project ToProject(Guid id)
|
||||
{
|
||||
return new Project()
|
||||
{
|
||||
Id = id,
|
||||
Name = Name,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,35 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Api.SecretManagerFeatures.Models.Request;
|
||||
|
||||
public class SecretCreateRequestModel
|
||||
{
|
||||
[Required]
|
||||
[EncryptedString]
|
||||
public string Key { get; set; }
|
||||
|
||||
[Required]
|
||||
[EncryptedString]
|
||||
public string Value { get; set; }
|
||||
|
||||
[Required]
|
||||
[EncryptedString]
|
||||
public string Note { get; set; }
|
||||
|
||||
public Guid[] ProjectIds { get; set; }
|
||||
|
||||
public Secret ToSecret(Guid organizationId)
|
||||
{
|
||||
return new Secret()
|
||||
{
|
||||
OrganizationId = organizationId,
|
||||
Key = Key,
|
||||
Value = Value,
|
||||
Note = Note,
|
||||
DeletedDate = null,
|
||||
Projects = ProjectIds != null && ProjectIds.Any() ? ProjectIds.Select(x => new Project() { Id = x }).ToList() : null,
|
||||
};
|
||||
}
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Api.SecretManagerFeatures.Models.Request;
|
||||
|
||||
public class SecretUpdateRequestModel
|
||||
{
|
||||
[Required]
|
||||
[EncryptedString]
|
||||
public string Key { get; set; }
|
||||
|
||||
[Required]
|
||||
[EncryptedString]
|
||||
public string Value { get; set; }
|
||||
|
||||
[Required]
|
||||
[EncryptedString]
|
||||
public string Note { get; set; }
|
||||
|
||||
public Guid[] ProjectIds { get; set; }
|
||||
|
||||
public Secret ToSecret(Guid id)
|
||||
{
|
||||
return new Secret()
|
||||
{
|
||||
Id = id,
|
||||
Key = Key,
|
||||
Value = Value,
|
||||
Note = Note,
|
||||
DeletedDate = null,
|
||||
Projects = ProjectIds != null && ProjectIds.Any() ? ProjectIds.Select(x => new Project() { Id = x }).ToList() : null,
|
||||
};
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Api.SecretManagerFeatures.Models.Request;
|
||||
|
||||
public class ServiceAccountUpdateRequestModel
|
||||
{
|
||||
[Required]
|
||||
[EncryptedString]
|
||||
public string Name { get; set; }
|
||||
|
||||
public ServiceAccount ToServiceAccount(Guid id)
|
||||
{
|
||||
return new ServiceAccount()
|
||||
{
|
||||
Id = id,
|
||||
Name = Name,
|
||||
};
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Api.SecretManagerFeatures.Models.Request;
|
||||
|
||||
public class ServiceAccountCreateRequestModel
|
||||
{
|
||||
[Required]
|
||||
[EncryptedString]
|
||||
public string Name { get; set; }
|
||||
|
||||
public ServiceAccount ToServiceAccount(Guid organizationId)
|
||||
{
|
||||
return new ServiceAccount()
|
||||
{
|
||||
OrganizationId = organizationId,
|
||||
Name = Name,
|
||||
};
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
#nullable enable
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Models.Api;
|
||||
|
||||
namespace Bit.Api.SecretManagerFeatures.Models.Response;
|
||||
|
||||
public class AccessTokenCreationResponseModel : ResponseModel
|
||||
{
|
||||
public AccessTokenCreationResponseModel(ApiKey apiKey, string obj = "accessTokenCreation") : base(obj)
|
||||
{
|
||||
Id = apiKey.Id;
|
||||
Name = apiKey.Name;
|
||||
ClientSecret = apiKey.ClientSecret;
|
||||
ExpireAt = apiKey.ExpireAt;
|
||||
CreationDate = apiKey.CreationDate;
|
||||
RevisionDate = apiKey.RevisionDate;
|
||||
}
|
||||
|
||||
public Guid Id { get; }
|
||||
public string Name { get; }
|
||||
public string ClientSecret { get; }
|
||||
public DateTime? ExpireAt { get; }
|
||||
public DateTime CreationDate { get; }
|
||||
public DateTime RevisionDate { get; }
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
#nullable enable
|
||||
using Bit.Core.Models.Api;
|
||||
|
||||
namespace Bit.Api.SecretManagerFeatures.Models.Response;
|
||||
|
||||
public class BulkDeleteResponseModel : ResponseModel
|
||||
{
|
||||
public BulkDeleteResponseModel(Guid id, string error, string obj = "BulkDeleteResponseModel") : base(obj)
|
||||
{
|
||||
Id = id;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(error))
|
||||
{
|
||||
Error = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
Error = error;
|
||||
}
|
||||
}
|
||||
|
||||
public Guid Id { get; set; }
|
||||
|
||||
public string? Error { get; set; }
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Models.Api;
|
||||
|
||||
namespace Bit.Api.SecretManagerFeatures.Models.Response;
|
||||
|
||||
public class ProjectResponseModel : ResponseModel
|
||||
{
|
||||
public ProjectResponseModel(Project project, string obj = "project")
|
||||
: base(obj)
|
||||
{
|
||||
if (project == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(project));
|
||||
}
|
||||
|
||||
Id = project.Id.ToString();
|
||||
OrganizationId = project.OrganizationId.ToString();
|
||||
Name = project.Name;
|
||||
CreationDate = project.CreationDate;
|
||||
RevisionDate = project.RevisionDate;
|
||||
}
|
||||
|
||||
public string Id { get; set; }
|
||||
|
||||
public string OrganizationId { get; set; }
|
||||
|
||||
public string Name { get; set; }
|
||||
|
||||
public DateTime CreationDate { get; set; }
|
||||
|
||||
public DateTime RevisionDate { get; set; }
|
||||
|
||||
public IEnumerable<Guid> Secrets { get; set; }
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Models.Api;
|
||||
|
||||
namespace Bit.Api.SecretManagerFeatures.Models.Response;
|
||||
|
||||
public class SecretResponseModel : ResponseModel
|
||||
{
|
||||
public SecretResponseModel(Secret secret, string obj = "secret")
|
||||
: base(obj)
|
||||
{
|
||||
if (secret == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(secret));
|
||||
}
|
||||
|
||||
Id = secret.Id.ToString();
|
||||
OrganizationId = secret.OrganizationId.ToString();
|
||||
Key = secret.Key;
|
||||
Value = secret.Value;
|
||||
Note = secret.Note;
|
||||
CreationDate = secret.CreationDate;
|
||||
RevisionDate = secret.RevisionDate;
|
||||
Projects = secret.Projects?.Select(p => new InnerProject(p));
|
||||
}
|
||||
|
||||
public string Id { get; set; }
|
||||
|
||||
public string OrganizationId { get; set; }
|
||||
|
||||
public string Key { get; set; }
|
||||
|
||||
public string Value { get; set; }
|
||||
|
||||
public string Note { get; set; }
|
||||
|
||||
public DateTime CreationDate { get; set; }
|
||||
|
||||
public DateTime RevisionDate { get; set; }
|
||||
|
||||
public IEnumerable<InnerProject> Projects { get; set; }
|
||||
|
||||
public class InnerProject
|
||||
{
|
||||
public InnerProject(Project project)
|
||||
{
|
||||
Id = project.Id;
|
||||
Name = project.Name;
|
||||
}
|
||||
|
||||
public Guid Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
}
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Models.Api;
|
||||
|
||||
namespace Bit.Api.SecretManagerFeatures.Models.Response;
|
||||
|
||||
public class SecretWithProjectsListResponseModel : ResponseModel
|
||||
{
|
||||
public SecretWithProjectsListResponseModel(IEnumerable<Secret> secrets, string obj = "SecretsWithProjectsList") : base(obj)
|
||||
{
|
||||
Secrets = secrets.Select(s => new InnerSecret(s));
|
||||
Projects = secrets.SelectMany(s => s.Projects).DistinctBy(p => p.Id).Select(p => new InnerProject(p));
|
||||
}
|
||||
|
||||
public IEnumerable<InnerSecret> Secrets { get; set; }
|
||||
public IEnumerable<InnerProject> Projects { get; set; }
|
||||
|
||||
public class InnerProject
|
||||
{
|
||||
public InnerProject(Project project)
|
||||
{
|
||||
Id = project.Id;
|
||||
Name = project.Name;
|
||||
}
|
||||
|
||||
public Guid Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
}
|
||||
|
||||
public class InnerSecret
|
||||
{
|
||||
public InnerSecret(Secret secret)
|
||||
{
|
||||
Id = secret.Id.ToString();
|
||||
OrganizationId = secret.OrganizationId.ToString();
|
||||
Key = secret.Key;
|
||||
CreationDate = secret.CreationDate;
|
||||
RevisionDate = secret.RevisionDate;
|
||||
Projects = secret.Projects?.Select(p => new InnerProject(p));
|
||||
}
|
||||
|
||||
public string Id { get; set; }
|
||||
|
||||
public string OrganizationId { get; set; }
|
||||
|
||||
public string Key { get; set; }
|
||||
|
||||
public DateTime CreationDate { get; set; }
|
||||
|
||||
public DateTime RevisionDate { get; set; }
|
||||
|
||||
public IEnumerable<InnerProject> Projects { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,33 @@
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Models.Api;
|
||||
|
||||
namespace Bit.Api.SecretManagerFeatures.Models.Response;
|
||||
|
||||
public class ServiceAccountResponseModel : ResponseModel
|
||||
{
|
||||
public ServiceAccountResponseModel(ServiceAccount serviceAccount, string obj = "serviceAccount")
|
||||
: base(obj)
|
||||
{
|
||||
if (serviceAccount == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(serviceAccount));
|
||||
}
|
||||
|
||||
Id = serviceAccount.Id.ToString();
|
||||
OrganizationId = serviceAccount.OrganizationId.ToString();
|
||||
Name = serviceAccount.Name;
|
||||
CreationDate = serviceAccount.CreationDate;
|
||||
RevisionDate = serviceAccount.RevisionDate;
|
||||
}
|
||||
|
||||
public string Id { get; set; }
|
||||
|
||||
public string OrganizationId { get; set; }
|
||||
|
||||
public string Name { get; set; }
|
||||
|
||||
public DateTime CreationDate { get; set; }
|
||||
|
||||
public DateTime RevisionDate { get; set; }
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ using Stripe;
|
||||
using Bit.Core.Utilities;
|
||||
using IdentityModel;
|
||||
using System.Globalization;
|
||||
using Bit.Core.IdentityServer;
|
||||
using Microsoft.IdentityModel.Logging;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Bit.SharedWeb.Utilities;
|
||||
@ -15,6 +16,7 @@ using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
|
||||
#if !OSS
|
||||
using Bit.Commercial.Core.Utilities;
|
||||
using Bit.Commercial.Infrastructure.EntityFramework;
|
||||
#endif
|
||||
|
||||
namespace Bit.Api;
|
||||
@ -84,34 +86,42 @@ public class Startup
|
||||
{
|
||||
policy.RequireAuthenticatedUser();
|
||||
policy.RequireClaim(JwtClaimTypes.AuthenticationMethod, "Application", "external");
|
||||
policy.RequireClaim(JwtClaimTypes.Scope, "api");
|
||||
policy.RequireClaim(JwtClaimTypes.Scope, ApiScopes.Api);
|
||||
});
|
||||
config.AddPolicy("Web", policy =>
|
||||
{
|
||||
policy.RequireAuthenticatedUser();
|
||||
policy.RequireClaim(JwtClaimTypes.AuthenticationMethod, "Application", "external");
|
||||
policy.RequireClaim(JwtClaimTypes.Scope, "api");
|
||||
policy.RequireClaim(JwtClaimTypes.Scope, ApiScopes.Api);
|
||||
policy.RequireClaim(JwtClaimTypes.ClientId, "web");
|
||||
});
|
||||
config.AddPolicy("Push", policy =>
|
||||
{
|
||||
policy.RequireAuthenticatedUser();
|
||||
policy.RequireClaim(JwtClaimTypes.Scope, "api.push");
|
||||
policy.RequireClaim(JwtClaimTypes.Scope, ApiScopes.ApiPush);
|
||||
});
|
||||
config.AddPolicy("Licensing", policy =>
|
||||
{
|
||||
policy.RequireAuthenticatedUser();
|
||||
policy.RequireClaim(JwtClaimTypes.Scope, "api.licensing");
|
||||
policy.RequireClaim(JwtClaimTypes.Scope, ApiScopes.ApiLicensing);
|
||||
});
|
||||
config.AddPolicy("Organization", policy =>
|
||||
{
|
||||
policy.RequireAuthenticatedUser();
|
||||
policy.RequireClaim(JwtClaimTypes.Scope, "api.organization");
|
||||
policy.RequireClaim(JwtClaimTypes.Scope, ApiScopes.ApiOrganization);
|
||||
});
|
||||
config.AddPolicy("Installation", policy =>
|
||||
{
|
||||
policy.RequireAuthenticatedUser();
|
||||
policy.RequireClaim(JwtClaimTypes.Scope, "api.installation");
|
||||
policy.RequireClaim(JwtClaimTypes.Scope, ApiScopes.ApiInstallation);
|
||||
});
|
||||
config.AddPolicy("Secrets", policy =>
|
||||
{
|
||||
policy.RequireAuthenticatedUser();
|
||||
policy.RequireAssertion(ctx => ctx.User.HasClaim(c =>
|
||||
c.Type == JwtClaimTypes.Scope &&
|
||||
(c.Value.Contains(ApiScopes.Api) || c.Value.Contains(ApiScopes.ApiSecrets))
|
||||
));
|
||||
});
|
||||
});
|
||||
|
||||
@ -125,7 +135,9 @@ public class Startup
|
||||
#if OSS
|
||||
services.AddOosServices();
|
||||
#else
|
||||
services.AddCommCoreServices();
|
||||
services.AddCommercialCoreServices();
|
||||
services.AddCommercialSecretsManagerServices();
|
||||
services.AddCommercialEFRepositories();
|
||||
#endif
|
||||
|
||||
// MVC
|
||||
|
@ -8,8 +8,9 @@ public class SecretsManagerAttribute : Attribute, IResourceFilter
|
||||
{
|
||||
public void OnResourceExecuting(ResourceExecutingContext context)
|
||||
{
|
||||
var env = context.HttpContext.RequestServices.GetService<IHostEnvironment>();
|
||||
if (!env.IsDevelopment())
|
||||
var isDev = context.HttpContext.RequestServices.GetService<IHostEnvironment>().IsDevelopment();
|
||||
var isEE = Environment.GetEnvironmentVariable("EE_TESTING_ENV") != null;
|
||||
if (!isDev && !isEE)
|
||||
{
|
||||
context.Result = new NotFoundResult();
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.IdentityServer;
|
||||
using Bit.Core.Settings;
|
||||
using Microsoft.OpenApi.Models;
|
||||
|
||||
namespace Bit.Api.Utilities;
|
||||
@ -38,7 +39,7 @@ public static class ServiceCollectionExtensions
|
||||
TokenUrl = new Uri($"{globalSettings.BaseServiceUri.Identity}/connect/token"),
|
||||
Scopes = new Dictionary<string, string>
|
||||
{
|
||||
{ "api.organization", "Organization APIs" },
|
||||
{ ApiScopes.ApiOrganization, "Organization APIs" },
|
||||
},
|
||||
}
|
||||
},
|
||||
@ -55,7 +56,7 @@ public static class ServiceCollectionExtensions
|
||||
Id = "oauth2-client-credentials"
|
||||
},
|
||||
},
|
||||
new[] { "api.organization" }
|
||||
new[] { ApiScopes.ApiOrganization }
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -562,6 +562,15 @@
|
||||
"Microsoft.Extensions.DependencyModel": "6.0.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.EntityFrameworkCore.SqlServer": {
|
||||
"type": "Transitive",
|
||||
"resolved": "6.0.12",
|
||||
"contentHash": "bdKnSz1w+WZz9QYWhs3wwGuMn4YssjdR+HOBpzChQ6C3+dblq4Pammm5fzugcPOhTgCiWftOT2jPOT5hEy4bYg==",
|
||||
"dependencies": {
|
||||
"Microsoft.Data.SqlClient": "2.1.4",
|
||||
"Microsoft.EntityFrameworkCore.Relational": "6.0.12"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.ApiDescription.Server": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.0.0",
|
||||
@ -2761,6 +2770,14 @@
|
||||
"Core": "[2022.12.0, )"
|
||||
}
|
||||
},
|
||||
"commercial.infrastructure.entityframework": {
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
"AutoMapper.Extensions.Microsoft.DependencyInjection": "[11.0.0, )",
|
||||
"Core": "[2022.12.0, )",
|
||||
"Infrastructure.EntityFramework": "[2022.12.0, )"
|
||||
}
|
||||
},
|
||||
"core": {
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
@ -2814,6 +2831,7 @@
|
||||
"AutoMapper.Extensions.Microsoft.DependencyInjection": "[11.0.0, )",
|
||||
"Core": "[2022.12.0, )",
|
||||
"Microsoft.EntityFrameworkCore.Relational": "[6.0.12, )",
|
||||
"Microsoft.EntityFrameworkCore.SqlServer": "[6.0.12, )",
|
||||
"Microsoft.EntityFrameworkCore.Sqlite": "[6.0.12, )",
|
||||
"Npgsql.EntityFrameworkCore.PostgreSQL": "[6.0.8, )",
|
||||
"Pomelo.EntityFrameworkCore.MySql": "[6.0.2, )",
|
||||
|
@ -796,6 +796,15 @@
|
||||
"Microsoft.Extensions.DependencyModel": "6.0.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.EntityFrameworkCore.SqlServer": {
|
||||
"type": "Transitive",
|
||||
"resolved": "6.0.12",
|
||||
"contentHash": "bdKnSz1w+WZz9QYWhs3wwGuMn4YssjdR+HOBpzChQ6C3+dblq4Pammm5fzugcPOhTgCiWftOT2jPOT5hEy4bYg==",
|
||||
"dependencies": {
|
||||
"Microsoft.Data.SqlClient": "2.1.4",
|
||||
"Microsoft.EntityFrameworkCore.Relational": "6.0.12"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Caching.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "6.0.0",
|
||||
@ -3273,6 +3282,7 @@
|
||||
"AutoMapper.Extensions.Microsoft.DependencyInjection": "[11.0.0, )",
|
||||
"Core": "[2022.12.0, )",
|
||||
"Microsoft.EntityFrameworkCore.Relational": "[6.0.12, )",
|
||||
"Microsoft.EntityFrameworkCore.SqlServer": "[6.0.12, )",
|
||||
"Microsoft.EntityFrameworkCore.Sqlite": "[6.0.12, )",
|
||||
"Npgsql.EntityFrameworkCore.PostgreSQL": "[6.0.8, )",
|
||||
"Pomelo.EntityFrameworkCore.MySql": "[6.0.2, )",
|
||||
|
@ -2,6 +2,7 @@
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Enums.Provider;
|
||||
using Bit.Core.Identity;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Settings;
|
||||
@ -137,7 +138,7 @@ public class CurrentContext : ICurrentContext
|
||||
}
|
||||
}
|
||||
|
||||
DeviceIdentifier = GetClaimValue(claimsDict, "device");
|
||||
DeviceIdentifier = GetClaimValue(claimsDict, Claims.Device);
|
||||
|
||||
Organizations = GetOrganizations(claimsDict, orgApi);
|
||||
|
||||
@ -149,9 +150,9 @@ public class CurrentContext : ICurrentContext
|
||||
private List<CurrentContentOrganization> GetOrganizations(Dictionary<string, IEnumerable<Claim>> claimsDict, bool orgApi)
|
||||
{
|
||||
var organizations = new List<CurrentContentOrganization>();
|
||||
if (claimsDict.ContainsKey("orgowner"))
|
||||
if (claimsDict.ContainsKey(Claims.OrganizationOwner))
|
||||
{
|
||||
organizations.AddRange(claimsDict["orgowner"].Select(c =>
|
||||
organizations.AddRange(claimsDict[Claims.OrganizationOwner].Select(c =>
|
||||
new CurrentContentOrganization
|
||||
{
|
||||
Id = new Guid(c.Value),
|
||||
@ -167,9 +168,9 @@ public class CurrentContext : ICurrentContext
|
||||
});
|
||||
}
|
||||
|
||||
if (claimsDict.ContainsKey("orgadmin"))
|
||||
if (claimsDict.ContainsKey(Claims.OrganizationAdmin))
|
||||
{
|
||||
organizations.AddRange(claimsDict["orgadmin"].Select(c =>
|
||||
organizations.AddRange(claimsDict[Claims.OrganizationAdmin].Select(c =>
|
||||
new CurrentContentOrganization
|
||||
{
|
||||
Id = new Guid(c.Value),
|
||||
@ -177,9 +178,9 @@ public class CurrentContext : ICurrentContext
|
||||
}));
|
||||
}
|
||||
|
||||
if (claimsDict.ContainsKey("orguser"))
|
||||
if (claimsDict.ContainsKey(Claims.OrganizationUser))
|
||||
{
|
||||
organizations.AddRange(claimsDict["orguser"].Select(c =>
|
||||
organizations.AddRange(claimsDict[Claims.OrganizationUser].Select(c =>
|
||||
new CurrentContentOrganization
|
||||
{
|
||||
Id = new Guid(c.Value),
|
||||
@ -187,9 +188,9 @@ public class CurrentContext : ICurrentContext
|
||||
}));
|
||||
}
|
||||
|
||||
if (claimsDict.ContainsKey("orgmanager"))
|
||||
if (claimsDict.ContainsKey(Claims.OrganizationManager))
|
||||
{
|
||||
organizations.AddRange(claimsDict["orgmanager"].Select(c =>
|
||||
organizations.AddRange(claimsDict[Claims.OrganizationManager].Select(c =>
|
||||
new CurrentContentOrganization
|
||||
{
|
||||
Id = new Guid(c.Value),
|
||||
@ -197,9 +198,9 @@ public class CurrentContext : ICurrentContext
|
||||
}));
|
||||
}
|
||||
|
||||
if (claimsDict.ContainsKey("orgcustom"))
|
||||
if (claimsDict.ContainsKey(Claims.OrganizationCustom))
|
||||
{
|
||||
organizations.AddRange(claimsDict["orgcustom"].Select(c =>
|
||||
organizations.AddRange(claimsDict[Claims.OrganizationCustom].Select(c =>
|
||||
new CurrentContentOrganization
|
||||
{
|
||||
Id = new Guid(c.Value),
|
||||
@ -214,9 +215,9 @@ public class CurrentContext : ICurrentContext
|
||||
private List<CurrentContentProvider> GetProviders(Dictionary<string, IEnumerable<Claim>> claimsDict)
|
||||
{
|
||||
var providers = new List<CurrentContentProvider>();
|
||||
if (claimsDict.ContainsKey("providerprovideradmin"))
|
||||
if (claimsDict.ContainsKey(Claims.ProviderAdmin))
|
||||
{
|
||||
providers.AddRange(claimsDict["providerprovideradmin"].Select(c =>
|
||||
providers.AddRange(claimsDict[Claims.ProviderAdmin].Select(c =>
|
||||
new CurrentContentProvider
|
||||
{
|
||||
Id = new Guid(c.Value),
|
||||
@ -224,9 +225,9 @@ public class CurrentContext : ICurrentContext
|
||||
}));
|
||||
}
|
||||
|
||||
if (claimsDict.ContainsKey("providerserviceuser"))
|
||||
if (claimsDict.ContainsKey(Claims.ProviderServiceUser))
|
||||
{
|
||||
providers.AddRange(claimsDict["providerserviceuser"].Select(c =>
|
||||
providers.AddRange(claimsDict[Claims.ProviderServiceUser].Select(c =>
|
||||
new CurrentContentProvider
|
||||
{
|
||||
Id = new Guid(c.Value),
|
||||
|
76
src/Core/Entities/AccessPolicy.cs
Normal file
76
src/Core/Entities/AccessPolicy.cs
Normal file
@ -0,0 +1,76 @@
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Core.Entities;
|
||||
|
||||
public class AccessPolicy : ITableObject<Guid>
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
|
||||
// Object to grant access from
|
||||
public Guid? OrganizationUserId { get; set; }
|
||||
public Guid? GroupId { get; set; }
|
||||
public Guid? ServiceAccountId { get; set; }
|
||||
|
||||
// Object to grant access to
|
||||
public Guid? GrantedProjectId { get; set; }
|
||||
public Guid? GrantedServiceAccountId { get; set; }
|
||||
|
||||
// Access
|
||||
public bool Read { get; set; }
|
||||
public bool Write { get; set; }
|
||||
|
||||
public DateTime CreationDate { get; set; }
|
||||
public DateTime RevisionDate { get; set; }
|
||||
|
||||
public void SetNewId()
|
||||
{
|
||||
Id = CoreHelpers.GenerateComb();
|
||||
}
|
||||
}
|
||||
|
||||
public abstract class BaseAccessPolicy
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
|
||||
// Access
|
||||
public bool Read { get; set; }
|
||||
public bool Write { get; set; }
|
||||
|
||||
public DateTime CreationDate { get; set; }
|
||||
public DateTime RevisionDate { get; set; }
|
||||
|
||||
public void SetNewId()
|
||||
{
|
||||
Id = CoreHelpers.GenerateComb();
|
||||
}
|
||||
}
|
||||
|
||||
public class UserProjectAccessPolicy : BaseAccessPolicy
|
||||
{
|
||||
public Guid? OrganizationUserId { get; set; }
|
||||
public Guid? GrantedProjectId { get; set; }
|
||||
}
|
||||
|
||||
public class UserServiceAccountAccessPolicy : BaseAccessPolicy
|
||||
{
|
||||
public Guid? OrganizationUserId { get; set; }
|
||||
public Guid? GrantedServiceAccountId { get; set; }
|
||||
}
|
||||
|
||||
public class GroupProjectAccessPolicy : BaseAccessPolicy
|
||||
{
|
||||
public Guid? GroupId { get; set; }
|
||||
public Guid? GrantedProjectId { get; set; }
|
||||
}
|
||||
|
||||
public class GroupServiceAccountAccessPolicy : BaseAccessPolicy
|
||||
{
|
||||
public Guid? GroupId { get; set; }
|
||||
public Guid? GrantedServiceAccountId { get; set; }
|
||||
}
|
||||
|
||||
public class ServiceAccountProjectAccessPolicy : BaseAccessPolicy
|
||||
{
|
||||
public Guid? ServiceAccountId { get; set; }
|
||||
public Guid? GrantedProjectId { get; set; }
|
||||
}
|
33
src/Core/Entities/ApiKey.cs
Normal file
33
src/Core/Entities/ApiKey.cs
Normal file
@ -0,0 +1,33 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Core.Entities;
|
||||
|
||||
public class ApiKey : ITableObject<Guid>
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid? ServiceAccountId { get; set; }
|
||||
[MaxLength(200)]
|
||||
public string Name { get; set; }
|
||||
[MaxLength(30)]
|
||||
public string ClientSecret { get; set; }
|
||||
[MaxLength(4000)]
|
||||
public string Scope { get; set; }
|
||||
[MaxLength(4000)]
|
||||
public string EncryptedPayload { get; set; }
|
||||
// Key for decrypting `EncryptedPayload`. Encrypted using the organization key.
|
||||
public string Key { get; set; }
|
||||
public DateTime? ExpireAt { get; set; }
|
||||
public DateTime CreationDate { get; internal set; } = DateTime.UtcNow;
|
||||
public DateTime RevisionDate { get; internal set; } = DateTime.UtcNow;
|
||||
|
||||
public void SetNewId()
|
||||
{
|
||||
Id = CoreHelpers.GenerateComb();
|
||||
}
|
||||
|
||||
public ICollection<string> GetScopes()
|
||||
{
|
||||
return CoreHelpers.LoadClassFromJsonData<List<string>>(Scope);
|
||||
}
|
||||
}
|
@ -45,6 +45,7 @@ public class Organization : ITableObject<Guid>, ISubscriber, IStorable, IStorabl
|
||||
public bool Use2fa { get; set; }
|
||||
public bool UseApi { get; set; }
|
||||
public bool UseResetPassword { get; set; }
|
||||
public bool UseSecretsManager { get; set; }
|
||||
public bool SelfHost { get; set; }
|
||||
public bool UsersGetPremium { get; set; }
|
||||
public bool UseCustomPermissions { get; set; }
|
||||
|
29
src/Core/Entities/Project.cs
Normal file
29
src/Core/Entities/Project.cs
Normal file
@ -0,0 +1,29 @@
|
||||
#nullable enable
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Core.Entities;
|
||||
|
||||
public class Project : ITableObject<Guid>
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
|
||||
public Guid OrganizationId { get; set; }
|
||||
|
||||
public string? Name { get; set; }
|
||||
|
||||
public DateTime CreationDate { get; set; } = DateTime.UtcNow;
|
||||
|
||||
public DateTime RevisionDate { get; set; } = DateTime.UtcNow;
|
||||
|
||||
public DateTime? DeletedDate { get; set; }
|
||||
|
||||
public virtual ICollection<Secret>? Secrets { get; set; }
|
||||
|
||||
public void SetNewId()
|
||||
{
|
||||
if (Id == default(Guid))
|
||||
{
|
||||
Id = CoreHelpers.GenerateComb();
|
||||
}
|
||||
}
|
||||
}
|
33
src/Core/Entities/Secret.cs
Normal file
33
src/Core/Entities/Secret.cs
Normal file
@ -0,0 +1,33 @@
|
||||
#nullable enable
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Core.Entities;
|
||||
|
||||
public class Secret : ITableObject<Guid>
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
|
||||
public Guid OrganizationId { get; set; }
|
||||
|
||||
public string? Key { get; set; }
|
||||
|
||||
public string? Value { get; set; }
|
||||
|
||||
public string? Note { get; set; }
|
||||
|
||||
public DateTime CreationDate { get; set; } = DateTime.UtcNow;
|
||||
|
||||
public DateTime RevisionDate { get; set; } = DateTime.UtcNow;
|
||||
|
||||
public DateTime? DeletedDate { get; set; }
|
||||
|
||||
public ICollection<Project>? Projects { get; set; }
|
||||
|
||||
public void SetNewId()
|
||||
{
|
||||
if (Id == default(Guid))
|
||||
{
|
||||
Id = CoreHelpers.GenerateComb();
|
||||
}
|
||||
}
|
||||
}
|
26
src/Core/Entities/ServiceAccount.cs
Normal file
26
src/Core/Entities/ServiceAccount.cs
Normal file
@ -0,0 +1,26 @@
|
||||
#nullable enable
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Core.Entities;
|
||||
|
||||
public class ServiceAccount : ITableObject<Guid>
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
|
||||
public Guid OrganizationId { get; set; }
|
||||
|
||||
public string? Name { get; set; }
|
||||
|
||||
public DateTime CreationDate { get; set; } = DateTime.UtcNow;
|
||||
|
||||
public DateTime RevisionDate { get; set; } = DateTime.UtcNow;
|
||||
|
||||
public void SetNewId()
|
||||
{
|
||||
if (Id == default(Guid))
|
||||
{
|
||||
Id = CoreHelpers.GenerateComb();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -45,5 +45,7 @@ public enum DeviceType : byte
|
||||
[Display(Name = "Vivaldi Extension")]
|
||||
VivaldiExtension = 19,
|
||||
[Display(Name = "Safari Extension")]
|
||||
SafariExtension = 20
|
||||
SafariExtension = 20,
|
||||
[Display(Name = "SDK")]
|
||||
SDK = 21,
|
||||
}
|
||||
|
19
src/Core/Identity/Claims.cs
Normal file
19
src/Core/Identity/Claims.cs
Normal file
@ -0,0 +1,19 @@
|
||||
namespace Bit.Core.Identity;
|
||||
|
||||
public static class Claims
|
||||
{
|
||||
// User
|
||||
public const string SecurityStamp = "sstamp";
|
||||
public const string Premium = "premium";
|
||||
public const string Device = "device";
|
||||
public const string OrganizationOwner = "orgowner";
|
||||
public const string OrganizationAdmin = "orgadmin";
|
||||
public const string OrganizationManager = "orgmanager";
|
||||
public const string OrganizationUser = "orguser";
|
||||
public const string OrganizationCustom = "orgcustom";
|
||||
public const string ProviderAdmin = "providerprovideradmin";
|
||||
public const string ProviderServiceUser = "providerserviceuser";
|
||||
|
||||
// Service Account
|
||||
public const string Organization = "organization";
|
||||
}
|
28
src/Core/IdentityServer/ApiScopes.cs
Normal file
28
src/Core/IdentityServer/ApiScopes.cs
Normal file
@ -0,0 +1,28 @@
|
||||
using IdentityServer4.Models;
|
||||
|
||||
namespace Bit.Core.IdentityServer;
|
||||
|
||||
public static class ApiScopes
|
||||
{
|
||||
public const string Api = "api";
|
||||
public const string ApiInstallation = "api.installation";
|
||||
public const string ApiLicensing = "api.licensing";
|
||||
public const string ApiOrganization = "api.organization";
|
||||
public const string ApiPush = "api.push";
|
||||
public const string ApiSecrets = "api.secrets";
|
||||
public const string Internal = "internal";
|
||||
|
||||
public static IEnumerable<ApiScope> GetApiScopes()
|
||||
{
|
||||
return new List<ApiScope>
|
||||
{
|
||||
new(Api, "API Access"),
|
||||
new(ApiPush, "API Push Access"),
|
||||
new(ApiLicensing, "API Licensing Access"),
|
||||
new(ApiOrganization, "API Organization Access"),
|
||||
new(ApiInstallation, "API Installation Access"),
|
||||
new(Internal, "Internal Access"),
|
||||
new(ApiSecrets, "Secrets Manager Access"),
|
||||
};
|
||||
}
|
||||
}
|
37
src/Core/Models/Data/ApiKeyDetails.cs
Normal file
37
src/Core/Models/Data/ApiKeyDetails.cs
Normal file
@ -0,0 +1,37 @@
|
||||
using Bit.Core.Entities;
|
||||
|
||||
namespace Bit.Core.Models.Data;
|
||||
|
||||
public class ApiKeyDetails : ApiKey
|
||||
{
|
||||
protected ApiKeyDetails() { }
|
||||
|
||||
protected ApiKeyDetails(ApiKey apiKey)
|
||||
{
|
||||
Id = apiKey.Id;
|
||||
ServiceAccountId = apiKey.ServiceAccountId;
|
||||
Name = apiKey.Name;
|
||||
ClientSecret = apiKey.ClientSecret;
|
||||
Scope = apiKey.Scope;
|
||||
EncryptedPayload = apiKey.EncryptedPayload;
|
||||
Key = apiKey.Key;
|
||||
ExpireAt = apiKey.ExpireAt;
|
||||
CreationDate = apiKey.CreationDate;
|
||||
RevisionDate = apiKey.RevisionDate;
|
||||
}
|
||||
}
|
||||
|
||||
public class ServiceAccountApiKeyDetails : ApiKeyDetails
|
||||
{
|
||||
public ServiceAccountApiKeyDetails()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public ServiceAccountApiKeyDetails(ApiKey apiKey, Guid organizationId) : base(apiKey)
|
||||
{
|
||||
ServiceAccountOrganizationId = organizationId;
|
||||
}
|
||||
|
||||
public Guid ServiceAccountOrganizationId { get; set; }
|
||||
}
|
@ -16,6 +16,7 @@ public class OrganizationUserOrganizationDetails
|
||||
public bool Use2fa { get; set; }
|
||||
public bool UseApi { get; set; }
|
||||
public bool UseResetPassword { get; set; }
|
||||
public bool UseSecretsManager { get; set; }
|
||||
public bool SelfHost { get; set; }
|
||||
public bool UsersGetPremium { get; set; }
|
||||
public bool UseCustomPermissions { get; set; }
|
||||
|
@ -1,5 +1,6 @@
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.IdentityServer;
|
||||
using Bit.Core.Models.Api.Request.OrganizationSponsorships;
|
||||
using Bit.Core.Models.Api.Response.OrganizationSponsorships;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationSponsorships;
|
||||
@ -30,7 +31,7 @@ public class SelfHostedSyncSponsorshipsCommand : BaseIdentityClientService, ISel
|
||||
httpFactory,
|
||||
globalSettings.Installation.ApiUri,
|
||||
globalSettings.Installation.IdentityUri,
|
||||
"api.installation",
|
||||
ApiScopes.ApiInstallation,
|
||||
$"installation.{globalSettings.Installation.Id}",
|
||||
globalSettings.Installation.Key,
|
||||
logger)
|
||||
|
7
src/Core/Repositories/IAccessPolicyRepository.cs
Normal file
7
src/Core/Repositories/IAccessPolicyRepository.cs
Normal file
@ -0,0 +1,7 @@
|
||||
using Bit.Core.Entities;
|
||||
|
||||
namespace Bit.Core.Repositories;
|
||||
|
||||
public interface IAccessPolicyRepository : IRepository<AccessPolicy, Guid>
|
||||
{
|
||||
}
|
10
src/Core/Repositories/IApiKeyRepository.cs
Normal file
10
src/Core/Repositories/IApiKeyRepository.cs
Normal file
@ -0,0 +1,10 @@
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Models.Data;
|
||||
|
||||
namespace Bit.Core.Repositories;
|
||||
|
||||
public interface IApiKeyRepository : IRepository<ApiKey, Guid>
|
||||
{
|
||||
Task<ApiKeyDetails> GetDetailsByIdAsync(Guid id);
|
||||
Task<ICollection<ApiKey>> GetManyByServiceAccountIdAsync(Guid id);
|
||||
}
|
13
src/Core/Repositories/IProjectRepository.cs
Normal file
13
src/Core/Repositories/IProjectRepository.cs
Normal file
@ -0,0 +1,13 @@
|
||||
using Bit.Core.Entities;
|
||||
|
||||
namespace Bit.Core.Repositories;
|
||||
|
||||
public interface IProjectRepository
|
||||
{
|
||||
Task<IEnumerable<Project>> GetManyByOrganizationIdAsync(Guid organizationId, Guid userId);
|
||||
Task<IEnumerable<Project>> GetManyByIds(IEnumerable<Guid> ids);
|
||||
Task<Project> GetByIdAsync(Guid id);
|
||||
Task<Project> CreateAsync(Project project);
|
||||
Task ReplaceAsync(Project project);
|
||||
Task DeleteManyByIdAsync(IEnumerable<Guid> ids);
|
||||
}
|
14
src/Core/Repositories/ISecretRepository.cs
Normal file
14
src/Core/Repositories/ISecretRepository.cs
Normal file
@ -0,0 +1,14 @@
|
||||
using Bit.Core.Entities;
|
||||
|
||||
namespace Bit.Core.Repositories;
|
||||
|
||||
public interface ISecretRepository
|
||||
{
|
||||
Task<IEnumerable<Secret>> GetManyByOrganizationIdAsync(Guid organizationId);
|
||||
Task<IEnumerable<Secret>> GetManyByIds(IEnumerable<Guid> ids);
|
||||
Task<IEnumerable<Secret>> GetManyByProjectIdAsync(Guid projectId);
|
||||
Task<Secret> GetByIdAsync(Guid id);
|
||||
Task<Secret> CreateAsync(Secret secret);
|
||||
Task<Secret> UpdateAsync(Secret secret);
|
||||
Task SoftDeleteManyByIdAsync(IEnumerable<Guid> ids);
|
||||
}
|
11
src/Core/Repositories/IServiceAccountRepository.cs
Normal file
11
src/Core/Repositories/IServiceAccountRepository.cs
Normal file
@ -0,0 +1,11 @@
|
||||
using Bit.Core.Entities;
|
||||
|
||||
namespace Bit.Core.Repositories;
|
||||
|
||||
public interface IServiceAccountRepository
|
||||
{
|
||||
Task<IEnumerable<ServiceAccount>> GetManyByOrganizationIdAsync(Guid organizationId);
|
||||
Task<ServiceAccount> GetByIdAsync(Guid id);
|
||||
Task<ServiceAccount> CreateAsync(ServiceAccount serviceAccount);
|
||||
Task ReplaceAsync(ServiceAccount serviceAccount);
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
using Bit.Core.Entities;
|
||||
|
||||
namespace Bit.Core.SecretManagerFeatures.AccessTokens.Interfaces;
|
||||
|
||||
public interface ICreateAccessTokenCommand
|
||||
{
|
||||
Task<ApiKey> CreateAsync(ApiKey apiKey);
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
using Bit.Core.Entities;
|
||||
|
||||
namespace Bit.Core.SecretManagerFeatures.Projects.Interfaces;
|
||||
|
||||
public interface ICreateProjectCommand
|
||||
{
|
||||
Task<Project> CreateAsync(Project project);
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
using Bit.Core.Entities;
|
||||
|
||||
namespace Bit.Core.SecretManagerFeatures.Projects.Interfaces;
|
||||
|
||||
public interface IDeleteProjectCommand
|
||||
{
|
||||
Task<List<Tuple<Project, string>>> DeleteProjects(List<Guid> ids);
|
||||
}
|
||||
|
@ -0,0 +1,8 @@
|
||||
using Bit.Core.Entities;
|
||||
|
||||
namespace Bit.Core.SecretManagerFeatures.Projects.Interfaces;
|
||||
|
||||
public interface IUpdateProjectCommand
|
||||
{
|
||||
Task<Project> UpdateAsync(Project project);
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
using Bit.Core.Entities;
|
||||
|
||||
namespace Bit.Core.SecretManagerFeatures.Secrets.Interfaces;
|
||||
|
||||
public interface ICreateSecretCommand
|
||||
{
|
||||
Task<Secret> CreateAsync(Secret secret);
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
using Bit.Core.Entities;
|
||||
|
||||
namespace Bit.Core.SecretManagerFeatures.Secrets.Interfaces;
|
||||
|
||||
public interface IDeleteSecretCommand
|
||||
{
|
||||
Task<List<Tuple<Secret, string>>> DeleteSecrets(List<Guid> ids);
|
||||
}
|
||||
|
@ -0,0 +1,8 @@
|
||||
using Bit.Core.Entities;
|
||||
|
||||
namespace Bit.Core.SecretManagerFeatures.Secrets.Interfaces;
|
||||
|
||||
public interface IUpdateSecretCommand
|
||||
{
|
||||
Task<Secret> UpdateAsync(Secret secret);
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
using Bit.Core.Entities;
|
||||
|
||||
namespace Bit.Core.SecretManagerFeatures.ServiceAccounts.Interfaces;
|
||||
|
||||
public interface ICreateServiceAccountCommand
|
||||
{
|
||||
Task<ServiceAccount> CreateAsync(ServiceAccount serviceAccount);
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
using Bit.Core.Entities;
|
||||
|
||||
namespace Bit.Core.SecretManagerFeatures.ServiceAccounts.Interfaces;
|
||||
|
||||
public interface IUpdateServiceAccountCommand
|
||||
{
|
||||
Task<ServiceAccount> UpdateAsync(ServiceAccount serviceAccount);
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.IdentityServer;
|
||||
using Bit.Core.Models;
|
||||
using Bit.Core.Models.Api;
|
||||
using Bit.Core.Repositories;
|
||||
@ -25,7 +26,7 @@ public class RelayPushNotificationService : BaseIdentityClientService, IPushNoti
|
||||
httpFactory,
|
||||
globalSettings.PushRelayBaseUri,
|
||||
globalSettings.Installation.IdentityUri,
|
||||
"api.push",
|
||||
ApiScopes.ApiPush,
|
||||
$"installation.{globalSettings.Installation.Id}",
|
||||
globalSettings.Installation.Key,
|
||||
logger)
|
||||
|
@ -1,4 +1,5 @@
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.IdentityServer;
|
||||
using Bit.Core.Models.Api;
|
||||
using Bit.Core.Settings;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@ -16,7 +17,7 @@ public class RelayPushRegistrationService : BaseIdentityClientService, IPushRegi
|
||||
httpFactory,
|
||||
globalSettings.PushRelayBaseUri,
|
||||
globalSettings.Installation.IdentityUri,
|
||||
"api.push",
|
||||
ApiScopes.ApiPush,
|
||||
$"installation.{globalSettings.Installation.Id}",
|
||||
globalSettings.Installation.Key,
|
||||
logger)
|
||||
|
@ -14,6 +14,7 @@ using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Enums.Provider;
|
||||
using Bit.Core.Identity;
|
||||
using Bit.Core.Settings;
|
||||
using IdentityModel;
|
||||
using Microsoft.AspNetCore.DataProtection;
|
||||
@ -631,10 +632,10 @@ public static class CoreHelpers
|
||||
{
|
||||
var claims = new List<KeyValuePair<string, string>>()
|
||||
{
|
||||
new KeyValuePair<string, string>("premium", isPremium ? "true" : "false"),
|
||||
new KeyValuePair<string, string>(JwtClaimTypes.Email, user.Email),
|
||||
new KeyValuePair<string, string>(JwtClaimTypes.EmailVerified, user.EmailVerified ? "true" : "false"),
|
||||
new KeyValuePair<string, string>("sstamp", user.SecurityStamp)
|
||||
new(Claims.Premium, isPremium ? "true" : "false"),
|
||||
new(JwtClaimTypes.Email, user.Email),
|
||||
new(JwtClaimTypes.EmailVerified, user.EmailVerified ? "true" : "false"),
|
||||
new(Claims.SecurityStamp, user.SecurityStamp),
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(user.Name))
|
||||
@ -652,31 +653,31 @@ public static class CoreHelpers
|
||||
case Enums.OrganizationUserType.Owner:
|
||||
foreach (var org in group)
|
||||
{
|
||||
claims.Add(new KeyValuePair<string, string>("orgowner", org.Id.ToString()));
|
||||
claims.Add(new KeyValuePair<string, string>(Claims.OrganizationOwner, org.Id.ToString()));
|
||||
}
|
||||
break;
|
||||
case Enums.OrganizationUserType.Admin:
|
||||
foreach (var org in group)
|
||||
{
|
||||
claims.Add(new KeyValuePair<string, string>("orgadmin", org.Id.ToString()));
|
||||
claims.Add(new KeyValuePair<string, string>(Claims.OrganizationAdmin, org.Id.ToString()));
|
||||
}
|
||||
break;
|
||||
case Enums.OrganizationUserType.Manager:
|
||||
foreach (var org in group)
|
||||
{
|
||||
claims.Add(new KeyValuePair<string, string>("orgmanager", org.Id.ToString()));
|
||||
claims.Add(new KeyValuePair<string, string>(Claims.OrganizationManager, org.Id.ToString()));
|
||||
}
|
||||
break;
|
||||
case Enums.OrganizationUserType.User:
|
||||
foreach (var org in group)
|
||||
{
|
||||
claims.Add(new KeyValuePair<string, string>("orguser", org.Id.ToString()));
|
||||
claims.Add(new KeyValuePair<string, string>(Claims.OrganizationUser, org.Id.ToString()));
|
||||
}
|
||||
break;
|
||||
case Enums.OrganizationUserType.Custom:
|
||||
foreach (var org in group)
|
||||
{
|
||||
claims.Add(new KeyValuePair<string, string>("orgcustom", org.Id.ToString()));
|
||||
claims.Add(new KeyValuePair<string, string>(Claims.OrganizationCustom, org.Id.ToString()));
|
||||
foreach (var (permission, claimName) in org.Permissions.ClaimsMap)
|
||||
{
|
||||
if (!permission)
|
||||
@ -703,13 +704,13 @@ public static class CoreHelpers
|
||||
case ProviderUserType.ProviderAdmin:
|
||||
foreach (var provider in group)
|
||||
{
|
||||
claims.Add(new KeyValuePair<string, string>("providerprovideradmin", provider.Id.ToString()));
|
||||
claims.Add(new KeyValuePair<string, string>(Claims.ProviderAdmin, provider.Id.ToString()));
|
||||
}
|
||||
break;
|
||||
case ProviderUserType.ServiceUser:
|
||||
foreach (var provider in group)
|
||||
{
|
||||
claims.Add(new KeyValuePair<string, string>("providerserviceuser", provider.Id.ToString()));
|
||||
claims.Add(new KeyValuePair<string, string>(Claims.ProviderServiceUser, provider.Id.ToString()));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
using System.Globalization;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.IdentityServer;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
@ -41,7 +42,7 @@ public class Startup
|
||||
{
|
||||
policy.RequireAuthenticatedUser();
|
||||
policy.RequireClaim(JwtClaimTypes.AuthenticationMethod, "Application", "external");
|
||||
policy.RequireClaim(JwtClaimTypes.Scope, "api");
|
||||
policy.RequireClaim(JwtClaimTypes.Scope, ApiScopes.Api);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -539,6 +539,15 @@
|
||||
"Microsoft.Extensions.DependencyModel": "6.0.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.EntityFrameworkCore.SqlServer": {
|
||||
"type": "Transitive",
|
||||
"resolved": "6.0.12",
|
||||
"contentHash": "bdKnSz1w+WZz9QYWhs3wwGuMn4YssjdR+HOBpzChQ6C3+dblq4Pammm5fzugcPOhTgCiWftOT2jPOT5hEy4bYg==",
|
||||
"dependencies": {
|
||||
"Microsoft.Data.SqlClient": "2.1.4",
|
||||
"Microsoft.EntityFrameworkCore.Relational": "6.0.12"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Caching.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "6.0.0",
|
||||
@ -2754,6 +2763,7 @@
|
||||
"AutoMapper.Extensions.Microsoft.DependencyInjection": "[11.0.0, )",
|
||||
"Core": "[2022.12.0, )",
|
||||
"Microsoft.EntityFrameworkCore.Relational": "[6.0.12, )",
|
||||
"Microsoft.EntityFrameworkCore.SqlServer": "[6.0.12, )",
|
||||
"Microsoft.EntityFrameworkCore.Sqlite": "[6.0.12, )",
|
||||
"Npgsql.EntityFrameworkCore.PostgreSQL": "[6.0.8, )",
|
||||
"Pomelo.EntityFrameworkCore.MySql": "[6.0.2, )",
|
||||
|
@ -539,6 +539,15 @@
|
||||
"Microsoft.Extensions.DependencyModel": "6.0.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.EntityFrameworkCore.SqlServer": {
|
||||
"type": "Transitive",
|
||||
"resolved": "6.0.12",
|
||||
"contentHash": "bdKnSz1w+WZz9QYWhs3wwGuMn4YssjdR+HOBpzChQ6C3+dblq4Pammm5fzugcPOhTgCiWftOT2jPOT5hEy4bYg==",
|
||||
"dependencies": {
|
||||
"Microsoft.Data.SqlClient": "2.1.4",
|
||||
"Microsoft.EntityFrameworkCore.Relational": "6.0.12"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Caching.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "6.0.0",
|
||||
@ -2754,6 +2763,7 @@
|
||||
"AutoMapper.Extensions.Microsoft.DependencyInjection": "[11.0.0, )",
|
||||
"Core": "[2022.12.0, )",
|
||||
"Microsoft.EntityFrameworkCore.Relational": "[6.0.12, )",
|
||||
"Microsoft.EntityFrameworkCore.SqlServer": "[6.0.12, )",
|
||||
"Microsoft.EntityFrameworkCore.Sqlite": "[6.0.12, )",
|
||||
"Npgsql.EntityFrameworkCore.PostgreSQL": "[6.0.8, )",
|
||||
"Pomelo.EntityFrameworkCore.MySql": "[6.0.2, )",
|
||||
|
@ -549,6 +549,15 @@
|
||||
"Microsoft.Extensions.DependencyModel": "6.0.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.EntityFrameworkCore.SqlServer": {
|
||||
"type": "Transitive",
|
||||
"resolved": "6.0.12",
|
||||
"contentHash": "bdKnSz1w+WZz9QYWhs3wwGuMn4YssjdR+HOBpzChQ6C3+dblq4Pammm5fzugcPOhTgCiWftOT2jPOT5hEy4bYg==",
|
||||
"dependencies": {
|
||||
"Microsoft.Data.SqlClient": "2.1.4",
|
||||
"Microsoft.EntityFrameworkCore.Relational": "6.0.12"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Caching.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "6.0.0",
|
||||
@ -2764,6 +2773,7 @@
|
||||
"AutoMapper.Extensions.Microsoft.DependencyInjection": "[11.0.0, )",
|
||||
"Core": "[2022.12.0, )",
|
||||
"Microsoft.EntityFrameworkCore.Relational": "[6.0.12, )",
|
||||
"Microsoft.EntityFrameworkCore.SqlServer": "[6.0.12, )",
|
||||
"Microsoft.EntityFrameworkCore.Sqlite": "[6.0.12, )",
|
||||
"Npgsql.EntityFrameworkCore.PostgreSQL": "[6.0.8, )",
|
||||
"Pomelo.EntityFrameworkCore.MySql": "[6.0.2, )",
|
||||
|
@ -1,4 +1,6 @@
|
||||
using IdentityModel;
|
||||
using Bit.Core.Identity;
|
||||
using Bit.Core.IdentityServer;
|
||||
using IdentityModel;
|
||||
using IdentityServer4.Models;
|
||||
|
||||
namespace Bit.Identity.IdentityServer;
|
||||
@ -9,27 +11,27 @@ public class ApiResources
|
||||
{
|
||||
return new List<ApiResource>
|
||||
{
|
||||
new ApiResource("api", new string[] {
|
||||
new("api", new[] {
|
||||
JwtClaimTypes.Name,
|
||||
JwtClaimTypes.Email,
|
||||
JwtClaimTypes.EmailVerified,
|
||||
"sstamp", // security stamp
|
||||
"premium",
|
||||
"device",
|
||||
"orgowner",
|
||||
"orgadmin",
|
||||
"orgmanager",
|
||||
"orguser",
|
||||
"orgcustom",
|
||||
"providerprovideradmin",
|
||||
"providerserviceuser",
|
||||
Claims.SecurityStamp,
|
||||
Claims.Premium,
|
||||
Claims.Device,
|
||||
Claims.OrganizationOwner,
|
||||
Claims.OrganizationAdmin,
|
||||
Claims.OrganizationManager,
|
||||
Claims.OrganizationUser,
|
||||
Claims.OrganizationCustom,
|
||||
Claims.ProviderAdmin,
|
||||
Claims.ProviderServiceUser,
|
||||
}),
|
||||
new ApiResource("internal", new string[] { JwtClaimTypes.Subject }),
|
||||
new ApiResource("api.push", new string[] { JwtClaimTypes.Subject }),
|
||||
new ApiResource("api.licensing", new string[] { JwtClaimTypes.Subject }),
|
||||
new ApiResource("api.organization", new string[] { JwtClaimTypes.Subject }),
|
||||
new ApiResource("api.provider", new string[] { JwtClaimTypes.Subject }),
|
||||
new ApiResource("api.installation", new string[] { JwtClaimTypes.Subject }),
|
||||
new(ApiScopes.Internal, new[] { JwtClaimTypes.Subject }),
|
||||
new(ApiScopes.ApiPush, new[] { JwtClaimTypes.Subject }),
|
||||
new(ApiScopes.ApiLicensing, new[] { JwtClaimTypes.Subject }),
|
||||
new(ApiScopes.ApiOrganization, new[] { JwtClaimTypes.Subject }),
|
||||
new(ApiScopes.ApiInstallation, new[] { JwtClaimTypes.Subject }),
|
||||
new(ApiScopes.ApiSecrets, new[] { JwtClaimTypes.Subject, Claims.Organization }),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -1,19 +0,0 @@
|
||||
using IdentityServer4.Models;
|
||||
|
||||
namespace Bit.Identity.IdentityServer;
|
||||
|
||||
public class ApiScopes
|
||||
{
|
||||
public static IEnumerable<ApiScope> GetApiScopes()
|
||||
{
|
||||
return new List<ApiScope>
|
||||
{
|
||||
new ApiScope("api", "API Access"),
|
||||
new ApiScope("api.push", "API Push Access"),
|
||||
new ApiScope("api.licensing", "API Licensing Access"),
|
||||
new ApiScope("api.organization", "API Organization Access"),
|
||||
new ApiScope("api.installation", "API Installation Access"),
|
||||
new ApiScope("internal", "Internal Access")
|
||||
};
|
||||
}
|
||||
}
|
@ -31,12 +31,11 @@ public abstract class BaseRequestValidator<T> where T : class
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
private readonly IApplicationCacheService _applicationCacheService;
|
||||
private readonly IMailService _mailService;
|
||||
private readonly ILogger<ResourceOwnerPasswordValidator> _logger;
|
||||
private readonly ILogger _logger;
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
private readonly IPolicyRepository _policyRepository;
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly ICaptchaValidationService _captchaValidationService;
|
||||
|
||||
public BaseRequestValidator(
|
||||
UserManager<User> userManager,
|
||||
@ -49,12 +48,11 @@ public abstract class BaseRequestValidator<T> where T : class
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IApplicationCacheService applicationCacheService,
|
||||
IMailService mailService,
|
||||
ILogger<ResourceOwnerPasswordValidator> logger,
|
||||
ILogger logger,
|
||||
ICurrentContext currentContext,
|
||||
GlobalSettings globalSettings,
|
||||
IPolicyRepository policyRepository,
|
||||
IUserRepository userRepository,
|
||||
ICaptchaValidationService captchaValidationService)
|
||||
IUserRepository userRepository)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_deviceRepository = deviceRepository;
|
||||
@ -71,7 +69,6 @@ public abstract class BaseRequestValidator<T> where T : class
|
||||
_globalSettings = globalSettings;
|
||||
_policyRepository = policyRepository;
|
||||
_userRepository = userRepository;
|
||||
_captchaValidationService = captchaValidationService;
|
||||
}
|
||||
|
||||
protected async Task ValidateAsync(T context, ValidatedTokenRequest request,
|
||||
@ -172,7 +169,7 @@ public abstract class BaseRequestValidator<T> where T : class
|
||||
|
||||
if (device != null)
|
||||
{
|
||||
claims.Add(new Claim("device", device.Identifier));
|
||||
claims.Add(new Claim(Claims.Device, device.Identifier));
|
||||
}
|
||||
|
||||
var customResponse = new Dictionary<string, object>();
|
||||
|
@ -2,6 +2,9 @@
|
||||
using System.Security.Claims;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Identity;
|
||||
using Bit.Core.IdentityServer;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
@ -23,8 +26,8 @@ public class ClientStore : IClientStore
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
private readonly IProviderUserRepository _providerUserRepository;
|
||||
private readonly IProviderOrganizationRepository _providerOrganizationRepository;
|
||||
private readonly IOrganizationApiKeyRepository _organizationApiKeyRepository;
|
||||
private readonly IApiKeyRepository _apiKeyRepository;
|
||||
|
||||
public ClientStore(
|
||||
IInstallationRepository installationRepository,
|
||||
@ -36,8 +39,8 @@ public class ClientStore : IClientStore
|
||||
ICurrentContext currentContext,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IProviderUserRepository providerUserRepository,
|
||||
IProviderOrganizationRepository providerOrganizationRepository,
|
||||
IOrganizationApiKeyRepository organizationApiKeyRepository)
|
||||
IOrganizationApiKeyRepository organizationApiKeyRepository,
|
||||
IApiKeyRepository apiKeyRepository)
|
||||
{
|
||||
_installationRepository = installationRepository;
|
||||
_organizationRepository = organizationRepository;
|
||||
@ -48,103 +51,96 @@ public class ClientStore : IClientStore
|
||||
_currentContext = currentContext;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
_providerUserRepository = providerUserRepository;
|
||||
_providerOrganizationRepository = providerOrganizationRepository;
|
||||
_organizationApiKeyRepository = organizationApiKeyRepository;
|
||||
_apiKeyRepository = apiKeyRepository;
|
||||
}
|
||||
|
||||
public async Task<Client> FindClientByIdAsync(string clientId)
|
||||
{
|
||||
if (!_globalSettings.SelfHosted && clientId.StartsWith("installation."))
|
||||
{
|
||||
var idParts = clientId.Split('.');
|
||||
if (idParts.Length > 1 && Guid.TryParse(idParts[1], out Guid id))
|
||||
{
|
||||
var installation = await _installationRepository.GetByIdAsync(id);
|
||||
if (installation != null)
|
||||
{
|
||||
return new Client
|
||||
{
|
||||
ClientId = $"installation.{installation.Id}",
|
||||
RequireClientSecret = true,
|
||||
ClientSecrets = { new Secret(installation.Key.Sha256()) },
|
||||
AllowedScopes = new string[] { "api.push", "api.licensing", "api.installation" },
|
||||
AllowedGrantTypes = GrantTypes.ClientCredentials,
|
||||
AccessTokenLifetime = 3600 * 24,
|
||||
Enabled = installation.Enabled,
|
||||
Claims = new List<ClientClaim>
|
||||
{
|
||||
new ClientClaim(JwtClaimTypes.Subject, installation.Id.ToString())
|
||||
return await CreateInstallationClientAsync(clientId);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (_globalSettings.SelfHosted && clientId.StartsWith("internal.") &&
|
||||
|
||||
if (_globalSettings.SelfHosted && clientId.StartsWith("internal.") &&
|
||||
CoreHelpers.SettingHasValue(_globalSettings.InternalIdentityKey))
|
||||
{
|
||||
var idParts = clientId.Split('.');
|
||||
if (idParts.Length > 1)
|
||||
return CreateInternalClient(clientId);
|
||||
}
|
||||
|
||||
if (clientId.StartsWith("organization."))
|
||||
{
|
||||
var id = idParts[1];
|
||||
if (!string.IsNullOrWhiteSpace(id))
|
||||
return await CreateOrganizationClientAsync(clientId);
|
||||
}
|
||||
|
||||
if (clientId.StartsWith("user."))
|
||||
{
|
||||
return new Client
|
||||
return await CreateUserClientAsync(clientId);
|
||||
}
|
||||
|
||||
if (_staticClientStore.ApiClients.ContainsKey(clientId))
|
||||
{
|
||||
ClientId = $"internal.{id}",
|
||||
return _staticClientStore.ApiClients[clientId];
|
||||
}
|
||||
|
||||
return await CreateApiKeyClientAsync(clientId);
|
||||
}
|
||||
|
||||
private async Task<Client> CreateApiKeyClientAsync(string clientId)
|
||||
{
|
||||
var apiKey = await _apiKeyRepository.GetDetailsByIdAsync(new Guid(clientId));
|
||||
|
||||
if (apiKey == null || apiKey.ExpireAt <= DateTime.Now)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var client = new Client
|
||||
{
|
||||
ClientId = clientId,
|
||||
RequireClientSecret = true,
|
||||
ClientSecrets = { new Secret(_globalSettings.InternalIdentityKey.Sha256()) },
|
||||
AllowedScopes = new string[] { "internal" },
|
||||
AllowedGrantTypes = GrantTypes.ClientCredentials,
|
||||
AccessTokenLifetime = 3600 * 24,
|
||||
Enabled = true,
|
||||
Claims = new List<ClientClaim>
|
||||
{
|
||||
new ClientClaim(JwtClaimTypes.Subject, id)
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (clientId.StartsWith("organization."))
|
||||
{
|
||||
var idParts = clientId.Split('.');
|
||||
if (idParts.Length > 1 && Guid.TryParse(idParts[1], out var id))
|
||||
{
|
||||
var org = await _organizationRepository.GetByIdAsync(id);
|
||||
if (org != null)
|
||||
{
|
||||
var orgApiKey = (await _organizationApiKeyRepository
|
||||
.GetManyByOrganizationIdTypeAsync(org.Id, OrganizationApiKeyType.Default))
|
||||
.First();
|
||||
return new Client
|
||||
{
|
||||
ClientId = $"organization.{org.Id}",
|
||||
RequireClientSecret = true,
|
||||
ClientSecrets = { new Secret(orgApiKey.ApiKey.Sha256()) },
|
||||
AllowedScopes = new string[] { "api.organization" },
|
||||
ClientSecrets = { new Secret(apiKey.ClientSecret.Sha256()) },
|
||||
AllowedScopes = apiKey.GetScopes(),
|
||||
AllowedGrantTypes = GrantTypes.ClientCredentials,
|
||||
AccessTokenLifetime = 3600 * 1,
|
||||
Enabled = org.Enabled && org.UseApi,
|
||||
ClientClaimsPrefix = null,
|
||||
Properties = new Dictionary<string, string> {
|
||||
{"encryptedPayload", apiKey.EncryptedPayload},
|
||||
},
|
||||
Claims = new List<ClientClaim>
|
||||
{
|
||||
new ClientClaim(JwtClaimTypes.Subject, org.Id.ToString())
|
||||
}
|
||||
new(JwtClaimTypes.Subject, apiKey.ServiceAccountId.ToString()),
|
||||
},
|
||||
};
|
||||
|
||||
switch (apiKey)
|
||||
{
|
||||
case ServiceAccountApiKeyDetails key:
|
||||
client.Claims.Add(new ClientClaim(Claims.Organization, key.ServiceAccountOrganizationId.ToString()));
|
||||
break;
|
||||
}
|
||||
|
||||
return client;
|
||||
}
|
||||
}
|
||||
else if (clientId.StartsWith("user."))
|
||||
|
||||
private async Task<Client> CreateUserClientAsync(string clientId)
|
||||
{
|
||||
var idParts = clientId.Split('.');
|
||||
if (idParts.Length > 1 && Guid.TryParse(idParts[1], out var id))
|
||||
if (idParts.Length <= 1 || !Guid.TryParse(idParts[1], out var id))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var user = await _userRepository.GetByIdAsync(id);
|
||||
if (user != null)
|
||||
if (user == null)
|
||||
{
|
||||
var claims = new Collection<ClientClaim>()
|
||||
return null;
|
||||
}
|
||||
|
||||
var claims = new Collection<ClientClaim>
|
||||
{
|
||||
new ClientClaim(JwtClaimTypes.Subject, user.Id.ToString()),
|
||||
new ClientClaim(JwtClaimTypes.AuthenticationMethod, "Application", "external")
|
||||
new(JwtClaimTypes.Subject, user.Id.ToString()),
|
||||
new(JwtClaimTypes.AuthenticationMethod, "Application", "external"),
|
||||
};
|
||||
var orgs = await _currentContext.OrganizationMembershipAsync(_organizationUserRepository, user.Id);
|
||||
var providers = await _currentContext.ProviderMembershipAsync(_providerUserRepository, user.Id);
|
||||
@ -152,10 +148,10 @@ public class ClientStore : IClientStore
|
||||
foreach (var claim in CoreHelpers.BuildIdentityClaims(user, orgs, providers, isPremium))
|
||||
{
|
||||
var upperValue = claim.Value.ToUpperInvariant();
|
||||
var isBool = upperValue == "TRUE" || upperValue == "FALSE";
|
||||
claims.Add(isBool ?
|
||||
new ClientClaim(claim.Key, claim.Value, ClaimValueTypes.Boolean) :
|
||||
new ClientClaim(claim.Key, claim.Value)
|
||||
var isBool = upperValue is "TRUE" or "FALSE";
|
||||
claims.Add(isBool
|
||||
? new ClientClaim(claim.Key, claim.Value, ClaimValueTypes.Boolean)
|
||||
: new ClientClaim(claim.Key, claim.Value)
|
||||
);
|
||||
}
|
||||
|
||||
@ -164,17 +160,110 @@ public class ClientStore : IClientStore
|
||||
ClientId = clientId,
|
||||
RequireClientSecret = true,
|
||||
ClientSecrets = { new Secret(user.ApiKey.Sha256()) },
|
||||
AllowedScopes = new string[] { "api" },
|
||||
AllowedScopes = new[] { "api" },
|
||||
AllowedGrantTypes = GrantTypes.ClientCredentials,
|
||||
AccessTokenLifetime = 3600 * 1,
|
||||
ClientClaimsPrefix = null,
|
||||
Claims = claims
|
||||
Claims = claims,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<Client> CreateOrganizationClientAsync(string clientId)
|
||||
{
|
||||
var idParts = clientId.Split('.');
|
||||
if (idParts.Length <= 1 || !Guid.TryParse(idParts[1], out var id))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return _staticClientStore.ApiClients.ContainsKey(clientId) ?
|
||||
_staticClientStore.ApiClients[clientId] : null;
|
||||
var org = await _organizationRepository.GetByIdAsync(id);
|
||||
if (org == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var orgApiKey = (await _organizationApiKeyRepository
|
||||
.GetManyByOrganizationIdTypeAsync(org.Id, OrganizationApiKeyType.Default))
|
||||
.First();
|
||||
|
||||
return new Client
|
||||
{
|
||||
ClientId = $"organization.{org.Id}",
|
||||
RequireClientSecret = true,
|
||||
ClientSecrets = { new Secret(orgApiKey.ApiKey.Sha256()) },
|
||||
AllowedScopes = new[] { ApiScopes.ApiOrganization },
|
||||
AllowedGrantTypes = GrantTypes.ClientCredentials,
|
||||
AccessTokenLifetime = 3600 * 1,
|
||||
Enabled = org.Enabled && org.UseApi,
|
||||
Claims = new List<ClientClaim>
|
||||
{
|
||||
new(JwtClaimTypes.Subject, org.Id.ToString()),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private Client CreateInternalClient(string clientId)
|
||||
{
|
||||
var idParts = clientId.Split('.');
|
||||
if (idParts.Length <= 1)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var id = idParts[1];
|
||||
if (string.IsNullOrWhiteSpace(id))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Client
|
||||
{
|
||||
ClientId = $"internal.{id}",
|
||||
RequireClientSecret = true,
|
||||
ClientSecrets = { new Secret(_globalSettings.InternalIdentityKey.Sha256()) },
|
||||
AllowedScopes = new[] { ApiScopes.Internal },
|
||||
AllowedGrantTypes = GrantTypes.ClientCredentials,
|
||||
AccessTokenLifetime = 3600 * 24,
|
||||
Enabled = true,
|
||||
Claims = new List<ClientClaim>
|
||||
{
|
||||
new(JwtClaimTypes.Subject, id),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<Client> CreateInstallationClientAsync(string clientId)
|
||||
{
|
||||
var idParts = clientId.Split('.');
|
||||
if (idParts.Length <= 1 || !Guid.TryParse(idParts[1], out Guid id))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var installation = await _installationRepository.GetByIdAsync(id);
|
||||
if (installation == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Client
|
||||
{
|
||||
ClientId = $"installation.{installation.Id}",
|
||||
RequireClientSecret = true,
|
||||
ClientSecrets = { new Secret(installation.Key.Sha256()) },
|
||||
AllowedScopes = new[]
|
||||
{
|
||||
ApiScopes.ApiPush,
|
||||
ApiScopes.ApiLicensing,
|
||||
ApiScopes.ApiInstallation,
|
||||
},
|
||||
AllowedGrantTypes = GrantTypes.ClientCredentials,
|
||||
AccessTokenLifetime = 3600 * 24,
|
||||
Enabled = installation.Enabled,
|
||||
Claims = new List<ClientClaim>
|
||||
{
|
||||
new(JwtClaimTypes.Subject, installation.Id.ToString()),
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Identity;
|
||||
using Bit.Core.IdentityServer;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
@ -17,7 +18,6 @@ public class CustomTokenRequestValidator : BaseRequestValidator<CustomTokenReque
|
||||
{
|
||||
private UserManager<User> _userManager;
|
||||
private readonly ISsoConfigRepository _ssoConfigRepository;
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
|
||||
public CustomTokenRequestValidator(
|
||||
UserManager<User> userManager,
|
||||
@ -30,21 +30,19 @@ public class CustomTokenRequestValidator : BaseRequestValidator<CustomTokenReque
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IApplicationCacheService applicationCacheService,
|
||||
IMailService mailService,
|
||||
ILogger<ResourceOwnerPasswordValidator> logger,
|
||||
ILogger<CustomTokenRequestValidator> logger,
|
||||
ICurrentContext currentContext,
|
||||
GlobalSettings globalSettings,
|
||||
IPolicyRepository policyRepository,
|
||||
ISsoConfigRepository ssoConfigRepository,
|
||||
IUserRepository userRepository,
|
||||
ICaptchaValidationService captchaValidationService)
|
||||
IUserRepository userRepository)
|
||||
: base(userManager, deviceRepository, deviceService, userService, eventService,
|
||||
organizationDuoWebTokenProvider, organizationRepository, organizationUserRepository,
|
||||
applicationCacheService, mailService, logger, currentContext, globalSettings, policyRepository,
|
||||
userRepository, captchaValidationService)
|
||||
userRepository)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_ssoConfigRepository = ssoConfigRepository;
|
||||
_organizationRepository = organizationRepository;
|
||||
}
|
||||
|
||||
public async Task ValidateAsync(CustomTokenRequestValidationContext context)
|
||||
@ -53,10 +51,18 @@ public class CustomTokenRequestValidator : BaseRequestValidator<CustomTokenReque
|
||||
if (!allowedGrantTypes.Contains(context.Result.ValidatedRequest.GrantType)
|
||||
|| context.Result.ValidatedRequest.ClientId.StartsWith("organization")
|
||||
|| context.Result.ValidatedRequest.ClientId.StartsWith("installation")
|
||||
|| context.Result.ValidatedRequest.ClientId.StartsWith("internal"))
|
||||
|| context.Result.ValidatedRequest.ClientId.StartsWith("internal")
|
||||
|| context.Result.ValidatedRequest.Client.AllowedScopes.Contains(ApiScopes.ApiSecrets))
|
||||
{
|
||||
if (context.Result.ValidatedRequest.Client.Properties.TryGetValue("encryptedPayload", out var payload) &&
|
||||
!string.IsNullOrWhiteSpace(payload))
|
||||
{
|
||||
context.Result.CustomResponse = new Dictionary<string, object> { { "encrypted_payload", payload } };
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await ValidateAsync(context, context.Result.ValidatedRequest,
|
||||
new CustomValidatorRequestContext { KnownDevice = true });
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
using System.Security.Claims;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Identity;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
@ -70,7 +71,7 @@ public class ProfileService : IProfileService
|
||||
|
||||
public async Task IsActiveAsync(IsActiveContext context)
|
||||
{
|
||||
var securityTokenClaim = context.Subject?.Claims.FirstOrDefault(c => c.Type == "sstamp");
|
||||
var securityTokenClaim = context.Subject?.Claims.FirstOrDefault(c => c.Type == Claims.SecurityStamp);
|
||||
var user = await _userService.GetUserByPrincipalAsync(context.Subject);
|
||||
|
||||
if (user != null && securityTokenClaim != null)
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user