mirror of
https://github.com/bitwarden/server.git
synced 2025-04-13 09:08:17 -05:00
stub out new scim api for dir sync
This commit is contained in:
parent
3d05c9208f
commit
7b359053d6
@ -47,6 +47,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Events", "src\Events\Events
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventsProcessor", "src\EventsProcessor\EventsProcessor.csproj", "{FCB77723-0FF5-4B1B-AB90-6930CF2E25F9}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventsProcessor", "src\EventsProcessor\EventsProcessor.csproj", "{FCB77723-0FF5-4B1B-AB90-6930CF2E25F9}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Scim", "src\Scim\Scim.csproj", "{B8C5FFEB-186A-46FF-B914-BB3D50AA8D61}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@ -107,6 +109,10 @@ Global
|
|||||||
{FCB77723-0FF5-4B1B-AB90-6930CF2E25F9}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{FCB77723-0FF5-4B1B-AB90-6930CF2E25F9}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{FCB77723-0FF5-4B1B-AB90-6930CF2E25F9}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{FCB77723-0FF5-4B1B-AB90-6930CF2E25F9}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{FCB77723-0FF5-4B1B-AB90-6930CF2E25F9}.Release|Any CPU.Build.0 = Release|Any CPU
|
{FCB77723-0FF5-4B1B-AB90-6930CF2E25F9}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{B8C5FFEB-186A-46FF-B914-BB3D50AA8D61}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{B8C5FFEB-186A-46FF-B914-BB3D50AA8D61}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{B8C5FFEB-186A-46FF-B914-BB3D50AA8D61}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{B8C5FFEB-186A-46FF-B914-BB3D50AA8D61}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
@ -125,6 +131,7 @@ Global
|
|||||||
{9CF59342-3912-4B45-A2BA-0F173666586D} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84D}
|
{9CF59342-3912-4B45-A2BA-0F173666586D} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84D}
|
||||||
{994DD611-F266-4BD3-8072-3B1B57267ED5} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84D}
|
{994DD611-F266-4BD3-8072-3B1B57267ED5} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84D}
|
||||||
{FCB77723-0FF5-4B1B-AB90-6930CF2E25F9} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84D}
|
{FCB77723-0FF5-4B1B-AB90-6930CF2E25F9} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84D}
|
||||||
|
{B8C5FFEB-186A-46FF-B914-BB3D50AA8D61} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84D}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
SolutionGuid = {E01CBF68-2E20-425F-9EDB-E0A6510CA92F}
|
SolutionGuid = {E01CBF68-2E20-425F-9EDB-E0A6510CA92F}
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
namespace Bit.Core.Models.Data
|
namespace Bit.Core.Models.Data
|
||||||
{
|
{
|
||||||
public class OrganizationUserUserDetails
|
public class OrganizationUserUserDetails : IExternal
|
||||||
{
|
{
|
||||||
public Guid Id { get; set; }
|
public Guid Id { get; set; }
|
||||||
public Guid OrganizationId { get; set; }
|
public Guid OrganizationId { get; set; }
|
||||||
|
7
src/Core/Models/IExternal.cs
Normal file
7
src/Core/Models/IExternal.cs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
namespace Bit.Core.Models
|
||||||
|
{
|
||||||
|
public interface IExternal
|
||||||
|
{
|
||||||
|
string ExternalId { get; }
|
||||||
|
}
|
||||||
|
}
|
@ -3,7 +3,7 @@ using Bit.Core.Utilities;
|
|||||||
|
|
||||||
namespace Bit.Core.Models.Table
|
namespace Bit.Core.Models.Table
|
||||||
{
|
{
|
||||||
public class Group : ITableObject<Guid>
|
public class Group : ITableObject<Guid>, IExternal
|
||||||
{
|
{
|
||||||
public Guid Id { get; set; }
|
public Guid Id { get; set; }
|
||||||
public Guid OrganizationId { get; set; }
|
public Guid OrganizationId { get; set; }
|
||||||
|
@ -4,7 +4,7 @@ using Bit.Core.Enums;
|
|||||||
|
|
||||||
namespace Bit.Core.Models.Table
|
namespace Bit.Core.Models.Table
|
||||||
{
|
{
|
||||||
public class OrganizationUser : ITableObject<Guid>
|
public class OrganizationUser : ITableObject<Guid>, IExternal
|
||||||
{
|
{
|
||||||
public Guid Id { get; set; }
|
public Guid Id { get; set; }
|
||||||
public Guid OrganizationId { get; set; }
|
public Guid OrganizationId { get; set; }
|
||||||
|
@ -26,15 +26,15 @@ namespace Bit.Core.Services
|
|||||||
Task UpdateExpirationDateAsync(Guid organizationId, DateTime? expirationDate);
|
Task UpdateExpirationDateAsync(Guid organizationId, DateTime? expirationDate);
|
||||||
Task EnableAsync(Guid organizationId);
|
Task EnableAsync(Guid organizationId);
|
||||||
Task UpdateAsync(Organization organization, bool updateBilling = false);
|
Task UpdateAsync(Organization organization, bool updateBilling = false);
|
||||||
Task<OrganizationUser> InviteUserAsync(Guid organizationId, Guid invitingUserId, string email,
|
Task<OrganizationUser> InviteUserAsync(Guid organizationId, Guid? invitingUserId, string email,
|
||||||
OrganizationUserType type, bool accessAll, string externalId, IEnumerable<SelectionReadOnly> collections);
|
OrganizationUserType type, bool accessAll, string externalId, IEnumerable<SelectionReadOnly> collections);
|
||||||
Task<List<OrganizationUser>> InviteUserAsync(Guid organizationId, Guid invitingUserId, IEnumerable<string> emails,
|
Task<List<OrganizationUser>> InviteUserAsync(Guid organizationId, Guid? invitingUserId, IEnumerable<string> emails,
|
||||||
OrganizationUserType type, bool accessAll, string externalId, IEnumerable<SelectionReadOnly> collections);
|
OrganizationUserType type, bool accessAll, string externalId, IEnumerable<SelectionReadOnly> collections);
|
||||||
Task ResendInviteAsync(Guid organizationId, Guid invitingUserId, Guid organizationUserId);
|
Task ResendInviteAsync(Guid organizationId, Guid invitingUserId, Guid organizationUserId);
|
||||||
Task<OrganizationUser> AcceptUserAsync(Guid organizationUserId, User user, string token);
|
Task<OrganizationUser> AcceptUserAsync(Guid organizationUserId, User user, string token);
|
||||||
Task<OrganizationUser> ConfirmUserAsync(Guid organizationId, Guid organizationUserId, string key, Guid confirmingUserId);
|
Task<OrganizationUser> ConfirmUserAsync(Guid organizationId, Guid organizationUserId, string key, Guid confirmingUserId);
|
||||||
Task SaveUserAsync(OrganizationUser user, Guid savingUserId, IEnumerable<SelectionReadOnly> collections);
|
Task SaveUserAsync(OrganizationUser user, Guid savingUserId, IEnumerable<SelectionReadOnly> collections);
|
||||||
Task DeleteUserAsync(Guid organizationId, Guid organizationUserId, Guid deletingUserId);
|
Task DeleteUserAsync(Guid organizationId, Guid organizationUserId, Guid? deletingUserId);
|
||||||
Task DeleteUserAsync(Guid organizationId, Guid userId);
|
Task DeleteUserAsync(Guid organizationId, Guid userId);
|
||||||
Task UpdateUserGroupsAsync(OrganizationUser organizationUser, IEnumerable<Guid> groupIds);
|
Task UpdateUserGroupsAsync(OrganizationUser organizationUser, IEnumerable<Guid> groupIds);
|
||||||
Task<OrganizationLicense> GenerateLicenseAsync(Guid organizationId, Guid installationId);
|
Task<OrganizationLicense> GenerateLicenseAsync(Guid organizationId, Guid installationId);
|
||||||
|
@ -819,7 +819,7 @@ namespace Bit.Core.Services
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<OrganizationUser> InviteUserAsync(Guid organizationId, Guid invitingUserId, string email,
|
public async Task<OrganizationUser> InviteUserAsync(Guid organizationId, Guid? invitingUserId, string email,
|
||||||
OrganizationUserType type, bool accessAll, string externalId, IEnumerable<SelectionReadOnly> collections)
|
OrganizationUserType type, bool accessAll, string externalId, IEnumerable<SelectionReadOnly> collections)
|
||||||
{
|
{
|
||||||
var result = await InviteUserAsync(organizationId, invitingUserId, new List<string> { email }, type, accessAll,
|
var result = await InviteUserAsync(organizationId, invitingUserId, new List<string> { email }, type, accessAll,
|
||||||
@ -827,7 +827,7 @@ namespace Bit.Core.Services
|
|||||||
return result.FirstOrDefault();
|
return result.FirstOrDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<List<OrganizationUser>> InviteUserAsync(Guid organizationId, Guid invitingUserId,
|
public async Task<List<OrganizationUser>> InviteUserAsync(Guid organizationId, Guid? invitingUserId,
|
||||||
IEnumerable<string> emails, OrganizationUserType type, bool accessAll, string externalId,
|
IEnumerable<string> emails, OrganizationUserType type, bool accessAll, string externalId,
|
||||||
IEnumerable<SelectionReadOnly> collections)
|
IEnumerable<SelectionReadOnly> collections)
|
||||||
{
|
{
|
||||||
@ -837,9 +837,9 @@ namespace Bit.Core.Services
|
|||||||
throw new NotFoundException();
|
throw new NotFoundException();
|
||||||
}
|
}
|
||||||
|
|
||||||
if(type == OrganizationUserType.Owner)
|
if(type == OrganizationUserType.Owner && invitingUserId.HasValue)
|
||||||
{
|
{
|
||||||
var invitingUserOrgs = await _organizationUserRepository.GetManyByUserAsync(invitingUserId);
|
var invitingUserOrgs = await _organizationUserRepository.GetManyByUserAsync(invitingUserId.Value);
|
||||||
if(!invitingUserOrgs.Any(u => u.OrganizationId == organizationId && u.Type == OrganizationUserType.Owner))
|
if(!invitingUserOrgs.Any(u => u.OrganizationId == organizationId && u.Type == OrganizationUserType.Owner))
|
||||||
{
|
{
|
||||||
throw new BadRequestException("Only owners can invite new owners.");
|
throw new BadRequestException("Only owners can invite new owners.");
|
||||||
@ -1065,7 +1065,7 @@ namespace Bit.Core.Services
|
|||||||
await _eventService.LogOrganizationUserEventAsync(user, EventType.OrganizationUser_Updated);
|
await _eventService.LogOrganizationUserEventAsync(user, EventType.OrganizationUser_Updated);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task DeleteUserAsync(Guid organizationId, Guid organizationUserId, Guid deletingUserId)
|
public async Task DeleteUserAsync(Guid organizationId, Guid organizationUserId, Guid? deletingUserId)
|
||||||
{
|
{
|
||||||
var orgUser = await _organizationUserRepository.GetByIdAsync(organizationUserId);
|
var orgUser = await _organizationUserRepository.GetByIdAsync(organizationUserId);
|
||||||
if(orgUser == null || orgUser.OrganizationId != organizationId)
|
if(orgUser == null || orgUser.OrganizationId != organizationId)
|
||||||
@ -1073,14 +1073,14 @@ namespace Bit.Core.Services
|
|||||||
throw new BadRequestException("User not valid.");
|
throw new BadRequestException("User not valid.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if(orgUser.UserId == deletingUserId)
|
if(deletingUserId.HasValue && orgUser.UserId == deletingUserId.Value)
|
||||||
{
|
{
|
||||||
throw new BadRequestException("You cannot remove yourself.");
|
throw new BadRequestException("You cannot remove yourself.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if(orgUser.Type == OrganizationUserType.Owner)
|
if(orgUser.Type == OrganizationUserType.Owner && deletingUserId.HasValue)
|
||||||
{
|
{
|
||||||
var deletingUserOrgs = await _organizationUserRepository.GetManyByUserAsync(deletingUserId);
|
var deletingUserOrgs = await _organizationUserRepository.GetManyByUserAsync(deletingUserId.Value);
|
||||||
if(!deletingUserOrgs.Any(u => u.OrganizationId == organizationId && u.Type == OrganizationUserType.Owner))
|
if(!deletingUserOrgs.Any(u => u.OrganizationId == organizationId && u.Type == OrganizationUserType.Owner))
|
||||||
{
|
{
|
||||||
throw new BadRequestException("Only owners can delete other owners.");
|
throw new BadRequestException("Only owners can delete other owners.");
|
||||||
|
20
src/Scim/Constants.cs
Normal file
20
src/Scim/Constants.cs
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
namespace Bit.Scim
|
||||||
|
{
|
||||||
|
public class Constants
|
||||||
|
{
|
||||||
|
public static class Schemas
|
||||||
|
{
|
||||||
|
public const string User = @"urn:ietf:params:scim:schemas:core:2.0:User";
|
||||||
|
public const string UserEnterprise = @"urn:ietf:params:scim:schemas:extension:enterprise:2.0:User";
|
||||||
|
public const string Group = @"urn:ietf:params:scim:schemas:core:2.0:Group";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Messages
|
||||||
|
{
|
||||||
|
public const string Error = @"urn:ietf:params:scim:api:messages:2.0:Error";
|
||||||
|
public const string PatchOp = @"urn:ietf:params:scim:api:messages:2.0:PatchOp";
|
||||||
|
public const string ListResponse = @"urn:ietf:params:scim:api:messages:2.0:ListResponse";
|
||||||
|
public const string SearchRequest = @"urn:ietf:params:scim:api:messages:2.0:SearchRequest";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
36
src/Scim/Controllers/BaseController.cs
Normal file
36
src/Scim/Controllers/BaseController.cs
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using Bit.Core.Models;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace Bit.Scim.Controllers
|
||||||
|
{
|
||||||
|
public class BaseController : Controller
|
||||||
|
{
|
||||||
|
protected ICollection<T> FilterResources<T>(ICollection<T> resources, string filter) where T : IExternal
|
||||||
|
{
|
||||||
|
if(!string.IsNullOrWhiteSpace(filter))
|
||||||
|
{
|
||||||
|
var filterMatch = Regex.Match(filter, "(\\w+) eq \"([^\"]*)\"");
|
||||||
|
if(filterMatch.Success && filterMatch.Groups.Count > 2)
|
||||||
|
{
|
||||||
|
var searchKey = filterMatch.Groups[1].Value;
|
||||||
|
var searchValue = filterMatch.Groups[2].Value;
|
||||||
|
|
||||||
|
if(!string.IsNullOrWhiteSpace(searchKey) && !string.IsNullOrWhiteSpace(searchValue))
|
||||||
|
{
|
||||||
|
var searchKeyLower = searchKey.ToLowerInvariant();
|
||||||
|
if(searchKeyLower == "externalid")
|
||||||
|
{
|
||||||
|
resources = resources.Where(u => u.ExternalId == searchValue).ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return resources;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
117
src/Scim/Controllers/GroupsController.cs
Normal file
117
src/Scim/Controllers/GroupsController.cs
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using System.Linq;
|
||||||
|
using Bit.Scim.Models;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using Bit.Core.Services;
|
||||||
|
using Bit.Core.Exceptions;
|
||||||
|
|
||||||
|
namespace Bit.Scim.Controllers
|
||||||
|
{
|
||||||
|
[Route("groups")]
|
||||||
|
[Route("scim/groups")]
|
||||||
|
public class GroupsController : BaseController
|
||||||
|
{
|
||||||
|
private readonly IGroupRepository _groupRepository;
|
||||||
|
private readonly IGroupService _groupService;
|
||||||
|
private Guid _orgId = new Guid("2933f760-9c0b-4efb-a437-a82a00ed3fc1"); // TODO: come from context
|
||||||
|
|
||||||
|
public GroupsController(
|
||||||
|
IGroupRepository groupRepository,
|
||||||
|
IGroupService groupService)
|
||||||
|
{
|
||||||
|
_groupRepository = groupRepository;
|
||||||
|
_groupService = groupService;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<IActionResult> GetAll([FromQuery]string filter, [FromQuery]string excludedAttributes,
|
||||||
|
[FromQuery]string attributes)
|
||||||
|
{
|
||||||
|
var groups = await _groupRepository.GetManyByOrganizationIdAsync(_orgId);
|
||||||
|
groups = FilterResources(groups, filter);
|
||||||
|
var groupsResult = groups.Select(g => new ScimGroup(g));
|
||||||
|
var result = new ScimListResponse(groupsResult);
|
||||||
|
return new OkObjectResult(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("{id}")]
|
||||||
|
public async Task<IActionResult> Get(string id)
|
||||||
|
{
|
||||||
|
var group = await _groupRepository.GetByIdAsync(new Guid(id));
|
||||||
|
if(group == null || group.OrganizationId != _orgId)
|
||||||
|
{
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = new ScimGroup(group);
|
||||||
|
return new OkObjectResult(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
public async Task<IActionResult> Post([FromBody]ScimGroup model)
|
||||||
|
{
|
||||||
|
var group = model.ToGroup(_orgId);
|
||||||
|
await _groupService.SaveAsync(group);
|
||||||
|
var result = new ScimGroup(group);
|
||||||
|
var getUrl = Url.Action("Get", "Groups", new { id = group.Id.ToString() }, Request.Protocol, Request.Host.Value);
|
||||||
|
return new CreatedResult(getUrl, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPut("{id}")]
|
||||||
|
public async Task<IActionResult> Put(string id, [FromBody]ScimGroup model)
|
||||||
|
{
|
||||||
|
var group = await _groupRepository.GetByIdAsync(new Guid(id));
|
||||||
|
if(group == null || group.OrganizationId != _orgId)
|
||||||
|
{
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
group = model.ToGroup(group);
|
||||||
|
await _groupService.SaveAsync(group);
|
||||||
|
|
||||||
|
var result = new ScimGroup(group);
|
||||||
|
return new OkObjectResult(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPatch("{id}")]
|
||||||
|
public async Task<IActionResult> Patch(string id)
|
||||||
|
{
|
||||||
|
var group = await _groupRepository.GetByIdAsync(new Guid(id));
|
||||||
|
if(group == null || group.OrganizationId != _orgId)
|
||||||
|
{
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
var memstream = new MemoryStream();
|
||||||
|
Request.Body.CopyTo(memstream);
|
||||||
|
memstream.Position = 0;
|
||||||
|
using(var reader = new StreamReader(memstream))
|
||||||
|
{
|
||||||
|
var text = reader.ReadToEnd();
|
||||||
|
Debug.WriteLine(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Do patch
|
||||||
|
|
||||||
|
var result = new ScimGroup(group);
|
||||||
|
return new OkObjectResult(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpDelete("{id}")]
|
||||||
|
public async Task<IActionResult> Delete(string id)
|
||||||
|
{
|
||||||
|
var group = await _groupRepository.GetByIdAsync(new Guid(id));
|
||||||
|
if(group == null || group.OrganizationId != _orgId)
|
||||||
|
{
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
await _groupService.DeleteAsync(group);
|
||||||
|
return new OkResult();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
122
src/Scim/Controllers/UsersController.cs
Normal file
122
src/Scim/Controllers/UsersController.cs
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
|
using Bit.Core.Services;
|
||||||
|
using Bit.Scim.Models;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Bit.Core.Models.Data;
|
||||||
|
using Bit.Core.Exceptions;
|
||||||
|
|
||||||
|
namespace Bit.Scim.Controllers
|
||||||
|
{
|
||||||
|
[Route("users")]
|
||||||
|
[Route("scim/users")]
|
||||||
|
public class UsersController : BaseController
|
||||||
|
{
|
||||||
|
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||||
|
private readonly IOrganizationService _organizationService;
|
||||||
|
private Guid _orgId = new Guid("2933f760-9c0b-4efb-a437-a82a00ed3fc1"); // TODO: come from context
|
||||||
|
|
||||||
|
public UsersController(
|
||||||
|
IOrganizationUserRepository organizationUserRepository,
|
||||||
|
IOrganizationService organizationService)
|
||||||
|
{
|
||||||
|
_organizationUserRepository = organizationUserRepository;
|
||||||
|
_organizationService = organizationService;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<IActionResult> GetAll([FromQuery]string filter, [FromQuery]string excludedAttributes,
|
||||||
|
[FromQuery]string attributes)
|
||||||
|
{
|
||||||
|
var users = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(_orgId);
|
||||||
|
users = FilterResources(users, filter);
|
||||||
|
var usersResult = users.Select(u => new ScimUser(u));
|
||||||
|
var result = new ScimListResponse(usersResult);
|
||||||
|
return new OkObjectResult(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("{id}")]
|
||||||
|
public async Task<IActionResult> Get(string id)
|
||||||
|
{
|
||||||
|
var users = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(_orgId);
|
||||||
|
var user = users.SingleOrDefault(u => u.Id == new Guid(id));
|
||||||
|
if(user == null)
|
||||||
|
{
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = new ScimUser(user);
|
||||||
|
return new OkObjectResult(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
public async Task<IActionResult> Post([FromBody]ScimUser model)
|
||||||
|
{
|
||||||
|
var email = model.Emails?.FirstOrDefault();
|
||||||
|
if(email == null)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("No email address available.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var orgUser = await _organizationService.InviteUserAsync(_orgId, null, email.Value,
|
||||||
|
OrganizationUserType.User, false, model.ExternalId, new List<SelectionReadOnly>());
|
||||||
|
var result = new ScimUser(orgUser);
|
||||||
|
var getUrl = Url.Action("Get", "Users", new { id = orgUser.Id.ToString() }, Request.Protocol, Request.Host.Value);
|
||||||
|
return new CreatedResult(getUrl, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPut("{id}")]
|
||||||
|
public async Task<IActionResult> Put(string id, [FromBody]ScimUser model)
|
||||||
|
{
|
||||||
|
var users = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(_orgId);
|
||||||
|
var user = users.SingleOrDefault(u => u.Id == new Guid(id));
|
||||||
|
if(user == null)
|
||||||
|
{
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: update
|
||||||
|
|
||||||
|
var result = new ScimUser(user);
|
||||||
|
return new OkObjectResult(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPatch("{id}")]
|
||||||
|
public async Task<IActionResult> Patch(string id)
|
||||||
|
{
|
||||||
|
var users = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(_orgId);
|
||||||
|
var user = users.SingleOrDefault(u => u.Id == new Guid(id));
|
||||||
|
if(user == null)
|
||||||
|
{
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
var memstream = new MemoryStream();
|
||||||
|
Request.Body.CopyTo(memstream);
|
||||||
|
memstream.Position = 0;
|
||||||
|
using(var reader = new StreamReader(memstream))
|
||||||
|
{
|
||||||
|
var text = reader.ReadToEnd();
|
||||||
|
Debug.WriteLine(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: patch
|
||||||
|
|
||||||
|
var result = new ScimUser(user);
|
||||||
|
return new OkObjectResult(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpDelete("{id}")]
|
||||||
|
public async Task<IActionResult> Delete(string id)
|
||||||
|
{
|
||||||
|
await _organizationService.DeleteUserAsync(_orgId, new Guid(id), null);
|
||||||
|
return new OkResult();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
34
src/Scim/Models/ScimError.cs
Normal file
34
src/Scim/Models/ScimError.cs
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Net;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace Bit.Scim.Models
|
||||||
|
{
|
||||||
|
public class ScimError
|
||||||
|
{
|
||||||
|
private IEnumerable<string> _schemas;
|
||||||
|
|
||||||
|
public ScimError()
|
||||||
|
{
|
||||||
|
_schemas = new[] { Constants.Messages.Error };
|
||||||
|
}
|
||||||
|
|
||||||
|
public ScimError(HttpStatusCode status, string detail = null)
|
||||||
|
: this()
|
||||||
|
{
|
||||||
|
Status = (int)status;
|
||||||
|
Detail = detail;
|
||||||
|
}
|
||||||
|
|
||||||
|
[JsonProperty("schemas")]
|
||||||
|
public IEnumerable<string> Schemas
|
||||||
|
{
|
||||||
|
get => _schemas;
|
||||||
|
set { _schemas = value; }
|
||||||
|
}
|
||||||
|
[JsonProperty("status")]
|
||||||
|
public int Status { get; set; }
|
||||||
|
[JsonProperty("detail")]
|
||||||
|
public string Detail { get; set; }
|
||||||
|
}
|
||||||
|
}
|
42
src/Scim/Models/ScimGroup.cs
Normal file
42
src/Scim/Models/ScimGroup.cs
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Bit.Core.Models.Table;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace Bit.Scim.Models
|
||||||
|
{
|
||||||
|
public class ScimGroup : ScimResource
|
||||||
|
{
|
||||||
|
public ScimGroup() { }
|
||||||
|
|
||||||
|
public ScimGroup(Group group)
|
||||||
|
{
|
||||||
|
Id = group.Id.ToString();
|
||||||
|
ExternalId = group.ExternalId;
|
||||||
|
DisplayName = group.Name;
|
||||||
|
Meta = new ScimResourceMetadata("Group");
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string SchemaIdentifier => Constants.Schemas.Group;
|
||||||
|
[JsonProperty("displayName")]
|
||||||
|
public string DisplayName { get; set; }
|
||||||
|
[JsonProperty("members")]
|
||||||
|
public IEnumerable<ScimMultiValuedAttribute> Members { get; set; }
|
||||||
|
|
||||||
|
public Group ToGroup(Guid orgId)
|
||||||
|
{
|
||||||
|
return new Group
|
||||||
|
{
|
||||||
|
ExternalId = ExternalId,
|
||||||
|
Name = DisplayName,
|
||||||
|
OrganizationId = orgId
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public Group ToGroup(Group group)
|
||||||
|
{
|
||||||
|
group.Name = DisplayName;
|
||||||
|
return group;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
24
src/Scim/Models/ScimListResponse.cs
Normal file
24
src/Scim/Models/ScimListResponse.cs
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace Bit.Scim.Models
|
||||||
|
{
|
||||||
|
public class ScimListResponse : ScimSchemaBase
|
||||||
|
{
|
||||||
|
public ScimListResponse(IEnumerable<ScimResource> resources)
|
||||||
|
{
|
||||||
|
Resources = resources;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string SchemaIdentifier => Constants.Messages.ListResponse;
|
||||||
|
[JsonProperty("totalResults", Order = 0)]
|
||||||
|
public int TotalResults => Resources == null ? 0 : Resources.Count();
|
||||||
|
[JsonProperty("Resources", Order = 1)]
|
||||||
|
public IEnumerable<ScimResource> Resources { get; private set; }
|
||||||
|
[JsonProperty("startIndex", Order = 2)]
|
||||||
|
public int StartIndex { get; set; } = 0;
|
||||||
|
[JsonProperty("itemsPerPage", Order = 3)]
|
||||||
|
public int ItemsPerPage => Resources == null ? 0 : Resources.Count();
|
||||||
|
}
|
||||||
|
}
|
19
src/Scim/Models/ScimMultiValuedAttribute.cs
Normal file
19
src/Scim/Models/ScimMultiValuedAttribute.cs
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
using System;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace Bit.Scim.Models
|
||||||
|
{
|
||||||
|
public class ScimMultiValuedAttribute
|
||||||
|
{
|
||||||
|
[JsonProperty("type")]
|
||||||
|
public string Type { get; set; }
|
||||||
|
[JsonProperty("primary")]
|
||||||
|
public bool Primary { get; set; }
|
||||||
|
[JsonProperty("display")]
|
||||||
|
public string Display { get; set; }
|
||||||
|
[JsonProperty("value")]
|
||||||
|
public string Value { get; set; }
|
||||||
|
[JsonProperty("$ref")]
|
||||||
|
public Uri Ref { get; set; }
|
||||||
|
}
|
||||||
|
}
|
14
src/Scim/Models/ScimResource.cs
Normal file
14
src/Scim/Models/ScimResource.cs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace Bit.Scim.Models
|
||||||
|
{
|
||||||
|
public abstract class ScimResource : ScimSchemaBase
|
||||||
|
{
|
||||||
|
[JsonProperty(Order = -5, PropertyName = "id")]
|
||||||
|
public string Id { get; set; }
|
||||||
|
[JsonProperty(PropertyName = "externalId")]
|
||||||
|
public string ExternalId { get; set; }
|
||||||
|
[JsonProperty(Order = 9999, PropertyName = "meta")]
|
||||||
|
public ScimResourceMetadata Meta { get; set; }
|
||||||
|
}
|
||||||
|
}
|
17
src/Scim/Models/ScimResourceMetadata.cs
Normal file
17
src/Scim/Models/ScimResourceMetadata.cs
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace Bit.Scim.Models
|
||||||
|
{
|
||||||
|
public class ScimResourceMetadata
|
||||||
|
{
|
||||||
|
private ScimResourceMetadata() { }
|
||||||
|
|
||||||
|
public ScimResourceMetadata(string resourceType)
|
||||||
|
{
|
||||||
|
ResourceType = resourceType;
|
||||||
|
}
|
||||||
|
|
||||||
|
[JsonProperty("resourceType")]
|
||||||
|
public string ResourceType { get; set; }
|
||||||
|
}
|
||||||
|
}
|
13
src/Scim/Models/ScimSchemaBase.cs
Normal file
13
src/Scim/Models/ScimSchemaBase.cs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace Bit.Scim.Models
|
||||||
|
{
|
||||||
|
public abstract class ScimSchemaBase
|
||||||
|
{
|
||||||
|
[JsonProperty("schemas", Order = -10)]
|
||||||
|
public virtual ISet<string> Schemas => new HashSet<string>(new[] { SchemaIdentifier });
|
||||||
|
[JsonIgnore]
|
||||||
|
public abstract string SchemaIdentifier { get; }
|
||||||
|
}
|
||||||
|
}
|
63
src/Scim/Models/ScimUser.cs
Normal file
63
src/Scim/Models/ScimUser.cs
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using Bit.Core.Models.Data;
|
||||||
|
using Bit.Core.Models.Table;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace Bit.Scim.Models
|
||||||
|
{
|
||||||
|
public class ScimUser : ScimResource
|
||||||
|
{
|
||||||
|
public ScimUser() { }
|
||||||
|
|
||||||
|
public ScimUser(OrganizationUserUserDetails userDetails)
|
||||||
|
{
|
||||||
|
Id = userDetails.Id.ToString();
|
||||||
|
ExternalId = userDetails.ExternalId;
|
||||||
|
UserName = userDetails.Email;
|
||||||
|
Name = new ScimName
|
||||||
|
{
|
||||||
|
Formatted = userDetails.Name
|
||||||
|
};
|
||||||
|
DisplayName = userDetails.Name;
|
||||||
|
Active = true;
|
||||||
|
Emails = new List<ScimMultiValuedAttribute> {
|
||||||
|
new ScimMultiValuedAttribute { Type = "work", Value = userDetails.Email } };
|
||||||
|
Meta = new ScimResourceMetadata("User");
|
||||||
|
}
|
||||||
|
|
||||||
|
public ScimUser(OrganizationUser orgUser)
|
||||||
|
{
|
||||||
|
Id = orgUser.Id.ToString();
|
||||||
|
ExternalId = orgUser.ExternalId;
|
||||||
|
UserName = orgUser.Email;
|
||||||
|
Active = true;
|
||||||
|
Emails = new List<ScimMultiValuedAttribute> {
|
||||||
|
new ScimMultiValuedAttribute { Type = "work", Value = orgUser.Email } };
|
||||||
|
Meta = new ScimResourceMetadata("User");
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string SchemaIdentifier => Constants.Schemas.User;
|
||||||
|
[JsonProperty("userName")]
|
||||||
|
public string UserName { get; set; }
|
||||||
|
[JsonProperty("name")]
|
||||||
|
public ScimName Name { get; set; }
|
||||||
|
[JsonProperty("displayName")]
|
||||||
|
public string DisplayName { get; set; }
|
||||||
|
[JsonProperty("active")]
|
||||||
|
public bool Active { get; set; }
|
||||||
|
[JsonProperty("emails")]
|
||||||
|
public IEnumerable<ScimMultiValuedAttribute> Emails { get; set; }
|
||||||
|
[JsonProperty("groups")]
|
||||||
|
public IEnumerable<ScimMultiValuedAttribute> Groups { get; set; }
|
||||||
|
|
||||||
|
public class ScimName
|
||||||
|
{
|
||||||
|
[JsonProperty("formatted")]
|
||||||
|
public string Formatted { get; set; }
|
||||||
|
[JsonProperty("familyName")]
|
||||||
|
public string FamilyName { get; set; }
|
||||||
|
[JsonProperty("givenName")]
|
||||||
|
public string GivenName { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
17
src/Scim/Program.cs
Normal file
17
src/Scim/Program.cs
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
using Microsoft.AspNetCore;
|
||||||
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
|
||||||
|
namespace Bit.Scim
|
||||||
|
{
|
||||||
|
public class Program
|
||||||
|
{
|
||||||
|
public static void Main(string[] args)
|
||||||
|
{
|
||||||
|
WebHost
|
||||||
|
.CreateDefaultBuilder(args)
|
||||||
|
.UseStartup<Startup>()
|
||||||
|
.Build()
|
||||||
|
.Run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
27
src/Scim/Properties/launchSettings.json
Normal file
27
src/Scim/Properties/launchSettings.json
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"iisSettings": {
|
||||||
|
"windowsAuthentication": false,
|
||||||
|
"anonymousAuthentication": true,
|
||||||
|
"iisExpress": {
|
||||||
|
"applicationUrl": "http://localhost:9000/",
|
||||||
|
"sslPort": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"profiles": {
|
||||||
|
"IIS Express": {
|
||||||
|
"commandName": "IISExpress",
|
||||||
|
"launchBrowser": false,
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Scim": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"launchBrowser": false,
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
},
|
||||||
|
"applicationUrl": "http://localhost:9000/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
25
src/Scim/Scim.csproj
Normal file
25
src/Scim/Scim.csproj
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<Version>1.15.1</Version>
|
||||||
|
<TargetFramework>netcoreapp2.0</TargetFramework>
|
||||||
|
<RootNamespace>Bit.Scim</RootNamespace>
|
||||||
|
<UserSecretsId>bitwarden-Scim</UserSecretsId>
|
||||||
|
<MvcRazorCompileOnPublish>false</MvcRazorCompileOnPublish>
|
||||||
|
<MvcRazorExcludeRefAssembliesFromPublish>false</MvcRazorExcludeRefAssembliesFromPublish>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Core\Core.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.3" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools" Version="2.0.0" />
|
||||||
|
<DotNetCliToolReference Include="Microsoft.Extensions.SecretManager.Tools" Version="2.0.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
79
src/Scim/Startup.cs
Normal file
79
src/Scim/Startup.cs
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
using Bit.Core;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
using Bit.Scim.Utilities;
|
||||||
|
using Microsoft.ApplicationInsights.Extensibility;
|
||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Serilog.Events;
|
||||||
|
|
||||||
|
namespace Bit.Scim
|
||||||
|
{
|
||||||
|
public class Startup
|
||||||
|
{
|
||||||
|
public Startup(IConfiguration configuration)
|
||||||
|
{
|
||||||
|
Configuration = configuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IConfiguration Configuration { get; }
|
||||||
|
|
||||||
|
public void ConfigureServices(IServiceCollection services)
|
||||||
|
{
|
||||||
|
// Options
|
||||||
|
services.AddOptions();
|
||||||
|
|
||||||
|
// Settings
|
||||||
|
var globalSettings = services.AddGlobalSettingsServices(Configuration);
|
||||||
|
|
||||||
|
// Repositories
|
||||||
|
services.AddSqlServerRepositories(globalSettings);
|
||||||
|
|
||||||
|
// Context
|
||||||
|
services.AddScoped<CurrentContext>();
|
||||||
|
|
||||||
|
// Identity
|
||||||
|
services.AddCustomIdentityServices(globalSettings);
|
||||||
|
|
||||||
|
// Services
|
||||||
|
services.AddBaseServices();
|
||||||
|
services.AddDefaultServices(globalSettings);
|
||||||
|
|
||||||
|
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
|
||||||
|
|
||||||
|
// Mvc
|
||||||
|
services.AddMvc(config =>
|
||||||
|
{
|
||||||
|
config.Filters.Add(new ExceptionHandlerFilterAttribute());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Configure(
|
||||||
|
IApplicationBuilder app,
|
||||||
|
IHostingEnvironment env,
|
||||||
|
IApplicationLifetime appLifetime,
|
||||||
|
GlobalSettings globalSettings,
|
||||||
|
ILoggerFactory loggerFactory)
|
||||||
|
{
|
||||||
|
// Disable app insights
|
||||||
|
var telConfig = app.ApplicationServices.GetService<TelemetryConfiguration>();
|
||||||
|
telConfig.DisableTelemetry = true;
|
||||||
|
|
||||||
|
loggerFactory.AddSerilog(env, appLifetime, globalSettings, (e) => e.Level >= LogEventLevel.Error);
|
||||||
|
|
||||||
|
if(env.IsDevelopment())
|
||||||
|
{
|
||||||
|
app.UseDeveloperExceptionPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default Middleware
|
||||||
|
app.UseDefaultMiddleware(env);
|
||||||
|
|
||||||
|
app.UseMvc();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
43
src/Scim/Utilities/ExceptionHandlerFilterAttribute.cs
Normal file
43
src/Scim/Utilities/ExceptionHandlerFilterAttribute.cs
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
using Bit.Core.Exceptions;
|
||||||
|
using Bit.Scim.Models;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Filters;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace Bit.Scim.Utilities
|
||||||
|
{
|
||||||
|
public class ExceptionHandlerFilterAttribute : ExceptionFilterAttribute
|
||||||
|
{
|
||||||
|
public override void OnException(ExceptionContext context)
|
||||||
|
{
|
||||||
|
var exception = context.Exception;
|
||||||
|
if(exception == null)
|
||||||
|
{
|
||||||
|
// Should never happen.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var error = new ScimError();
|
||||||
|
if(exception is BadRequestException)
|
||||||
|
{
|
||||||
|
context.HttpContext.Response.StatusCode = error.Status = 400;
|
||||||
|
error.Detail = exception.Message;
|
||||||
|
}
|
||||||
|
else if(exception is NotFoundException)
|
||||||
|
{
|
||||||
|
context.HttpContext.Response.StatusCode = error.Status = 404;
|
||||||
|
error.Detail = "Resource not found.";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
context.HttpContext.Response.StatusCode = error.Status = 500;
|
||||||
|
error.Detail = "An unhandled server error has occurred.";
|
||||||
|
var logger = context.HttpContext.RequestServices.GetRequiredService<ILogger<ExceptionHandlerFilterAttribute>>();
|
||||||
|
logger.LogError(0, exception, exception.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
context.Result = new ObjectResult(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
13
src/Scim/appsettings.Production.json
Normal file
13
src/Scim/appsettings.Production.json
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"globalSettings": {
|
||||||
|
"baseServiceUri": {
|
||||||
|
"vault": "https://vault.bitwarden.com",
|
||||||
|
"api": "https://api.bitwarden.com",
|
||||||
|
"identity": "https://identity.bitwarden.com",
|
||||||
|
"internalIdentity": "https://identity.bitwarden.com"
|
||||||
|
},
|
||||||
|
"braintree": {
|
||||||
|
"production": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
49
src/Scim/appsettings.json
Normal file
49
src/Scim/appsettings.json
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
{
|
||||||
|
"globalSettings": {
|
||||||
|
"selfHosted": false,
|
||||||
|
"siteName": "bitwarden",
|
||||||
|
"projectName": "Billing",
|
||||||
|
"stripeApiKey": "SECRET",
|
||||||
|
"baseServiceUri": {
|
||||||
|
"vault": "http://localhost:4001",
|
||||||
|
"api": "http://localhost:4000",
|
||||||
|
"identity": "http://localhost:33656",
|
||||||
|
"internalIdentity": "http://localhost:33656"
|
||||||
|
},
|
||||||
|
"sqlServer": {
|
||||||
|
"connectionString": "SECRET"
|
||||||
|
},
|
||||||
|
"mail": {
|
||||||
|
"sendGridApiKey": "SECRET",
|
||||||
|
"replyToEmail": "hello@bitwarden.com"
|
||||||
|
},
|
||||||
|
"identityServer": {
|
||||||
|
"certificateThumbprint": "SECRET"
|
||||||
|
},
|
||||||
|
"dataProtection": {
|
||||||
|
"certificateThumbprint": "SECRET"
|
||||||
|
},
|
||||||
|
"storage": {
|
||||||
|
"connectionString": "SECRET"
|
||||||
|
},
|
||||||
|
"documentDb": {
|
||||||
|
"uri": "SECRET",
|
||||||
|
"key": "SECRET"
|
||||||
|
},
|
||||||
|
"notificationHub": {
|
||||||
|
"connectionString": "SECRET",
|
||||||
|
"hubName": "SECRET"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"billingSettings": {
|
||||||
|
"stripeWebhookKey": "SECRET",
|
||||||
|
"stripeWebhookSecret": "SECRET",
|
||||||
|
"braintreeWebhookKey": "SECRET"
|
||||||
|
},
|
||||||
|
"braintree": {
|
||||||
|
"production": false,
|
||||||
|
"merchantId": "SECRET",
|
||||||
|
"publicKey": "SECRET",
|
||||||
|
"privateKey": "SECRET"
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user