1
0
mirror of https://github.com/bitwarden/server synced 2025-12-06 00:03:34 +00: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:
Oscar Hinton
2023-01-13 15:02:53 +01:00
committed by GitHub
parent 09e524c9a2
commit 1f0fc43278
188 changed files with 21346 additions and 329 deletions

View File

@@ -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;

View File

@@ -83,9 +83,9 @@ public class Startup
services.AddDefaultServices(globalSettings);
#if OSS
services.AddOosServices();
services.AddOosServices();
#else
services.AddCommCoreServices();
services.AddCommercialCoreServices();
#endif
// Mvc

View File

@@ -191,62 +191,78 @@
</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="form-check mb-2">
<input type="checkbox" class="form-check-input" asp-for="SelfHost">
<label class="form-check-label" asp-for="SelfHost"></label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" asp-for="Use2fa">
<label class="form-check-label" asp-for="Use2fa"></label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" asp-for="UseApi">
<label class="form-check-label" asp-for="UseApi"></label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" asp-for="UseGroups">
<label class="form-check-label" asp-for="UseGroups"></label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" asp-for="UsePolicies">
<label class="form-check-label" asp-for="UsePolicies"></label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" asp-for="UseSso">
<label class="form-check-label" asp-for="UseSso"></label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" asp-for="UseKeyConnector">
<label class="form-check-label" asp-for="UseKeyConnector"></label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" asp-for="UseScim">
<label class="form-check-label" asp-for="UseScim"></label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" asp-for="UseDirectory">
<label class="form-check-label" asp-for="UseDirectory"></label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" asp-for="UseEvents">
<label class="form-check-label" asp-for="UseEvents"></label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" asp-for="UseResetPassword">
<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 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>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" asp-for="Use2fa">
<label class="form-check-label" asp-for="Use2fa"></label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" asp-for="UseApi">
<label class="form-check-label" asp-for="UseApi"></label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" asp-for="UseGroups">
<label class="form-check-label" asp-for="UseGroups"></label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" asp-for="UsePolicies">
<label class="form-check-label" asp-for="UsePolicies"></label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" asp-for="UseSso">
<label class="form-check-label" asp-for="UseSso"></label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" asp-for="UseKeyConnector">
<label class="form-check-label" asp-for="UseKeyConnector"></label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" asp-for="UseScim">
<label class="form-check-label" asp-for="UseScim"></label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" asp-for="UseDirectory">
<label class="form-check-label" asp-for="UseDirectory"></label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" asp-for="UseEvents">
<label class="form-check-label" asp-for="UseEvents"></label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" asp-for="UseResetPassword">
<label class="form-check-label" asp-for="UseResetPassword"></label>
</div>
<div class="form-check">
<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">

View File

@@ -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, )",

View File

@@ -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>

View 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);
}
}

View 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);
}
}

View 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);
}
}

View File

@@ -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; }

View File

@@ -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; }

View File

@@ -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; }
}

View File

@@ -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,
};
}
}

View File

@@ -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,
};
}
}

View File

@@ -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,
};
}
}

View File

@@ -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,
};
}
}

View File

@@ -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,
};
}
}

View File

@@ -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,
};
}
}

View File

@@ -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,
};
}
}

View File

@@ -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; }
}

View File

@@ -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; }
}

View File

@@ -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; }
}

View File

@@ -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; }
}
}

View File

@@ -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; }
}
}

View File

@@ -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; }
}

View File

@@ -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

View File

@@ -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();
}

View File

@@ -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 }
}
});

View File

@@ -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, )",

View File

@@ -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, )",

View File

@@ -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),

View 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; }
}

View 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);
}
}

View File

@@ -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; }

View 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();
}
}
}

View 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();
}
}
}

View 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();
}
}
}

View File

@@ -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,
}

View 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";
}

View 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"),
};
}
}

View 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; }
}

View File

@@ -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; }

View File

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

View File

@@ -0,0 +1,7 @@
using Bit.Core.Entities;
namespace Bit.Core.Repositories;
public interface IAccessPolicyRepository : IRepository<AccessPolicy, Guid>
{
}

View 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);
}

View 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);
}

View 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);
}

View 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);
}

View File

@@ -0,0 +1,8 @@
using Bit.Core.Entities;
namespace Bit.Core.SecretManagerFeatures.AccessTokens.Interfaces;
public interface ICreateAccessTokenCommand
{
Task<ApiKey> CreateAsync(ApiKey apiKey);
}

View File

@@ -0,0 +1,8 @@
using Bit.Core.Entities;
namespace Bit.Core.SecretManagerFeatures.Projects.Interfaces;
public interface ICreateProjectCommand
{
Task<Project> CreateAsync(Project project);
}

View File

@@ -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);
}

View File

@@ -0,0 +1,8 @@
using Bit.Core.Entities;
namespace Bit.Core.SecretManagerFeatures.Projects.Interfaces;
public interface IUpdateProjectCommand
{
Task<Project> UpdateAsync(Project project);
}

View File

@@ -0,0 +1,8 @@
using Bit.Core.Entities;
namespace Bit.Core.SecretManagerFeatures.Secrets.Interfaces;
public interface ICreateSecretCommand
{
Task<Secret> CreateAsync(Secret secret);
}

View File

@@ -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);
}

View File

@@ -0,0 +1,8 @@
using Bit.Core.Entities;
namespace Bit.Core.SecretManagerFeatures.Secrets.Interfaces;
public interface IUpdateSecretCommand
{
Task<Secret> UpdateAsync(Secret secret);
}

View File

