1
0
mirror of https://github.com/bitwarden/server.git synced 2024-11-21 12:05:42 +01:00

stub out new scim api for dir sync

This commit is contained in:
Kyle Spearrin 2017-12-12 13:21:15 -05:00
parent 3d05c9208f
commit 7b359053d6
26 changed files with 802 additions and 14 deletions

View File

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

View File

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

View File

@ -0,0 +1,7 @@
namespace Bit.Core.Models
{
public interface IExternal
{
string ExternalId { get; }
}
}

View File

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

View File

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

View File

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

View File

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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