Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[EC-508] SCIM CQRS Refactor - Users/GetList #2265

Merged
merged 14 commits into from
Oct 18, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 11 additions & 56 deletions bitwarden_license/src/Scim/Controllers/v2/UsersController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public class UsersController : Controller
private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly IOrganizationService _organizationService;
private readonly IGetUserQuery _getUserQuery;
private readonly IGetUsersListQuery _getUsersListQuery;
private readonly IDeleteOrganizationUserCommand _deleteOrganizationUserCommand;
private readonly IPostUserCommand _postUserCommand;
private readonly ILogger<UsersController> _logger;
Expand All @@ -28,6 +29,7 @@ public UsersController(
IOrganizationUserRepository organizationUserRepository,
IOrganizationService organizationService,
IGetUserQuery getUserQuery,
IGetUsersListQuery getUsersListQuery,
IDeleteOrganizationUserCommand deleteOrganizationUserCommand,
IPostUserCommand postUserCommand,
ILogger<UsersController> logger)
Expand All @@ -36,6 +38,7 @@ public UsersController(
_organizationUserRepository = organizationUserRepository;
_organizationService = organizationService;
_getUserQuery = getUserQuery;
_getUsersListQuery = getUsersListQuery;
_deleteOrganizationUserCommand = deleteOrganizationUserCommand;
_postUserCommand = postUserCommand;
_logger = logger;
Expand All @@ -44,7 +47,8 @@ public UsersController(
[HttpGet("{id}")]
public async Task<IActionResult> Get(Guid organizationId, Guid id)
{
var scimUserResponseModel = await _getUserQuery.GetUserAsync(organizationId, id);
var orgUser = await _getUserQuery.GetUserAsync(organizationId, id);
var scimUserResponseModel = new ScimUserResponseModel(orgUser);
return Ok(scimUserResponseModel);
}

Expand All @@ -55,64 +59,15 @@ public async Task<IActionResult> Get(
[FromQuery] int? count,
[FromQuery] int? startIndex)
{
string emailFilter = null;
string usernameFilter = null;
string externalIdFilter = null;
if (!string.IsNullOrWhiteSpace(filter))
{
if (filter.StartsWith("userName eq "))
{
usernameFilter = filter.Substring(12).Trim('"').ToLowerInvariant();
if (usernameFilter.Contains("@"))
{
emailFilter = usernameFilter;
}
}
else if (filter.StartsWith("externalId eq "))
{
externalIdFilter = filter.Substring(14).Trim('"');
}
}

var userList = new List<ScimUserResponseModel> { };
var orgUsers = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(organizationId);
var totalResults = 0;
if (!string.IsNullOrWhiteSpace(emailFilter))
{
var orgUser = orgUsers.FirstOrDefault(ou => ou.Email.ToLowerInvariant() == emailFilter);
if (orgUser != null)
{
userList.Add(new ScimUserResponseModel(orgUser));
}
totalResults = userList.Count;
}
else if (!string.IsNullOrWhiteSpace(externalIdFilter))
{
var orgUser = orgUsers.FirstOrDefault(ou => ou.ExternalId == externalIdFilter);
if (orgUser != null)
{
userList.Add(new ScimUserResponseModel(orgUser));
}
totalResults = userList.Count;
}
else if (string.IsNullOrWhiteSpace(filter) && startIndex.HasValue && count.HasValue)
{
userList = orgUsers.OrderBy(ou => ou.Email)
.Skip(startIndex.Value - 1)
.Take(count.Value)
.Select(ou => new ScimUserResponseModel(ou))
.ToList();
totalResults = orgUsers.Count;
}

var result = new ScimListResponseModel<ScimUserResponseModel>
var usersListQueryResult = await _getUsersListQuery.GetUsersListAsync(organizationId, filter, count, startIndex);
var scimListResponseModel = new ScimListResponseModel<ScimUserResponseModel>
{
Resources = userList,
ItemsPerPage = count.GetValueOrDefault(userList.Count),
TotalResults = totalResults,
Resources = usersListQueryResult.userList.Select(u => new ScimUserResponseModel(u)).ToList(),
ItemsPerPage = count.GetValueOrDefault(usersListQueryResult.userList.Count()),
TotalResults = usersListQueryResult.totalResults,
StartIndex = startIndex.GetValueOrDefault(1),
};
return new ObjectResult(result);
return Ok(scimListResponseModel);
}

[HttpPost("")]
Expand Down
6 changes: 3 additions & 3 deletions bitwarden_license/src/Scim/Users/GetUserQuery.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using Bit.Core.Exceptions;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's just pull this GetUserQuery back out into the controller (i.e. leave it how it was). It was an early one that I approved before we had that discussion with Oscar about when it's worth using a query. In this case, the repository call is basically the query, there's so little code here that we can just have it in the controller. (sorry for the busywork)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can do this in a separate PR though, if you want.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll do that on the next PR when merging to master. Thanks!

using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.Repositories;
using Bit.Scim.Models;
using Bit.Scim.Users.Interfaces;

namespace Bit.Scim.Users;
Expand All @@ -14,14 +14,14 @@ public GetUserQuery(IOrganizationUserRepository organizationUserRepository)
_organizationUserRepository = organizationUserRepository;
}

public async Task<ScimUserResponseModel> GetUserAsync(Guid organizationId, Guid id)
public async Task<OrganizationUserUserDetails> 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);
return orgUser;
}
}
69 changes: 69 additions & 0 deletions bitwarden_license/src/Scim/Users/GetUsersListQuery.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.Repositories;
using Bit.Scim.Users.Interfaces;

