mirror of
https://github.com/bitwarden/server.git
synced 2024-11-21 12:05:42 +01:00
[EC-508] SCIM CQRS Refactor - Users/Get (#2266)
* [EC-390] Added Scim.Test unit tests project * [EC-390] Added ConflictException type. Updated BadRequestException to have parameterless constructor. Updated NotFoundException to have constructor with a message parameter * [EC-534] Implemented CQRS for Users Get and added unit tests * [EC-508] Renamed GetUserCommand to GetUserQuery * [EC-508] Created ScimServiceCollectionExtensions * [EC-508] Renamed AddScimCommands to AddScimUserQueries * [EC-508] Created ExceptionHandlerFilterAttribute on SCIM project Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com>
This commit is contained in:
parent
707a39972b
commit
8325f0eed4
@ -98,6 +98,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Api.IntegrationTest", "test
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Scim.IntegrationTest", "bitwarden_license\test\Scim.IntegrationTest\Scim.IntegrationTest.csproj", "{FE998849-5FC8-41A2-B7C9-9227901471A0}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Scim.IntegrationTest", "bitwarden_license\test\Scim.IntegrationTest\Scim.IntegrationTest.csproj", "{FE998849-5FC8-41A2-B7C9-9227901471A0}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Scim.Test", "bitwarden_license\test\Scim.Test\Scim.Test.csproj", "{B1595DA3-4C60-41AA-8BF0-499A5F75A885}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@ -238,6 +240,10 @@ Global
|
|||||||
{FE998849-5FC8-41A2-B7C9-9227901471A0}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{FE998849-5FC8-41A2-B7C9-9227901471A0}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{FE998849-5FC8-41A2-B7C9-9227901471A0}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{FE998849-5FC8-41A2-B7C9-9227901471A0}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{FE998849-5FC8-41A2-B7C9-9227901471A0}.Release|Any CPU.Build.0 = Release|Any CPU
|
{FE998849-5FC8-41A2-B7C9-9227901471A0}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{B1595DA3-4C60-41AA-8BF0-499A5F75A885}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{B1595DA3-4C60-41AA-8BF0-499A5F75A885}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{B1595DA3-4C60-41AA-8BF0-499A5F75A885}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{B1595DA3-4C60-41AA-8BF0-499A5F75A885}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
@ -276,6 +282,7 @@ Global
|
|||||||
{7EFB1124-F40A-40EB-9EDA-94FD540AA8FD} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
|
{7EFB1124-F40A-40EB-9EDA-94FD540AA8FD} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
|
||||||
{CBE96C6D-A4D6-46E1-94C5-42D6CAD8531C} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
|
{CBE96C6D-A4D6-46E1-94C5-42D6CAD8531C} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
|
||||||
{FE998849-5FC8-41A2-B7C9-9227901471A0} = {287CFF34-BBDB-4BC4-AF88-1E19A5A4679B}
|
{FE998849-5FC8-41A2-B7C9-9227901471A0} = {287CFF34-BBDB-4BC4-AF88-1E19A5A4679B}
|
||||||
|
{B1595DA3-4C60-41AA-8BF0-499A5F75A885} = {287CFF34-BBDB-4BC4-AF88-1E19A5A4679B}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
SolutionGuid = {E01CBF68-2E20-425F-9EDB-E0A6510CA92F}
|
SolutionGuid = {E01CBF68-2E20-425F-9EDB-E0A6510CA92F}
|
||||||
|
@ -5,6 +5,8 @@ using Bit.Core.Services;
|
|||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Bit.Scim.Context;
|
using Bit.Scim.Context;
|
||||||
using Bit.Scim.Models;
|
using Bit.Scim.Models;
|
||||||
|
using Bit.Scim.Queries.Users.Interfaces;
|
||||||
|
using Bit.Scim.Utilities;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
@ -13,6 +15,7 @@ namespace Bit.Scim.Controllers.v2;
|
|||||||
|
|
||||||
[Authorize("Scim")]
|
[Authorize("Scim")]
|
||||||
[Route("v2/{organizationId}/users")]
|
[Route("v2/{organizationId}/users")]
|
||||||
|
[ExceptionHandlerFilter]
|
||||||
public class UsersController : Controller
|
public class UsersController : Controller
|
||||||
{
|
{
|
||||||
private readonly IUserService _userService;
|
private readonly IUserService _userService;
|
||||||
@ -21,6 +24,7 @@ public class UsersController : Controller
|
|||||||
private readonly IOrganizationService _organizationService;
|
private readonly IOrganizationService _organizationService;
|
||||||
private readonly IScimContext _scimContext;
|
private readonly IScimContext _scimContext;
|
||||||
private readonly ScimSettings _scimSettings;
|
private readonly ScimSettings _scimSettings;
|
||||||
|
private readonly IGetUserQuery _getUserQuery;
|
||||||
private readonly ILogger<UsersController> _logger;
|
private readonly ILogger<UsersController> _logger;
|
||||||
|
|
||||||
public UsersController(
|
public UsersController(
|
||||||
@ -30,6 +34,7 @@ public class UsersController : Controller
|
|||||||
IOrganizationService organizationService,
|
IOrganizationService organizationService,
|
||||||
IScimContext scimContext,
|
IScimContext scimContext,
|
||||||
IOptions<ScimSettings> scimSettings,
|
IOptions<ScimSettings> scimSettings,
|
||||||
|
IGetUserQuery getUserQuery,
|
||||||
ILogger<UsersController> logger)
|
ILogger<UsersController> logger)
|
||||||
{
|
{
|
||||||
_userService = userService;
|
_userService = userService;
|
||||||
@ -38,22 +43,15 @@ public class UsersController : Controller
|
|||||||
_organizationService = organizationService;
|
_organizationService = organizationService;
|
||||||
_scimContext = scimContext;
|
_scimContext = scimContext;
|
||||||
_scimSettings = scimSettings?.Value;
|
_scimSettings = scimSettings?.Value;
|
||||||
|
_getUserQuery = getUserQuery;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{id}")]
|
[HttpGet("{id}")]
|
||||||
public async Task<IActionResult> Get(Guid organizationId, Guid id)
|
public async Task<IActionResult> Get(Guid organizationId, Guid id)
|
||||||
{
|
{
|
||||||
var orgUser = await _organizationUserRepository.GetDetailsByIdAsync(id);
|
var scimUserResponseModel = await _getUserQuery.GetUserAsync(organizationId, id);
|
||||||
if (orgUser == null || orgUser.OrganizationId != organizationId)
|
return Ok(scimUserResponseModel);
|
||||||
{
|
|
||||||
return new NotFoundObjectResult(new ScimErrorResponseModel
|
|
||||||
{
|
|
||||||
Status = 404,
|
|
||||||
Detail = "User not found."
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return new ObjectResult(new ScimUserResponseModel(orgUser));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("")]
|
[HttpGet("")]
|
||||||
|
27
bitwarden_license/src/Scim/Queries/Users/GetUserQuery.cs
Normal file
27
bitwarden_license/src/Scim/Queries/Users/GetUserQuery.cs
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
using Bit.Core.Exceptions;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
|
using Bit.Scim.Models;
|
||||||
|
using Bit.Scim.Queries.Users.Interfaces;
|
||||||
|
|
||||||
|
namespace Bit.Scim.Queries.Users;
|
||||||
|
|
||||||
|
public class GetUserQuery : IGetUserQuery
|
||||||
|
{
|
||||||
|
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||||
|
|
||||||
|
public GetUserQuery(IOrganizationUserRepository organizationUserRepository)
|
||||||
|
{
|
||||||
|
_organizationUserRepository = organizationUserRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ScimUserResponseModel> GetUserAsync(Guid organizationId, Guid id)
|
||||||
|
{
|
||||||
|
var orgUser = await _organizationUserRepository.GetDetailsByIdAsync(id);
|
||||||
|
if (orgUser == null || orgUser.OrganizationId != organizationId)
|
||||||
|
{
|
||||||
|
throw new NotFoundException("User not found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ScimUserResponseModel(orgUser);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
using Bit.Scim.Models;
|
||||||
|
|
||||||
|
namespace Bit.Scim.Queries.Users.Interfaces;
|
||||||
|
|
||||||
|
public interface IGetUserQuery
|
||||||
|
{
|
||||||
|
Task<ScimUserResponseModel> GetUserAsync(Guid organizationId, Guid id);
|
||||||
|
}
|
@ -75,6 +75,8 @@ public class Startup
|
|||||||
config.Filters.Add(new LoggingExceptionHandlerFilterAttribute());
|
config.Filters.Add(new LoggingExceptionHandlerFilterAttribute());
|
||||||
});
|
});
|
||||||
services.Configure<RouteOptions>(options => options.LowercaseUrls = true);
|
services.Configure<RouteOptions>(options => options.LowercaseUrls = true);
|
||||||
|
|
||||||
|
services.AddScimUserQueries();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Configure(
|
public void Configure(
|
||||||
|
@ -0,0 +1,35 @@
|
|||||||
|
using Bit.Core.Exceptions;
|
||||||
|
using Bit.Scim.Models;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Filters;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
int statusCode = StatusCodes.Status500InternalServerError;
|
||||||
|
var scimErrorResponseModel = new ScimErrorResponseModel
|
||||||
|
{
|
||||||
|
Detail = exception.Message
|
||||||
|
};
|
||||||
|
|
||||||
|
if (exception is NotFoundException)
|
||||||
|
{
|
||||||
|
statusCode = StatusCodes.Status404NotFound;
|
||||||
|
}
|
||||||
|
|
||||||
|
scimErrorResponseModel.Status = statusCode;
|
||||||
|
|
||||||
|
context.HttpContext.Response.StatusCode = statusCode;
|
||||||
|
context.Result = new ObjectResult(scimErrorResponseModel);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
using Bit.Scim.Queries.Users;
|
||||||
|
using Bit.Scim.Queries.Users.Interfaces;
|
||||||
|
|
||||||
|
namespace Bit.Scim.Utilities;
|
||||||
|
|
||||||
|
public static class ScimServiceCollectionExtensions
|
||||||
|
{
|
||||||
|
public static void AddScimUserQueries(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.AddScoped<IGetUserQuery, GetUserQuery>();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,66 @@
|
|||||||
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.Exceptions;
|
||||||
|
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
|
using Bit.Scim.Queries.Users;
|
||||||
|
using Bit.Scim.Utilities;
|
||||||
|
using Bit.Test.Common.AutoFixture;
|
||||||
|
using Bit.Test.Common.AutoFixture.Attributes;
|
||||||
|
using Bit.Test.Common.Helpers;
|
||||||
|
using NSubstitute;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Bit.Scim.Test.Queries.Users;
|
||||||
|
|
||||||
|
[SutProviderCustomize]
|
||||||
|
public class GetUserQueryTests
|
||||||
|
{
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task GetUser_Success(SutProvider<GetUserQuery> sutProvider, OrganizationUserUserDetails organizationUserUserDetails)
|
||||||
|
{
|
||||||
|
var expectedResult = new Models.ScimUserResponseModel
|
||||||
|
{
|
||||||
|
Id = organizationUserUserDetails.Id.ToString(),
|
||||||
|
UserName = organizationUserUserDetails.Email,
|
||||||
|
Name = new Models.BaseScimUserModel.NameModel(organizationUserUserDetails.Name),
|
||||||
|
Emails = new List<Models.BaseScimUserModel.EmailModel> { new Models.BaseScimUserModel.EmailModel(organizationUserUserDetails.Email) },
|
||||||
|
DisplayName = organizationUserUserDetails.Name,
|
||||||
|
Active = organizationUserUserDetails.Status != Core.Enums.OrganizationUserStatusType.Revoked ? true : false,
|
||||||
|
Groups = new List<string>(),
|
||||||
|
ExternalId = organizationUserUserDetails.ExternalId,
|
||||||
|
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
|
||||||
|
};
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||||
|
.GetDetailsByIdAsync(organizationUserUserDetails.Id)
|
||||||
|
.Returns(organizationUserUserDetails);
|
||||||
|
|
||||||
|
var result = await sutProvider.Sut.GetUserAsync(organizationUserUserDetails.OrganizationId, organizationUserUserDetails.Id);
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1).GetDetailsByIdAsync(organizationUserUserDetails.Id);
|
||||||
|
AssertHelper.AssertPropertyEqual(expectedResult, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task GetUser_NotFound_Throws(SutProvider<GetUserQuery> sutProvider, Guid organizationId, Guid organizationUserId)
|
||||||
|
{
|
||||||
|
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.GetUserAsync(organizationId, organizationUserId));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task GetUser_MismatchingOrganizationId_Throws(SutProvider<GetUserQuery> sutProvider, Guid organizationId, Guid organizationUserId)
|
||||||
|
{
|
||||||
|
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||||
|
.GetByIdAsync(organizationUserId)
|
||||||
|
.Returns(new OrganizationUser
|
||||||
|
{
|
||||||
|
Id = organizationUserId,
|
||||||
|
OrganizationId = Guid.NewGuid()
|
||||||
|
});
|
||||||
|
|
||||||
|
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.GetUserAsync(organizationId, organizationUserId));
|
||||||
|
}
|
||||||
|
}
|
25
bitwarden_license/test/Scim.Test/Scim.Test.csproj
Normal file
25
bitwarden_license/test/Scim.Test/Scim.Test.csproj
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(MicrosoftNetTestSdkVersion)" />
|
||||||
|
<PackageReference Include="xunit" Version="$(XUnitVersion)" />
|
||||||
|
<PackageReference Include="xunit.runner.visualstudio" Version="$(XUnitRunnerVisualStudioVersion)">
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="coverlet.collector" Version="$(CoverletCollectorVersion)">
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="NSubstitute" Version="$(NSubstitueVersion)" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\..\src\Scim\Scim.csproj" />
|
||||||
|
<ProjectReference Include="..\..\..\test\Common\Common.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
3249
bitwarden_license/test/Scim.Test/packages.lock.json
Normal file
3249
bitwarden_license/test/Scim.Test/packages.lock.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -4,6 +4,9 @@ namespace Bit.Core.Exceptions;
|
|||||||
|
|
||||||
public class BadRequestException : Exception
|
public class BadRequestException : Exception
|
||||||
{
|
{
|
||||||
|
public BadRequestException() : base()
|
||||||
|
{ }
|
||||||
|
|
||||||
public BadRequestException(string message)
|
public BadRequestException(string message)
|
||||||
: base(message)
|
: base(message)
|
||||||
{ }
|
{ }
|
||||||
|
3
src/Core/Exceptions/ConflictException.cs
Normal file
3
src/Core/Exceptions/ConflictException.cs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
namespace Bit.Core.Exceptions;
|
||||||
|
|
||||||
|
public class ConflictException : Exception { }
|
@ -1,3 +1,11 @@
|
|||||||
namespace Bit.Core.Exceptions;
|
namespace Bit.Core.Exceptions;
|
||||||
|
|
||||||
public class NotFoundException : Exception { }
|
public class NotFoundException : Exception
|
||||||
|
{
|
||||||
|
public NotFoundException() : base()
|
||||||
|
{ }
|
||||||
|
|
||||||
|
public NotFoundException(string message)
|
||||||
|
: base(message)
|
||||||
|
{ }
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user