mirror of
https://github.com/bitwarden/server.git
synced 2024-11-24 12:35:25 +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
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Scim.IntegrationTest", "bitwarden_license\test\Scim.IntegrationTest\Scim.IntegrationTest.csproj", "{FE998849-5FC8-41A2-B7C9-9227901471A0}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Scim.Test", "bitwarden_license\test\Scim.Test\Scim.Test.csproj", "{B1595DA3-4C60-41AA-8BF0-499A5F75A885}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
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}.Release|Any CPU.ActiveCfg = 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
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
@ -276,6 +282,7 @@ Global
|
||||
{7EFB1124-F40A-40EB-9EDA-94FD540AA8FD} = {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}
|
||||
{B1595DA3-4C60-41AA-8BF0-499A5F75A885} = {287CFF34-BBDB-4BC4-AF88-1E19A5A4679B}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {E01CBF68-2E20-425F-9EDB-E0A6510CA92F}
|
||||
|
@ -5,6 +5,8 @@ using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Scim.Context;
|
||||
using Bit.Scim.Models;
|
||||
using Bit.Scim.Queries.Users.Interfaces;
|
||||
using Bit.Scim.Utilities;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
@ -13,6 +15,7 @@ namespace Bit.Scim.Controllers.v2;
|
||||
|
||||
[Authorize("Scim")]
|
||||
[Route("v2/{organizationId}/users")]
|
||||
[ExceptionHandlerFilter]
|
||||
public class UsersController : Controller
|
||||
{
|
||||
private readonly IUserService _userService;
|
||||
@ -21,6 +24,7 @@ public class UsersController : Controller
|
||||
private readonly IOrganizationService _organizationService;
|
||||
private readonly IScimContext _scimContext;
|
||||
private readonly ScimSettings _scimSettings;
|
||||
private readonly IGetUserQuery _getUserQuery;
|
||||
private readonly ILogger<UsersController> _logger;
|
||||
|
||||
public UsersController(
|
||||
@ -30,6 +34,7 @@ public class UsersController : Controller
|
||||
IOrganizationService organizationService,
|
||||
IScimContext scimContext,
|
||||
IOptions<ScimSettings> scimSettings,
|
||||
IGetUserQuery getUserQuery,
|
||||
ILogger<UsersController> logger)
|
||||
{
|
||||
_userService = userService;
|
||||
@ -38,22 +43,15 @@ public class UsersController : Controller
|
||||
_organizationService = organizationService;
|
||||
_scimContext = scimContext;
|
||||
_scimSettings = scimSettings?.Value;
|
||||
_getUserQuery = getUserQuery;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
public async Task<IActionResult> Get(Guid organizationId, Guid id)
|
||||
{
|
||||
var orgUser = await _organizationUserRepository.GetDetailsByIdAsync(id);
|
||||
if (orgUser == null || orgUser.OrganizationId != organizationId)
|
||||
{
|
||||
return new NotFoundObjectResult(new ScimErrorResponseModel
|
||||
{
|
||||
Status = 404,
|
||||
Detail = "User not found."
|
||||
});
|
||||
}
|
||||
return new ObjectResult(new ScimUserResponseModel(orgUser));
|
||||
var scimUserResponseModel = await _getUserQuery.GetUserAsync(organizationId, id);
|
||||
return Ok(scimUserResponseModel);
|
||||
}
|
||||
|
||||
[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());
|
||||
});
|
||||
services.Configure<RouteOptions>(options => options.LowercaseUrls = true);
|
||||
|
||||
services.AddScimUserQueries();
|
||||
}
|
||||
|
||||
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 BadRequestException() : base()
|
||||
{ }
|
||||
|
||||
public BadRequestException(string 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;
|
||||
|
||||
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