mirror of
https://github.com/bitwarden/server.git
synced 2024-11-27 13:05:23 +01:00
[EC-261] SCIM (#2105)
* scim project stub * some scim models and v2 controllers * implement some v2 scim endpoints * fix spacing * api key auth * EC-261 - SCIM Org API Key and connection type config * EC-261 - Fix lint errors/formatting * updates for okta implementation testing * fix var ref * updates from testing with Okta * implement scim context via provider parsing * support single and list of ids for add/remove groups * log ops not handled * touch up scim context * group list filtering * EC-261 - Additional SCIM provider types * EC-265 - UseScim flag and license update * EC-265 - SCIM provider type of default (0) * EC-265 - Add Scim URL and update connection validation * EC-265 - Model validation and cleanup for SCIM keys * implement scim org connection * EC-265 - Ensure ServiceUrl is not persisted to DB * EC-265 - Exclude provider type from DB if not configured * EC-261 - EF Migrations for SCIM * add docker builds for scim * EC-261 - Fix failing permissions tests * EC-261 - Fix unit tests and pgsql migrations * Formatting fixes from linter * EC-265 - Remove service URL from scim config * EC-265 - Fix unit tests, removed wayward validation * EC-265 - Require self-hosted for billing sync org conn * EC-265 - Fix formatting issues - whitespace * EC-261 - PR feedback and cleanup * scim constants rename * no scim settings right now * update project name * delete package lock * update appsettings configs for scim * use default scim provider for context Co-authored-by: Kyle Spearrin <kyle.spearrin@gmail.com>
This commit is contained in:
parent
c5852db6ed
commit
19b8d8281a
@ -70,25 +70,27 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommCore.Test", "bitwarden_
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test - Bitwarden License", "test - Bitwarden License", "{287CFF34-BBDB-4BC4-AF88-1E19A5A4679B}"
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test - Bitwarden License", "test - Bitwarden License", "{287CFF34-BBDB-4BC4-AF88-1E19A5A4679B}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MySqlMigrations", "util\MySqlMigrations\MySqlMigrations.csproj", "{BDC1D592-5947-47ED-9903-7CDBB12A50C8}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MySqlMigrations", "util\MySqlMigrations\MySqlMigrations.csproj", "{BDC1D592-5947-47ED-9903-7CDBB12A50C8}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PostgresMigrations", "util\PostgresMigrations\PostgresMigrations.csproj", "{F72E0229-2EF7-49B3-9004-FF4C0043816E}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PostgresMigrations", "util\PostgresMigrations\PostgresMigrations.csproj", "{F72E0229-2EF7-49B3-9004-FF4C0043816E}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Common", "test\Common\Common.csproj", "{17DA09D7-0212-4009-879E-6B9CFDE5FA60}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Common", "test\Common\Common.csproj", "{17DA09D7-0212-4009-879E-6B9CFDE5FA60}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Infrastructure.Dapper", "src\Infrastructure.Dapper\Infrastructure.Dapper.csproj", "{AD933445-27CE-4D30-A6ED-9065309464AD}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Infrastructure.Dapper", "src\Infrastructure.Dapper\Infrastructure.Dapper.csproj", "{AD933445-27CE-4D30-A6ED-9065309464AD}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharedWeb", "src\SharedWeb\SharedWeb.csproj", "{713D44C0-1BC1-4024-96A3-A98A49F33908}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SharedWeb", "src\SharedWeb\SharedWeb.csproj", "{713D44C0-1BC1-4024-96A3-A98A49F33908}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Infrastructure.EntityFramework", "src\Infrastructure.EntityFramework\Infrastructure.EntityFramework.csproj", "{ED880735-0250-43C7-9662-FDC7C7416E7F}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Infrastructure.EntityFramework", "src\Infrastructure.EntityFramework\Infrastructure.EntityFramework.csproj", "{ED880735-0250-43C7-9662-FDC7C7416E7F}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Billing.Test", "test\Billing.Test\Billing.Test.csproj", "{B8639B10-2157-44BC-8CE1-D9EB4B50971F}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Billing.Test", "test\Billing.Test\Billing.Test.csproj", "{B8639B10-2157-44BC-8CE1-D9EB4B50971F}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Identity.Test", "test\Identity.Test\Identity.Test.csproj", "{310A1D8E-2D3F-4FA0-84D4-FFE31FCE193E}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Identity.Test", "test\Identity.Test\Identity.Test.csproj", "{310A1D8E-2D3F-4FA0-84D4-FFE31FCE193E}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Identity.IntegrationTest", "test\Identity.IntegrationTest\Identity.IntegrationTest.csproj", "{0D3B2BD2-53F3-421D-AD8F-C19B954C796B}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Identity.IntegrationTest", "test\Identity.IntegrationTest\Identity.IntegrationTest.csproj", "{0D3B2BD2-53F3-421D-AD8F-C19B954C796B}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IntegrationTestCommon", "test\IntegrationTestCommon\IntegrationTestCommon.csproj", "{0923DE59-5FB1-44F2-9302-A09D2236B470}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IntegrationTestCommon", "test\IntegrationTestCommon\IntegrationTestCommon.csproj", "{0923DE59-5FB1-44F2-9302-A09D2236B470}"
|
||||||
|
EndProject
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Scim", "bitwarden_license\src\Scim\Scim.csproj", "{BC3B3F8C-621A-4CB8-9563-6EC0A2C8C747}"
|
||||||
EndProject
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
@ -214,6 +216,10 @@ Global
|
|||||||
{0923DE59-5FB1-44F2-9302-A09D2236B470}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{0923DE59-5FB1-44F2-9302-A09D2236B470}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{0923DE59-5FB1-44F2-9302-A09D2236B470}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{0923DE59-5FB1-44F2-9302-A09D2236B470}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{0923DE59-5FB1-44F2-9302-A09D2236B470}.Release|Any CPU.Build.0 = Release|Any CPU
|
{0923DE59-5FB1-44F2-9302-A09D2236B470}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{BC3B3F8C-621A-4CB8-9563-6EC0A2C8C747}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{BC3B3F8C-621A-4CB8-9563-6EC0A2C8C747}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{BC3B3F8C-621A-4CB8-9563-6EC0A2C8C747}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{BC3B3F8C-621A-4CB8-9563-6EC0A2C8C747}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
@ -248,6 +254,7 @@ Global
|
|||||||
{310A1D8E-2D3F-4FA0-84D4-FFE31FCE193E} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
|
{310A1D8E-2D3F-4FA0-84D4-FFE31FCE193E} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
|
||||||
{0D3B2BD2-53F3-421D-AD8F-C19B954C796B} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
|
{0D3B2BD2-53F3-421D-AD8F-C19B954C796B} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
|
||||||
{0923DE59-5FB1-44F2-9302-A09D2236B470} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
|
{0923DE59-5FB1-44F2-9302-A09D2236B470} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
|
||||||
|
{BC3B3F8C-621A-4CB8-9563-6EC0A2C8C747} = {4FDB6543-F68B-4202-9EA6-7FEA984D2D0A}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
SolutionGuid = {E01CBF68-2E20-425F-9EDB-E0A6510CA92F}
|
SolutionGuid = {E01CBF68-2E20-425F-9EDB-E0A6510CA92F}
|
||||||
|
4
bitwarden_license/src/Scim/.dockerignore
Normal file
4
bitwarden_license/src/Scim/.dockerignore
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
*
|
||||||
|
!obj/build-output/publish/*
|
||||||
|
!obj/Docker/empty/
|
||||||
|
!entrypoint.sh
|
21
bitwarden_license/src/Scim/Context/IScimContext.cs
Normal file
21
bitwarden_license/src/Scim/Context/IScimContext.cs
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Models.OrganizationConnectionConfigs;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
|
using Bit.Core.Settings;
|
||||||
|
|
||||||
|
namespace Bit.Scim.Context
|
||||||
|
{
|
||||||
|
public interface IScimContext
|
||||||
|
{
|
||||||
|
ScimProviderType RequestScimProvider { get; set; }
|
||||||
|
ScimConfig ScimConfiguration { get; set; }
|
||||||
|
Guid? OrganizationId { get; set; }
|
||||||
|
Organization Organization { get; set; }
|
||||||
|
Task BuildAsync(
|
||||||
|
HttpContext httpContext,
|
||||||
|
GlobalSettings globalSettings,
|
||||||
|
IOrganizationRepository organizationRepository,
|
||||||
|
IOrganizationConnectionRepository organizationConnectionRepository);
|
||||||
|
}
|
||||||
|
}
|
64
bitwarden_license/src/Scim/Context/ScimContext.cs
Normal file
64
bitwarden_license/src/Scim/Context/ScimContext.cs
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Models.OrganizationConnectionConfigs;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
|
using Bit.Core.Settings;
|
||||||
|
|
||||||
|
namespace Bit.Scim.Context
|
||||||
|
{
|
||||||
|
public class ScimContext : IScimContext
|
||||||
|
{
|
||||||
|
private bool _builtHttpContext;
|
||||||
|
|
||||||
|
public ScimProviderType RequestScimProvider { get; set; } = ScimProviderType.Default;
|
||||||
|
public ScimConfig ScimConfiguration { get; set; }
|
||||||
|
public Guid? OrganizationId { get; set; }
|
||||||
|
public Organization Organization { get; set; }
|
||||||
|
|
||||||
|
public async virtual Task BuildAsync(
|
||||||
|
HttpContext httpContext,
|
||||||
|
GlobalSettings globalSettings,
|
||||||
|
IOrganizationRepository organizationRepository,
|
||||||
|
IOrganizationConnectionRepository organizationConnectionRepository)
|
||||||
|
{
|
||||||
|
if (_builtHttpContext)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_builtHttpContext = true;
|
||||||
|
|
||||||
|
string orgIdString = null;
|
||||||
|
if (httpContext.Request.RouteValues.TryGetValue("organizationId", out var orgIdObject))
|
||||||
|
{
|
||||||
|
orgIdString = orgIdObject?.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Guid.TryParse(orgIdString, out var orgId))
|
||||||
|
{
|
||||||
|
OrganizationId = orgId;
|
||||||
|
Organization = await organizationRepository.GetByIdAsync(orgId);
|
||||||
|
if (Organization != null)
|
||||||
|
{
|
||||||
|
var scimConnections = await organizationConnectionRepository.GetByOrganizationIdTypeAsync(Organization.Id,
|
||||||
|
OrganizationConnectionType.Scim);
|
||||||
|
ScimConfiguration = scimConnections?.FirstOrDefault()?.GetConfig<ScimConfig>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (RequestScimProvider == ScimProviderType.Default &&
|
||||||
|
httpContext.Request.Headers.TryGetValue("User-Agent", out var userAgent))
|
||||||
|
{
|
||||||
|
if (userAgent.ToString().StartsWith("Okta"))
|
||||||
|
{
|
||||||
|
RequestScimProvider = ScimProviderType.Okta;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (RequestScimProvider == ScimProviderType.Default &&
|
||||||
|
httpContext.Request.Headers.ContainsKey("Adscimversion"))
|
||||||
|
{
|
||||||
|
RequestScimProvider = ScimProviderType.AzureAd;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
21
bitwarden_license/src/Scim/Controllers/InfoController.cs
Normal file
21
bitwarden_license/src/Scim/Controllers/InfoController.cs
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
using Bit.Core.Utilities;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace Bit.Scim.Controllers
|
||||||
|
{
|
||||||
|
public class InfoController : Controller
|
||||||
|
{
|
||||||
|
[HttpGet("~/alive")]
|
||||||
|
[HttpGet("~/now")]
|
||||||
|
public DateTime GetAlive()
|
||||||
|
{
|
||||||
|
return DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("~/version")]
|
||||||
|
public JsonResult GetVersion()
|
||||||
|
{
|
||||||
|
return Json(CoreHelpers.GetVersion());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
290
bitwarden_license/src/Scim/Controllers/v2/GroupsController.cs
Normal file
290
bitwarden_license/src/Scim/Controllers/v2/GroupsController.cs
Normal file
@ -0,0 +1,290 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
|
using Bit.Core.Services;
|
||||||
|
using Bit.Scim.Context;
|
||||||
|
using Bit.Scim.Models;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace Bit.Scim.Controllers.v2
|
||||||
|
{
|
||||||
|
[Authorize("Scim")]
|
||||||
|
[Route("v2/{organizationId}/groups")]
|
||||||
|
public class GroupsController : Controller
|
||||||
|
{
|
||||||
|
private readonly ScimSettings _scimSettings;
|
||||||
|
private readonly IGroupRepository _groupRepository;
|
||||||
|
private readonly IGroupService _groupService;
|
||||||
|
private readonly IScimContext _scimContext;
|
||||||
|
private readonly ILogger<GroupsController> _logger;
|
||||||
|
|
||||||
|
public GroupsController(
|
||||||
|
IGroupRepository groupRepository,
|
||||||
|
IGroupService groupService,
|
||||||
|
IOptions<ScimSettings> scimSettings,
|
||||||
|
IScimContext scimContext,
|
||||||
|
ILogger<GroupsController> logger)
|
||||||
|
{
|
||||||
|
_scimSettings = scimSettings?.Value;
|
||||||
|
_groupRepository = groupRepository;
|
||||||
|
_groupService = groupService;
|
||||||
|
_scimContext = scimContext;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("{id}")]
|
||||||
|
public async Task<IActionResult> Get(Guid organizationId, Guid id)
|
||||||
|
{
|
||||||
|
var group = await _groupRepository.GetByIdAsync(id);
|
||||||
|
if (group == null || group.OrganizationId != organizationId)
|
||||||
|
{
|
||||||
|
return new NotFoundObjectResult(new ScimErrorResponseModel
|
||||||
|
{
|
||||||
|
Status = 404,
|
||||||
|
Detail = "Group not found."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return new ObjectResult(new ScimGroupResponseModel(group));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("")]
|
||||||
|
public async Task<IActionResult> Get(
|
||||||
|
Guid organizationId,
|
||||||
|
[FromQuery] string filter,
|
||||||
|
[FromQuery] int? count,
|
||||||
|
[FromQuery] int? startIndex)
|
||||||
|
{
|
||||||
|
string nameFilter = null;
|
||||||
|
string externalIdFilter = null;
|
||||||
|
if (!string.IsNullOrWhiteSpace(filter))
|
||||||
|
{
|
||||||
|
if (filter.StartsWith("displayName eq "))
|
||||||
|
{
|
||||||
|
nameFilter = filter.Substring(15).Trim('"');
|
||||||
|
}
|
||||||
|
else if (filter.StartsWith("externalId eq "))
|
||||||
|
{
|
||||||
|
externalIdFilter = filter.Substring(14).Trim('"');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var groupList = new List<ScimGroupResponseModel>();
|
||||||
|
var groups = await _groupRepository.GetManyByOrganizationIdAsync(organizationId);
|
||||||
|
var totalResults = 0;
|
||||||
|
if (!string.IsNullOrWhiteSpace(nameFilter))
|
||||||
|
{
|
||||||
|
var group = groups.FirstOrDefault(g => g.Name == nameFilter);
|
||||||
|
if (group != null)
|
||||||
|
{
|
||||||
|
groupList.Add(new ScimGroupResponseModel(group));
|
||||||
|
}
|
||||||
|
totalResults = groupList.Count;
|
||||||
|
}
|
||||||
|
else if (!string.IsNullOrWhiteSpace(externalIdFilter))
|
||||||
|
{
|
||||||
|
var group = groups.FirstOrDefault(ou => ou.ExternalId == externalIdFilter);
|
||||||
|
if (group != null)
|
||||||
|
{
|
||||||
|
groupList.Add(new ScimGroupResponseModel(group));
|
||||||
|
}
|
||||||
|
totalResults = groupList.Count;
|
||||||
|
}
|
||||||
|
else if (string.IsNullOrWhiteSpace(filter) && startIndex.HasValue && count.HasValue)
|
||||||
|
{
|
||||||
|
groupList = groups.OrderBy(g => g.Name)
|
||||||
|
.Skip(startIndex.Value - 1)
|
||||||
|
.Take(count.Value)
|
||||||
|
.Select(g => new ScimGroupResponseModel(g))
|
||||||
|
.ToList();
|
||||||
|
totalResults = groups.Count;
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = new ScimListResponseModel<ScimGroupResponseModel>
|
||||||
|
{
|
||||||
|
Resources = groupList,
|
||||||
|
ItemsPerPage = count.GetValueOrDefault(groupList.Count),
|
||||||
|
TotalResults = totalResults,
|
||||||
|
StartIndex = startIndex.GetValueOrDefault(1),
|
||||||
|
};
|
||||||
|
return new ObjectResult(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("")]
|
||||||
|
public async Task<IActionResult> Post(Guid organizationId, [FromBody] ScimGroupRequestModel model)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(model.DisplayName))
|
||||||
|
{
|
||||||
|
return new BadRequestResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
var groups = await _groupRepository.GetManyByOrganizationIdAsync(organizationId);
|
||||||
|
if (!string.IsNullOrWhiteSpace(model.ExternalId) && groups.Any(g => g.ExternalId == model.ExternalId))
|
||||||
|
{
|
||||||
|
return new ConflictResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
var group = model.ToGroup(organizationId);
|
||||||
|
await _groupService.SaveAsync(group, null);
|
||||||
|
var response = new ScimGroupResponseModel(group);
|
||||||
|
return new CreatedResult(Url.Action(nameof(Get), new { group.OrganizationId, group.Id }), response);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPut("{id}")]
|
||||||
|
public async Task<IActionResult> Put(Guid organizationId, Guid id, [FromBody] ScimGroupRequestModel model)
|
||||||
|
{
|
||||||
|
var group = await _groupRepository.GetByIdAsync(id);
|
||||||
|
if (group == null || group.OrganizationId != organizationId)
|
||||||
|
{
|
||||||
|
return new NotFoundObjectResult(new ScimErrorResponseModel
|
||||||
|
{
|
||||||
|
Status = 404,
|
||||||
|
Detail = "Group not found."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
group.Name = model.DisplayName;
|
||||||
|
await _groupService.SaveAsync(group);
|
||||||
|
return new ObjectResult(new ScimGroupResponseModel(group));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPatch("{id}")]
|
||||||
|
public async Task<IActionResult> Patch(Guid organizationId, Guid id, [FromBody] ScimPatchModel model)
|
||||||
|
{
|
||||||
|
var group = await _groupRepository.GetByIdAsync(id);
|
||||||
|
if (group == null || group.OrganizationId != organizationId)
|
||||||
|
{
|
||||||
|
return new NotFoundObjectResult(new ScimErrorResponseModel
|
||||||
|
{
|
||||||
|
Status = 404,
|
||||||
|
Detail = "Group not found."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var operationHandled = false;
|
||||||
|
|
||||||
|
var replaceOp = model.Operations?.FirstOrDefault(o => o.Op == "replace");
|
||||||
|
if (replaceOp != null)
|
||||||
|
{
|
||||||
|
// Replace a list of members
|
||||||
|
if (replaceOp.Path == "members")
|
||||||
|
{
|
||||||
|
var ids = GetOperationValueIds(replaceOp.Value);
|
||||||
|
await _groupRepository.UpdateUsersAsync(group.Id, ids);
|
||||||
|
operationHandled = true;
|
||||||
|
}
|
||||||
|
// Replace group name
|
||||||
|
else if (replaceOp.Value.TryGetProperty("displayName", out var displayNameProperty))
|
||||||
|
{
|
||||||
|
group.Name = displayNameProperty.GetString();
|
||||||
|
await _groupService.SaveAsync(group);
|
||||||
|
operationHandled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a single member
|
||||||
|
var addMemberOp = model.Operations?.FirstOrDefault(
|
||||||
|
o => o.Op == "add" && !string.IsNullOrWhiteSpace(o.Path) && o.Path.StartsWith("members[value eq "));
|
||||||
|
if (addMemberOp != null)
|
||||||
|
{
|
||||||
|
var addId = GetOperationPathId(addMemberOp.Path);
|
||||||
|
if (addId.HasValue)
|
||||||
|
{
|
||||||
|
var orgUserIds = (await _groupRepository.GetManyUserIdsByIdAsync(group.Id)).ToHashSet();
|
||||||
|
orgUserIds.Add(addId.Value);
|
||||||
|
await _groupRepository.UpdateUsersAsync(group.Id, orgUserIds);
|
||||||
|
operationHandled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a list of members
|
||||||
|
var addMembersOp = model.Operations?.FirstOrDefault(o => o.Op == "add" && o.Path == "members");
|
||||||
|
if (addMembersOp != null)
|
||||||
|
{
|
||||||
|
var orgUserIds = (await _groupRepository.GetManyUserIdsByIdAsync(group.Id)).ToHashSet();
|
||||||
|
foreach (var v in GetOperationValueIds(addMembersOp.Value))
|
||||||
|
{
|
||||||
|
orgUserIds.Add(v);
|
||||||
|
}
|
||||||
|
await _groupRepository.UpdateUsersAsync(group.Id, orgUserIds);
|
||||||
|
operationHandled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove a single member
|
||||||
|
var removeMemberOp = model.Operations?.FirstOrDefault(
|
||||||
|
o => o.Op == "remove" && !string.IsNullOrWhiteSpace(o.Path) && o.Path.StartsWith("members[value eq "));
|
||||||
|
if (removeMemberOp != null)
|
||||||
|
{
|
||||||
|
var removeId = GetOperationPathId(removeMemberOp.Path);
|
||||||
|
if (removeId.HasValue)
|
||||||
|
{
|
||||||
|
await _groupService.DeleteUserAsync(group, removeId.Value);
|
||||||
|
operationHandled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove a list of members
|
||||||
|
var removeMembersOp = model.Operations?.FirstOrDefault(o => o.Op == "remove" && o.Path == "members");
|
||||||
|
if (removeMembersOp != null)
|
||||||
|
{
|
||||||
|
var orgUserIds = (await _groupRepository.GetManyUserIdsByIdAsync(group.Id)).ToHashSet();
|
||||||
|
foreach (var v in GetOperationValueIds(removeMembersOp.Value))
|
||||||
|
{
|
||||||
|
orgUserIds.Remove(v);
|
||||||
|
}
|
||||||
|
await _groupRepository.UpdateUsersAsync(group.Id, orgUserIds);
|
||||||
|
operationHandled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!operationHandled)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Group patch operation not handled: {0} : ",
|
||||||
|
string.Join(", ", model.Operations.Select(o => $"{o.Op}:{o.Path}")));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new NoContentResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpDelete("{id}")]
|
||||||
|
public async Task<IActionResult> Delete(Guid organizationId, Guid id)
|
||||||
|
{
|
||||||
|
var group = await _groupRepository.GetByIdAsync(id);
|
||||||
|
if (group == null || group.OrganizationId != organizationId)
|
||||||
|
{
|
||||||
|
return new NotFoundObjectResult(new ScimErrorResponseModel
|
||||||
|
{
|
||||||
|
Status = 404,
|
||||||
|
Detail = "Group not found."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await _groupService.DeleteAsync(group);
|
||||||
|
return new NoContentResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Guid> GetOperationValueIds(JsonElement objArray)
|
||||||
|
{
|
||||||
|
var ids = new List<Guid>();
|
||||||
|
foreach (var obj in objArray.EnumerateArray())
|
||||||
|
{
|
||||||
|
if (obj.TryGetProperty("value", out var valueProperty))
|
||||||
|
{
|
||||||
|
if (valueProperty.TryGetGuid(out var guid))
|
||||||
|
{
|
||||||
|
ids.Add(guid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Guid? GetOperationPathId(string path)
|
||||||
|
{
|
||||||
|
// Parse Guid from string like: members[value eq "{GUID}"}]
|
||||||
|
if (Guid.TryParse(path.Substring(18).Replace("\"]", string.Empty), out var id))
|
||||||
|
{
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
267
bitwarden_license/src/Scim/Controllers/v2/UsersController.cs
Normal file
267
bitwarden_license/src/Scim/Controllers/v2/UsersController.cs
Normal file
@ -0,0 +1,267 @@
|
|||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Models.Data;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
|
using Bit.Core.Services;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
using Bit.Scim.Context;
|
||||||
|
using Bit.Scim.Models;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace Bit.Scim.Controllers.v2
|
||||||
|
{
|
||||||
|
[Authorize("Scim")]
|
||||||
|
[Route("v2/{organizationId}/users")]
|
||||||
|
public class UsersController : Controller
|
||||||
|
{
|
||||||
|
private readonly IUserService _userService;
|
||||||
|
private readonly IUserRepository _userRepository;
|
||||||
|
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||||
|
private readonly IOrganizationService _organizationService;
|
||||||
|
private readonly IScimContext _scimContext;
|
||||||
|
private readonly ScimSettings _scimSettings;
|
||||||
|
private readonly ILogger<UsersController> _logger;
|
||||||
|
|
||||||
|
public UsersController(
|
||||||
|
IUserService userService,
|
||||||
|
IUserRepository userRepository,
|
||||||
|
IOrganizationUserRepository organizationUserRepository,
|
||||||
|
IOrganizationService organizationService,
|
||||||
|
IScimContext scimContext,
|
||||||
|
IOptions<ScimSettings> scimSettings,
|
||||||
|
ILogger<UsersController> logger)
|
||||||
|
{
|
||||||
|
_userService = userService;
|
||||||
|
_userRepository = userRepository;
|
||||||
|
_organizationUserRepository = organizationUserRepository;
|
||||||
|
_organizationService = organizationService;
|
||||||
|
_scimContext = scimContext;
|
||||||
|
_scimSettings = scimSettings?.Value;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("{id}")]
|
||||||
|
public async Task<IActionResult> Get(Guid organizationId, Guid id)
|
||||||
|
{
|
||||||
|
var orgUser = await _organizationUserRepository.GetDetailsByIdAsync(id);
|
||||||
|
if (orgUser == null || orgUser.OrganizationId != organizationId)
|
||||||
|
{
|
||||||
|
return new NotFoundObjectResult(new ScimErrorResponseModel
|
||||||
|
{
|
||||||
|
Status = 404,
|
||||||
|
Detail = "User not found."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return new ObjectResult(new ScimUserResponseModel(orgUser));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("")]
|
||||||
|
public async Task<IActionResult> Get(
|
||||||
|
Guid organizationId,
|
||||||
|
[FromQuery] string filter,
|
||||||
|
[FromQuery] int? count,
|
||||||
|
[FromQuery] int? startIndex)
|
||||||
|
{
|
||||||
|
string emailFilter = null;
|
||||||
|
string usernameFilter = null;
|
||||||
|
string externalIdFilter = null;
|
||||||
|
if (!string.IsNullOrWhiteSpace(filter))
|
||||||
|
{
|
||||||
|
if (filter.StartsWith("userName eq "))
|
||||||
|
{
|
||||||
|
usernameFilter = filter.Substring(12).Trim('"').ToLowerInvariant();
|
||||||
|
if (usernameFilter.Contains("@"))
|
||||||
|
{
|
||||||
|
emailFilter = usernameFilter;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (filter.StartsWith("externalId eq "))
|
||||||
|
{
|
||||||
|
externalIdFilter = filter.Substring(14).Trim('"');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var userList = new List<ScimUserResponseModel> { };
|
||||||
|
var orgUsers = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(organizationId);
|
||||||
|
var totalResults = 0;
|
||||||
|
if (!string.IsNullOrWhiteSpace(emailFilter))
|
||||||
|
{
|
||||||
|
var orgUser = orgUsers.FirstOrDefault(ou => ou.Email.ToLowerInvariant() == emailFilter);
|
||||||
|
if (orgUser != null)
|
||||||
|
{
|
||||||
|
userList.Add(new ScimUserResponseModel(orgUser));
|
||||||
|
}
|
||||||
|
totalResults = userList.Count;
|
||||||
|
}
|
||||||
|
else if (!string.IsNullOrWhiteSpace(externalIdFilter))
|
||||||
|
{
|
||||||
|
var orgUser = orgUsers.FirstOrDefault(ou => ou.ExternalId == externalIdFilter);
|
||||||
|
if (orgUser != null)
|
||||||
|
{
|
||||||
|
userList.Add(new ScimUserResponseModel(orgUser));
|
||||||
|
}
|
||||||
|
totalResults = userList.Count;
|
||||||
|
}
|
||||||
|
else if (string.IsNullOrWhiteSpace(filter) && startIndex.HasValue && count.HasValue)
|
||||||
|
{
|
||||||
|
userList = orgUsers.OrderBy(ou => ou.Email)
|
||||||
|
.Skip(startIndex.Value - 1)
|
||||||
|
.Take(count.Value)
|
||||||
|
.Select(ou => new ScimUserResponseModel(ou))
|
||||||
|
.ToList();
|
||||||
|
totalResults = orgUsers.Count;
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = new ScimListResponseModel<ScimUserResponseModel>
|
||||||
|
{
|
||||||
|
Resources = userList,
|
||||||
|
ItemsPerPage = count.GetValueOrDefault(userList.Count),
|
||||||
|
TotalResults = totalResults,
|
||||||
|
StartIndex = startIndex.GetValueOrDefault(1),
|
||||||
|
};
|
||||||
|
return new ObjectResult(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("")]
|
||||||
|
public async Task<IActionResult> Post(Guid organizationId, [FromBody] ScimUserRequestModel model)
|
||||||
|
{
|
||||||
|
var email = model.PrimaryEmail?.ToLowerInvariant();
|
||||||
|
if (string.IsNullOrWhiteSpace(email))
|
||||||
|
{
|
||||||
|
switch (_scimContext.RequestScimProvider)
|
||||||
|
{
|
||||||
|
case ScimProviderType.AzureAd:
|
||||||
|
email = model.UserName?.ToLowerInvariant();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(email) || !model.Active)
|
||||||
|
{
|
||||||
|
return new BadRequestResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
var orgUsers = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(organizationId);
|
||||||
|
var orgUserByEmail = orgUsers.FirstOrDefault(ou => ou.Email?.ToLowerInvariant() == email);
|
||||||
|
if (orgUserByEmail != null)
|
||||||
|
{
|
||||||
|
return new ConflictResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
string externalId = null;
|
||||||
|
if (!string.IsNullOrWhiteSpace(model.ExternalId))
|
||||||
|
{
|
||||||
|
externalId = model.ExternalId;
|
||||||
|
}
|
||||||
|
else if (!string.IsNullOrWhiteSpace(model.UserName))
|
||||||
|
{
|
||||||
|
externalId = model.UserName;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
externalId = CoreHelpers.RandomString(15);
|
||||||
|
}
|
||||||
|
|
||||||
|
var orgUserByExternalId = orgUsers.FirstOrDefault(ou => ou.ExternalId == externalId);
|
||||||
|
if (orgUserByExternalId != null)
|
||||||
|
{
|
||||||
|
return new ConflictResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
var invitedOrgUser = await _organizationService.InviteUserAsync(organizationId, null, email,
|
||||||
|
OrganizationUserType.User, false, externalId, new List<SelectionReadOnly>());
|
||||||
|
var orgUser = await _organizationUserRepository.GetDetailsByIdAsync(invitedOrgUser.Id);
|
||||||
|
var response = new ScimUserResponseModel(orgUser);
|
||||||
|
return new CreatedResult(Url.Action(nameof(Get), new { orgUser.OrganizationId, orgUser.Id }), response);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPut("{id}")]
|
||||||
|
public async Task<IActionResult> Put(Guid organizationId, Guid id, [FromBody] ScimUserRequestModel model)
|
||||||
|
{
|
||||||
|
var orgUser = await _organizationUserRepository.GetByIdAsync(id);
|
||||||
|
if (orgUser == null || orgUser.OrganizationId != organizationId)
|
||||||
|
{
|
||||||
|
return new NotFoundObjectResult(new ScimErrorResponseModel
|
||||||
|
{
|
||||||
|
Status = 404,
|
||||||
|
Detail = "User not found."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (model.Active && orgUser.Status == OrganizationUserStatusType.Deactivated)
|
||||||
|
{
|
||||||
|
await _organizationService.ActivateUserAsync(orgUser, null);
|
||||||
|
}
|
||||||
|
else if (!model.Active && orgUser.Status != OrganizationUserStatusType.Deactivated)
|
||||||
|
{
|
||||||
|
await _organizationService.DeactivateUserAsync(orgUser, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Have to get full details object for response model
|
||||||
|
var orgUserDetails = await _organizationUserRepository.GetDetailsByIdAsync(id);
|
||||||
|
return new ObjectResult(new ScimUserResponseModel(orgUserDetails));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPatch("{id}")]
|
||||||
|
public async Task<IActionResult> Patch(Guid organizationId, Guid id, [FromBody] ScimPatchModel model)
|
||||||
|
{
|
||||||
|
var orgUser = await _organizationUserRepository.GetByIdAsync(id);
|
||||||
|
if (orgUser == null || orgUser.OrganizationId != organizationId)
|
||||||
|
{
|
||||||
|
return new NotFoundObjectResult(new ScimErrorResponseModel
|
||||||
|
{
|
||||||
|
Status = 404,
|
||||||
|
Detail = "User not found."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var operationHandled = false;
|
||||||
|
|
||||||
|
var replaceOp = model.Operations?.FirstOrDefault(o => o.Op == "replace");
|
||||||
|
if (replaceOp != null)
|
||||||
|
{
|
||||||
|
if (replaceOp.Value.TryGetProperty("active", out var activeProperty))
|
||||||
|
{
|
||||||
|
var active = activeProperty.GetBoolean();
|
||||||
|
if (active && orgUser.Status == OrganizationUserStatusType.Deactivated)
|
||||||
|
{
|
||||||
|
await _organizationService.ActivateUserAsync(orgUser, null);
|
||||||
|
operationHandled = true;
|
||||||
|
}
|
||||||
|
else if (!active && orgUser.Status != OrganizationUserStatusType.Deactivated)
|
||||||
|
{
|
||||||
|
await _organizationService.DeactivateUserAsync(orgUser, null);
|
||||||
|
operationHandled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!operationHandled)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("User patch operation not handled: {operation} : ",
|
||||||
|
string.Join(", ", model.Operations.Select(o => $"{o.Op}:{o.Path}")));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new NoContentResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpDelete("{id}")]
|
||||||
|
public async Task<IActionResult> Delete(Guid organizationId, Guid id, [FromBody] ScimUserRequestModel model)
|
||||||
|
{
|
||||||
|
var orgUser = await _organizationUserRepository.GetByIdAsync(id);
|
||||||
|
if (orgUser == null || orgUser.OrganizationId != organizationId)
|
||||||
|
{
|
||||||
|
return new NotFoundObjectResult(new ScimErrorResponseModel
|
||||||
|
{
|
||||||
|
Status = 404,
|
||||||
|
Detail = "User not found."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await _organizationService.DeleteUserAsync(organizationId, id, null);
|
||||||
|
return new NoContentResult();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
20
bitwarden_license/src/Scim/Dockerfile
Normal file
20
bitwarden_license/src/Scim/Dockerfile
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
FROM mcr.microsoft.com/dotnet/aspnet:6.0
|
||||||
|
|
||||||
|
LABEL com.bitwarden.product="bitwarden"
|
||||||
|
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends \
|
||||||
|
gosu \
|
||||||
|
curl \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
ENV ASPNETCORE_URLS http://+:5000
|
||||||
|
WORKDIR /app
|
||||||
|
EXPOSE 5000
|
||||||
|
COPY obj/build-output/publish .
|
||||||
|
COPY entrypoint.sh /
|
||||||
|
RUN chmod +x /entrypoint.sh
|
||||||
|
|
||||||
|
HEALTHCHECK CMD curl -f http://localhost:5000/alive || exit 1
|
||||||
|
|
||||||
|
ENTRYPOINT ["/entrypoint.sh"]
|
18
bitwarden_license/src/Scim/Models/BaseScimGroupModel.cs
Normal file
18
bitwarden_license/src/Scim/Models/BaseScimGroupModel.cs
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
using Bit.Scim.Utilities;
|
||||||
|
|
||||||
|
namespace Bit.Scim.Models
|
||||||
|
{
|
||||||
|
public abstract class BaseScimGroupModel : BaseScimModel
|
||||||
|
{
|
||||||
|
public BaseScimGroupModel(bool initSchema = false)
|
||||||
|
{
|
||||||
|
if (initSchema)
|
||||||
|
{
|
||||||
|
Schemas = new List<string> { ScimConstants.Scim2SchemaGroup };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string DisplayName { get; set; }
|
||||||
|
public string ExternalId { get; set; }
|
||||||
|
}
|
||||||
|
}
|
15
bitwarden_license/src/Scim/Models/BaseScimModel.cs
Normal file
15
bitwarden_license/src/Scim/Models/BaseScimModel.cs
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
namespace Bit.Scim.Models
|
||||||
|
{
|
||||||
|
public abstract class BaseScimModel
|
||||||
|
{
|
||||||
|
public BaseScimModel()
|
||||||
|
{ }
|
||||||
|
|
||||||
|
public BaseScimModel(string schema)
|
||||||
|
{
|
||||||
|
Schemas = new List<string> { schema };
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<string> Schemas { get; set; }
|
||||||
|
}
|
||||||
|
}
|
55
bitwarden_license/src/Scim/Models/BaseScimUserModel.cs
Normal file
55
bitwarden_license/src/Scim/Models/BaseScimUserModel.cs
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
using Bit.Scim.Utilities;
|
||||||
|
|
||||||
|
namespace Bit.Scim.Models
|
||||||
|
{
|
||||||
|
public abstract class BaseScimUserModel : BaseScimModel
|
||||||
|
{
|
||||||
|
public BaseScimUserModel(bool initSchema = false)
|
||||||
|
{
|
||||||
|
if (initSchema)
|
||||||
|
{
|
||||||
|
Schemas = new List<string> { ScimConstants.Scim2SchemaUser };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string UserName { get; set; }
|
||||||
|
public NameModel Name { get; set; }
|
||||||
|
public List<EmailModel> Emails { get; set; }
|
||||||
|
public string PrimaryEmail => Emails?.FirstOrDefault(e => e.Primary)?.Value;
|
||||||
|
public string DisplayName { get; set; }
|
||||||
|
public bool Active { get; set; }
|
||||||
|
public List<string> Groups { get; set; }
|
||||||
|
public string ExternalId { get; set; }
|
||||||
|
|
||||||
|
public class NameModel
|
||||||
|
{
|
||||||
|
public NameModel() { }
|
||||||
|
|
||||||
|
public NameModel(string name)
|
||||||
|
{
|
||||||
|
Formatted = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Formatted { get; set; }
|
||||||
|
public string GivenName { get; set; }
|
||||||
|
public string MiddleName { get; set; }
|
||||||
|
public string FamilyName { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class EmailModel
|
||||||
|
{
|
||||||
|
public EmailModel() { }
|
||||||
|
|
||||||
|
public EmailModel(string email)
|
||||||
|
{
|
||||||
|
Primary = true;
|
||||||
|
Value = email;
|
||||||
|
Type = "work";
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Primary { get; set; }
|
||||||
|
public string Value { get; set; }
|
||||||
|
public string Type { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
14
bitwarden_license/src/Scim/Models/ScimErrorResponseModel.cs
Normal file
14
bitwarden_license/src/Scim/Models/ScimErrorResponseModel.cs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
using Bit.Scim.Utilities;
|
||||||
|
|
||||||
|
namespace Bit.Scim.Models
|
||||||
|
{
|
||||||
|
public class ScimErrorResponseModel : BaseScimModel
|
||||||
|
{
|
||||||
|
public ScimErrorResponseModel()
|
||||||
|
: base(ScimConstants.Scim2SchemaError)
|
||||||
|
{ }
|
||||||
|
|
||||||
|
public string Detail { get; set; }
|
||||||
|
public int Status { get; set; }
|
||||||
|
}
|
||||||
|
}
|
23
bitwarden_license/src/Scim/Models/ScimGroupRequestModel.cs
Normal file
23
bitwarden_license/src/Scim/Models/ScimGroupRequestModel.cs
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
|
||||||
|
namespace Bit.Scim.Models
|
||||||
|
{
|
||||||
|
public class ScimGroupRequestModel : BaseScimGroupModel
|
||||||
|
{
|
||||||
|
public ScimGroupRequestModel()
|
||||||
|
: base(false)
|
||||||
|
{ }
|
||||||
|
|
||||||
|
public Group ToGroup(Guid organizationId)
|
||||||
|
{
|
||||||
|
var externalId = string.IsNullOrWhiteSpace(ExternalId) ? CoreHelpers.RandomString(15) : ExternalId;
|
||||||
|
return new Group
|
||||||
|
{
|
||||||
|
Name = DisplayName,
|
||||||
|
ExternalId = externalId,
|
||||||
|
OrganizationId = organizationId
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
26
bitwarden_license/src/Scim/Models/ScimGroupResponseModel.cs
Normal file
26
bitwarden_license/src/Scim/Models/ScimGroupResponseModel.cs
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
using Bit.Core.Entities;
|
||||||
|
|
||||||
|
namespace Bit.Scim.Models
|
||||||
|
{
|
||||||
|
public class ScimGroupResponseModel : BaseScimGroupModel
|
||||||
|
{
|
||||||
|
public ScimGroupResponseModel()
|
||||||
|
: base(true)
|
||||||
|
{
|
||||||
|
Meta = new ScimMetaModel("Group");
|
||||||
|
}
|
||||||
|
|
||||||
|
public ScimGroupResponseModel(Group group)
|
||||||
|
: this()
|
||||||
|
{
|
||||||
|
Id = group.Id.ToString();
|
||||||
|
DisplayName = group.Name;
|
||||||
|
ExternalId = group.ExternalId;
|
||||||
|
Meta.Created = group.CreationDate;
|
||||||
|
Meta.LastModified = group.RevisionDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Id { get; set; }
|
||||||
|
public ScimMetaModel Meta { get; private set; }
|
||||||
|
}
|
||||||
|
}
|
16
bitwarden_license/src/Scim/Models/ScimListResponseModel.cs
Normal file
16
bitwarden_license/src/Scim/Models/ScimListResponseModel.cs
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
using Bit.Scim.Utilities;
|
||||||
|
|
||||||
|
namespace Bit.Scim.Models
|
||||||
|
{
|
||||||
|
public class ScimListResponseModel<T> : BaseScimModel
|
||||||
|
{
|
||||||
|
public ScimListResponseModel()
|
||||||
|
: base(ScimConstants.Scim2SchemaListResponse)
|
||||||
|
{ }
|
||||||
|
|
||||||
|
public int TotalResults { get; set; }
|
||||||
|
public int StartIndex { get; set; }
|
||||||
|
public int ItemsPerPage { get; set; }
|
||||||
|
public List<T> Resources { get; set; }
|
||||||
|
}
|
||||||
|
}
|
14
bitwarden_license/src/Scim/Models/ScimMetaModel.cs
Normal file
14
bitwarden_license/src/Scim/Models/ScimMetaModel.cs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
namespace Bit.Scim.Models
|
||||||
|
{
|
||||||
|
public class ScimMetaModel
|
||||||
|
{
|
||||||
|
public ScimMetaModel(string resourceType)
|
||||||
|
{
|
||||||
|
ResourceType = resourceType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string ResourceType { get; set; }
|
||||||
|
public DateTime? Created { get; set; }
|
||||||
|
public DateTime? LastModified { get; set; }
|
||||||
|
}
|
||||||
|
}
|
19
bitwarden_license/src/Scim/Models/ScimPatchModel.cs
Normal file
19
bitwarden_license/src/Scim/Models/ScimPatchModel.cs
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace Bit.Scim.Models
|
||||||
|
{
|
||||||
|
public class ScimPatchModel : BaseScimModel
|
||||||
|
{
|
||||||
|
public ScimPatchModel()
|
||||||
|
: base() { }
|
||||||
|
|
||||||
|
public List<OperationModel> Operations { get; set; }
|
||||||
|
|
||||||
|
public class OperationModel
|
||||||
|
{
|
||||||
|
public string Op { get; set; }
|
||||||
|
public string Path { get; set; }
|
||||||
|
public JsonElement Value { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
namespace Bit.Scim.Models
|
||||||
|
{
|
||||||
|
public class ScimUserRequestModel : BaseScimUserModel
|
||||||
|
{
|
||||||
|
public ScimUserRequestModel()
|
||||||
|
: base(false)
|
||||||
|
{ }
|
||||||
|
}
|
||||||
|
}
|
29
bitwarden_license/src/Scim/Models/ScimUserResponseModel.cs
Normal file
29
bitwarden_license/src/Scim/Models/ScimUserResponseModel.cs
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||||
|
|
||||||
|
namespace Bit.Scim.Models
|
||||||
|
{
|
||||||
|
public class ScimUserResponseModel : BaseScimUserModel
|
||||||
|
{
|
||||||
|
public ScimUserResponseModel()
|
||||||
|
: base(true)
|
||||||
|
{
|
||||||
|
Meta = new ScimMetaModel("User");
|
||||||
|
Groups = new List<string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ScimUserResponseModel(OrganizationUserUserDetails orgUser)
|
||||||
|
: this()
|
||||||
|
{
|
||||||
|
Id = orgUser.Id.ToString();
|
||||||
|
ExternalId = orgUser.ExternalId;
|
||||||
|
UserName = orgUser.Email;
|
||||||
|
DisplayName = orgUser.Name;
|
||||||
|
Emails = new List<EmailModel> { new EmailModel(orgUser.Email) };
|
||||||
|
Name = new NameModel(orgUser.Name);
|
||||||
|
Active = orgUser.Status != Core.Enums.OrganizationUserStatusType.Deactivated;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Id { get; set; }
|
||||||
|
public ScimMetaModel Meta { get; private set; }
|
||||||
|
}
|
||||||
|
}
|
34
bitwarden_license/src/Scim/Program.cs
Normal file
34
bitwarden_license/src/Scim/Program.cs
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
using Bit.Core.Utilities;
|
||||||
|
using Serilog.Events;
|
||||||
|
|
||||||
|
namespace Bit.Scim
|
||||||
|
{
|
||||||
|
public class Program
|
||||||
|
{
|
||||||
|
public static void Main(string[] args)
|
||||||
|
{
|
||||||
|
Host
|
||||||
|
.CreateDefaultBuilder(args)
|
||||||
|
.ConfigureWebHostDefaults(webBuilder =>
|
||||||
|
{
|
||||||
|
webBuilder.UseStartup<Startup>();
|
||||||
|
webBuilder.ConfigureLogging((hostingContext, logging) =>
|
||||||
|
logging.AddSerilog(hostingContext, e =>
|
||||||
|
{
|
||||||
|
var context = e.Properties["SourceContext"].ToString();
|
||||||
|
|
||||||
|
if (e.Properties.ContainsKey("RequestPath") &&
|
||||||
|
!string.IsNullOrWhiteSpace(e.Properties["RequestPath"]?.ToString()) &&
|
||||||
|
(context.Contains(".Server.Kestrel") || context.Contains(".Core.IISHttpServer")))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.Level >= LogEventLevel.Warning;
|
||||||
|
}));
|
||||||
|
})
|
||||||
|
.Build()
|
||||||
|
.Run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
29
bitwarden_license/src/Scim/Properties/launchSettings.json
Normal file
29
bitwarden_license/src/Scim/Properties/launchSettings.json
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"iisSettings": {
|
||||||
|
"windowsAuthentication": false,
|
||||||
|
"anonymousAuthentication": true,
|
||||||
|
"iisExpress": {
|
||||||
|
"applicationUrl": "http://localhost:44558/",
|
||||||
|
"sslPort": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"profiles": {
|
||||||
|
"IIS Express": {
|
||||||
|
"commandName": "IISExpress",
|
||||||
|
"launchBrowser": false,
|
||||||
|
"launchUrl": "http://localhost:44558",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Scim": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"launchBrowser": false,
|
||||||
|
"launchUrl": "http://localhost:44558",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
},
|
||||||
|
"applicationUrl": "http://localhost:44559"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
17
bitwarden_license/src/Scim/Scim.csproj
Normal file
17
bitwarden_license/src/Scim/Scim.csproj
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<UserSecretsId>bitwarden-Scim</UserSecretsId>
|
||||||
|
<MvcRazorCompileOnPublish>false</MvcRazorCompileOnPublish>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="5.0.2" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\..\..\src\Core\Core.csproj" />
|
||||||
|
<ProjectReference Include="..\..\..\src\SharedWeb\SharedWeb.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
6
bitwarden_license/src/Scim/ScimSettings.cs
Normal file
6
bitwarden_license/src/Scim/ScimSettings.cs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
namespace Bit.Scim
|
||||||
|
{
|
||||||
|
public class ScimSettings
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
113
bitwarden_license/src/Scim/Startup.cs
Normal file
113
bitwarden_license/src/Scim/Startup.cs
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using Bit.Core.Context;
|
||||||
|
using Bit.Core.Settings;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
using Bit.Scim.Context;
|
||||||
|
using Bit.Scim.Utilities;
|
||||||
|
using Bit.SharedWeb.Utilities;
|
||||||
|
using IdentityModel;
|
||||||
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
|
using Stripe;
|
||||||
|
|
||||||
|
namespace Bit.Scim
|
||||||
|
{
|
||||||
|
public class Startup
|
||||||
|
{
|
||||||
|
public Startup(IWebHostEnvironment env, IConfiguration configuration)
|
||||||
|
{
|
||||||
|
CultureInfo.DefaultThreadCurrentCulture = new CultureInfo("en-US");
|
||||||
|
Configuration = configuration;
|
||||||
|
Environment = env;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IConfiguration Configuration { get; }
|
||||||
|
public IWebHostEnvironment Environment { get; set; }
|
||||||
|
|
||||||
|
public void ConfigureServices(IServiceCollection services)
|
||||||
|
{
|
||||||
|
// Options
|
||||||
|
services.AddOptions();
|
||||||
|
|
||||||
|
// Settings
|
||||||
|
var globalSettings = services.AddGlobalSettingsServices(Configuration, Environment);
|
||||||
|
services.Configure<ScimSettings>(Configuration.GetSection("ScimSettings"));
|
||||||
|
|
||||||
|
// Stripe Billing
|
||||||
|
StripeConfiguration.ApiKey = globalSettings.Stripe.ApiKey;
|
||||||
|
StripeConfiguration.MaxNetworkRetries = globalSettings.Stripe.MaxNetworkRetries;
|
||||||
|
|
||||||
|
// Repositories
|
||||||
|
services.AddSqlServerRepositories(globalSettings);
|
||||||
|
|
||||||
|
// Context
|
||||||
|
services.AddScoped<ICurrentContext, CurrentContext>();
|
||||||
|
services.AddScoped<IScimContext, ScimContext>();
|
||||||
|
|
||||||
|
// Authentication
|
||||||
|
services.AddAuthentication(ApiKeyAuthenticationOptions.DefaultScheme)
|
||||||
|
.AddScheme<ApiKeyAuthenticationOptions, ApiKeyAuthenticationHandler>(
|
||||||
|
ApiKeyAuthenticationOptions.DefaultScheme, null);
|
||||||
|
|
||||||
|
services.AddAuthorization(config =>
|
||||||
|
{
|
||||||
|
config.AddPolicy("Scim", policy =>
|
||||||
|
{
|
||||||
|
policy.RequireAuthenticatedUser();
|
||||||
|
policy.RequireClaim(JwtClaimTypes.Scope, "api.scim");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Identity
|
||||||
|
services.AddCustomIdentityServices(globalSettings);
|
||||||
|
|
||||||
|
// Services
|
||||||
|
services.AddBaseServices(globalSettings);
|
||||||
|
services.AddDefaultServices(globalSettings);
|
||||||
|
|
||||||
|
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
|
||||||
|
|
||||||
|
// Mvc
|
||||||
|
services.AddMvc(config =>
|
||||||
|
{
|
||||||
|
config.Filters.Add(new LoggingExceptionHandlerFilterAttribute());
|
||||||
|
});
|
||||||
|
services.Configure<RouteOptions>(options => options.LowercaseUrls = true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Configure(
|
||||||
|
IApplicationBuilder app,
|
||||||
|
IWebHostEnvironment env,
|
||||||
|
IHostApplicationLifetime appLifetime,
|
||||||
|
GlobalSettings globalSettings)
|
||||||
|
{
|
||||||
|
app.UseSerilog(env, appLifetime, globalSettings);
|
||||||
|
|
||||||
|
// Add general security headers
|
||||||
|
app.UseMiddleware<SecurityHeadersMiddleware>();
|
||||||
|
|
||||||
|
if (env.IsDevelopment())
|
||||||
|
{
|
||||||
|
app.UseDeveloperExceptionPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default Middleware
|
||||||
|
app.UseDefaultMiddleware(env, globalSettings);
|
||||||
|
|
||||||
|
// Add routing
|
||||||
|
app.UseRouting();
|
||||||
|
|
||||||
|
// Add Scim context
|
||||||
|
app.UseMiddleware<ScimContextMiddleware>();
|
||||||
|
|
||||||
|
// Add authentication and authorization to the request pipeline.
|
||||||
|
app.UseAuthentication();
|
||||||
|
app.UseAuthorization();
|
||||||
|
|
||||||
|
// Add current context
|
||||||
|
app.UseMiddleware<CurrentContextMiddleware>();
|
||||||
|
|
||||||
|
// Add MVC to the request pipeline.
|
||||||
|
app.UseEndpoints(endpoints => endpoints.MapDefaultControllerRoute());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,83 @@
|
|||||||
|
using System.Security.Claims;
|
||||||
|
using System.Text.Encodings.Web;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
|
using Bit.Scim.Context;
|
||||||
|
using IdentityModel;
|
||||||
|
using Microsoft.AspNetCore.Authentication;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace Bit.Scim.Utilities
|
||||||
|
{
|
||||||
|
public class ApiKeyAuthenticationHandler : AuthenticationHandler<ApiKeyAuthenticationOptions>
|
||||||
|
{
|
||||||
|
private readonly IOrganizationRepository _organizationRepository;
|
||||||
|
private readonly IOrganizationApiKeyRepository _organizationApiKeyRepository;
|
||||||
|
private readonly IScimContext _scimContext;
|
||||||
|
|
||||||
|
public ApiKeyAuthenticationHandler(
|
||||||
|
IOptionsMonitor<ApiKeyAuthenticationOptions> options,
|
||||||
|
ILoggerFactory logger,
|
||||||
|
UrlEncoder encoder,
|
||||||
|
ISystemClock clock,
|
||||||
|
IOrganizationRepository organizationRepository,
|
||||||
|
IOrganizationApiKeyRepository organizationApiKeyRepository,
|
||||||
|
IScimContext scimContext) :
|
||||||
|
base(options, logger, encoder, clock)
|
||||||
|
{
|
||||||
|
_organizationRepository = organizationRepository;
|
||||||
|
_organizationApiKeyRepository = organizationApiKeyRepository;
|
||||||
|
_scimContext = scimContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
|
||||||
|
{
|
||||||
|
if (!_scimContext.OrganizationId.HasValue || _scimContext.Organization == null)
|
||||||
|
{
|
||||||
|
Logger.LogWarning("No organization.");
|
||||||
|
return AuthenticateResult.Fail("Invalid parameters");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Request.Headers.TryGetValue("Authorization", out var authHeader) || authHeader.Count != 1)
|
||||||
|
{
|
||||||
|
Logger.LogWarning("An API request was received without the Authorization header");
|
||||||
|
return AuthenticateResult.Fail("Invalid parameters");
|
||||||
|
}
|
||||||
|
var apiKey = authHeader.ToString();
|
||||||
|
if (apiKey.StartsWith("Bearer "))
|
||||||
|
{
|
||||||
|
apiKey = apiKey.Substring(7);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_scimContext.Organization.Enabled || !_scimContext.Organization.UseScim ||
|
||||||
|
_scimContext.ScimConfiguration == null || !_scimContext.ScimConfiguration.Enabled)
|
||||||
|
{
|
||||||
|
Logger.LogInformation("Org {organizationId} not able to use Scim.", _scimContext.OrganizationId);
|
||||||
|
return AuthenticateResult.Fail("Invalid parameters");
|
||||||
|
}
|
||||||
|
|
||||||
|
var orgApiKey = (await _organizationApiKeyRepository
|
||||||
|
.GetManyByOrganizationIdTypeAsync(_scimContext.Organization.Id, OrganizationApiKeyType.Scim))
|
||||||
|
.FirstOrDefault();
|
||||||
|
if (orgApiKey?.ApiKey != apiKey)
|
||||||
|
{
|
||||||
|
Logger.LogWarning("An API request was received with an invalid API key: {apiKey}", apiKey);
|
||||||
|
return AuthenticateResult.Fail("Invalid parameters");
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.LogInformation("Org {organizationId} authenticated", _scimContext.OrganizationId);
|
||||||
|
|
||||||
|
var claims = new[]
|
||||||
|
{
|
||||||
|
new Claim(JwtClaimTypes.ClientId, $"organization.{_scimContext.OrganizationId.Value}"),
|
||||||
|
new Claim("client_sub", _scimContext.OrganizationId.Value.ToString()),
|
||||||
|
new Claim(JwtClaimTypes.Scope, "api.scim"),
|
||||||
|
};
|
||||||
|
var identity = new ClaimsIdentity(claims, nameof(ApiKeyAuthenticationHandler));
|
||||||
|
var ticket = new AuthenticationTicket(new ClaimsPrincipal(identity),
|
||||||
|
ApiKeyAuthenticationOptions.DefaultScheme);
|
||||||
|
|
||||||
|
return AuthenticateResult.Success(ticket);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
using Microsoft.AspNetCore.Authentication;
|
||||||
|
|
||||||
|
namespace Bit.Scim.Utilities
|
||||||
|
{
|
||||||
|
public class ApiKeyAuthenticationOptions : AuthenticationSchemeOptions
|
||||||
|
{
|
||||||
|
public const string DefaultScheme = "ScimApiKey";
|
||||||
|
}
|
||||||
|
}
|
10
bitwarden_license/src/Scim/Utilities/ScimConstants.cs
Normal file
10
bitwarden_license/src/Scim/Utilities/ScimConstants.cs
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
namespace Bit.Scim.Utilities
|
||||||
|
{
|
||||||
|
public static class ScimConstants
|
||||||
|
{
|
||||||
|
public const string Scim2SchemaListResponse = "urn:ietf:params:scim:api:messages:2.0:ListResponse";
|
||||||
|
public const string Scim2SchemaError = "urn:ietf:params:scim:api:messages:2.0:Error";
|
||||||
|
public const string Scim2SchemaUser = "urn:ietf:params:scim:schemas:core:2.0:User";
|
||||||
|
public const string Scim2SchemaGroup = "urn:ietf:params:scim:schemas:core:2.0:Group";
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
using Bit.Core.Repositories;
|
||||||
|
using Bit.Core.Settings;
|
||||||
|
using Bit.Scim.Context;
|
||||||
|
|
||||||
|
namespace Bit.Scim.Utilities
|
||||||
|
{
|
||||||
|
public class ScimContextMiddleware
|
||||||
|
{
|
||||||
|
private readonly RequestDelegate _next;
|
||||||
|
|
||||||
|
public ScimContextMiddleware(RequestDelegate next)
|
||||||
|
{
|
||||||
|
_next = next;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Invoke(HttpContext httpContext, IScimContext scimContext, GlobalSettings globalSettings,
|
||||||
|
IOrganizationRepository organizationRepository, IOrganizationConnectionRepository organizationConnectionRepository)
|
||||||
|
{
|
||||||
|
await scimContext.BuildAsync(httpContext, globalSettings, organizationRepository, organizationConnectionRepository);
|
||||||
|
await _next.Invoke(httpContext);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
35
bitwarden_license/src/Scim/appsettings.Development.json
Normal file
35
bitwarden_license/src/Scim/appsettings.Development.json
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"globalSettings": {
|
||||||
|
"baseServiceUri": {
|
||||||
|
"vault": "https://localhost:8080",
|
||||||
|
"api": "http://localhost:4000",
|
||||||
|
"identity": "http://localhost:33656",
|
||||||
|
"admin": "http://localhost:62911",
|
||||||
|
"notifications": "http://localhost:61840",
|
||||||
|
"sso": "http://localhost:51822",
|
||||||
|
"internalNotifications": "http://localhost:61840",
|
||||||
|
"internalAdmin": "http://localhost:62911",
|
||||||
|
"internalIdentity": "http://localhost:33656",
|
||||||
|
"internalApi": "http://localhost:4000",
|
||||||
|
"internalVault": "https://localhost:8080",
|
||||||
|
"internalSso": "http://localhost:51822",
|
||||||
|
"internalScim": "http://localhost:44559"
|
||||||
|
},
|
||||||
|
"mail": {
|
||||||
|
"smtp": {
|
||||||
|
"host": "localhost",
|
||||||
|
"port": 10250
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"attachment": {
|
||||||
|
"connectionString": "UseDevelopmentStorage=true",
|
||||||
|
"baseUrl": "http://localhost:4000/attachments/"
|
||||||
|
},
|
||||||
|
"events": {
|
||||||
|
"connectionString": "UseDevelopmentStorage=true"
|
||||||
|
},
|
||||||
|
"storage": {
|
||||||
|
"connectionString": "UseDevelopmentStorage=true"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
42
bitwarden_license/src/Scim/appsettings.Production.json
Normal file
42
bitwarden_license/src/Scim/appsettings.Production.json
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
{
|
||||||
|
"globalSettings": {
|
||||||
|
"baseServiceUri": {
|
||||||
|
"vault": "https://vault.bitwarden.com",
|
||||||
|
"api": "https://api.bitwarden.com",
|
||||||
|
"identity": "https://identity.bitwarden.com",
|
||||||
|
"admin": "https://admin.bitwarden.com",
|
||||||
|
"notifications": "https://notifications.bitwarden.com",
|
||||||
|
"sso": "https://sso.bitwarden.com",
|
||||||
|
"internalNotifications": "https://notifications.bitwarden.com",
|
||||||
|
"internalAdmin": "https://admin.bitwarden.com",
|
||||||
|
"internalIdentity": "https://identity.bitwarden.com",
|
||||||
|
"internalApi": "https://api.bitwarden.com",
|
||||||
|
"internalVault": "https://vault.bitwarden.com",
|
||||||
|
"internalSso": "https://sso.bitwarden.com",
|
||||||
|
"internalScim": "https://scim.bitwarden.com"
|
||||||
|
},
|
||||||
|
"braintree": {
|
||||||
|
"production": true
|
||||||
|
},
|
||||||
|
"bitPay": {
|
||||||
|
"production": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Logging": {
|
||||||
|
"IncludeScopes": false,
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Debug",
|
||||||
|
"System": "Information",
|
||||||
|
"Microsoft": "Information"
|
||||||
|
},
|
||||||
|
"Console": {
|
||||||
|
"IncludeScopes": true,
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Warning",
|
||||||
|
"System": "Warning",
|
||||||
|
"Microsoft": "Warning",
|
||||||
|
"Microsoft.Hosting.Lifetime": "Information"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
42
bitwarden_license/src/Scim/appsettings.QA.json
Normal file
42
bitwarden_license/src/Scim/appsettings.QA.json
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
{
|
||||||
|
"globalSettings": {
|
||||||
|
"baseServiceUri": {
|
||||||
|
"vault": "https://vault.qa.bitwarden.pw",
|
||||||
|
"api": "https://api.qa.bitwarden.pw",
|
||||||
|
"identity": "https://identity.qa.bitwarden.pw",
|
||||||
|
"admin": "https://admin.qa.bitwarden.pw",
|
||||||
|
"notifications": "https://notifications.qa.bitwarden.pw",
|
||||||
|
"sso": "https://sso.qa.bitwarden.pw",
|
||||||
|
"internalNotifications": "https://notifications.qa.bitwarden.pw",
|
||||||
|
"internalAdmin": "https://admin.qa.bitwarden.pw",
|
||||||
|
"internalIdentity": "https://identity.qa.bitwarden.pw",
|
||||||
|
"internalApi": "https://api.qa.bitwarden.pw",
|
||||||
|
"internalVault": "https://vault.qa.bitwarden.pw",
|
||||||
|
"internalSso": "https://sso.qa.bitwarden.pw",
|
||||||
|
"internalScim": "https://scim.qa.bitwarden.pw"
|
||||||
|
},
|
||||||
|
"braintree": {
|
||||||
|
"production": false
|
||||||
|
},
|
||||||
|
"bitPay": {
|
||||||
|
"production": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Logging": {
|
||||||
|
"IncludeScopes": false,
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Debug",
|
||||||
|
"System": "Information",
|
||||||
|
"Microsoft": "Information"
|
||||||
|
},
|
||||||
|
"Console": {
|
||||||
|
"IncludeScopes": true,
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Debug",
|
||||||
|
"System": "Debug",
|
||||||
|
"Microsoft": "Debug",
|
||||||
|
"Microsoft.Hosting.Lifetime": "Information"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
63
bitwarden_license/src/Scim/appsettings.json
Normal file
63
bitwarden_license/src/Scim/appsettings.json
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
{
|
||||||
|
"globalSettings": {
|
||||||
|
"selfHosted": false,
|
||||||
|
"siteName": "Bitwarden",
|
||||||
|
"projectName": "Scim",
|
||||||
|
"stripe": {
|
||||||
|
"apiKey": "SECRET"
|
||||||
|
},
|
||||||
|
"sqlServer": {
|
||||||
|
"connectionString": "SECRET"
|
||||||
|
},
|
||||||
|
"mail": {
|
||||||
|
"sendGridApiKey": "SECRET",
|
||||||
|
"amazonConfigSetName": "Email",
|
||||||
|
"replyToEmail": "no-reply@bitwarden.com"
|
||||||
|
},
|
||||||
|
"identityServer": {
|
||||||
|
"certificateThumbprint": "SECRET"
|
||||||
|
},
|
||||||
|
"dataProtection": {
|
||||||
|
"certificateThumbprint": "SECRET"
|
||||||
|
},
|
||||||
|
"storage": {
|
||||||
|
"connectionString": "SECRET"
|
||||||
|
},
|
||||||
|
"events": {
|
||||||
|
"connectionString": "SECRET"
|
||||||
|
},
|
||||||
|
"serviceBus": {
|
||||||
|
"connectionString": "SECRET",
|
||||||
|
"applicationCacheTopicName": "SECRET"
|
||||||
|
},
|
||||||
|
"documentDb": {
|
||||||
|
"uri": "SECRET",
|
||||||
|
"key": "SECRET"
|
||||||
|
},
|
||||||
|
"sentry": {
|
||||||
|
"dsn": "SECRET"
|
||||||
|
},
|
||||||
|
"notificationHub": {
|
||||||
|
"connectionString": "SECRET",
|
||||||
|
"hubName": "SECRET"
|
||||||
|
},
|
||||||
|
"braintree": {
|
||||||
|
"production": false,
|
||||||
|
"merchantId": "SECRET",
|
||||||
|
"publicKey": "SECRET",
|
||||||
|
"privateKey": "SECRET"
|
||||||
|
},
|
||||||
|
"bitPay": {
|
||||||
|
"production": false,
|
||||||
|
"token": "SECRET",
|
||||||
|
"notificationUrl": "https://bitwarden.com/SECRET"
|
||||||
|
},
|
||||||
|
"amazon": {
|
||||||
|
"accessKeyId": "SECRET",
|
||||||
|
"accessKeySecret": "SECRET",
|
||||||
|
"region": "SECRET"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scimSettings": {
|
||||||
|
}
|
||||||
|
}
|
12
bitwarden_license/src/Scim/build.ps1
Normal file
12
bitwarden_license/src/Scim/build.ps1
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
$dir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||||
|
|
||||||
|
echo "`n## Building Scim"
|
||||||
|
|
||||||
|
echo "`nBuilding app"
|
||||||
|
echo ".NET Core version $(dotnet --version)"
|
||||||
|
echo "Restore"
|
||||||
|
dotnet restore $dir\Scim.csproj
|
||||||
|
echo "Clean"
|
||||||
|
dotnet clean $dir\Scim.csproj -c "Release" -o $dir\obj\Azure\publish
|
||||||
|
echo "Publish"
|
||||||
|
dotnet publish $dir\Scim.csproj -c "Release" -o $dir\obj\Azure\publish
|
15
bitwarden_license/src/Scim/build.sh
Normal file
15
bitwarden_license/src/Scim/build.sh
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && "pwd" )"
|
||||||
|
|
||||||
|
echo -e "\n## Building SCIM"
|
||||||
|
|
||||||
|
echo -e "\nBuilding app"
|
||||||
|
echo ".NET Core version $(dotnet --version)"
|
||||||
|
echo "Restore"
|
||||||
|
dotnet restore "$DIR/Scim.csproj"
|
||||||
|
echo "Clean"
|
||||||
|
dotnet clean "$DIR/Scim.csproj" -c "Release" -o "$DIR/obj/build-output/publish"
|
||||||
|
echo "Publish"
|
||||||
|
dotnet publish "$DIR/Scim.csproj" -c "Release" -o "$DIR/obj/build-output/publish"
|
41
bitwarden_license/src/Scim/entrypoint.sh
Normal file
41
bitwarden_license/src/Scim/entrypoint.sh
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Setup
|
||||||
|
|
||||||
|
GROUPNAME="bitwarden"
|
||||||
|
USERNAME="bitwarden"
|
||||||
|
|
||||||
|
LUID=${LOCAL_UID:-0}
|
||||||
|
LGID=${LOCAL_GID:-0}
|
||||||
|
|
||||||
|
# Step down from host root to well-known nobody/nogroup user
|
||||||
|
|
||||||
|
if [ $LUID -eq 0 ]
|
||||||
|
then
|
||||||
|
LUID=65534
|
||||||
|
fi
|
||||||
|
if [ $LGID -eq 0 ]
|
||||||
|
then
|
||||||
|
LGID=65534
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create user and group
|
||||||
|
|
||||||
|
groupadd -o -g $LGID $GROUPNAME >/dev/null 2>&1 ||
|
||||||
|
groupmod -o -g $LGID $GROUPNAME >/dev/null 2>&1
|
||||||
|
useradd -o -u $LUID -g $GROUPNAME -s /bin/false $USERNAME >/dev/null 2>&1 ||
|
||||||
|
usermod -o -u $LUID -g $GROUPNAME -s /bin/false $USERNAME >/dev/null 2>&1
|
||||||
|
mkhomedir_helper $USERNAME
|
||||||
|
|
||||||
|
# The rest...
|
||||||
|
|
||||||
|
chown -R $USERNAME:$GROUPNAME /app
|
||||||
|
mkdir -p /etc/bitwarden/core
|
||||||
|
mkdir -p /etc/bitwarden/logs
|
||||||
|
mkdir -p /etc/bitwarden/ca-certificates
|
||||||
|
chown -R $USERNAME:$GROUPNAME /etc/bitwarden
|
||||||
|
|
||||||
|
cp /etc/bitwarden/ca-certificates/*.crt /usr/local/share/ca-certificates/ >/dev/null 2>&1 \
|
||||||
|
&& update-ca-certificates
|
||||||
|
|
||||||
|
exec gosu $USERNAME:$GROUPNAME dotnet /app/Scim.dll
|
2930
bitwarden_license/src/Scim/packages.lock.json
Normal file
2930
bitwarden_license/src/Scim/packages.lock.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -12,7 +12,8 @@
|
|||||||
"internalIdentity": "http://localhost:33656",
|
"internalIdentity": "http://localhost:33656",
|
||||||
"internalApi": "http://localhost:4000",
|
"internalApi": "http://localhost:4000",
|
||||||
"internalVault": "https://localhost:8080",
|
"internalVault": "https://localhost:8080",
|
||||||
"internalSso": "http://localhost:51822"
|
"internalSso": "http://localhost:51822",
|
||||||
|
"internalScim": "http://localhost:44559"
|
||||||
},
|
},
|
||||||
"events": {
|
"events": {
|
||||||
"connectionString": "UseDevelopmentStorage=true"
|
"connectionString": "UseDevelopmentStorage=true"
|
||||||
|
@ -12,7 +12,8 @@
|
|||||||
"internalIdentity": "https://identity.bitwarden.com",
|
"internalIdentity": "https://identity.bitwarden.com",
|
||||||
"internalApi": "https://api.bitwarden.com",
|
"internalApi": "https://api.bitwarden.com",
|
||||||
"internalVault": "https://vault.bitwarden.com",
|
"internalVault": "https://vault.bitwarden.com",
|
||||||
"internalSso": "https://sso.bitwarden.com"
|
"internalSso": "https://sso.bitwarden.com",
|
||||||
|
"internalScim": "https://scim.bitwarden.com"
|
||||||
},
|
},
|
||||||
"braintree": {
|
"braintree": {
|
||||||
"production": true
|
"production": true
|
||||||
|
@ -12,7 +12,8 @@
|
|||||||
"internalIdentity": "https://identity.qa.bitwarden.pw",
|
"internalIdentity": "https://identity.qa.bitwarden.pw",
|
||||||
"internalApi": "https://api.qa.bitwarden.pw",
|
"internalApi": "https://api.qa.bitwarden.pw",
|
||||||
"internalVault": "https://vault.qa.bitwarden.pw",
|
"internalVault": "https://vault.qa.bitwarden.pw",
|
||||||
"internalSso": "https://sso.qa.bitwarden.pw"
|
"internalSso": "https://sso.qa.bitwarden.pw",
|
||||||
|
"internalScim": "https://scim.qa.bitwarden.pw"
|
||||||
},
|
},
|
||||||
"braintree": {
|
"braintree": {
|
||||||
"production": false
|
"production": false
|
||||||
|
@ -12,7 +12,8 @@
|
|||||||
"internalIdentity": null,
|
"internalIdentity": null,
|
||||||
"internalApi": null,
|
"internalApi": null,
|
||||||
"internalVault": null,
|
"internalVault": null,
|
||||||
"internalSso": null
|
"internalSso": null,
|
||||||
|
"internalScim": null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,7 @@ $projects = @{
|
|||||||
Identity = "../src/Identity"
|
Identity = "../src/Identity"
|
||||||
Notifications = "../src/Notifications"
|
Notifications = "../src/Notifications"
|
||||||
Sso = "../bitwarden_license/src/Sso"
|
Sso = "../bitwarden_license/src/Sso"
|
||||||
|
Scim = "../bitwarden_license/src/Scim"
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($key in $projects.keys) {
|
foreach ($key in $projects.keys) {
|
||||||
|
@ -32,6 +32,7 @@ namespace Bit.Admin.Models
|
|||||||
UsePolicies = org.UsePolicies;
|
UsePolicies = org.UsePolicies;
|
||||||
UseSso = org.UseSso;
|
UseSso = org.UseSso;
|
||||||
UseKeyConnector = org.UseKeyConnector;
|
UseKeyConnector = org.UseKeyConnector;
|
||||||
|
UseScim = org.UseScim;
|
||||||
UseGroups = org.UseGroups;
|
UseGroups = org.UseGroups;
|
||||||
UseDirectory = org.UseDirectory;
|
UseDirectory = org.UseDirectory;
|
||||||
UseEvents = org.UseEvents;
|
UseEvents = org.UseEvents;
|
||||||
@ -94,6 +95,8 @@ namespace Bit.Admin.Models
|
|||||||
public bool UseApi { get; set; }
|
public bool UseApi { get; set; }
|
||||||
[Display(Name = "Reset Password")]
|
[Display(Name = "Reset Password")]
|
||||||
public bool UseResetPassword { get; set; }
|
public bool UseResetPassword { get; set; }
|
||||||
|
[Display(Name = "SCIM")]
|
||||||
|
public bool UseScim { get; set; }
|
||||||
[Display(Name = "Self Host")]
|
[Display(Name = "Self Host")]
|
||||||
public bool SelfHost { get; set; }
|
public bool SelfHost { get; set; }
|
||||||
[Display(Name = "Users Get Premium")]
|
[Display(Name = "Users Get Premium")]
|
||||||
@ -126,6 +129,7 @@ namespace Bit.Admin.Models
|
|||||||
existingOrganization.UsePolicies = UsePolicies;
|
existingOrganization.UsePolicies = UsePolicies;
|
||||||
existingOrganization.UseSso = UseSso;
|
existingOrganization.UseSso = UseSso;
|
||||||
existingOrganization.UseKeyConnector = UseKeyConnector;
|
existingOrganization.UseKeyConnector = UseKeyConnector;
|
||||||
|
existingOrganization.UseScim = UseScim;
|
||||||
existingOrganization.UseGroups = UseGroups;
|
existingOrganization.UseGroups = UseGroups;
|
||||||
existingOrganization.UseDirectory = UseDirectory;
|
existingOrganization.UseDirectory = UseDirectory;
|
||||||
existingOrganization.UseEvents = UseEvents;
|
existingOrganization.UseEvents = UseEvents;
|
||||||
|
@ -32,6 +32,7 @@
|
|||||||
document.getElementById('@(nameof(Model.UseApi))').checked = true;
|
document.getElementById('@(nameof(Model.UseApi))').checked = true;
|
||||||
document.getElementById('@(nameof(Model.SelfHost))').checked = false;
|
document.getElementById('@(nameof(Model.SelfHost))').checked = false;
|
||||||
document.getElementById('@(nameof(Model.UseResetPassword))').checked = false;
|
document.getElementById('@(nameof(Model.UseResetPassword))').checked = false;
|
||||||
|
document.getElementById('@(nameof(Model.UseScim))').checked = false;
|
||||||
// Licensing
|
// Licensing
|
||||||
document.getElementById('@(nameof(Model.LicenseKey))').value = '@Model.RandomLicenseKey';
|
document.getElementById('@(nameof(Model.LicenseKey))').value = '@Model.RandomLicenseKey';
|
||||||
document.getElementById('@(nameof(Model.ExpirationDate))').value = '@Model.FourteenDayExpirationDate';
|
document.getElementById('@(nameof(Model.ExpirationDate))').value = '@Model.FourteenDayExpirationDate';
|
||||||
@ -65,6 +66,7 @@
|
|||||||
document.getElementById('@(nameof(Model.UseApi))').checked = true;
|
document.getElementById('@(nameof(Model.UseApi))').checked = true;
|
||||||
document.getElementById('@(nameof(Model.SelfHost))').checked = true;
|
document.getElementById('@(nameof(Model.SelfHost))').checked = true;
|
||||||
document.getElementById('@(nameof(Model.UseResetPassword))').checked = true;
|
document.getElementById('@(nameof(Model.UseResetPassword))').checked = true;
|
||||||
|
document.getElementById('@(nameof(Model.UseScim))').checked = true;
|
||||||
// Licensing
|
// Licensing
|
||||||
document.getElementById('@(nameof(Model.LicenseKey))').value = '@Model.RandomLicenseKey';
|
document.getElementById('@(nameof(Model.LicenseKey))').value = '@Model.RandomLicenseKey';
|
||||||
document.getElementById('@(nameof(Model.ExpirationDate))').value = '@Model.FourteenDayExpirationDate';
|
document.getElementById('@(nameof(Model.ExpirationDate))').value = '@Model.FourteenDayExpirationDate';
|
||||||
@ -219,6 +221,10 @@
|
|||||||
<input type="checkbox" class="form-check-input" asp-for="UseKeyConnector">
|
<input type="checkbox" class="form-check-input" asp-for="UseKeyConnector">
|
||||||
<label class="form-check-label" asp-for="UseKeyConnector"></label>
|
<label class="form-check-label" asp-for="UseKeyConnector"></label>
|
||||||
</div>
|
</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">
|
<div class="form-check">
|
||||||
<input type="checkbox" class="form-check-input" asp-for="UseDirectory">
|
<input type="checkbox" class="form-check-input" asp-for="UseDirectory">
|
||||||
<label class="form-check-label" asp-for="UseDirectory"></label>
|
<label class="form-check-label" asp-for="UseDirectory"></label>
|
||||||
|
@ -12,7 +12,8 @@
|
|||||||
"internalIdentity": "http://localhost:33656",
|
"internalIdentity": "http://localhost:33656",
|
||||||
"internalApi": "http://localhost:4000",
|
"internalApi": "http://localhost:4000",
|
||||||
"internalVault": "https://localhost:8080",
|
"internalVault": "https://localhost:8080",
|
||||||
"internalSso": "http://localhost:51822"
|
"internalSso": "http://localhost:51822",
|
||||||
|
"internalScim": "http://localhost:44559"
|
||||||
},
|
},
|
||||||
"mail": {
|
"mail": {
|
||||||
"smtp": {
|
"smtp": {
|
||||||
|
@ -12,7 +12,8 @@
|
|||||||
"internalIdentity": "https://identity.bitwarden.com",
|
"internalIdentity": "https://identity.bitwarden.com",
|
||||||
"internalApi": "https://api.bitwarden.com",
|
"internalApi": "https://api.bitwarden.com",
|
||||||
"internalVault": "https://vault.bitwarden.com",
|
"internalVault": "https://vault.bitwarden.com",
|
||||||
"internalSso": "https://sso.bitwarden.com"
|
"internalSso": "https://sso.bitwarden.com",
|
||||||
|
"internalScim": "https://scim.bitwarden.com"
|
||||||
},
|
},
|
||||||
"braintree": {
|
"braintree": {
|
||||||
"production": true
|
"production": true
|
||||||
|
@ -12,7 +12,8 @@
|
|||||||
"internalIdentity": "https://identity.qa.bitwarden.pw",
|
"internalIdentity": "https://identity.qa.bitwarden.pw",
|
||||||
"internalApi": "https://api.qa.bitwarden.pw",
|
"internalApi": "https://api.qa.bitwarden.pw",
|
||||||
"internalVault": "https://vault.qa.bitwarden.pw",
|
"internalVault": "https://vault.qa.bitwarden.pw",
|
||||||
"internalSso": "https://sso.qa.bitwarden.pw"
|
"internalSso": "https://sso.qa.bitwarden.pw",
|
||||||
|
"internalScim": "https://scim.qa.bitwarden.pw"
|
||||||
},
|
},
|
||||||
"braintree": {
|
"braintree": {
|
||||||
"production": false
|
"production": false
|
||||||
|
@ -12,7 +12,8 @@
|
|||||||
"internalIdentity": null,
|
"internalIdentity": null,
|
||||||
"internalApi": null,
|
"internalApi": null,
|
||||||
"internalVault": null,
|
"internalVault": null,
|
||||||
"internalSso": null
|
"internalSso": null,
|
||||||
|
"internalScim": null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,13 +9,11 @@ using Bit.Core.OrganizationFeatures.OrganizationConnections.Interfaces;
|
|||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Settings;
|
using Bit.Core.Settings;
|
||||||
using Bit.Core.Utilities;
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
namespace Bit.Api.Controllers
|
namespace Bit.Api.Controllers
|
||||||
{
|
{
|
||||||
[SelfHosted(SelfHostedOnly = true)]
|
|
||||||
[Authorize("Application")]
|
[Authorize("Application")]
|
||||||
[Route("organizations/connections")]
|
[Route("organizations/connections")]
|
||||||
public class OrganizationConnectionsController : Controller
|
public class OrganizationConnectionsController : Controller
|
||||||
@ -57,10 +55,10 @@ namespace Bit.Api.Controllers
|
|||||||
{
|
{
|
||||||
if (!await HasPermissionAsync(model?.OrganizationId))
|
if (!await HasPermissionAsync(model?.OrganizationId))
|
||||||
{
|
{
|
||||||
throw new BadRequestException("Only the owner of an organization can create a connection.");
|
throw new BadRequestException($"You do not have permission to create a connection of type {model.Type}.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (await HasConnectionTypeAsync(model))
|
if (await HasConnectionTypeAsync(model, null, model.Type))
|
||||||
{
|
{
|
||||||
throw new BadRequestException($"The requested organization already has a connection of type {model.Type}. Only one of each connection type may exist per organization.");
|
throw new BadRequestException($"The requested organization already has a connection of type {model.Type}. Only one of each connection type may exist per organization.");
|
||||||
}
|
}
|
||||||
@ -68,15 +66,9 @@ namespace Bit.Api.Controllers
|
|||||||
switch (model.Type)
|
switch (model.Type)
|
||||||
{
|
{
|
||||||
case OrganizationConnectionType.CloudBillingSync:
|
case OrganizationConnectionType.CloudBillingSync:
|
||||||
var typedModel = new OrganizationConnectionRequestModel<BillingSyncConfig>(model);
|
return await CreateOrUpdateOrganizationConnectionAsync<BillingSyncConfig>(null, model, ValidateBillingSyncConfig);
|
||||||
var license = await _licensingService.ReadOrganizationLicenseAsync(model.OrganizationId);
|
case OrganizationConnectionType.Scim:
|
||||||
if (!_licensingService.VerifyLicense(license))
|
return await CreateOrUpdateOrganizationConnectionAsync<ScimConfig>(null, model);
|
||||||
{
|
|
||||||
throw new BadRequestException("Cannot verify license file.");
|
|
||||||
}
|
|
||||||
typedModel.ParsedConfig.CloudOrganizationId = license.Id;
|
|
||||||
var connection = await _createOrganizationConnectionCommand.CreateAsync(typedModel.ToData());
|
|
||||||
return new OrganizationConnectionResponseModel(connection, typeof(BillingSyncConfig));
|
|
||||||
default:
|
default:
|
||||||
throw new BadRequestException($"Unknown Organization connection Type: {model.Type}");
|
throw new BadRequestException($"Unknown Organization connection Type: {model.Type}");
|
||||||
}
|
}
|
||||||
@ -91,12 +83,12 @@ namespace Bit.Api.Controllers
|
|||||||
throw new NotFoundException();
|
throw new NotFoundException();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!await HasPermissionAsync(model?.OrganizationId))
|
if (!await HasPermissionAsync(model?.OrganizationId, model?.Type))
|
||||||
{
|
{
|
||||||
throw new BadRequestException("Only the owner of an organization can update a connection.");
|
throw new BadRequestException("You do not have permission to update this connection.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (await HasConnectionTypeAsync(model, organizationConnectionId))
|
if (await HasConnectionTypeAsync(model, organizationConnectionId, model.Type))
|
||||||
{
|
{
|
||||||
throw new BadRequestException($"The requested organization already has a connection of type {model.Type}. Only one of each connection type may exist per organization.");
|
throw new BadRequestException($"The requested organization already has a connection of type {model.Type}. Only one of each connection type may exist per organization.");
|
||||||
}
|
}
|
||||||
@ -104,11 +96,9 @@ namespace Bit.Api.Controllers
|
|||||||
switch (model.Type)
|
switch (model.Type)
|
||||||
{
|
{
|
||||||
case OrganizationConnectionType.CloudBillingSync:
|
case OrganizationConnectionType.CloudBillingSync:
|
||||||
var typedModel = new OrganizationConnectionRequestModel<BillingSyncConfig>(model);
|
return await CreateOrUpdateOrganizationConnectionAsync<BillingSyncConfig>(organizationConnectionId, model);
|
||||||
// We don't allow overwriting or changing the CloudOrganizationId so save it from the existing connection
|
case OrganizationConnectionType.Scim:
|
||||||
typedModel.ParsedConfig.CloudOrganizationId = existingOrganizationConnection.GetConfig<BillingSyncConfig>().CloudOrganizationId;
|
return await CreateOrUpdateOrganizationConnectionAsync<ScimConfig>(organizationConnectionId, model);
|
||||||
var connection = await _updateOrganizationConnectionCommand.UpdateAsync(typedModel.ToData(organizationConnectionId));
|
|
||||||
return new OrganizationConnectionResponseModel(connection, typeof(BillingSyncConfig));
|
|
||||||
default:
|
default:
|
||||||
throw new BadRequestException($"Unkown Organization connection Type: {model.Type}");
|
throw new BadRequestException($"Unkown Organization connection Type: {model.Type}");
|
||||||
}
|
}
|
||||||
@ -117,22 +107,27 @@ namespace Bit.Api.Controllers
|
|||||||
[HttpGet("{organizationId}/{type}")]
|
[HttpGet("{organizationId}/{type}")]
|
||||||
public async Task<OrganizationConnectionResponseModel> GetConnection(Guid organizationId, OrganizationConnectionType type)
|
public async Task<OrganizationConnectionResponseModel> GetConnection(Guid organizationId, OrganizationConnectionType type)
|
||||||
{
|
{
|
||||||
if (!await HasPermissionAsync(organizationId))
|
if (!await HasPermissionAsync(organizationId, type))
|
||||||
{
|
{
|
||||||
throw new BadRequestException("Only the owner of an organization can retrieve a connection.");
|
throw new BadRequestException($"You do not have permission to retrieve a connection of type {type}.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var connections = await GetConnectionsAsync(organizationId);
|
var connections = await GetConnectionsAsync(organizationId, type);
|
||||||
var connection = connections.FirstOrDefault(c => c.Type == type);
|
var connection = connections.FirstOrDefault(c => c.Type == type);
|
||||||
|
|
||||||
switch (type)
|
switch (type)
|
||||||
{
|
{
|
||||||
case OrganizationConnectionType.CloudBillingSync:
|
case OrganizationConnectionType.CloudBillingSync:
|
||||||
|
if (!_globalSettings.SelfHosted)
|
||||||
|
{
|
||||||
|
throw new BadRequestException($"Cannot get a {type} connection outside of a self-hosted instance.");
|
||||||
|
}
|
||||||
return new OrganizationConnectionResponseModel(connection, typeof(BillingSyncConfig));
|
return new OrganizationConnectionResponseModel(connection, typeof(BillingSyncConfig));
|
||||||
|
case OrganizationConnectionType.Scim:
|
||||||
|
return new OrganizationConnectionResponseModel(connection, typeof(ScimConfig));
|
||||||
default:
|
default:
|
||||||
throw new BadRequestException($"Unkown Organization connection Type: {type}");
|
throw new BadRequestException($"Unkown Organization connection Type: {type}");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpDelete("{organizationConnectionId}")]
|
[HttpDelete("{organizationConnectionId}")]
|
||||||
@ -146,25 +141,70 @@ namespace Bit.Api.Controllers
|
|||||||
throw new NotFoundException();
|
throw new NotFoundException();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!await HasPermissionAsync(connection.OrganizationId))
|
if (!await HasPermissionAsync(connection.OrganizationId, connection.Type))
|
||||||
{
|
{
|
||||||
throw new BadRequestException("Only the owner of an organization can remove a connection.");
|
throw new BadRequestException($"You do not have permission to remove this connection of type {connection.Type}.");
|
||||||
}
|
}
|
||||||
|
|
||||||
await _deleteOrganizationConnectionCommand.DeleteAsync(connection);
|
await _deleteOrganizationConnectionCommand.DeleteAsync(connection);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<ICollection<OrganizationConnection>> GetConnectionsAsync(Guid organizationId) =>
|
private async Task<ICollection<OrganizationConnection>> GetConnectionsAsync(Guid organizationId, OrganizationConnectionType type) =>
|
||||||
await _organizationConnectionRepository.GetByOrganizationIdTypeAsync(organizationId, OrganizationConnectionType.CloudBillingSync);
|
await _organizationConnectionRepository.GetByOrganizationIdTypeAsync(organizationId, type);
|
||||||
|
|
||||||
private async Task<bool> HasConnectionTypeAsync(OrganizationConnectionRequestModel model, Guid? connectionId = null)
|
private async Task<bool> HasConnectionTypeAsync(OrganizationConnectionRequestModel model, Guid? connectionId,
|
||||||
|
OrganizationConnectionType type)
|
||||||
{
|
{
|
||||||
var existingConnections = await GetConnectionsAsync(model.OrganizationId);
|
var existingConnections = await GetConnectionsAsync(model.OrganizationId, type);
|
||||||
|
|
||||||
return existingConnections.Any(c => c.Type == model.Type && (!connectionId.HasValue || c.Id != connectionId.Value));
|
return existingConnections.Any(c => c.Type == model.Type && (!connectionId.HasValue || c.Id != connectionId.Value));
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<bool> HasPermissionAsync(Guid? organizationId) =>
|
private async Task<bool> HasPermissionAsync(Guid? organizationId, OrganizationConnectionType? type = null)
|
||||||
organizationId.HasValue && await _currentContext.OrganizationOwner(organizationId.Value);
|
{
|
||||||
|
if (!organizationId.HasValue)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return type switch
|
||||||
|
{
|
||||||
|
OrganizationConnectionType.Scim => await _currentContext.ManageScim(organizationId.Value),
|
||||||
|
_ => await _currentContext.OrganizationOwner(organizationId.Value),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ValidateBillingSyncConfig(OrganizationConnectionRequestModel<BillingSyncConfig> typedModel)
|
||||||
|
{
|
||||||
|
if (!_globalSettings.SelfHosted)
|
||||||
|
{
|
||||||
|
throw new BadRequestException($"Cannot create a {typedModel.Type} connection outside of a self-hosted instance.");
|
||||||
|
}
|
||||||
|
var license = await _licensingService.ReadOrganizationLicenseAsync(typedModel.OrganizationId);
|
||||||
|
if (!_licensingService.VerifyLicense(license))
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Cannot verify license file.");
|
||||||
|
}
|
||||||
|
typedModel.ParsedConfig.CloudOrganizationId = license.Id;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<OrganizationConnectionResponseModel> CreateOrUpdateOrganizationConnectionAsync<T>(
|
||||||
|
Guid? organizationConnectionId,
|
||||||
|
OrganizationConnectionRequestModel model,
|
||||||
|
Func<OrganizationConnectionRequestModel<T>, Task> validateAction = null)
|
||||||
|
where T : new()
|
||||||
|
{
|
||||||
|
var typedModel = new OrganizationConnectionRequestModel<T>(model);
|
||||||
|
if (validateAction != null)
|
||||||
|
{
|
||||||
|
await validateAction(typedModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
var data = typedModel.ToData(organizationConnectionId);
|
||||||
|
var connection = organizationConnectionId.HasValue
|
||||||
|
? await _updateOrganizationConnectionCommand.UpdateAsync(data)
|
||||||
|
: await _createOrganizationConnectionCommand.CreateAsync(data);
|
||||||
|
|
||||||
|
return new OrganizationConnectionResponseModel(connection, typeof(T));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -493,7 +493,7 @@ namespace Bit.Api.Controllers
|
|||||||
public async Task<ApiKeyResponseModel> ApiKey(string id, [FromBody] OrganizationApiKeyRequestModel model)
|
public async Task<ApiKeyResponseModel> ApiKey(string id, [FromBody] OrganizationApiKeyRequestModel model)
|
||||||
{
|
{
|
||||||
var orgIdGuid = new Guid(id);
|
var orgIdGuid = new Guid(id);
|
||||||
if (!await _currentContext.OrganizationOwner(orgIdGuid))
|
if (!await HasApiKeyAccessAsync(orgIdGuid, model.Type))
|
||||||
{
|
{
|
||||||
throw new NotFoundException();
|
throw new NotFoundException();
|
||||||
}
|
}
|
||||||
@ -504,9 +504,9 @@ namespace Bit.Api.Controllers
|
|||||||
throw new NotFoundException();
|
throw new NotFoundException();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (model.Type == OrganizationApiKeyType.BillingSync)
|
if (model.Type == OrganizationApiKeyType.BillingSync || model.Type == OrganizationApiKeyType.Scim)
|
||||||
{
|
{
|
||||||
// Non-enterprise orgs should not be able to create or view an apikey of billing sync key type
|
// Non-enterprise orgs should not be able to create or view an apikey of billing sync/scim key types
|
||||||
var plan = StaticStore.GetPlan(organization.PlanType);
|
var plan = StaticStore.GetPlan(organization.PlanType);
|
||||||
if (plan.Product != ProductType.Enterprise)
|
if (plan.Product != ProductType.Enterprise)
|
||||||
{
|
{
|
||||||
@ -523,7 +523,8 @@ namespace Bit.Api.Controllers
|
|||||||
throw new UnauthorizedAccessException();
|
throw new UnauthorizedAccessException();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!await _userService.VerifySecretAsync(user, model.Secret))
|
if (model.Type != OrganizationApiKeyType.Scim
|
||||||
|
&& !await _userService.VerifySecretAsync(user, model.Secret))
|
||||||
{
|
{
|
||||||
await Task.Delay(2000);
|
await Task.Delay(2000);
|
||||||
throw new BadRequestException("MasterPasswordHash", "Invalid password.");
|
throw new BadRequestException("MasterPasswordHash", "Invalid password.");
|
||||||
@ -535,15 +536,15 @@ namespace Bit.Api.Controllers
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{id}/api-key-information")]
|
[HttpGet("{id}/api-key-information/{type?}")]
|
||||||
public async Task<ListResponseModel<OrganizationApiKeyInformation>> ApiKeyInformation(Guid id)
|
public async Task<ListResponseModel<OrganizationApiKeyInformation>> ApiKeyInformation(Guid id, OrganizationApiKeyType? type)
|
||||||
{
|
{
|
||||||
if (!await _currentContext.OrganizationOwner(id))
|
if (!await HasApiKeyAccessAsync(id, type))
|
||||||
{
|
{
|
||||||
throw new NotFoundException();
|
throw new NotFoundException();
|
||||||
}
|
}
|
||||||
|
|
||||||
var apiKeys = await _organizationApiKeyRepository.GetManyByOrganizationIdTypeAsync(id);
|
var apiKeys = await _organizationApiKeyRepository.GetManyByOrganizationIdTypeAsync(id, type);
|
||||||
|
|
||||||
return new ListResponseModel<OrganizationApiKeyInformation>(
|
return new ListResponseModel<OrganizationApiKeyInformation>(
|
||||||
apiKeys.Select(k => new OrganizationApiKeyInformation(k)));
|
apiKeys.Select(k => new OrganizationApiKeyInformation(k)));
|
||||||
@ -553,7 +554,7 @@ namespace Bit.Api.Controllers
|
|||||||
public async Task<ApiKeyResponseModel> RotateApiKey(string id, [FromBody] OrganizationApiKeyRequestModel model)
|
public async Task<ApiKeyResponseModel> RotateApiKey(string id, [FromBody] OrganizationApiKeyRequestModel model)
|
||||||
{
|
{
|
||||||
var orgIdGuid = new Guid(id);
|
var orgIdGuid = new Guid(id);
|
||||||
if (!await _currentContext.OrganizationOwner(orgIdGuid))
|
if (!await HasApiKeyAccessAsync(orgIdGuid, model.Type))
|
||||||
{
|
{
|
||||||
throw new NotFoundException();
|
throw new NotFoundException();
|
||||||
}
|
}
|
||||||
@ -573,7 +574,8 @@ namespace Bit.Api.Controllers
|
|||||||
throw new UnauthorizedAccessException();
|
throw new UnauthorizedAccessException();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!await _userService.VerifySecretAsync(user, model.Secret))
|
if (model.Type != OrganizationApiKeyType.Scim
|
||||||
|
&& !await _userService.VerifySecretAsync(user, model.Secret))
|
||||||
{
|
{
|
||||||
await Task.Delay(2000);
|
await Task.Delay(2000);
|
||||||
throw new BadRequestException("MasterPasswordHash", "Invalid password.");
|
throw new BadRequestException("MasterPasswordHash", "Invalid password.");
|
||||||
@ -586,6 +588,15 @@ namespace Bit.Api.Controllers
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<bool> HasApiKeyAccessAsync(Guid orgId, OrganizationApiKeyType? type)
|
||||||
|
{
|
||||||
|
return type switch
|
||||||
|
{
|
||||||
|
OrganizationApiKeyType.Scim => await _currentContext.ManageScim(orgId),
|
||||||
|
_ => await _currentContext.OrganizationOwner(orgId),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
[HttpGet("{id}/tax")]
|
[HttpGet("{id}/tax")]
|
||||||
[SelfHosted(NotSelfHostedOnly = true)]
|
[SelfHosted(NotSelfHostedOnly = true)]
|
||||||
public async Task<TaxInfoResponseModel> GetTaxInfo(string id)
|
public async Task<TaxInfoResponseModel> GetTaxInfo(string id)
|
||||||
|
@ -35,6 +35,7 @@ namespace Bit.Api.Models.Response.Organizations
|
|||||||
UsePolicies = organization.UsePolicies;
|
UsePolicies = organization.UsePolicies;
|
||||||
UseSso = organization.UseSso;
|
UseSso = organization.UseSso;
|
||||||
UseKeyConnector = organization.UseKeyConnector;
|
UseKeyConnector = organization.UseKeyConnector;
|
||||||
|
UseScim = organization.UseScim;
|
||||||
UseGroups = organization.UseGroups;
|
UseGroups = organization.UseGroups;
|
||||||
UseDirectory = organization.UseDirectory;
|
UseDirectory = organization.UseDirectory;
|
||||||
UseEvents = organization.UseEvents;
|
UseEvents = organization.UseEvents;
|
||||||
@ -66,6 +67,7 @@ namespace Bit.Api.Models.Response.Organizations
|
|||||||
public bool UsePolicies { get; set; }
|
public bool UsePolicies { get; set; }
|
||||||
public bool UseSso { get; set; }
|
public bool UseSso { get; set; }
|
||||||
public bool UseKeyConnector { get; set; }
|
public bool UseKeyConnector { get; set; }
|
||||||
|
public bool UseScim { get; set; }
|
||||||
public bool UseGroups { get; set; }
|
public bool UseGroups { get; set; }
|
||||||
public bool UseDirectory { get; set; }
|
public bool UseDirectory { get; set; }
|
||||||
public bool UseEvents { get; set; }
|
public bool UseEvents { get; set; }
|
||||||
|
@ -17,6 +17,7 @@ namespace Bit.Api.Models.Response
|
|||||||
UsePolicies = organization.UsePolicies;
|
UsePolicies = organization.UsePolicies;
|
||||||
UseSso = organization.UseSso;
|
UseSso = organization.UseSso;
|
||||||
UseKeyConnector = organization.UseKeyConnector;
|
UseKeyConnector = organization.UseKeyConnector;
|
||||||
|
UseScim = organization.UseScim;
|
||||||
UseGroups = organization.UseGroups;
|
UseGroups = organization.UseGroups;
|
||||||
UseDirectory = organization.UseDirectory;
|
UseDirectory = organization.UseDirectory;
|
||||||
UseEvents = organization.UseEvents;
|
UseEvents = organization.UseEvents;
|
||||||
@ -63,6 +64,7 @@ namespace Bit.Api.Models.Response
|
|||||||
public bool UsePolicies { get; set; }
|
public bool UsePolicies { get; set; }
|
||||||
public bool UseSso { get; set; }
|
public bool UseSso { get; set; }
|
||||||
public bool UseKeyConnector { get; set; }
|
public bool UseKeyConnector { get; set; }
|
||||||
|
public bool UseScim { get; set; }
|
||||||
public bool UseGroups { get; set; }
|
public bool UseGroups { get; set; }
|
||||||
public bool UseDirectory { get; set; }
|
public bool UseDirectory { get; set; }
|
||||||
public bool UseEvents { get; set; }
|
public bool UseEvents { get; set; }
|
||||||
|
@ -13,6 +13,7 @@ namespace Bit.Api.Models.Response
|
|||||||
UsePolicies = organization.UsePolicies;
|
UsePolicies = organization.UsePolicies;
|
||||||
UseSso = organization.UseSso;
|
UseSso = organization.UseSso;
|
||||||
UseKeyConnector = organization.UseKeyConnector;
|
UseKeyConnector = organization.UseKeyConnector;
|
||||||
|
UseScim = organization.UseScim;
|
||||||
UseGroups = organization.UseGroups;
|
UseGroups = organization.UseGroups;
|
||||||
UseDirectory = organization.UseDirectory;
|
UseDirectory = organization.UseDirectory;
|
||||||
UseEvents = organization.UseEvents;
|
UseEvents = organization.UseEvents;
|
||||||
|
@ -12,7 +12,8 @@
|
|||||||
"internalIdentity": "http://localhost:33656",
|
"internalIdentity": "http://localhost:33656",
|
||||||
"internalApi": "http://localhost:4000",
|
"internalApi": "http://localhost:4000",
|
||||||
"internalVault": "https://localhost:8080",
|
"internalVault": "https://localhost:8080",
|
||||||
"internalSso": "http://localhost:51822"
|
"internalSso": "http://localhost:51822",
|
||||||
|
"internalScim": "http://localhost:44559"
|
||||||
},
|
},
|
||||||
"mail": {
|
"mail": {
|
||||||
"smtp": {
|
"smtp": {
|
||||||
|
@ -12,7 +12,8 @@
|
|||||||
"internalIdentity": "https://identity.bitwarden.com",
|
"internalIdentity": "https://identity.bitwarden.com",
|
||||||
"internalApi": "https://api.bitwarden.com",
|
"internalApi": "https://api.bitwarden.com",
|
||||||
"internalVault": "https://vault.bitwarden.com",
|
"internalVault": "https://vault.bitwarden.com",
|
||||||
"internalSso": "https://sso.bitwarden.com"
|
"internalSso": "https://sso.bitwarden.com",
|
||||||
|
"internalScim": "https://scim.bitwarden.com"
|
||||||
},
|
},
|
||||||
"braintree": {
|
"braintree": {
|
||||||
"production": true
|
"production": true
|
||||||
|
@ -12,7 +12,8 @@
|
|||||||
"internalIdentity": "https://identity.qa.bitwarden.pw",
|
"internalIdentity": "https://identity.qa.bitwarden.pw",
|
||||||
"internalApi": "https://api.qa.bitwarden.pw",
|
"internalApi": "https://api.qa.bitwarden.pw",
|
||||||
"internalVault": "https://vault.qa.bitwarden.pw",
|
"internalVault": "https://vault.qa.bitwarden.pw",
|
||||||
"internalSso": "https://sso.qa.bitwarden.pw"
|
"internalSso": "https://sso.qa.bitwarden.pw",
|
||||||
|
"internalScim": "https://scim.qa.bitwarden.pw"
|
||||||
},
|
},
|
||||||
"braintree": {
|
"braintree": {
|
||||||
"production": false
|
"production": false
|
||||||
|
@ -12,7 +12,8 @@
|
|||||||
"internalIdentity": null,
|
"internalIdentity": null,
|
||||||
"internalApi": null,
|
"internalApi": null,
|
||||||
"internalVault": null,
|
"internalVault": null,
|
||||||
"internalSso": null
|
"internalSso": null,
|
||||||
|
"internalScim": null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,8 @@
|
|||||||
"internalIdentity": "http://localhost:33656",
|
"internalIdentity": "http://localhost:33656",
|
||||||
"internalApi": "http://localhost:4000",
|
"internalApi": "http://localhost:4000",
|
||||||
"internalVault": "https://localhost:8080",
|
"internalVault": "https://localhost:8080",
|
||||||
"internalSso": "http://localhost:51822"
|
"internalSso": "http://localhost:51822",
|
||||||
|
"internalScim": "http://localhost:44559"
|
||||||
},
|
},
|
||||||
"mail": {
|
"mail": {
|
||||||
"smtp": {
|
"smtp": {
|
||||||
|
@ -12,7 +12,8 @@
|
|||||||
"internalIdentity": "https://identity.bitwarden.com",
|
"internalIdentity": "https://identity.bitwarden.com",
|
||||||
"internalApi": "https://api.bitwarden.com",
|
"internalApi": "https://api.bitwarden.com",
|
||||||
"internalVault": "https://vault.bitwarden.com",
|
"internalVault": "https://vault.bitwarden.com",
|
||||||
"internalSso": "https://sso.bitwarden.com"
|
"internalSso": "https://sso.bitwarden.com",
|
||||||
|
"internalScim": "https://scim.bitwarden.com"
|
||||||
},
|
},
|
||||||
"braintree": {
|
"braintree": {
|
||||||
"production": true
|
"production": true
|
||||||
|
@ -12,7 +12,8 @@
|
|||||||
"internalIdentity": "https://identity.qa.bitwarden.pw",
|
"internalIdentity": "https://identity.qa.bitwarden.pw",
|
||||||
"internalApi": "https://api.qa.bitwarden.pw",
|
"internalApi": "https://api.qa.bitwarden.pw",
|
||||||
"internalVault": "https://vault.qa.bitwarden.pw",
|
"internalVault": "https://vault.qa.bitwarden.pw",
|
||||||
"internalSso": "https://sso.qa.bitwarden.pw"
|
"internalSso": "https://sso.qa.bitwarden.pw",
|
||||||
|
"internalScim": "https://scim.qa.bitwarden.pw"
|
||||||
},
|
},
|
||||||
"braintree": {
|
"braintree": {
|
||||||
"production": false
|
"production": false
|
||||||
|
@ -344,6 +344,12 @@ namespace Bit.Core.Context
|
|||||||
&& (o.Permissions?.ManageSso ?? false)) ?? false);
|
&& (o.Permissions?.ManageSso ?? false)) ?? false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<bool> ManageScim(Guid orgId)
|
||||||
|
{
|
||||||
|
return await OrganizationAdmin(orgId) || (Organizations?.Any(o => o.Id == orgId
|
||||||
|
&& (o.Permissions?.ManageScim ?? false)) ?? false);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<bool> ManageUsers(Guid orgId)
|
public async Task<bool> ManageUsers(Guid orgId)
|
||||||
{
|
{
|
||||||
return await OrganizationAdmin(orgId) || (Organizations?.Any(o => o.Id == orgId
|
return await OrganizationAdmin(orgId) || (Organizations?.Any(o => o.Id == orgId
|
||||||
@ -469,7 +475,8 @@ namespace Bit.Core.Context
|
|||||||
ManagePolicies = hasClaim("managepolicies"),
|
ManagePolicies = hasClaim("managepolicies"),
|
||||||
ManageSso = hasClaim("managesso"),
|
ManageSso = hasClaim("managesso"),
|
||||||
ManageUsers = hasClaim("manageusers"),
|
ManageUsers = hasClaim("manageusers"),
|
||||||
ManageResetPassword = hasClaim("manageresetpassword")
|
ManageResetPassword = hasClaim("manageresetpassword"),
|
||||||
|
ManageScim = hasClaim("managescim"),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -47,6 +47,7 @@ namespace Bit.Core.Context
|
|||||||
Task<bool> ManagePolicies(Guid orgId);
|
Task<bool> ManagePolicies(Guid orgId);
|
||||||
Task<bool> ManageSso(Guid orgId);
|
Task<bool> ManageSso(Guid orgId);
|
||||||
Task<bool> ManageUsers(Guid orgId);
|
Task<bool> ManageUsers(Guid orgId);
|
||||||
|
Task<bool> ManageScim(Guid orgId);
|
||||||
Task<bool> ManageResetPassword(Guid orgId);
|
Task<bool> ManageResetPassword(Guid orgId);
|
||||||
Task<bool> ManageBilling(Guid orgId);
|
Task<bool> ManageBilling(Guid orgId);
|
||||||
Task<bool> ProviderUserForOrgAsync(Guid orgId);
|
Task<bool> ProviderUserForOrgAsync(Guid orgId);
|
||||||
|
@ -37,6 +37,7 @@ namespace Bit.Core.Entities
|
|||||||
public bool UsePolicies { get; set; }
|
public bool UsePolicies { get; set; }
|
||||||
public bool UseSso { get; set; }
|
public bool UseSso { get; set; }
|
||||||
public bool UseKeyConnector { get; set; }
|
public bool UseKeyConnector { get; set; }
|
||||||
|
public bool UseScim { get; set; }
|
||||||
public bool UseGroups { get; set; }
|
public bool UseGroups { get; set; }
|
||||||
public bool UseDirectory { get; set; }
|
public bool UseDirectory { get; set; }
|
||||||
public bool UseEvents { get; set; }
|
public bool UseEvents { get; set; }
|
||||||
|
@ -2,7 +2,8 @@
|
|||||||
{
|
{
|
||||||
public enum OrganizationApiKeyType : byte
|
public enum OrganizationApiKeyType : byte
|
||||||
{
|
{
|
||||||
Default,
|
Default = 0,
|
||||||
BillingSync,
|
BillingSync = 1,
|
||||||
|
Scim = 2,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,5 +3,6 @@
|
|||||||
public enum OrganizationConnectionType : byte
|
public enum OrganizationConnectionType : byte
|
||||||
{
|
{
|
||||||
CloudBillingSync = 1,
|
CloudBillingSync = 1,
|
||||||
|
Scim = 2,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
13
src/Core/Enums/ScimProviderType.cs
Normal file
13
src/Core/Enums/ScimProviderType.cs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
namespace Bit.Core.Enums
|
||||||
|
{
|
||||||
|
public enum ScimProviderType : byte
|
||||||
|
{
|
||||||
|
Default = 0,
|
||||||
|
AzureAd = 1,
|
||||||
|
Okta = 2,
|
||||||
|
OneLogin = 3,
|
||||||
|
JumpCloud = 4,
|
||||||
|
GoogleWorkspace = 5,
|
||||||
|
Rippling = 6,
|
||||||
|
}
|
||||||
|
}
|
@ -34,6 +34,7 @@ namespace Bit.Core.Models.Business
|
|||||||
UsePolicies = org.UsePolicies;
|
UsePolicies = org.UsePolicies;
|
||||||
UseSso = org.UseSso;
|
UseSso = org.UseSso;
|
||||||
UseKeyConnector = org.UseKeyConnector;
|
UseKeyConnector = org.UseKeyConnector;
|
||||||
|
UseScim = org.UseScim;
|
||||||
UseGroups = org.UseGroups;
|
UseGroups = org.UseGroups;
|
||||||
UseEvents = org.UseEvents;
|
UseEvents = org.UseEvents;
|
||||||
UseDirectory = org.UseDirectory;
|
UseDirectory = org.UseDirectory;
|
||||||
@ -105,6 +106,7 @@ namespace Bit.Core.Models.Business
|
|||||||
public bool UsePolicies { get; set; }
|
public bool UsePolicies { get; set; }
|
||||||
public bool UseSso { get; set; }
|
public bool UseSso { get; set; }
|
||||||
public bool UseKeyConnector { get; set; }
|
public bool UseKeyConnector { get; set; }
|
||||||
|
public bool UseScim { get; set; }
|
||||||
public bool UseGroups { get; set; }
|
public bool UseGroups { get; set; }
|
||||||
public bool UseEvents { get; set; }
|
public bool UseEvents { get; set; }
|
||||||
public bool UseDirectory { get; set; }
|
public bool UseDirectory { get; set; }
|
||||||
@ -129,10 +131,10 @@ namespace Bit.Core.Models.Business
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents the current version of the license format. Should be updated whenever new fields are added.
|
/// Represents the current version of the license format. Should be updated whenever new fields are added.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private const int CURRENT_LICENSE_FILE_VERSION = 8;
|
private const int CURRENT_LICENSE_FILE_VERSION = 10;
|
||||||
private bool ValidLicenseVersion
|
private bool ValidLicenseVersion
|
||||||
{
|
{
|
||||||
get => Version is >= 1 and <= 9;
|
get => Version is >= 1 and <= 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
public byte[] GetDataBytes(bool forHash = false)
|
public byte[] GetDataBytes(bool forHash = false)
|
||||||
@ -162,6 +164,8 @@ namespace Bit.Core.Models.Business
|
|||||||
(Version >= 8 || !p.Name.Equals(nameof(UseResetPassword))) &&
|
(Version >= 8 || !p.Name.Equals(nameof(UseResetPassword))) &&
|
||||||
// UseKeyConnector was added in Version 9
|
// UseKeyConnector was added in Version 9
|
||||||
(Version >= 9 || !p.Name.Equals(nameof(UseKeyConnector))) &&
|
(Version >= 9 || !p.Name.Equals(nameof(UseKeyConnector))) &&
|
||||||
|
// UseScim was added in Version 10
|
||||||
|
(Version >= 10 || !p.Name.Equals(nameof(UseScim))) &&
|
||||||
(
|
(
|
||||||
!forHash ||
|
!forHash ||
|
||||||
(
|
(
|
||||||
@ -270,6 +274,11 @@ namespace Bit.Core.Models.Business
|
|||||||
valid = organization.UseKeyConnector == UseKeyConnector;
|
valid = organization.UseKeyConnector == UseKeyConnector;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (valid && Version >= 10)
|
||||||
|
{
|
||||||
|
valid = organization.UseScim == UseScim;
|
||||||
|
}
|
||||||
|
|
||||||
return valid;
|
return valid;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
@ -17,6 +17,7 @@ namespace Bit.Core.Models.Data.Organizations
|
|||||||
Enabled = organization.Enabled;
|
Enabled = organization.Enabled;
|
||||||
UseSso = organization.UseSso;
|
UseSso = organization.UseSso;
|
||||||
UseKeyConnector = organization.UseKeyConnector;
|
UseKeyConnector = organization.UseKeyConnector;
|
||||||
|
UseScim = organization.UseScim;
|
||||||
UseResetPassword = organization.UseResetPassword;
|
UseResetPassword = organization.UseResetPassword;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -28,6 +29,7 @@ namespace Bit.Core.Models.Data.Organizations
|
|||||||
public bool Enabled { get; set; }
|
public bool Enabled { get; set; }
|
||||||
public bool UseSso { get; set; }
|
public bool UseSso { get; set; }
|
||||||
public bool UseKeyConnector { get; set; }
|
public bool UseKeyConnector { get; set; }
|
||||||
|
public bool UseScim { get; set; }
|
||||||
public bool UseResetPassword { get; set; }
|
public bool UseResetPassword { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
public bool UsePolicies { get; set; }
|
public bool UsePolicies { get; set; }
|
||||||
public bool UseSso { get; set; }
|
public bool UseSso { get; set; }
|
||||||
public bool UseKeyConnector { get; set; }
|
public bool UseKeyConnector { get; set; }
|
||||||
|
public bool UseScim { get; set; }
|
||||||
public bool UseGroups { get; set; }
|
public bool UseGroups { get; set; }
|
||||||
public bool UseDirectory { get; set; }
|
public bool UseDirectory { get; set; }
|
||||||
public bool UseEvents { get; set; }
|
public bool UseEvents { get; set; }
|
||||||
|
@ -21,6 +21,7 @@ namespace Bit.Core.Models.Data
|
|||||||
public bool ManageSso { get; set; }
|
public bool ManageSso { get; set; }
|
||||||
public bool ManageUsers { get; set; }
|
public bool ManageUsers { get; set; }
|
||||||
public bool ManageResetPassword { get; set; }
|
public bool ManageResetPassword { get; set; }
|
||||||
|
public bool ManageScim { get; set; }
|
||||||
|
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public List<(bool Permission, string ClaimName)> ClaimsMap => new()
|
public List<(bool Permission, string ClaimName)> ClaimsMap => new()
|
||||||
@ -38,6 +39,7 @@ namespace Bit.Core.Models.Data
|
|||||||
(ManageSso, "managesso"),
|
(ManageSso, "managesso"),
|
||||||
(ManageUsers, "manageusers"),
|
(ManageUsers, "manageusers"),
|
||||||
(ManageResetPassword, "manageresetpassword"),
|
(ManageResetPassword, "manageresetpassword"),
|
||||||
|
(ManageScim, "managescim"),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,7 @@ namespace Bit.Core.Models.Data
|
|||||||
public bool UsePolicies { get; set; }
|
public bool UsePolicies { get; set; }
|
||||||
public bool UseSso { get; set; }
|
public bool UseSso { get; set; }
|
||||||
public bool UseKeyConnector { get; set; }
|
public bool UseKeyConnector { get; set; }
|
||||||
|
public bool UseScim { get; set; }
|
||||||
public bool UseGroups { get; set; }
|
public bool UseGroups { get; set; }
|
||||||
public bool UseDirectory { get; set; }
|
public bool UseDirectory { get; set; }
|
||||||
public bool UseEvents { get; set; }
|
public bool UseEvents { get; set; }
|
||||||
|
12
src/Core/Models/OrganizationConnectionConfigs/ScimConfig.cs
Normal file
12
src/Core/Models/OrganizationConnectionConfigs/ScimConfig.cs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
|
||||||
|
namespace Bit.Core.Models.OrganizationConnectionConfigs
|
||||||
|
{
|
||||||
|
public class ScimConfig
|
||||||
|
{
|
||||||
|
public bool Enabled { get; set; }
|
||||||
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||||
|
public ScimProviderType? ScimProvider { get; set; }
|
||||||
|
}
|
||||||
|
}
|
@ -34,6 +34,7 @@ namespace Bit.Core.Models.StaticStore
|
|||||||
public bool HasApi { get; set; }
|
public bool HasApi { get; set; }
|
||||||
public bool HasSso { get; set; }
|
public bool HasSso { get; set; }
|
||||||
public bool HasKeyConnector { get; set; }
|
public bool HasKeyConnector { get; set; }
|
||||||
|
public bool HasScim { get; set; }
|
||||||
public bool HasResetPassword { get; set; }
|
public bool HasResetPassword { get; set; }
|
||||||
public bool UsersGetPremium { get; set; }
|
public bool UsersGetPremium { get; set; }
|
||||||
|
|
||||||
|
@ -6,6 +6,7 @@ using Bit.Core.Exceptions;
|
|||||||
using Bit.Core.Models.Business;
|
using Bit.Core.Models.Business;
|
||||||
using Bit.Core.Models.Data;
|
using Bit.Core.Models.Data;
|
||||||
using Bit.Core.Models.Data.Organizations.Policies;
|
using Bit.Core.Models.Data.Organizations.Policies;
|
||||||
|
using Bit.Core.Models.OrganizationConnectionConfigs;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Settings;
|
using Bit.Core.Settings;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
@ -39,6 +40,7 @@ namespace Bit.Core.Services
|
|||||||
private readonly IGlobalSettings _globalSettings;
|
private readonly IGlobalSettings _globalSettings;
|
||||||
private readonly ITaxRateRepository _taxRateRepository;
|
private readonly ITaxRateRepository _taxRateRepository;
|
||||||
private readonly IOrganizationApiKeyRepository _organizationApiKeyRepository;
|
private readonly IOrganizationApiKeyRepository _organizationApiKeyRepository;
|
||||||
|
private readonly IOrganizationConnectionRepository _organizationConnectionRepository;
|
||||||
private readonly ICurrentContext _currentContext;
|
private readonly ICurrentContext _currentContext;
|
||||||
private readonly ILogger<OrganizationService> _logger;
|
private readonly ILogger<OrganizationService> _logger;
|
||||||
|
|
||||||
@ -66,6 +68,7 @@ namespace Bit.Core.Services
|
|||||||
IGlobalSettings globalSettings,
|
IGlobalSettings globalSettings,
|
||||||
ITaxRateRepository taxRateRepository,
|
ITaxRateRepository taxRateRepository,
|
||||||
IOrganizationApiKeyRepository organizationApiKeyRepository,
|
IOrganizationApiKeyRepository organizationApiKeyRepository,
|
||||||
|
IOrganizationConnectionRepository organizationConnectionRepository,
|
||||||
ICurrentContext currentContext,
|
ICurrentContext currentContext,
|
||||||
ILogger<OrganizationService> logger)
|
ILogger<OrganizationService> logger)
|
||||||
{
|
{
|
||||||
@ -91,6 +94,7 @@ namespace Bit.Core.Services
|
|||||||
_globalSettings = globalSettings;
|
_globalSettings = globalSettings;
|
||||||
_taxRateRepository = taxRateRepository;
|
_taxRateRepository = taxRateRepository;
|
||||||
_organizationApiKeyRepository = organizationApiKeyRepository;
|
_organizationApiKeyRepository = organizationApiKeyRepository;
|
||||||
|
_organizationConnectionRepository = organizationConnectionRepository;
|
||||||
_currentContext = currentContext;
|
_currentContext = currentContext;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
@ -266,6 +270,17 @@ namespace Bit.Core.Services
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!newPlan.HasScim && organization.UseScim)
|
||||||
|
{
|
||||||
|
var scimConnections = await _organizationConnectionRepository.GetByOrganizationIdTypeAsync(organization.Id,
|
||||||
|
OrganizationConnectionType.Scim);
|
||||||
|
if (scimConnections != null && scimConnections.Any(c => c.GetConfig<ScimConfig>()?.Enabled == true))
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Your new plan does not allow the SCIM feature. " +
|
||||||
|
"Disable your SCIM configuration.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Check storage?
|
// TODO: Check storage?
|
||||||
|
|
||||||
string paymentIntentClientSecret = null;
|
string paymentIntentClientSecret = null;
|
||||||
@ -304,6 +319,7 @@ namespace Bit.Core.Services
|
|||||||
organization.UseApi = newPlan.HasApi;
|
organization.UseApi = newPlan.HasApi;
|
||||||
organization.UseSso = newPlan.HasSso;
|
organization.UseSso = newPlan.HasSso;
|
||||||
organization.UseKeyConnector = newPlan.HasKeyConnector;
|
organization.UseKeyConnector = newPlan.HasKeyConnector;
|
||||||
|
organization.UseScim = newPlan.HasScim;
|
||||||
organization.UseResetPassword = newPlan.HasResetPassword;
|
organization.UseResetPassword = newPlan.HasResetPassword;
|
||||||
organization.SelfHost = newPlan.HasSelfHost;
|
organization.SelfHost = newPlan.HasSelfHost;
|
||||||
organization.UsersGetPremium = newPlan.UsersGetPremium || upgrade.PremiumAccessAddon;
|
organization.UsersGetPremium = newPlan.UsersGetPremium || upgrade.PremiumAccessAddon;
|
||||||
@ -702,6 +718,7 @@ namespace Bit.Core.Services
|
|||||||
UsePolicies = license.UsePolicies,
|
UsePolicies = license.UsePolicies,
|
||||||
UseSso = license.UseSso,
|
UseSso = license.UseSso,
|
||||||
UseKeyConnector = license.UseKeyConnector,
|
UseKeyConnector = license.UseKeyConnector,
|
||||||
|
UseScim = license.UseScim,
|
||||||
UseGroups = license.UseGroups,
|
UseGroups = license.UseGroups,
|
||||||
UseDirectory = license.UseDirectory,
|
UseDirectory = license.UseDirectory,
|
||||||
UseEvents = license.UseEvents,
|
UseEvents = license.UseEvents,
|
||||||
@ -902,6 +919,17 @@ namespace Bit.Core.Services
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!license.UseScim && organization.UseScim)
|
||||||
|
{
|
||||||
|
var scimConnections = await _organizationConnectionRepository.GetByOrganizationIdTypeAsync(organization.Id,
|
||||||
|
OrganizationConnectionType.Scim);
|
||||||
|
if (scimConnections != null && scimConnections.Any(c => c.GetConfig<ScimConfig>()?.Enabled == true))
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Your new plan does not allow the SCIM feature. " +
|
||||||
|
"Disable your SCIM configuration.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!license.UseResetPassword && organization.UseResetPassword)
|
if (!license.UseResetPassword && organization.UseResetPassword)
|
||||||
{
|
{
|
||||||
var resetPasswordPolicy =
|
var resetPasswordPolicy =
|
||||||
@ -933,6 +961,7 @@ namespace Bit.Core.Services
|
|||||||
organization.UsePolicies = license.UsePolicies;
|
organization.UsePolicies = license.UsePolicies;
|
||||||
organization.UseSso = license.UseSso;
|
organization.UseSso = license.UseSso;
|
||||||
organization.UseKeyConnector = license.UseKeyConnector;
|
organization.UseKeyConnector = license.UseKeyConnector;
|
||||||
|
organization.UseScim = license.UseScim;
|
||||||
organization.UseResetPassword = license.UseResetPassword;
|
organization.UseResetPassword = license.UseResetPassword;
|
||||||
organization.SelfHost = license.SelfHost;
|
organization.SelfHost = license.SelfHost;
|
||||||
organization.UsersGetPremium = license.UsersGetPremium;
|
organization.UsersGetPremium = license.UsersGetPremium;
|
||||||
|
@ -117,12 +117,14 @@
|
|||||||
private string _admin;
|
private string _admin;
|
||||||
private string _notifications;
|
private string _notifications;
|
||||||
private string _sso;
|
private string _sso;
|
||||||
|
private string _scim;
|
||||||
private string _internalApi;
|
private string _internalApi;
|
||||||
private string _internalIdentity;
|
private string _internalIdentity;
|
||||||
private string _internalAdmin;
|
private string _internalAdmin;
|
||||||
private string _internalNotifications;
|
private string _internalNotifications;
|
||||||
private string _internalSso;
|
private string _internalSso;
|
||||||
private string _internalVault;
|
private string _internalVault;
|
||||||
|
private string _internalScim;
|
||||||
|
|
||||||
public BaseServiceUriSettings(GlobalSettings globalSettings)
|
public BaseServiceUriSettings(GlobalSettings globalSettings)
|
||||||
{
|
{
|
||||||
@ -157,6 +159,11 @@
|
|||||||
get => _globalSettings.BuildExternalUri(_sso, "sso");
|
get => _globalSettings.BuildExternalUri(_sso, "sso");
|
||||||
set => _sso = value;
|
set => _sso = value;
|
||||||
}
|
}
|
||||||
|
public string Scim
|
||||||
|
{
|
||||||
|
get => _globalSettings.BuildExternalUri(_scim, "scim");
|
||||||
|
set => _scim = value;
|
||||||
|
}
|
||||||
|
|
||||||
public string InternalNotifications
|
public string InternalNotifications
|
||||||
{
|
{
|
||||||
@ -188,6 +195,11 @@
|
|||||||
get => _globalSettings.BuildInternalUri(_internalSso, "sso");
|
get => _globalSettings.BuildInternalUri(_internalSso, "sso");
|
||||||
set => _internalSso = value;
|
set => _internalSso = value;
|
||||||
}
|
}
|
||||||
|
public string InternalScim
|
||||||
|
{
|
||||||
|
get => _globalSettings.BuildInternalUri(_scim, "scim");
|
||||||
|
set => _internalScim = value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class SqlSettings
|
public class SqlSettings
|
||||||
|
@ -10,11 +10,13 @@ namespace Bit.Core.Settings
|
|||||||
public string Admin { get; set; }
|
public string Admin { get; set; }
|
||||||
public string Notifications { get; set; }
|
public string Notifications { get; set; }
|
||||||
public string Sso { get; set; }
|
public string Sso { get; set; }
|
||||||
|
public string Scim { get; set; }
|
||||||
public string InternalNotifications { get; set; }
|
public string InternalNotifications { get; set; }
|
||||||
public string InternalAdmin { get; set; }
|
public string InternalAdmin { get; set; }
|
||||||
public string InternalIdentity { get; set; }
|
public string InternalIdentity { get; set; }
|
||||||
public string InternalApi { get; set; }
|
public string InternalApi { get; set; }
|
||||||
public string InternalVault { get; set; }
|
public string InternalVault { get; set; }
|
||||||
public string InternalSso { get; set; }
|
public string InternalSso { get; set; }
|
||||||
|
public string InternalScim { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -414,6 +414,7 @@ namespace Bit.Core.Utilities
|
|||||||
HasApi = true,
|
HasApi = true,
|
||||||
HasSso = true,
|
HasSso = true,
|
||||||
HasKeyConnector = true,
|
HasKeyConnector = true,
|
||||||
|
HasScim = true,
|
||||||
HasResetPassword = true,
|
HasResetPassword = true,
|
||||||
UsersGetPremium = true,
|
UsersGetPremium = true,
|
||||||
|
|
||||||
@ -453,6 +454,7 @@ namespace Bit.Core.Utilities
|
|||||||
HasSelfHost = true,
|
HasSelfHost = true,
|
||||||
HasSso = true,
|
HasSso = true,
|
||||||
HasKeyConnector = true,
|
HasKeyConnector = true,
|
||||||
|
HasScim = true,
|
||||||
HasResetPassword = true,
|
HasResetPassword = true,
|
||||||
UsersGetPremium = true,
|
UsersGetPremium = true,
|
||||||
|
|
||||||
|
@ -12,7 +12,8 @@
|
|||||||
"internalIdentity": "http://localhost:33656",
|
"internalIdentity": "http://localhost:33656",
|
||||||
"internalApi": "http://localhost:4000",
|
"internalApi": "http://localhost:4000",
|
||||||
"internalVault": "https://localhost:8080",
|
"internalVault": "https://localhost:8080",
|
||||||
"internalSso": "http://localhost:51822"
|
"internalSso": "http://localhost:51822",
|
||||||
|
"internalScim": "http://localhost:44559"
|
||||||
},
|
},
|
||||||
"events": {
|
"events": {
|
||||||
"connectionString": "UseDevelopmentStorage=true"
|
"connectionString": "UseDevelopmentStorage=true"
|
||||||
|
@ -12,7 +12,8 @@
|
|||||||
"internalIdentity": "https://identity.bitwarden.com",
|
"internalIdentity": "https://identity.bitwarden.com",
|
||||||
"internalApi": "https://api.bitwarden.com",
|
"internalApi": "https://api.bitwarden.com",
|
||||||
"internalVault": "https://vault.bitwarden.com",
|
"internalVault": "https://vault.bitwarden.com",
|
||||||
"internalSso": "https://sso.bitwarden.com"
|
"internalSso": "https://sso.bitwarden.com",
|
||||||
|
"internalScim": "https://scim.bitwarden.com"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Logging": {
|
"Logging": {
|
||||||
|
@ -12,7 +12,8 @@
|
|||||||
"internalIdentity": "https://identity.qa.bitwarden.pw",
|
"internalIdentity": "https://identity.qa.bitwarden.pw",
|
||||||
"internalApi": "https://api.qa.bitwarden.pw",
|
"internalApi": "https://api.qa.bitwarden.pw",
|
||||||
"internalVault": "https://vault.qa.bitwarden.pw",
|
"internalVault": "https://vault.qa.bitwarden.pw",
|
||||||
"internalSso": "https://sso.qa.bitwarden.pw"
|
"internalSso": "https://sso.qa.bitwarden.pw",
|
||||||
|
"internalScim": "https://scim.qa.bitwarden.pw"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Logging": {
|
"Logging": {
|
||||||
|
@ -12,7 +12,8 @@
|
|||||||
"internalIdentity": null,
|
"internalIdentity": null,
|
||||||
"internalApi": null,
|
"internalApi": null,
|
||||||
"internalVault": null,
|
"internalVault": null,
|
||||||
"internalSso": null
|
"internalSso": null,
|
||||||
|
"internalScim": null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,8 @@
|
|||||||
"internalIdentity": "https://identity.bitwarden.com",
|
"internalIdentity": "https://identity.bitwarden.com",
|
||||||
"internalApi": "https://api.bitwarden.com",
|
"internalApi": "https://api.bitwarden.com",
|
||||||
"internalVault": "https://vault.bitwarden.com",
|
"internalVault": "https://vault.bitwarden.com",
|
||||||
"internalSso": "https://sso.bitwarden.com"
|
"internalSso": "https://sso.bitwarden.com",
|
||||||
|
"internalScim": "https://scim.bitwarden.com"
|
||||||
},
|
},
|
||||||
"braintree": {
|
"braintree": {
|
||||||
"production": true
|
"production": true
|
||||||
|
@ -12,7 +12,8 @@
|
|||||||
"internalIdentity": "https://identity.qa.bitwarden.pw",
|
"internalIdentity": "https://identity.qa.bitwarden.pw",
|
||||||
"internalApi": "https://api.qa.bitwarden.pw",
|
"internalApi": "https://api.qa.bitwarden.pw",
|
||||||
"internalVault": "https://vault.qa.bitwarden.pw",
|
"internalVault": "https://vault.qa.bitwarden.pw",
|
||||||
"internalSso": "https://sso.qa.bitwarden.pw"
|
"internalSso": "https://sso.qa.bitwarden.pw",
|
||||||
|
"internalScim": "https://scim.qa.bitwarden.pw"
|
||||||
},
|
},
|
||||||
"braintree": {
|
"braintree": {
|
||||||
"production": false
|
"production": false
|
||||||
|
@ -12,7 +12,8 @@
|
|||||||
"internalIdentity": null,
|
"internalIdentity": null,
|
||||||
"internalApi": null,
|
"internalApi": null,
|
||||||
"internalVault": null,
|
"internalVault": null,
|
||||||
"internalSso": null
|
"internalSso": null,
|
||||||
|
"internalScim": null
|
||||||
},
|
},
|
||||||
"captcha": {
|
"captcha": {
|
||||||
"maximumFailedLoginAttempts": 0
|
"maximumFailedLoginAttempts": 0
|
||||||
|
@ -23,6 +23,8 @@ namespace Bit.Infrastructure.EntityFramework
|
|||||||
if (provider == SupportedDatabaseProviders.Postgres)
|
if (provider == SupportedDatabaseProviders.Postgres)
|
||||||
{
|
{
|
||||||
options.UseNpgsql(connectionString);
|
options.UseNpgsql(connectionString);
|
||||||
|
// Handle NpgSql Legacy Support for `timestamp without timezone` issue
|
||||||
|
AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true);
|
||||||
}
|
}
|
||||||
else if (provider == SupportedDatabaseProviders.MySql)
|
else if (provider == SupportedDatabaseProviders.MySql)
|
||||||
{
|
{
|
||||||
|
@ -83,6 +83,8 @@ namespace Bit.Infrastructure.EntityFramework.Repositories
|
|||||||
Using2fa = e.Use2fa && e.TwoFactorProviders != null,
|
Using2fa = e.Use2fa && e.TwoFactorProviders != null,
|
||||||
UseSso = e.UseSso,
|
UseSso = e.UseSso,
|
||||||
UseKeyConnector = e.UseKeyConnector,
|
UseKeyConnector = e.UseKeyConnector,
|
||||||
|
UseResetPassword = e.UseResetPassword,
|
||||||
|
UseScim = e.UseScim,
|
||||||
}).ToListAsync();
|
}).ToListAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -31,6 +31,7 @@ namespace Bit.Infrastructure.EntityFramework.Repositories.Queries
|
|||||||
UsePolicies = x.o.UsePolicies,
|
UsePolicies = x.o.UsePolicies,
|
||||||
UseSso = x.o.UseSso,
|
UseSso = x.o.UseSso,
|
||||||
UseKeyConnector = x.o.UseKeyConnector,
|
UseKeyConnector = x.o.UseKeyConnector,
|
||||||
|
UseScim = x.o.UseScim,
|
||||||
UseGroups = x.o.UseGroups,
|
UseGroups = x.o.UseGroups,
|
||||||
UseDirectory = x.o.UseDirectory,
|
UseDirectory = x.o.UseDirectory,
|
||||||
UseEvents = x.o.UseEvents,
|
UseEvents = x.o.UseEvents,
|
||||||
|
@ -20,6 +20,7 @@ namespace Bit.Infrastructure.EntityFramework.Repositories.Queries
|
|||||||
UsePolicies = x.o.UsePolicies,
|
UsePolicies = x.o.UsePolicies,
|
||||||
UseSso = x.o.UseSso,
|
UseSso = x.o.UseSso,
|
||||||
UseKeyConnector = x.o.UseKeyConnector,
|
UseKeyConnector = x.o.UseKeyConnector,
|
||||||
|
UseScim = x.o.UseScim,
|
||||||
UseGroups = x.o.UseGroups,
|
UseGroups = x.o.UseGroups,
|
||||||
UseDirectory = x.o.UseDirectory,
|
UseDirectory = x.o.UseDirectory,
|
||||||
UseEvents = x.o.UseEvents,
|
UseEvents = x.o.UseEvents,
|
||||||
|
@ -12,7 +12,8 @@
|
|||||||
"internalIdentity": "http://localhost:33656",
|
"internalIdentity": "http://localhost:33656",
|
||||||
"internalApi": "http://localhost:4000",
|
"internalApi": "http://localhost:4000",
|
||||||
"internalVault": "https://localhost:8080",
|
"internalVault": "https://localhost:8080",
|
||||||
"internalSso": "http://localhost:51822"
|
"internalSso": "http://localhost:51822",
|
||||||
|
"internalScim": "http://localhost:44559"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"connectionString": "UseDevelopmentStorage=true"
|
"connectionString": "UseDevelopmentStorage=true"
|
||||||
|
@ -12,7 +12,8 @@
|
|||||||
"internalIdentity": "https://identity.bitwarden.com",
|
"internalIdentity": "https://identity.bitwarden.com",
|
||||||
"internalApi": "https://api.bitwarden.com",
|
"internalApi": "https://api.bitwarden.com",
|
||||||
"internalVault": "https://vault.bitwarden.com",
|
"internalVault": "https://vault.bitwarden.com",
|
||||||
"internalSso": "https://sso.bitwarden.com"
|
"internalSso": "https://sso.bitwarden.com",
|
||||||
|
"internalScim": "https://scim.bitwarden.com"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Logging": {
|
"Logging": {
|
||||||
|
@ -12,7 +12,8 @@
|
|||||||
"internalIdentity": "https://identity.qa.bitwarden.pw",
|
"internalIdentity": "https://identity.qa.bitwarden.pw",
|
||||||
"internalApi": "https://api.qa.bitwarden.pw",
|
"internalApi": "https://api.qa.bitwarden.pw",
|
||||||
"internalVault": "https://vault.qa.bitwarden.pw",
|
"internalVault": "https://vault.qa.bitwarden.pw",
|
||||||
"internalSso": "https://sso.qa.bitwarden.pw"
|
"internalSso": "https://sso.qa.bitwarden.pw",
|
||||||
|
"internalScim": "https://scim.qa.bitwarden.pw"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Logging": {
|
"Logging": {
|
||||||
|
@ -12,7 +12,8 @@
|
|||||||
"internalIdentity": null,
|
"internalIdentity": null,
|
||||||
"internalApi": null,
|
"internalApi": null,
|
||||||
"internalVault": null,
|
"internalVault": null,
|
||||||
"internalSso": null
|
"internalSso": null,
|
||||||
|
"internalScim": null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -40,7 +40,8 @@
|
|||||||
@RevisionDate DATETIME2(7),
|
@RevisionDate DATETIME2(7),
|
||||||
@OwnersNotifiedOfAutoscaling DATETIME2(7),
|
@OwnersNotifiedOfAutoscaling DATETIME2(7),
|
||||||
@MaxAutoscaleSeats INT,
|
@MaxAutoscaleSeats INT,
|
||||||
@UseKeyConnector BIT = 0
|
@UseKeyConnector BIT = 0,
|
||||||
|
@UseScim BIT = 0
|
||||||
AS
|
AS
|
||||||
BEGIN
|
BEGIN
|
||||||
SET NOCOUNT ON
|
SET NOCOUNT ON
|
||||||
@ -88,7 +89,8 @@ BEGIN
|
|||||||
[RevisionDate],
|
[RevisionDate],
|
||||||
[OwnersNotifiedOfAutoscaling],
|
[OwnersNotifiedOfAutoscaling],
|
||||||
[MaxAutoscaleSeats],
|
[MaxAutoscaleSeats],
|
||||||
[UseKeyConnector]
|
[UseKeyConnector],
|
||||||
|
[UseScim]
|
||||||
)
|
)
|
||||||
VALUES
|
VALUES
|
||||||
(
|
(
|
||||||
@ -133,6 +135,7 @@ BEGIN
|
|||||||
@RevisionDate,
|
@RevisionDate,
|
||||||
@OwnersNotifiedOfAutoscaling,
|
@OwnersNotifiedOfAutoscaling,
|
||||||
@MaxAutoscaleSeats,
|
@MaxAutoscaleSeats,
|
||||||
@UseKeyConnector
|
@UseKeyConnector,
|
||||||
|
@UseScim
|
||||||
)
|
)
|
||||||
END
|
END
|
@ -16,6 +16,7 @@ BEGIN
|
|||||||
[UsersGetPremium],
|
[UsersGetPremium],
|
||||||
[UseSso],
|
[UseSso],
|
||||||
[UseKeyConnector],
|
[UseKeyConnector],
|
||||||
|
[UseScim],
|
||||||
[UseResetPassword],
|
[UseResetPassword],
|
||||||
[Enabled]
|
[Enabled]
|
||||||
FROM
|
FROM
|
||||||
|
@ -40,7 +40,8 @@
|
|||||||
@RevisionDate DATETIME2(7),
|
@RevisionDate DATETIME2(7),
|
||||||
@OwnersNotifiedOfAutoscaling DATETIME2(7),
|
@OwnersNotifiedOfAutoscaling DATETIME2(7),
|
||||||
@MaxAutoscaleSeats INT,
|
@MaxAutoscaleSeats INT,
|
||||||
@UseKeyConnector BIT = 0
|
@UseKeyConnector BIT = 0,
|
||||||
|
@UseScim BIT = 0
|
||||||
AS
|
AS
|
||||||
BEGIN
|
BEGIN
|
||||||
SET NOCOUNT ON
|
SET NOCOUNT ON
|
||||||
@ -88,7 +89,8 @@ BEGIN
|
|||||||
[RevisionDate] = @RevisionDate,
|
[RevisionDate] = @RevisionDate,
|
||||||
[OwnersNotifiedOfAutoscaling] = @OwnersNotifiedOfAutoscaling,
|
[OwnersNotifiedOfAutoscaling] = @OwnersNotifiedOfAutoscaling,
|
||||||
[MaxAutoscaleSeats] = @MaxAutoscaleSeats,
|
[MaxAutoscaleSeats] = @MaxAutoscaleSeats,
|
||||||
[UseKeyConnector] = @UseKeyConnector
|
[UseKeyConnector] = @UseKeyConnector,
|
||||||
|
[UseScim] = @UseScim
|
||||||
WHERE
|
WHERE
|
||||||
[Id] = @Id
|
[Id] = @Id
|
||||||
END
|
END
|
||||||
|
@ -41,6 +41,7 @@
|
|||||||
[OwnersNotifiedOfAutoscaling] DATETIME2(7) NULL,
|
[OwnersNotifiedOfAutoscaling] DATETIME2(7) NULL,
|
||||||
[MaxAutoscaleSeats] INT NULL,
|
[MaxAutoscaleSeats] INT NULL,
|
||||||
[UseKeyConnector] BIT NOT NULL,
|
[UseKeyConnector] BIT NOT NULL,
|
||||||
|
[UseScim] BIT NOT NULL CONSTRAINT [DF_Organization_UseScim] DEFAULT (0),
|
||||||
CONSTRAINT [PK_Organization] PRIMARY KEY CLUSTERED ([Id] ASC)
|
CONSTRAINT [PK_Organization] PRIMARY KEY CLUSTERED ([Id] ASC)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -8,6 +8,7 @@ SELECT
|
|||||||
O.[UsePolicies],
|
O.[UsePolicies],
|
||||||
O.[UseSso],
|
O.[UseSso],
|
||||||
O.[UseKeyConnector],
|
O.[UseKeyConnector],
|
||||||
|
O.[UseScim],
|
||||||
O.[UseGroups],
|
O.[UseGroups],
|
||||||
O.[UseDirectory],
|
O.[UseDirectory],
|
||||||
O.[UseEvents],
|
O.[UseEvents],
|
||||||
|
@ -8,6 +8,7 @@ SELECT
|
|||||||
O.[UsePolicies],
|
O.[UsePolicies],
|
||||||
O.[UseSso],
|
O.[UseSso],
|
||||||
O.[UseKeyConnector],
|
O.[UseKeyConnector],
|
||||||
|
O.[UseScim],
|
||||||
O.[UseGroups],
|
O.[UseGroups],
|
||||||
O.[UseDirectory],
|
O.[UseDirectory],
|
||||||
O.[UseEvents],
|
O.[UseEvents],
|
||||||
|
@ -50,11 +50,15 @@ namespace Bit.Api.Test.Controllers
|
|||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[BitAutoData]
|
[BitAutoData]
|
||||||
public async Task CreateConnection_RequiresOwnerPermissions(SutProvider<OrganizationConnectionsController> sutProvider)
|
public async Task CreateConnection_CloudBillingSync_RequiresOwnerPermissions(SutProvider<OrganizationConnectionsController> sutProvider)
|
||||||
{
|
{
|
||||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.CreateConnection(null));
|
var model = new OrganizationConnectionRequestModel
|
||||||
|
{
|
||||||
|
Type = OrganizationConnectionType.CloudBillingSync,
|
||||||
|
};
|
||||||
|
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.CreateConnection(model));
|
||||||
|
|
||||||
Assert.Contains("Only the owner of an organization can create a connection.", exception.Message);
|
Assert.Contains($"You do not have permission to create a connection of type", exception.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
@ -116,6 +120,7 @@ namespace Bit.Api.Test.Controllers
|
|||||||
var typedModel = new OrganizationConnectionRequestModel<BillingSyncConfig>(model);
|
var typedModel = new OrganizationConnectionRequestModel<BillingSyncConfig>(model);
|
||||||
typedModel.ParsedConfig.CloudOrganizationId = cloudOrgId;
|
typedModel.ParsedConfig.CloudOrganizationId = cloudOrgId;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IGlobalSettings>().SelfHosted.Returns(true);
|
||||||
sutProvider.GetDependency<ICreateOrganizationConnectionCommand>().CreateAsync<BillingSyncConfig>(default)
|
sutProvider.GetDependency<ICreateOrganizationConnectionCommand>().CreateAsync<BillingSyncConfig>(default)
|
||||||
.ReturnsForAnyArgs(typedModel.ToData(Guid.NewGuid()).ToEntity());
|
.ReturnsForAnyArgs(typedModel.ToData(Guid.NewGuid()).ToEntity());
|
||||||
sutProvider.GetDependency<ICurrentContext>().OrganizationOwner(model.OrganizationId).Returns(true);
|
sutProvider.GetDependency<ICurrentContext>().OrganizationOwner(model.OrganizationId).Returns(true);
|
||||||
@ -143,17 +148,40 @@ namespace Bit.Api.Test.Controllers
|
|||||||
|
|
||||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateConnection(default, null));
|
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateConnection(default, null));
|
||||||
|
|
||||||
Assert.Contains("Only the owner of an organization can update a connection.", exception.Message);
|
Assert.Contains("You do not have permission to update this connection.", exception.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[BitMemberAutoData(nameof(ConnectionTypes))]
|
[BitAutoData(OrganizationConnectionType.CloudBillingSync)]
|
||||||
public async Task UpdateConnection_OnlyOneConnectionOfEachType(OrganizationConnectionType type,
|
public async Task UpdateConnection_BillingSync_OnlyOneConnectionOfEachType(OrganizationConnectionType type,
|
||||||
OrganizationConnection existing1, OrganizationConnection existing2, BillingSyncConfig config,
|
OrganizationConnection existing1, OrganizationConnection existing2, BillingSyncConfig config,
|
||||||
SutProvider<OrganizationConnectionsController> sutProvider)
|
SutProvider<OrganizationConnectionsController> sutProvider)
|
||||||
{
|
{
|
||||||
|
existing1.Type = existing2.Type = type;
|
||||||
existing1.Config = JsonSerializer.Serialize(config);
|
existing1.Config = JsonSerializer.Serialize(config);
|
||||||
var typedModel = RequestModelFromEntity(existing1);
|
var typedModel = RequestModelFromEntity<BillingSyncConfig>(existing1);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().OrganizationOwner(typedModel.OrganizationId).Returns(true);
|
||||||
|
|
||||||
|
var orgConnectionRepository = sutProvider.GetDependency<IOrganizationConnectionRepository>();
|
||||||
|
orgConnectionRepository.GetByIdAsync(existing1.Id).Returns(existing1);
|
||||||
|
orgConnectionRepository.GetByIdAsync(existing2.Id).Returns(existing2);
|
||||||
|
orgConnectionRepository.GetByOrganizationIdTypeAsync(typedModel.OrganizationId, type).Returns(new[] { existing1, existing2 });
|
||||||
|
|
||||||
|
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateConnection(existing1.Id, typedModel));
|
||||||
|
|
||||||
|
Assert.Contains($"The requested organization already has a connection of type {typedModel.Type}. Only one of each connection type may exist per organization.", exception.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData(OrganizationConnectionType.Scim)]
|
||||||
|
public async Task UpdateConnection_Scim_OnlyOneConnectionOfEachType(OrganizationConnectionType type,
|
||||||
|
OrganizationConnection existing1, OrganizationConnection existing2, ScimConfig config,
|
||||||
|
SutProvider<OrganizationConnectionsController> sutProvider)
|
||||||
|
{
|
||||||
|
existing1.Type = existing2.Type = type;
|
||||||
|
existing1.Config = JsonSerializer.Serialize(config);
|
||||||
|
var typedModel = RequestModelFromEntity<ScimConfig>(existing1);
|
||||||
|
|
||||||
sutProvider.GetDependency<ICurrentContext>().OrganizationOwner(typedModel.OrganizationId).Returns(true);
|
sutProvider.GetDependency<ICurrentContext>().OrganizationOwner(typedModel.OrganizationId).Returns(true);
|
||||||
|
|
||||||
@ -161,7 +189,11 @@ namespace Bit.Api.Test.Controllers
|
|||||||
.GetByIdAsync(existing1.Id)
|
.GetByIdAsync(existing1.Id)
|
||||||
.Returns(existing1);
|
.Returns(existing1);
|
||||||
|
|
||||||
sutProvider.GetDependency<IOrganizationConnectionRepository>().GetByOrganizationIdTypeAsync(typedModel.OrganizationId, type).Returns(new[] { existing1, existing2 });
|
sutProvider.GetDependency<ICurrentContext>().ManageScim(typedModel.OrganizationId).Returns(true);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IOrganizationConnectionRepository>()
|
||||||
|
.GetByOrganizationIdTypeAsync(typedModel.OrganizationId, type)
|
||||||
|
.Returns(new[] { existing1, existing2 });
|
||||||
|
|
||||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateConnection(existing1.Id, typedModel));
|
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateConnection(existing1.Id, typedModel));
|
||||||
|
|
||||||
@ -180,11 +212,16 @@ namespace Bit.Api.Test.Controllers
|
|||||||
});
|
});
|
||||||
updated.Config = JsonSerializer.Serialize(config);
|
updated.Config = JsonSerializer.Serialize(config);
|
||||||
updated.Id = existing.Id;
|
updated.Id = existing.Id;
|
||||||
var model = RequestModelFromEntity(updated);
|
updated.Type = OrganizationConnectionType.CloudBillingSync;
|
||||||
|
var model = RequestModelFromEntity<BillingSyncConfig>(updated);
|
||||||
|
|
||||||
sutProvider.GetDependency<ICurrentContext>().OrganizationOwner(model.OrganizationId).Returns(true);
|
sutProvider.GetDependency<ICurrentContext>().OrganizationOwner(model.OrganizationId).Returns(true);
|
||||||
sutProvider.GetDependency<IOrganizationConnectionRepository>().GetByOrganizationIdTypeAsync(model.OrganizationId, model.Type).Returns(new[] { existing });
|
sutProvider.GetDependency<IOrganizationConnectionRepository>()
|
||||||
sutProvider.GetDependency<IUpdateOrganizationConnectionCommand>().UpdateAsync<BillingSyncConfig>(default).ReturnsForAnyArgs(updated);
|
.GetByOrganizationIdTypeAsync(model.OrganizationId, model.Type)
|
||||||
|
.Returns(new[] { existing });
|
||||||
|
sutProvider.GetDependency<IUpdateOrganizationConnectionCommand>()
|
||||||
|
.UpdateAsync<BillingSyncConfig>(default)
|
||||||
|
.ReturnsForAnyArgs(updated);
|
||||||
sutProvider.GetDependency<IOrganizationConnectionRepository>()
|
sutProvider.GetDependency<IOrganizationConnectionRepository>()
|
||||||
.GetByIdAsync(existing.Id)
|
.GetByIdAsync(existing.Id)
|
||||||
.Returns(existing);
|
.Returns(existing);
|
||||||
@ -211,7 +248,7 @@ namespace Bit.Api.Test.Controllers
|
|||||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
|
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
|
||||||
sutProvider.Sut.GetConnection(connectionId, OrganizationConnectionType.CloudBillingSync));
|
sutProvider.Sut.GetConnection(connectionId, OrganizationConnectionType.CloudBillingSync));
|
||||||
|
|
||||||
Assert.Contains("Only the owner of an organization can retrieve a connection.", exception.Message);
|
Assert.Contains("You do not have permission to retrieve a connection of type", exception.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
@ -221,7 +258,10 @@ namespace Bit.Api.Test.Controllers
|
|||||||
{
|
{
|
||||||
connection.Config = JsonSerializer.Serialize(config);
|
connection.Config = JsonSerializer.Serialize(config);
|
||||||
|
|
||||||
sutProvider.GetDependency<IOrganizationConnectionRepository>().GetByOrganizationIdTypeAsync(connection.OrganizationId, connection.Type).Returns(new[] { connection });
|
sutProvider.GetDependency<IGlobalSettings>().SelfHosted.Returns(true);
|
||||||
|
sutProvider.GetDependency<IOrganizationConnectionRepository>()
|
||||||
|
.GetByOrganizationIdTypeAsync(connection.OrganizationId, connection.Type)
|
||||||
|
.Returns(new[] { connection });
|
||||||
sutProvider.GetDependency<ICurrentContext>().OrganizationOwner(connection.OrganizationId).Returns(true);
|
sutProvider.GetDependency<ICurrentContext>().OrganizationOwner(connection.OrganizationId).Returns(true);
|
||||||
|
|
||||||
var expected = new OrganizationConnectionResponseModel(connection, typeof(BillingSyncConfig));
|
var expected = new OrganizationConnectionResponseModel(connection, typeof(BillingSyncConfig));
|
||||||
@ -247,7 +287,7 @@ namespace Bit.Api.Test.Controllers
|
|||||||
|
|
||||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.DeleteConnection(connection.Id));
|
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.DeleteConnection(connection.Id));
|
||||||
|
|
||||||
Assert.Contains("Only the owner of an organization can remove a connection.", exception.Message);
|
Assert.Contains("You do not have permission to remove this connection of type", exception.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
@ -263,7 +303,8 @@ namespace Bit.Api.Test.Controllers
|
|||||||
await sutProvider.GetDependency<IDeleteOrganizationConnectionCommand>().DeleteAsync(connection);
|
await sutProvider.GetDependency<IDeleteOrganizationConnectionCommand>().DeleteAsync(connection);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static OrganizationConnectionRequestModel<BillingSyncConfig> RequestModelFromEntity(OrganizationConnection entity)
|
private static OrganizationConnectionRequestModel<T> RequestModelFromEntity<T>(OrganizationConnection entity)
|
||||||
|
where T : new()
|
||||||
{
|
{
|
||||||
return new(new OrganizationConnectionRequestModel()
|
return new(new OrganizationConnectionRequestModel()
|
||||||
{
|
{
|
||||||
|
@ -13,6 +13,7 @@ namespace Bit.Core.Test.Helpers.Factories
|
|||||||
var globalSettings = GlobalSettingsFactory.GlobalSettings;
|
var globalSettings = GlobalSettingsFactory.GlobalSettings;
|
||||||
if (!string.IsNullOrWhiteSpace(GlobalSettingsFactory.GlobalSettings.PostgreSql?.ConnectionString))
|
if (!string.IsNullOrWhiteSpace(GlobalSettingsFactory.GlobalSettings.PostgreSql?.ConnectionString))
|
||||||
{
|
{
|
||||||
|
AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true);
|
||||||
Options.Add(new DbContextOptionsBuilder<DatabaseContext>().UseNpgsql(globalSettings.PostgreSql.ConnectionString).Options);
|
Options.Add(new DbContextOptionsBuilder<DatabaseContext>().UseNpgsql(globalSettings.PostgreSql.ConnectionString).Options);
|
||||||
}
|
}
|
||||||
if (!string.IsNullOrWhiteSpace(GlobalSettingsFactory.GlobalSettings.MySql?.ConnectionString))
|
if (!string.IsNullOrWhiteSpace(GlobalSettingsFactory.GlobalSettings.MySql?.ConnectionString))
|
||||||
|
@ -23,7 +23,8 @@ namespace Bit.Core.Test.Models
|
|||||||
"\"managePolicies\": false,",
|
"\"managePolicies\": false,",
|
||||||
"\"manageSso\": false,",
|
"\"manageSso\": false,",
|
||||||
"\"manageUsers\": false,",
|
"\"manageUsers\": false,",
|
||||||
"\"manageResetPassword\": false",
|
"\"manageResetPassword\": false,",
|
||||||
|
"\"manageScim\": false",
|
||||||
"}");
|
"}");
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@ -44,6 +45,7 @@ namespace Bit.Core.Test.Models
|
|||||||
ManageSso = false,
|
ManageSso = false,
|
||||||
ManageUsers = false,
|
ManageUsers = false,
|
||||||
ManageResetPassword = false,
|
ManageResetPassword = false,
|
||||||
|
ManageScim = false,
|
||||||
};
|
};
|
||||||
|
|
||||||
// minify expected json
|
// minify expected json
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user