namespace Bit.Scim.Users;

public class GetUsersListQuery : IGetUsersListQuery
{
private readonly IOrganizationUserRepository _organizationUserRepository;

public GetUsersListQuery(IOrganizationUserRepository organizationUserRepository)
{
_organizationUserRepository = organizationUserRepository;
}

public async Task<(IEnumerable<OrganizationUserUserDetails> userList, int totalResults)> GetUsersListAsync(Guid organizationId, string filter, int? count, int? startIndex)
{
string emailFilter = null;
string usernameFilter = null;
string externalIdFilter = null;
if (!string.IsNullOrWhiteSpace(filter))
{
if (filter.StartsWith("userName eq "))
{
usernameFilter = filter.Substring(12).Trim('"').ToLowerInvariant();
if (usernameFilter.Contains("@"))
{
emailFilter = usernameFilter;
}
}
else if (filter.StartsWith("externalId eq "))
{
externalIdFilter = filter.Substring(14).Trim('"');
}
}

var userList = new List<OrganizationUserUserDetails>();
var orgUsers = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(organizationId);
var totalResults = 0;
if (!string.IsNullOrWhiteSpace(emailFilter))
{
var orgUser = orgUsers.FirstOrDefault(ou => ou.Email.ToLowerInvariant() == emailFilter);
if (orgUser != null)
{
userList.Add(orgUser);
}
totalResults = userList.Count;
}
else if (!string.IsNullOrWhiteSpace(externalIdFilter))
{
var orgUser = orgUsers.FirstOrDefault(ou => ou.ExternalId == externalIdFilter);
if (orgUser != null)
{
userList.Add(orgUser);
}
totalResults = userList.Count;
}
else if (string.IsNullOrWhiteSpace(filter) && startIndex.HasValue && count.HasValue)
{
userList = orgUsers.OrderBy(ou => ou.Email)
.Skip(startIndex.Value - 1)
.Take(count.Value)
.ToList();
totalResults = orgUsers.Count;
}

return (userList, totalResults);
}
}
4 changes: 2 additions & 2 deletions bitwarden_license/src/Scim/Users/Interfaces/IGetUserQuery.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
using Bit.Scim.Models;
using Bit.Core.Models.Data.Organizations.OrganizationUsers;

namespace Bit.Scim.Users.Interfaces;

public interface IGetUserQuery
{
Task<ScimUserResponseModel> GetUserAsync(Guid organizationId, Guid id);
Task<OrganizationUserUserDetails> GetUserAsync(Guid organizationId, Guid id);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using Bit.Core.Models.Data.Organizations.OrganizationUsers;

namespace Bit.Scim.Users.Interfaces;

public interface IGetUsersListQuery
{
Task<(IEnumerable<OrganizationUserUserDetails> userList, int totalResults)> GetUsersListAsync(Guid organizationId, string filter, int? count, int? startIndex);
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public static void AddScimGroupQueries(this IServiceCollection services)
public static void AddScimUserQueries(this IServiceCollection services)
{
services.AddScoped<IGetUserQuery, GetUserQuery>();
services.AddScoped<IGetUsersListQuery, GetUsersListQuery>();
}

public static void AddScimUserCommands(this IServiceCollection services)
Expand Down
16 changes: 1 addition & 15 deletions bitwarden_license/test/Scim.Test/Users/GetUserQueryTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.Repositories;
using Bit.Scim.Users;
using Bit.Scim.Utilities;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Bit.Test.Common.Helpers;
Expand All @@ -19,27 +18,14 @@ public class GetUserQueryTests
[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);
AssertHelper.AssertPropertyEqual(organizationUserUserDetails, result);
}

[Theory]
Expand Down
Loading