mirror of
https://github.com/bitwarden/server.git
synced 2024-11-22 12:15:36 +01: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
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventsProcessor", "src\EventsProcessor\EventsProcessor.csproj", "{FCB77723-0FF5-4B1B-AB90-6930CF2E25F9}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Scim", "src\Scim\Scim.csproj", "{B8C5FFEB-186A-46FF-B914-BB3D50AA8D61}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
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}.Release|Any CPU.ActiveCfg = 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
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
@ -125,6 +131,7 @@ Global
|
||||
{9CF59342-3912-4B45-A2BA-0F173666586D} = {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}
|
||||
{B8C5FFEB-186A-46FF-B914-BB3D50AA8D61} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84D}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {E01CBF68-2E20-425F-9EDB-E0A6510CA92F}
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
namespace Bit.Core.Models.Data
|
||||
{
|
||||
public class OrganizationUserUserDetails
|
||||
public class OrganizationUserUserDetails : IExternal
|
||||
{
|
||||
public Guid Id { 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
|
||||
{
|
||||
public class Group : ITableObject<Guid>
|
||||
public class Group : ITableObject<Guid>, IExternal
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid OrganizationId { get; set; }
|
||||
|
@ -4,7 +4,7 @@ using Bit.Core.Enums;
|
||||
|
||||
namespace Bit.Core.Models.Table
|
||||
{
|
||||
public class OrganizationUser : ITableObject<Guid>
|
||||
public class OrganizationUser : ITableObject<Guid>, IExternal
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid OrganizationId { get; set; }
|
||||
|
@ -26,15 +26,15 @@ namespace Bit.Core.Services
|
||||
Task UpdateExpirationDateAsync(Guid organizationId, DateTime? expirationDate);
|
||||
Task EnableAsync(Guid organizationId);
|
||||
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);
|
||||
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);
|
||||
Task ResendInviteAsync(Guid organizationId, Guid invitingUserId, Guid organizationUserId);
|
||||
Task<OrganizationUser> AcceptUserAsync(Guid organizationUserId, User user, string token);
|
||||
Task<OrganizationUser> ConfirmUserAsync(Guid organizationId, Guid organizationUserId, string key, Guid confirmingUserId);
|
||||
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 UpdateUserGroupsAsync(OrganizationUser organizationUser, IEnumerable<Guid> groupIds);
|
||||
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)
|
||||
{
|
||||
var result = await InviteUserAsync(organizationId, invitingUserId, new List<string> { email }, type, accessAll,
|
||||
@ -827,7 +827,7 @@ namespace Bit.Core.Services
|
||||
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<SelectionReadOnly> collections)
|
||||
{
|
||||
@ -837,9 +837,9 @@ namespace Bit.Core.Services
|
||||
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))
|
||||
{
|
||||
throw new BadRequestException("Only owners can invite new owners.");
|
||||
@ -1065,7 +1065,7 @@ namespace Bit.Core.Services
|
||||
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);
|
||||
if(orgUser == null || orgUser.OrganizationId != organizationId)
|
||||
@ -1073,14 +1073,14 @@ namespace Bit.Core.Services
|
||||
throw new BadRequestException("User not valid.");
|
||||
}
|
||||
|
||||
if(orgUser.UserId == deletingUserId)
|
||||
if(deletingUserId.HasValue && orgUser.UserId == deletingUserId.Value)
|
||||
{
|
||||
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))
|
||||
{
|
||||
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…
Reference in New Issue
Block a user