@@ -0,0 +1,8 @@
using Bit.Core.Entities;
namespace Bit.Core.SecretManagerFeatures.ServiceAccounts.Interfaces;
public interface ICreateServiceAccountCommand
{
Task<ServiceAccount> CreateAsync(ServiceAccount serviceAccount);
}

View File

@@ -0,0 +1,8 @@
using Bit.Core.Entities;
namespace Bit.Core.SecretManagerFeatures.ServiceAccounts.Interfaces;
public interface IUpdateServiceAccountCommand
{
Task<ServiceAccount> UpdateAsync(ServiceAccount serviceAccount);
}

View File

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

View File

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

View File

@@ -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;
}

View File

@@ -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);
});
});

View File

@@ -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, )",

View File

@@ -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, )",

View File

@@ -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, )",

View File

@@ -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 }),
};
}
}

View File

@@ -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")
};
}
}

View File

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

View File

@@ -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,133 +51,219 @@ 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)
{
var id = idParts[1];
if (!string.IsNullOrWhiteSpace(id))
{
return new Client
{
ClientId = $"internal.{id}",
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" },
AllowedGrantTypes = GrantTypes.ClientCredentials,
AccessTokenLifetime = 3600 * 1,
Enabled = org.Enabled && org.UseApi,
Claims = new List<ClientClaim>
{
new ClientClaim(JwtClaimTypes.Subject, org.Id.ToString())
}
};
}
}
}
else if (clientId.StartsWith("user."))
{
var idParts = clientId.Split('.');
if (idParts.Length > 1 && Guid.TryParse(idParts[1], out var id))
{
var user = await _userRepository.GetByIdAsync(id);
if (user != null)
{
var claims = new Collection<ClientClaim>()
{
new ClientClaim(JwtClaimTypes.Subject, user.Id.ToString()),
new ClientClaim(JwtClaimTypes.AuthenticationMethod, "Application", "external")
};
var orgs = await _currentContext.OrganizationMembershipAsync(_organizationUserRepository, user.Id);
var providers = await _currentContext.ProviderMembershipAsync(_providerUserRepository, user.Id);
var isPremium = await _licensingService.ValidateUserPremiumAsync(user);
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)
);
}
return new Client
{
ClientId = clientId,
RequireClientSecret = true,
ClientSecrets = { new Secret(user.ApiKey.Sha256()) },
AllowedScopes = new string[] { "api" },
AllowedGrantTypes = GrantTypes.ClientCredentials,
AccessTokenLifetime = 3600 * 1,
ClientClaimsPrefix = null,
Claims = claims
};
}
}
return CreateInternalClient(clientId);
}
return _staticClientStore.ApiClients.ContainsKey(clientId) ?
_staticClientStore.ApiClients[clientId] : null;
if (clientId.StartsWith("organization."))
{
return await CreateOrganizationClientAsync(clientId);
}
if (clientId.StartsWith("user."))
{
return await CreateUserClientAsync(clientId);
}
if (_staticClientStore.ApiClients.ContainsKey(clientId))
{
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(apiKey.ClientSecret.Sha256()) },
AllowedScopes = apiKey.GetScopes(),
AllowedGrantTypes = GrantTypes.ClientCredentials,
AccessTokenLifetime = 3600 * 1,
ClientClaimsPrefix = null,
Properties = new Dictionary<string, string> {
{"encryptedPayload", apiKey.EncryptedPayload},
},
Claims = new List<ClientClaim>
{
new(JwtClaimTypes.Subject, apiKey.ServiceAccountId.ToString()),
},
};
switch (apiKey)
{
case ServiceAccountApiKeyDetails key:
client.Claims.Add(new ClientClaim(Claims.Organization, key.ServiceAccountOrganizationId.ToString()));
break;
}
return client;
}
private async Task<Client> CreateUserClientAsync(string clientId)
{
var idParts = clientId.Split('.');
if (idParts.Length <= 1 || !Guid.TryParse(idParts[1], out var id))
{
return null;
}
var user = await _userRepository.GetByIdAsync(id);
if (user == null)
{
return null;
}
var claims = new Collection<ClientClaim>
{
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);
var isPremium = await _licensingService.ValidateUserPremiumAsync(user);
foreach (var claim in CoreHelpers.BuildIdentityClaims(user, orgs, providers, isPremium))
{
var upperValue = claim.Value.ToUpperInvariant();
var isBool = upperValue is "TRUE" or "FALSE";
claims.Add(isBool
? new ClientClaim(claim.Key, claim.Value, ClaimValueTypes.Boolean)
: new ClientClaim(claim.Key, claim.Value)
);
}
return new Client
{
ClientId = clientId,
RequireClientSecret = true,
ClientSecrets = { new Secret(user.ApiKey.Sha256()) },
AllowedScopes = new[] { "api" },
AllowedGrantTypes = GrantTypes.ClientCredentials,
AccessTokenLifetime = 3600 * 1,
ClientClaimsPrefix = null,
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;
}
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()),
},
};
}
}

View File

@@ -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 });
}

View File

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

View File

@@ -41,7 +41,7 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator<ResourceOwner
: base(userManager, deviceRepository, deviceService, userService, eventService,
organizationDuoWebTokenProvider, organizationRepository, organizationUserRepository,
applicationCacheService, mailService, logger, currentContext, globalSettings, policyRepository,
userRepository, captchaValidationService)
userRepository)
{
_userManager = userManager;
_userService = userService;

View File

@@ -1,4 +1,5 @@
using Bit.Core.Settings;
using Bit.Core.IdentityServer;
using Bit.Core.Settings;
using Bit.Identity.IdentityServer;
using Bit.SharedWeb.Utilities;
using IdentityServer4.ResponseHandling;

View File

@@ -548,6 +548,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",
@@ -2776,6 +2785,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, )",

