1
0
mirror of https://github.com/bitwarden/server.git synced 2024-11-25 12:45:18 +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:
Rui Tomé 2022-10-04 02:40:28 +01:00 committed by GitHub
parent 707a39972b
commit 8325f0eed4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 3454 additions and 11 deletions

View File

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

View File

@ -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("")]

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

View File

@ -0,0 +1,8 @@
using Bit.Scim.Models;
namespace Bit.Scim.Queries.Users.Interfaces;
public interface IGetUserQuery
{
Task<ScimUserResponseModel> GetUserAsync(Guid organizationId, Guid id);
}

View File

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

View File

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

View File

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

View File

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

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -0,0 +1,3 @@
namespace Bit.Core.Exceptions;
public class ConflictException : Exception { }

View File

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