View File

@@ -8,33 +8,34 @@ public static class DapperServiceCollectionExtensions
{
public static void AddDapperRepositories(this IServiceCollection services, bool selfHosted)
{
services.AddSingleton<IApiKeyRepository, ApiKeyRepository>();
services.AddSingleton<IAuthRequestRepository, AuthRequestRepository>();
services.AddSingleton<ICipherRepository, CipherRepository>();
services.AddSingleton<ICollectionCipherRepository, CollectionCipherRepository>();
services.AddSingleton<ICollectionRepository, CollectionRepository>();
services.AddSingleton<IDeviceRepository, DeviceRepository>();
services.AddSingleton<IEmergencyAccessRepository, EmergencyAccessRepository>();
services.AddSingleton<IEmergencyAccessRepository, EmergencyAccessRepository>();
services.AddSingleton<IFolderRepository, FolderRepository>();
services.AddSingleton<IGrantRepository, GrantRepository>();
services.AddSingleton<IGroupRepository, GroupRepository>();
services.AddSingleton<IInstallationRepository, InstallationRepository>();
services.AddSingleton<IMaintenanceRepository, MaintenanceRepository>();
services.AddSingleton<IOrganizationApiKeyRepository, OrganizationApiKeyRepository>();
services.AddSingleton<IOrganizationConnectionRepository, OrganizationConnectionRepository>();
services.AddSingleton<IOrganizationRepository, OrganizationRepository>();
services.AddSingleton<IOrganizationSponsorshipRepository, OrganizationSponsorshipRepository>();
services.AddSingleton<IOrganizationUserRepository, OrganizationUserRepository>();
services.AddSingleton<IPolicyRepository, PolicyRepository>();
services.AddSingleton<IProviderOrganizationRepository, ProviderOrganizationRepository>();
services.AddSingleton<IProviderRepository, ProviderRepository>();
services.AddSingleton<IProviderUserRepository, ProviderUserRepository>();
services.AddSingleton<ISendRepository, SendRepository>();
services.AddSingleton<ISsoConfigRepository, SsoConfigRepository>();
services.AddSingleton<ISsoUserRepository, SsoUserRepository>();
services.AddSingleton<ITaxRateRepository, TaxRateRepository>();
services.AddSingleton<IEmergencyAccessRepository, EmergencyAccessRepository>();
services.AddSingleton<IProviderRepository, ProviderRepository>();
services.AddSingleton<IProviderUserRepository, ProviderUserRepository>();
services.AddSingleton<IProviderOrganizationRepository, ProviderOrganizationRepository>();
services.AddSingleton<ITransactionRepository, TransactionRepository>();
services.AddSingleton<IUserRepository, UserRepository>();
services.AddSingleton<IOrganizationApiKeyRepository, OrganizationApiKeyRepository>();
services.AddSingleton<IOrganizationConnectionRepository, OrganizationConnectionRepository>();
services.AddSingleton<IAuthRequestRepository, AuthRequestRepository>();
if (selfHosted)
{

View File

@@ -0,0 +1,44 @@
using System.Data;
using System.Data.SqlClient;
using Bit.Core.Entities;
using Bit.Core.Models.Data;
using Bit.Core.Repositories;
using Bit.Core.Settings;
using Dapper;
namespace Bit.Infrastructure.Dapper.Repositories;
public class ApiKeyRepository : Repository<ApiKey, Guid>, IApiKeyRepository
{
public ApiKeyRepository(GlobalSettings globalSettings)
: this(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString)
{ }
public ApiKeyRepository(string connectionString, string readOnlyConnectionString)
: base(connectionString, readOnlyConnectionString)
{ }
public async Task<ApiKeyDetails> GetDetailsByIdAsync(Guid id)
{
using var connection = new SqlConnection(ConnectionString);
// When adding different key details, we should change the QueryAsync type to match the database data,
// but cast it to the appropriate data model.
var results = await connection.QueryAsync<ServiceAccountApiKeyDetails>(
$"[{Schema}].[ApiKeyDetails_ReadById]",
new { Id = id },
commandType: CommandType.StoredProcedure);
return results.SingleOrDefault();
}
public async Task<ICollection<ApiKey>> GetManyByServiceAccountIdAsync(Guid serviceAccountId)
{
using var connection = new SqlConnection(ConnectionString);
var results = await connection.QueryAsync<ApiKey>(
$"[{Schema}].[ApiKey_ReadByServiceAccountId]",
new { ServiceAccountId = serviceAccountId },
commandType: CommandType.StoredProcedure);
return results.ToList();
}
}

View File

@@ -0,0 +1,99 @@
using Bit.Infrastructure.EntityFramework.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace Bit.Infrastructure.EntityFramework.Configurations;
public class AccessPolicyEntityTypeConfiguration : IEntityTypeConfiguration<AccessPolicy>
{
public void Configure(EntityTypeBuilder<AccessPolicy> builder)
{
builder
.HasDiscriminator<string>("Discriminator")
.HasValue<UserProjectAccessPolicy>("user_project")
.HasValue<UserServiceAccountAccessPolicy>("user_service_account")
.HasValue<GroupProjectAccessPolicy>("group_project")
.HasValue<GroupServiceAccountAccessPolicy>("group_service_account")
.HasValue<ServiceAccountProjectAccessPolicy>("service_account_project");
builder
.Property(s => s.Id)
.ValueGeneratedNever();
builder
.HasKey(s => s.Id)
.IsClustered();
builder.ToTable(nameof(AccessPolicy));
}
}
public class UserProjectAccessPolicyEntityTypeConfiguration : IEntityTypeConfiguration<UserProjectAccessPolicy>
{
public void Configure(EntityTypeBuilder<UserProjectAccessPolicy> builder)
{
builder
.Property(e => e.OrganizationUserId)
.HasColumnName(nameof(UserProjectAccessPolicy.OrganizationUserId));
builder
.Property(e => e.GrantedProjectId)
.HasColumnName(nameof(UserProjectAccessPolicy.GrantedProjectId));
}
}
public class UserServiceAccountAccessPolicyEntityTypeConfiguration : IEntityTypeConfiguration<UserServiceAccountAccessPolicy>
{
public void Configure(EntityTypeBuilder<UserServiceAccountAccessPolicy> builder)
{
builder
.Property(e => e.OrganizationUserId)
.HasColumnName(nameof(UserServiceAccountAccessPolicy.OrganizationUserId));
builder
.Property(e => e.GrantedServiceAccountId)
.HasColumnName(nameof(UserServiceAccountAccessPolicy.GrantedServiceAccountId));
}
}
public class GroupProjectAccessPolicyEntityTypeConfiguration : IEntityTypeConfiguration<GroupProjectAccessPolicy>
{
public void Configure(EntityTypeBuilder<GroupProjectAccessPolicy> builder)
{
builder
.Property(e => e.GroupId)
.HasColumnName(nameof(GroupProjectAccessPolicy.GroupId));
builder
.Property(e => e.GrantedProjectId)
.HasColumnName(nameof(GroupProjectAccessPolicy.GrantedProjectId));
}
}
public class GroupServiceAccountAccessPolicyEntityTypeConfiguration : IEntityTypeConfiguration<GroupServiceAccountAccessPolicy>
{
public void Configure(EntityTypeBuilder<GroupServiceAccountAccessPolicy> builder)
{
builder
.Property(e => e.GroupId)
.HasColumnName(nameof(GroupServiceAccountAccessPolicy.GroupId));
builder
.Property(e => e.GrantedServiceAccountId)
.HasColumnName(nameof(GroupServiceAccountAccessPolicy.GrantedServiceAccountId));
}
}
public class ServiceAccountProjectAccessPolicyEntityTypeConfiguration : IEntityTypeConfiguration<ServiceAccountProjectAccessPolicy>
{
public void Configure(EntityTypeBuilder<ServiceAccountProjectAccessPolicy> builder)
{
builder
.Property(e => e.ServiceAccountId)
.HasColumnName(nameof(ServiceAccountProjectAccessPolicy.ServiceAccountId));
builder
.Property(e => e.GrantedProjectId)
.HasColumnName(nameof(ServiceAccountProjectAccessPolicy.GrantedProjectId));
}
}

View File

@@ -0,0 +1,25 @@
using Bit.Infrastructure.EntityFramework.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace Bit.Infrastructure.EntityFramework.Configurations;
public class ApiKeyEntityTypeConfiguration : IEntityTypeConfiguration<ApiKey>
{
public void Configure(EntityTypeBuilder<ApiKey> builder)
{
builder
.Property(s => s.Id)
.ValueGeneratedNever();
builder
.HasKey(s => s.Id)
.IsClustered();
builder
.HasIndex(s => s.ServiceAccountId)
.IsClustered(false);
builder.ToTable(nameof(ApiKey));
}
}

View File

@@ -0,0 +1,29 @@
using Bit.Infrastructure.EntityFramework.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace Bit.Infrastructure.EntityFramework.Configurations;
public class ProjectEntityTypeConfiguration : IEntityTypeConfiguration<Project>
{
public void Configure(EntityTypeBuilder<Project> builder)
{
builder
.Property(s => s.Id)
.ValueGeneratedNever();
builder
.HasKey(s => s.Id)
.IsClustered();
builder
.HasIndex(s => s.DeletedDate)
.IsClustered(false);
builder
.HasIndex(s => s.OrganizationId)
.IsClustered(false);
builder.ToTable(nameof(Project));
}
}

View File

@@ -0,0 +1,29 @@
using Bit.Infrastructure.EntityFramework.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace Bit.Infrastructure.EntityFramework.Configurations;
public class SecretEntityTypeConfiguration : IEntityTypeConfiguration<Secret>
{
public void Configure(EntityTypeBuilder<Secret> builder)
{
builder
.Property(s => s.Id)
.ValueGeneratedNever();
builder
.HasKey(s => s.Id)
.IsClustered();
builder
.HasIndex(s => s.DeletedDate)
.IsClustered(false);
builder
.HasIndex(s => s.OrganizationId)
.IsClustered(false);
builder.ToTable(nameof(Secret));
}
}

View File

@@ -0,0 +1,23 @@
using Bit.Infrastructure.EntityFramework.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
public class ServiceAccountEntityTypeConfiguration : IEntityTypeConfiguration<ServiceAccount>
{
public void Configure(EntityTypeBuilder<ServiceAccount> builder)
{
builder
.Property(s => s.Id)
.ValueGeneratedNever();
builder
.HasKey(s => s.Id)
.IsClustered();
builder
.HasIndex(s => s.OrganizationId)
.IsClustered(false);
builder.ToTable(nameof(ServiceAccount));
}
}

View File

@@ -0,0 +1,19 @@
using Microsoft.EntityFrameworkCore;
namespace Bit.Infrastructure.EntityFramework;
public static class EfExtensions
{
public static T AttachToOrGet<T>(this DbContext context, Func<T, bool> predicate, Func<T> factory)
where T : class, new()
{
var match = context.Set<T>().Local.FirstOrDefault(predicate);
if (match == null)
{
match = factory();
context.Attach(match);
}
return match;
}
}

View File

@@ -5,18 +5,18 @@ using LinqToDB.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
namespace Bit.Infrastructure.EntityFramework;
public static class EntityFrameworkServiceCollectionExtensions
{
public static void AddEFRepositories(this IServiceCollection services, bool selfHosted, string connectionString,
SupportedDatabaseProviders provider)
public static void SetupEntityFramework(this IServiceCollection services, string connectionString, SupportedDatabaseProviders provider)
{
if (string.IsNullOrWhiteSpace(connectionString))
{
throw new Exception($"Database provider type {provider} was selected but no connection string was found.");
}
// TODO: We should move away from using LINQ syntax for EF (TDL-48).
LinqToDBForEFTools.Initialize();
services.AddAutoMapper(typeof(UserRepository));
services.AddDbContext<DatabaseContext>(options =>
{
@@ -35,7 +35,17 @@ public static class EntityFrameworkServiceCollectionExtensions
{
options.UseSqlite(connectionString, b => b.MigrationsAssembly("SqliteMigrations"));
}
else if (provider == SupportedDatabaseProviders.SqlServer)
{
options.UseSqlServer(connectionString);
}
});
}
public static void AddPasswordManagerEFRepositories(this IServiceCollection services, bool selfHosted)
{
services.AddSingleton<IApiKeyRepository, ApiKeyRepository>();
services.AddSingleton<IAuthRequestRepository, AuthRequestRepository>();
services.AddSingleton<ICipherRepository, CipherRepository>();
services.AddSingleton<ICollectionCipherRepository, CollectionCipherRepository>();
services.AddSingleton<ICollectionRepository, CollectionRepository>();
@@ -46,22 +56,21 @@ public static class EntityFrameworkServiceCollectionExtensions
services.AddSingleton<IGroupRepository, GroupRepository>();
services.AddSingleton<IInstallationRepository, InstallationRepository>();
services.AddSingleton<IMaintenanceRepository, MaintenanceRepository>();
services.AddSingleton<IOrganizationRepository, OrganizationRepository>();
services.AddSingleton<IOrganizationApiKeyRepository, OrganizationApiKeyRepository>();
services.AddSingleton<IOrganizationConnectionRepository, OrganizationConnectionRepository>();
services.AddSingleton<IOrganizationRepository, OrganizationRepository>();
services.AddSingleton<IOrganizationSponsorshipRepository, OrganizationSponsorshipRepository>();
services.AddSingleton<IOrganizationUserRepository, OrganizationUserRepository>();
services.AddSingleton<IPolicyRepository, PolicyRepository>();
services.AddSingleton<IProviderOrganizationRepository, ProviderOrganizationRepository>();
services.AddSingleton<IProviderRepository, ProviderRepository>();
services.AddSingleton<IProviderUserRepository, ProviderUserRepository>();
services.AddSingleton<ISendRepository, SendRepository>();
services.AddSingleton<ISsoConfigRepository, SsoConfigRepository>();
services.AddSingleton<ISsoUserRepository, SsoUserRepository>();
services.AddSingleton<ITaxRateRepository, TaxRateRepository>();
services.AddSingleton<ITransactionRepository, TransactionRepository>();
services.AddSingleton<IUserRepository, UserRepository>();
services.AddSingleton<IProviderRepository, ProviderRepository>();
services.AddSingleton<IProviderUserRepository, ProviderUserRepository>();
services.AddSingleton<IProviderOrganizationRepository, ProviderOrganizationRepository>();
services.AddSingleton<IAuthRequestRepository, AuthRequestRepository>();
if (selfHosted)
{

View File

@@ -2,15 +2,15 @@
<ItemGroup>
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="11.0.0" />
<PackageReference Include="linq2db.EntityFrameworkCore" Version="6.11.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.12" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="6.0.12" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.12" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="6.0.8" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="6.0.2" />
<PackageReference Include="linq2db.EntityFrameworkCore" Version="6.11.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Core\Core.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,60 @@
using AutoMapper;
namespace Bit.Infrastructure.EntityFramework.Models;
public class BaseAccessPolicy : Core.Entities.BaseAccessPolicy
{
public string Discriminator { get; set; }
}
public class AccessPolicyMapperProfile : Profile
{
public AccessPolicyMapperProfile()
{
CreateMap<Core.Entities.AccessPolicy, AccessPolicy>().ReverseMap();
}
}
public class AccessPolicy : BaseAccessPolicy
{
}
public class UserProjectAccessPolicy : AccessPolicy
{
public Guid? OrganizationUserId { get; set; }
public virtual OrganizationUser OrganizationUser { get; set; }
public Guid? GrantedProjectId { get; set; }
public virtual Project GrantedProject { get; set; }
}
public class UserServiceAccountAccessPolicy : AccessPolicy
{
public Guid? OrganizationUserId { get; set; }
public virtual OrganizationUser OrganizationUser { get; set; }
public Guid? GrantedServiceAccountId { get; set; }
public virtual ServiceAccount GrantedServiceAccount { get; set; }
}
public class GroupProjectAccessPolicy : AccessPolicy
{
public Guid? GroupId { get; set; }
public virtual Group Group { get; set; }
public Guid? GrantedProjectId { get; set; }
public virtual Project GrantedProject { get; set; }
}
public class GroupServiceAccountAccessPolicy : AccessPolicy
{
public Guid? GroupId { get; set; }
public virtual Group Group { get; set; }
public Guid? GrantedServiceAccountId { get; set; }
public virtual ServiceAccount GrantedServiceAccount { get; set; }
}
public class ServiceAccountProjectAccessPolicy : AccessPolicy
{
public Guid? ServiceAccountId { get; set; }
public virtual ServiceAccount ServiceAccount { get; set; }
public Guid? GrantedProjectId { get; set; }
public virtual Project GrantedProject { get; set; }
}

View File

@@ -0,0 +1,16 @@
using AutoMapper;
namespace Bit.Infrastructure.EntityFramework.Models;
public class ApiKey : Core.Entities.ApiKey
{
public virtual ServiceAccount ServiceAccount { get; set; }
}
public class ApiKeyMapperProfile : Profile
{
public ApiKeyMapperProfile()
{
CreateMap<Core.Entities.ApiKey, ApiKey>().ReverseMap();
}
}

View File

@@ -0,0 +1,22 @@
using AutoMapper;
namespace Bit.Infrastructure.EntityFramework.Models;
public class Project : Core.Entities.Project
{
public virtual new ICollection<Secret> Secrets { get; set; }
public virtual Organization Organization { get; set; }
public virtual ICollection<GroupProjectAccessPolicy> GroupAccessPolicies { get; set; }
public virtual ICollection<UserProjectAccessPolicy> UserAccessPolicies { get; set; }
public virtual ICollection<ServiceAccountProjectAccessPolicy> ServiceAccountAccessPolicies { get; set; }
}
public class ProjectMapperProfile : Profile
{
public ProjectMapperProfile()
{
CreateMap<Core.Entities.Project, Project>()
.PreserveReferences()
.ReverseMap();
}
}

View File

@@ -0,0 +1,19 @@
using AutoMapper;
namespace Bit.Infrastructure.EntityFramework.Models;
public class Secret : Core.Entities.Secret
{
public virtual new ICollection<Project> Projects { get; set; }
public virtual Organization Organization { get; set; }
}
public class SecretMapperProfile : Profile
{
public SecretMapperProfile()
{
CreateMap<Core.Entities.Secret, Secret>()
.PreserveReferences()
.ReverseMap();
}
}

View File

@@ -0,0 +1,16 @@
using AutoMapper;
namespace Bit.Infrastructure.EntityFramework.Models;
public class ServiceAccount : Core.Entities.ServiceAccount
{
public virtual Organization Organization { get; set; }
}
public class ServiceAccountMapperProfile : Profile
{
public ServiceAccountMapperProfile()
{
CreateMap<Core.Entities.ServiceAccount, ServiceAccount>().ReverseMap();
}
}

View File

@@ -0,0 +1,27 @@
using AutoMapper;
using Bit.Core.Repositories;
using Bit.Infrastructure.EntityFramework.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using CoreAccessPolicy = Bit.Core.Entities.AccessPolicy;
namespace Bit.Infrastructure.EntityFramework.Repositories;
public class AccessPolicyRepository : IAccessPolicyRepository
{
public AccessPolicyRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper)
{
}
protected Func<DatabaseContext, DbSet<AccessPolicy>> GetDbSet { get; private set; }
public Task<CoreAccessPolicy> GetByIdAsync(Guid id) => throw new NotImplementedException();
public Task<CoreAccessPolicy> CreateAsync(CoreAccessPolicy obj) => throw new NotImplementedException();
public Task ReplaceAsync(CoreAccessPolicy obj) => throw new NotImplementedException();
public Task UpsertAsync(CoreAccessPolicy obj) => throw new NotImplementedException();
public Task DeleteAsync(CoreAccessPolicy obj) => throw new NotImplementedException();
}

View File

@@ -0,0 +1,38 @@
using AutoMapper;
using Bit.Core.Models.Data;
using Bit.Core.Repositories;
using Bit.Infrastructure.EntityFramework.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
namespace Bit.Infrastructure.EntityFramework.Repositories;
public class ApiKeyRepository : Repository<Core.Entities.ApiKey, ApiKey, Guid>, IApiKeyRepository
{
public ApiKeyRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper)
: base(serviceScopeFactory, mapper, (DatabaseContext context) => context.ApiKeys)
{
}
public async Task<ApiKeyDetails> GetDetailsByIdAsync(Guid id)
{
using var scope = ServiceScopeFactory.CreateScope();
var dbContext = GetDatabaseContext(scope);
var entity = await GetDbSet(dbContext)
.Where(apiKey => apiKey.Id == id)
.Include(apiKey => apiKey.ServiceAccount)
.Select(apiKey => new ServiceAccountApiKeyDetails(apiKey, apiKey.ServiceAccount.OrganizationId))
.FirstOrDefaultAsync();
return Mapper.Map<ServiceAccountApiKeyDetails>(entity);
}
public async Task<ICollection<Core.Entities.ApiKey>> GetManyByServiceAccountIdAsync(Guid id)
{
using var scope = ServiceScopeFactory.CreateScope();
var dbContext = GetDatabaseContext(scope);
var apiKeys = await GetDbSet(dbContext).Where(e => e.ServiceAccountId == id).ToListAsync();
return Mapper.Map<List<Core.Entities.ApiKey>>(apiKeys);
}
}

View File

@@ -12,6 +12,8 @@ public class DatabaseContext : DbContext
: base(options)
{ }
public DbSet<AccessPolicy> AccessPolicies { get; set; }
public DbSet<ApiKey> ApiKeys { get; set; }
public DbSet<Cipher> Ciphers { get; set; }
public DbSet<Collection> Collections { get; set; }
public DbSet<CollectionCipher> CollectionCiphers { get; set; }
@@ -32,6 +34,9 @@ public class DatabaseContext : DbContext
public DbSet<OrganizationUser> OrganizationUsers { get; set; }
public DbSet<Policy> Policies { get; set; }
public DbSet<Provider> Providers { get; set; }
public DbSet<Secret> Secret { get; set; }
public DbSet<ServiceAccount> ServiceAccount { get; set; }
public DbSet<Project> Project { get; set; }
public DbSet<ProviderUser> ProviderUsers { get; set; }
public DbSet<ProviderOrganization> ProviderOrganizations { get; set; }
public DbSet<Send> Sends { get; set; }
@@ -44,6 +49,13 @@ public class DatabaseContext : DbContext
protected override void OnModelCreating(ModelBuilder builder)
{
// Scans and loads all configurations implementing the `IEntityTypeConfiguration` from the
// `Infrastructure.EntityFramework` Module. Note to get the assembly we can use a random class
// from this module.
builder.ApplyConfigurationsFromAssembly(typeof(DatabaseContext).Assembly);
// Going forward use `IEntityTypeConfiguration` in the Configurations folder for managing
// Entity Framework code first database configurations.
var eCipher = builder.Entity<Cipher>();
var eCollection = builder.Entity<Collection>();
var eCollectionCipher = builder.Entity<CollectionCipher>();
@@ -101,7 +113,6 @@ public class DatabaseContext : DbContext
eGrant.HasKey(x => x.Key);
eGroupUser.HasKey(gu => new { gu.GroupId, gu.OrganizationUserId });
if (Database.IsNpgsql())
{
// the postgres provider doesn't currently support database level non-deterministic collations.

View File

@@ -42,6 +42,16 @@
"SQLitePCLRaw.bundle_e_sqlite3": "2.1.2"
}
},
"Microsoft.EntityFrameworkCore.SqlServer": {
"type": "Direct",
"requested": "[6.0.12, )",
"resolved": "6.0.12",
"contentHash": "bdKnSz1w+WZz9QYWhs3wwGuMn4YssjdR+HOBpzChQ6C3+dblq4Pammm5fzugcPOhTgCiWftOT2jPOT5hEy4bYg==",
"dependencies": {
"Microsoft.Data.SqlClient": "2.1.4",
"Microsoft.EntityFrameworkCore.Relational": "6.0.12"
}
},
"Npgsql.EntityFrameworkCore.PostgreSQL": {
"type": "Direct",
"requested": "[6.0.8, )",

View File

@@ -1,4 +1,5 @@
using System.Globalization;
using Bit.Core.IdentityServer;
using Bit.Core.Settings;
using Bit.Core.Utilities;
using Bit.SharedWeb.Utilities;
@@ -35,12 +36,12 @@ public class Startup
{
policy.RequireAuthenticatedUser();
policy.RequireClaim(JwtClaimTypes.AuthenticationMethod, "Application", "external");
policy.RequireClaim(JwtClaimTypes.Scope, "api");
policy.RequireClaim(JwtClaimTypes.Scope, ApiScopes.Api);
});
config.AddPolicy("Internal", policy =>
{
policy.RequireAuthenticatedUser();
policy.RequireClaim(JwtClaimTypes.Scope, "internal");
policy.RequireClaim(JwtClaimTypes.Scope, ApiScopes.Internal);
});
});

View File

@@ -597,6 +597,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",
@@ -2804,6 +2813,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, )",

View File

@@ -17,7 +17,6 @@ using Bit.Core.Settings;
using Bit.Core.Tokens;
using Bit.Core.Utilities;
using Bit.Infrastructure.Dapper;
using Bit.Infrastructure.EntityFramework;
using IdentityModel;
using IdentityServer4.AccessTokenValidation;
using IdentityServer4.Configuration;
@@ -52,6 +51,7 @@ public static class ServiceCollectionExtensions
var selectedDatabaseProvider = globalSettings.DatabaseProvider;
var provider = SupportedDatabaseProviders.SqlServer;
var connectionString = string.Empty;
if (!string.IsNullOrWhiteSpace(selectedDatabaseProvider))
{
switch (selectedDatabaseProvider.ToLowerInvariant())
@@ -70,16 +70,24 @@ public static class ServiceCollectionExtensions
provider = SupportedDatabaseProviders.Sqlite;
connectionString = globalSettings.Sqlite.ConnectionString;
break;
case "sqlserver":
connectionString = globalSettings.SqlServer.ConnectionString;
break;
default:
break;
}
}
var useEf = (provider != SupportedDatabaseProviders.SqlServer);
if (useEf)
else
{
services.AddEFRepositories(globalSettings.SelfHosted, connectionString, provider);
// Default to attempting to use SqlServer connection string if globalSettings.DatabaseProvider has no value.
connectionString = globalSettings.SqlServer.ConnectionString;
}
services.SetupEntityFramework(connectionString, provider);
if (provider != SupportedDatabaseProviders.SqlServer)
{
services.AddPasswordManagerEFRepositories(globalSettings.SelfHosted);
}
else
{
@@ -337,9 +345,9 @@ public static class ServiceCollectionExtensions
};
options.ClaimsIdentity = new ClaimsIdentityOptions
{
SecurityStampClaimType = "sstamp",
SecurityStampClaimType = Claims.SecurityStamp,
UserNameClaimType = JwtClaimTypes.Email,
UserIdClaimType = JwtClaimTypes.Subject
UserIdClaimType = JwtClaimTypes.Subject,
};
options.Tokens.ChangeEmailTokenProvider = TokenOptions.DefaultEmailProvider;
});
@@ -623,7 +631,7 @@ public static class ServiceCollectionExtensions
services.AddSingleton<IConnectionMultiplexer>(
_ => ConnectionMultiplexer.Connect(globalSettings.Redis.ConnectionString));
// Explicitly register IDistributedCache to re-use existing IConnectionMultiplexer
// Explicitly register IDistributedCache to re-use existing IConnectionMultiplexer
// to reduce the number of redundant connections to the Redis instance
services.AddSingleton<IDistributedCache>(s =>
{

View File

@@ -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, )",

View File

@@ -73,6 +73,9 @@
<Build Include="dbo\Functions\PolicyApplicableToUser.sql" />
<Build Include="dbo\Functions\UserCipherDetails.sql" />
<Build Include="dbo\Functions\UserCollectionDetails.sql" />
<Build Include="dbo\Stored Procedures\ApiKey\ApiKeyDetails_ReadById.sql" />
<Build Include="dbo\Stored Procedures\ApiKey\ApiKey_Create.sql" />
<Build Include="dbo\Stored Procedures\ApiKey\ApiKey_ReadByServiceAccountId.sql" />
<Build Include="dbo\Stored Procedures\AuthRequest_Create.sql" />
<Build Include="dbo\Stored Procedures\AuthRequest_DeleteById.sql" />
<Build Include="dbo\Stored Procedures\AuthRequest_DeleteIfExpired.sql" />
@@ -350,6 +353,8 @@
<Build Include="dbo\Stored Procedures\User_UpdateKeys.sql" />
<Build Include="dbo\Stored Procedures\User_UpdateRenewalReminderDate.sql" />
<Build Include="dbo\Stored Procedures\User_UpdateStorage.sql" />
<Build Include="dbo\Tables\AccessPolicy.sql" />
<Build Include="dbo\Tables\ApiKey.sql" />
<Build Include="dbo\Tables\AuthRequest.sql" />
<Build Include="dbo\Tables\Cipher.sql" />
<Build Include="dbo\Tables\Collection.sql" />
@@ -370,10 +375,14 @@
<Build Include="dbo\Tables\OrganizationSponsorship.sql" />
<Build Include="dbo\Tables\OrganizationUser.sql" />
<Build Include="dbo\Tables\Policy.sql" />
<Build Include="dbo\Tables\Project.sql" />
<Build Include="dbo\Tables\ProjectSecret.sql" />
<Build Include="dbo\Tables\Provider.sql" />
<Build Include="dbo\Tables\ProviderOrganization.sql" />
<Build Include="dbo\Tables\ProviderUser.sql" />
<Build Include="dbo\Tables\Secret.sql" />
<Build Include="dbo\Tables\Send.sql" />
<Build Include="dbo\Tables\ServiceAccount.sql" />
<Build Include="dbo\Tables\SsoConfig.sql" />
<Build Include="dbo\Tables\SsoUser.sql" />
<Build Include="dbo\Tables\TaxRate.sql" />
@@ -385,6 +394,8 @@
<Build Include="dbo\User Defined Types\OrganizationUserType.sql" />
<Build Include="dbo\User Defined Types\SelectionReadOnlyArray.sql" />
<Build Include="dbo\User Defined Types\TwoGuidIdArray.sql" />
<Build Include="dbo\Views\ApiKeyDetailsView.sql" />
<Build Include="dbo\Views\ApiKeyView.sql" />
<Build Include="dbo\Views\AuthRequestView.sql" />
<Build Include="dbo\Views\CipherView.sql" />
<Build Include="dbo\Views\CollectionView.sql" />

View File

@@ -0,0 +1,13 @@
CREATE PROCEDURE [dbo].[ApiKeyDetails_ReadById]
@Id UNIQUEIDENTIFIER
AS
BEGIN
SET NOCOUNT ON
SELECT
*
FROM
[dbo].[ApiKeyDetailsView]
WHERE
[Id] = @Id
END

View File

@@ -0,0 +1,42 @@
CREATE PROCEDURE [dbo].[ApiKey_Create]
@Id UNIQUEIDENTIFIER OUTPUT,
@ServiceAccountId UNIQUEIDENTIFIER,
@Name VARCHAR(200),
@ClientSecret VARCHAR(30),
@Scope NVARCHAR(4000),
@EncryptedPayload NVARCHAR(4000),
@Key VARCHAR(MAX),
@ExpireAt DATETIME2(7),
@CreationDate DATETIME2(7),
@RevisionDate DATETIME2(7)
AS
BEGIN
SET NOCOUNT ON
INSERT INTO [dbo].[ApiKey]
(
[Id],
[ServiceAccountId],
[Name],
[ClientSecret],
[Scope],
[EncryptedPayload],
[Key],
[ExpireAt],
[CreationDate],
[RevisionDate]
)
VALUES
(
@Id,
@ServiceAccountId,
@Name,
@ClientSecret,
@Scope,
@EncryptedPayload,
@Key,
@ExpireAt,
@CreationDate,
@RevisionDate
)
END

View File

@@ -0,0 +1,13 @@
CREATE PROCEDURE [dbo].[ApiKey_ReadByServiceAccountId]
@ServiceAccountId UNIQUEIDENTIFIER
AS
BEGIN
SET NOCOUNT ON
SELECT
*
FROM
[dbo].[ApiKeyView]
WHERE
[ServiceAccountId] = @ServiceAccountId
END

View File

@@ -3,9 +3,9 @@
AS
BEGIN
SET NOCOUNT ON
EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationUserId] @Id
DECLARE @OrganizationId UNIQUEIDENTIFIER
DECLARE @UserId UNIQUEIDENTIFIER
@@ -34,6 +34,12 @@ BEGIN
WHERE
[OrganizationUserId] = @Id
DELETE
FROM
[dbo].[AccessPolicy]
WHERE
[OrganizationUserId] = @Id
EXEC [dbo].[OrganizationSponsorship_OrganizationUserDeleted] @Id
DELETE

Some files were not shown because too many files have changed in this diff Show More