mirror of
https://github.com/bitwarden/server.git
synced 2024-11-21 12:05:42 +01:00
[EC-507 / EC-508] SCIM CQRS Refactor - Groups/Users (#2344)
* [EC-507] SCIM CQRS Refactor - Groups/Put (#2269) * [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-531] Implemented CQRS for Groups Put and added unit tests * [EC-507] Created ScimServiceCollectionExtensions * [EC-507] Renamed AddScimCommands to AddScimGroupCommands * [EC-507] Created ExceptionHandlerFilterAttribute on SCIM project * [EC-507] Removed unneeded dependencies from GroupsController * [EC-507] Update PutGroupCommand to return Group PutGroupCommand returns Group and GroupsController creates ScimGroupResponseModel response * [EC-507] Remove Queries/Commands folders from Scim and Scim.Tests * [EC-507] Remove unneeded check on empty provided memberIds * [EC-507] SCIM CQRS Refactor - Groups/GetList (#2272) * [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-508] Implemented CQRS for Groups GetList and added unit tests * [EC-507] Created ScimServiceCollectionExtensions and renamed GetGroupsListCommand to GetGroupsListQuery * [EC-507] Renamed AddScimCommands to AddScimGroupQueries * [EC-507] Removed unneeded dependencies from GroupsController * [EC-507] Remove 'Queries' folder from Scim and Scim.Test * [EC-507] Move ScimListResponseModel from GetGroupsListQuery to Scim.GroupsController * [EC-507] Remove asserts on IGroupRepository.GetManyByOrganizationIdAsync from unit tests * [EC-507] SCIM CQRS Refactor - Groups/Get (#2271) * [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-507] Implemented CQRS for Groups Get and added unit tests * [EC-507] Created ScimServiceCollectionExtensions and renamed GetGroupCommand to GetGroupQuery * [EC-507] Renamed AddScimCommands to AddScimGroupQueries * [EC-507] Created ExceptionHandlerFilterAttribute on SCIM project * [EC-507] Removed unneeded dependencies from GroupsController * [EC-507] Sorted order of methods * [EC-507] Removed GetGroupQuery and moved logic to controller * [EC-507] Remove 'Queries' folder from Scim and Scim.Test * [EC-507] SCIM CQRS Refactor - Groups/Patch (#2268) * [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-532] Implemented CQRS for Groups Patch and added unit tests * [EC-507] Created ScimServiceCollectionExtensions * [EC-507] Renamed AddScimCommands to AddScimGroupCommands * [EC-507] Created ExceptionHandlerFilterAttribute on SCIM project * [EC-507] Removed unneeded dependencies from GroupsController * [EC-507] Remove Queries/Commands folders from Scim and Scim.Tests * [EC-507] Assert group.Name after saving. Assert userIds saved. * [EC-508] SCIM CQRS Refactor - Users/Delete (#2261) * [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-539] Implemented CQRS for Users Delete and added unit tests * [EC-508] Created ScimServiceCollectionExtensions * [EC-508] Created ExceptionHandlerFilterAttribute on SCIM project * [EC-508] Removed unneeded model from DeleteUserCommand. Removed unneeded dependencies from UsersController * [EC-508] Removed Bit.Scim.Models dependency from DeleteUserCommandTests * [EC-508] Deleted 'DeleteUserCommand' from SCIM; Created commands on Core 'DeleteOrganizationUserCommand', 'PushDeleteUserRegistrationOrganizationCommand' and 'OrganizationHasConfirmedOwnersExceptQuery' * [EC-508] Changed DeleteOrganizationUserCommand back to using IOrganizationService * [EC-508] Fixed DeleteOrganizationUserCommand unit tests * [EC-508] Remove unneeded obsolete comments. Update DeleteUserAsync Obsolete comment with ticket reference * [EC-508] Move DeleteOrganizationUserCommand to OrganizationFeatures folder * [EC-508] SCIM CQRS Refactor - Users/Post (#2264) * [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-536] Implemented CQRS for Users Post and added unit tests * [EC-508] Created ScimServiceCollectionExtensions * [EC-508] Renamed AddScimCommands to AddScimUserCommands * [EC-508] Created ExceptionHandlerFilterAttribute on SCIM project * [EC-508] Catching NotFoundException on ExceptionHandlerFilter * [EC-508] Remove Queries/Commands folders from Scim and Scim.Tests * [EC-508] SCIM CQRS Refactor - Users/Patch (#2262) * [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-538] Implemented CQRS for Users Patch and added unit tests * [EC-508] Added ScimServiceCollectionExtensions * [EC-508] Removed HandleActiveOperationAsync method from UsersController * [EC-508] Renamed AddScimCommands to AddScimUserCommands * [EC-508] Created ExceptionHandlerFilterAttribute on SCIM project * [EC-508] Removed unneeded dependencies from UsersController * [EC-508] Remove 'Query' folder from Scim and Scim.Test * [EC-507] SCIM CQRS Refactor - Groups/Post (#2270) * [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-530] Implemented CQRS for Groups Post and added unit tests * [EC-507] Created ScimServiceCollectionExtensions * [EC-507] Renamed AddScimCommands to AddScimGroupCommands * [EC-507] Created ExceptionHandlerFilterAttribute on SCIM project * [EC-507] Removed unneeded dependencies from GroupsController * [EC-507] Remove Queries/Commands folders from Scim and Scim.Test * [EC-507] Remove unneeded skipIfEmpty argument. Updated unit test to check provided userIds * [EC-507] Remove UpdateGroupMembersAsync from GroupsController * [EC-508] SCIM CQRS Refactor - Users/GetList (#2265) * [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-535] Implemented CQRS for Users GetList and added unit tests * [EC-508] Created ScimServiceCollectionExtensions and renamed GetUsersListCommand to GetUsersListQuery * [EC-508] Renamed AddScimCommands to AddScimUserQueries * [EC-508] Removed unneeded IUserRepository and IOptions<ScimSettings> from UsersController * [EC-508] Sorted UsersController properties and dependencies * [EC-508] Remove 'Queries' folder from Scim and Scim.Test * [EC-508] Move ScimListResponseModel creation to Scim.UsersController * [EC-508] Move ScimUserResponseModel creation to Scim.UsersController Co-authored-by: Thomas Rittson <trittson@bitwarden.com> * [EC-507] SCIM CQRS Refactor - Groups/Delete (#2267) * [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-533] Implemented CQRS for Groups Delete and added unit tests * [EC-507] Created ScimServiceCollectionExtensions * [EC-507] Renamed AddScimCommands to AddScimGroupCommands * [EC-507] Created ExceptionHandlerFilterAttribute on SCIM project * [EC-507] Removed unneeded dependencies from GroupsController * [EC-507] Move DeleteGroupCommand to OrganizationFeatures/OrganizationUsers * [EC-507] Remove IGetUserQuery and move logic to UsersController. Remove unused references. * [EC-507] Move IDeleteGroupCommand to Groups folder Co-authored-by: Thomas Rittson <trittson@bitwarden.com>
This commit is contained in:
parent
9703fb6874
commit
0a01051d83
@ -1,36 +1,42 @@
|
||||
using System.Text.Json;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.OrganizationFeatures.Groups.Interfaces;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Scim.Context;
|
||||
using Bit.Scim.Groups.Interfaces;
|
||||
using Bit.Scim.Models;
|
||||
using Bit.Scim.Utilities;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Bit.Scim.Controllers.v2;
|
||||
|
||||
[Authorize("Scim")]
|
||||
[Route("v2/{organizationId}/groups")]
|
||||
[ExceptionHandlerFilter]
|
||||
public class GroupsController : Controller
|
||||
{
|
||||
private readonly ScimSettings _scimSettings;
|
||||
private readonly IGroupRepository _groupRepository;
|
||||
private readonly IGroupService _groupService;
|
||||
private readonly IScimContext _scimContext;
|
||||
private readonly IGetGroupsListQuery _getGroupsListQuery;
|
||||
private readonly IDeleteGroupCommand _deleteGroupCommand;
|
||||
private readonly IPatchGroupCommand _patchGroupCommand;
|
||||
private readonly IPostGroupCommand _postGroupCommand;
|
||||
private readonly IPutGroupCommand _putGroupCommand;
|
||||
private readonly ILogger<GroupsController> _logger;
|
||||
|
||||
public GroupsController(
|
||||
IGroupRepository groupRepository,
|
||||
IGroupService groupService,
|
||||
IOptions<ScimSettings> scimSettings,
|
||||
IScimContext scimContext,
|
||||
IGetGroupsListQuery getGroupsListQuery,
|
||||
IDeleteGroupCommand deleteGroupCommand,
|
||||
IPatchGroupCommand patchGroupCommand,
|
||||
IPostGroupCommand postGroupCommand,
|
||||
IPutGroupCommand putGroupCommand,
|
||||
ILogger<GroupsController> logger)
|
||||
{
|
||||
_scimSettings = scimSettings?.Value;
|
||||
_groupRepository = groupRepository;
|
||||
_groupService = groupService;
|
||||
_scimContext = scimContext;
|
||||
_getGroupsListQuery = getGroupsListQuery;
|
||||
_deleteGroupCommand = deleteGroupCommand;
|
||||
_patchGroupCommand = patchGroupCommand;
|
||||
_postGroupCommand = postGroupCommand;
|
||||
_putGroupCommand = putGroupCommand;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
@ -40,13 +46,9 @@ public class GroupsController : Controller
|
||||
var group = await _groupRepository.GetByIdAsync(id);
|
||||
if (group == null || group.OrganizationId != organizationId)
|
||||
{
|
||||
return new NotFoundObjectResult(new ScimErrorResponseModel
|
||||
{
|
||||
Status = 404,
|
||||
Detail = "Group not found."
|
||||
});
|
||||
throw new NotFoundException("Group not found.");
|
||||
}
|
||||
return new ObjectResult(new ScimGroupResponseModel(group));
|
||||
return Ok(new ScimGroupResponseModel(group));
|
||||
}
|
||||
|
||||
[HttpGet("")]
|
||||
@ -56,272 +58,45 @@ public class GroupsController : Controller
|
||||
[FromQuery] int? count,
|
||||
[FromQuery] int? startIndex)
|
||||
{
|
||||
string nameFilter = null;
|
||||
string externalIdFilter = null;
|
||||
if (!string.IsNullOrWhiteSpace(filter))
|
||||
var groupsListQueryResult = await _getGroupsListQuery.GetGroupsListAsync(organizationId, filter, count, startIndex);
|
||||
var scimListResponseModel = new ScimListResponseModel<ScimGroupResponseModel>
|
||||
{
|
||||
if (filter.StartsWith("displayName eq "))
|
||||
{
|
||||
nameFilter = filter.Substring(15).Trim('"');
|
||||
}
|
||||
else if (filter.StartsWith("externalId eq "))
|
||||
{
|
||||
externalIdFilter = filter.Substring(14).Trim('"');
|
||||
}
|
||||
}
|
||||
|
||||
var groupList = new List<ScimGroupResponseModel>();
|
||||
var groups = await _groupRepository.GetManyByOrganizationIdAsync(organizationId);
|
||||
var totalResults = 0;
|
||||
if (!string.IsNullOrWhiteSpace(nameFilter))
|
||||
{
|
||||
var group = groups.FirstOrDefault(g => g.Name == nameFilter);
|
||||
if (group != null)
|
||||
{
|
||||
groupList.Add(new ScimGroupResponseModel(group));
|
||||
}
|
||||
totalResults = groupList.Count;
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(externalIdFilter))
|
||||
{
|
||||
var group = groups.FirstOrDefault(ou => ou.ExternalId == externalIdFilter);
|
||||
if (group != null)
|
||||
{
|
||||
groupList.Add(new ScimGroupResponseModel(group));
|
||||
}
|
||||
totalResults = groupList.Count;
|
||||
}
|
||||
else if (string.IsNullOrWhiteSpace(filter) && startIndex.HasValue && count.HasValue)
|
||||
{
|
||||
groupList = groups.OrderBy(g => g.Name)
|
||||
.Skip(startIndex.Value - 1)
|
||||
.Take(count.Value)
|
||||
.Select(g => new ScimGroupResponseModel(g))
|
||||
.ToList();
|
||||
totalResults = groups.Count;
|
||||
}
|
||||
|
||||
var result = new ScimListResponseModel<ScimGroupResponseModel>
|
||||
{
|
||||
Resources = groupList,
|
||||
ItemsPerPage = count.GetValueOrDefault(groupList.Count),
|
||||
TotalResults = totalResults,
|
||||
Resources = groupsListQueryResult.groupList.Select(g => new ScimGroupResponseModel(g)).ToList(),
|
||||
ItemsPerPage = count.GetValueOrDefault(groupsListQueryResult.groupList.Count()),
|
||||
TotalResults = groupsListQueryResult.totalResults,
|
||||
StartIndex = startIndex.GetValueOrDefault(1),
|
||||
};
|
||||
return new ObjectResult(result);
|
||||
return Ok(scimListResponseModel);
|
||||
}
|
||||
|
||||
[HttpPost("")]
|
||||
public async Task<IActionResult> Post(Guid organizationId, [FromBody] ScimGroupRequestModel model)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(model.DisplayName))
|
||||
{
|
||||
return new BadRequestResult();
|
||||
}
|
||||
|
||||
var groups = await _groupRepository.GetManyByOrganizationIdAsync(organizationId);
|
||||
if (!string.IsNullOrWhiteSpace(model.ExternalId) && groups.Any(g => g.ExternalId == model.ExternalId))
|
||||
{
|
||||
return new ConflictResult();
|
||||
}
|
||||
|
||||
var group = model.ToGroup(organizationId);
|
||||
await _groupService.SaveAsync(group, null);
|
||||
await UpdateGroupMembersAsync(group, model, true);
|
||||
var response = new ScimGroupResponseModel(group);
|
||||
return new CreatedResult(Url.Action(nameof(Get), new { group.OrganizationId, group.Id }), response);
|
||||
var group = await _postGroupCommand.PostGroupAsync(organizationId, model);
|
||||
var scimGroupResponseModel = new ScimGroupResponseModel(group);
|
||||
return new CreatedResult(Url.Action(nameof(Get), new { group.OrganizationId, group.Id }), scimGroupResponseModel);
|
||||
}
|
||||
|
||||
[HttpPut("{id}")]
|
||||
public async Task<IActionResult> Put(Guid organizationId, Guid id, [FromBody] ScimGroupRequestModel model)
|
||||
{
|
||||
var group = await _groupRepository.GetByIdAsync(id);
|
||||
if (group == null || group.OrganizationId != organizationId)
|
||||
{
|
||||
return new NotFoundObjectResult(new ScimErrorResponseModel
|
||||
{
|
||||
Status = 404,
|
||||
Detail = "Group not found."
|
||||
});
|
||||
}
|
||||
var group = await _putGroupCommand.PutGroupAsync(organizationId, id, model);
|
||||
var response = new ScimGroupResponseModel(group);
|
||||
|
||||
group.Name = model.DisplayName;
|
||||
await _groupService.SaveAsync(group);
|
||||
await UpdateGroupMembersAsync(group, model, false);
|
||||
return new ObjectResult(new ScimGroupResponseModel(group));
|
||||
return Ok(response);
|
||||
}
|
||||
|
||||
[HttpPatch("{id}")]
|
||||
public async Task<IActionResult> Patch(Guid organizationId, Guid id, [FromBody] ScimPatchModel model)
|
||||
{
|
||||
var group = await _groupRepository.GetByIdAsync(id);
|
||||
if (group == null || group.OrganizationId != organizationId)
|
||||
{
|
||||
return new NotFoundObjectResult(new ScimErrorResponseModel
|
||||
{
|
||||
Status = 404,
|
||||
Detail = "Group not found."
|
||||
});
|
||||
}
|
||||
|
||||
var operationHandled = false;
|
||||
foreach (var operation in model.Operations)
|
||||
{
|
||||
// Replace operations
|
||||
if (operation.Op?.ToLowerInvariant() == "replace")
|
||||
{
|
||||
// Replace a list of members
|
||||
if (operation.Path?.ToLowerInvariant() == "members")
|
||||
{
|
||||
var ids = GetOperationValueIds(operation.Value);
|
||||
await _groupRepository.UpdateUsersAsync(group.Id, ids);
|
||||
operationHandled = true;
|
||||
}
|
||||
// Replace group name from path
|
||||
else if (operation.Path?.ToLowerInvariant() == "displayname")
|
||||
{
|
||||
group.Name = operation.Value.GetString();
|
||||
await _groupService.SaveAsync(group);
|
||||
operationHandled = true;
|
||||
}
|
||||
// Replace group name from value object
|
||||
else if (string.IsNullOrWhiteSpace(operation.Path) &&
|
||||
operation.Value.TryGetProperty("displayName", out var displayNameProperty))
|
||||
{
|
||||
group.Name = displayNameProperty.GetString();
|
||||
await _groupService.SaveAsync(group);
|
||||
operationHandled = true;
|
||||
}
|
||||
}
|
||||
// Add a single member
|
||||
else if (operation.Op?.ToLowerInvariant() == "add" &&
|
||||
!string.IsNullOrWhiteSpace(operation.Path) &&
|
||||
operation.Path.ToLowerInvariant().StartsWith("members[value eq "))
|
||||
{
|
||||
var addId = GetOperationPathId(operation.Path);
|
||||
if (addId.HasValue)
|
||||
{
|
||||
var orgUserIds = (await _groupRepository.GetManyUserIdsByIdAsync(group.Id)).ToHashSet();
|
||||
orgUserIds.Add(addId.Value);
|
||||
await _groupRepository.UpdateUsersAsync(group.Id, orgUserIds);
|
||||
operationHandled = true;
|
||||
}
|
||||
}
|
||||
// Add a list of members
|
||||
else if (operation.Op?.ToLowerInvariant() == "add" &&
|
||||
operation.Path?.ToLowerInvariant() == "members")
|
||||
{
|
||||
var orgUserIds = (await _groupRepository.GetManyUserIdsByIdAsync(group.Id)).ToHashSet();
|
||||
foreach (var v in GetOperationValueIds(operation.Value))
|
||||
{
|
||||
orgUserIds.Add(v);
|
||||
}
|
||||
await _groupRepository.UpdateUsersAsync(group.Id, orgUserIds);
|
||||
operationHandled = true;
|
||||
}
|
||||
// Remove a single member
|
||||
else if (operation.Op?.ToLowerInvariant() == "remove" &&
|
||||
!string.IsNullOrWhiteSpace(operation.Path) &&
|
||||
operation.Path.ToLowerInvariant().StartsWith("members[value eq "))
|
||||
{
|
||||
var removeId = GetOperationPathId(operation.Path);
|
||||
if (removeId.HasValue)
|
||||
{
|
||||
await _groupService.DeleteUserAsync(group, removeId.Value);
|
||||
operationHandled = true;
|
||||
}
|
||||
}
|
||||
// Remove a list of members
|
||||
else if (operation.Op?.ToLowerInvariant() == "remove" &&
|
||||
operation.Path?.ToLowerInvariant() == "members")
|
||||
{
|
||||
var orgUserIds = (await _groupRepository.GetManyUserIdsByIdAsync(group.Id)).ToHashSet();
|
||||
foreach (var v in GetOperationValueIds(operation.Value))
|
||||
{
|
||||
orgUserIds.Remove(v);
|
||||
}
|
||||
await _groupRepository.UpdateUsersAsync(group.Id, orgUserIds);
|
||||
operationHandled = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!operationHandled)
|
||||
{
|
||||
_logger.LogWarning("Group patch operation not handled: {0} : ",
|
||||
string.Join(", ", model.Operations.Select(o => $"{o.Op}:{o.Path}")));
|
||||
}
|
||||
|
||||
await _patchGroupCommand.PatchGroupAsync(organizationId, id, model);
|
||||
return new NoContentResult();
|
||||
}
|
||||
|
||||
[HttpDelete("{id}")]
|
||||
public async Task<IActionResult> Delete(Guid organizationId, Guid id)
|
||||
{
|
||||
var group = await _groupRepository.GetByIdAsync(id);
|
||||
if (group == null || group.OrganizationId != organizationId)
|
||||
{
|
||||
return new NotFoundObjectResult(new ScimErrorResponseModel
|
||||
{
|
||||
Status = 404,
|
||||
Detail = "Group not found."
|
||||
});
|
||||
}
|
||||
await _groupService.DeleteAsync(group);
|
||||
await _deleteGroupCommand.DeleteGroupAsync(organizationId, id);
|
||||
return new NoContentResult();
|
||||
}
|
||||
|
||||
private List<Guid> GetOperationValueIds(JsonElement objArray)
|
||||
{
|
||||
var ids = new List<Guid>();
|
||||
foreach (var obj in objArray.EnumerateArray())
|
||||
{
|
||||
if (obj.TryGetProperty("value", out var valueProperty))
|
||||
{
|
||||
if (valueProperty.TryGetGuid(out var guid))
|
||||
{
|
||||
ids.Add(guid);
|
||||
}
|
||||
}
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
private Guid? GetOperationPathId(string path)
|
||||
{
|
||||
// Parse Guid from string like: members[value eq "{GUID}"}]
|
||||
if (Guid.TryParse(path.Substring(18).Replace("\"]", string.Empty), out var id))
|
||||
{
|
||||
return id;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private async Task UpdateGroupMembersAsync(Group group, ScimGroupRequestModel model, bool skipIfEmpty)
|
||||
{
|
||||
if (_scimContext.RequestScimProvider != Core.Enums.ScimProviderType.Okta)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (model.Members == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var memberIds = new List<Guid>();
|
||||
foreach (var id in model.Members.Select(i => i.Value))
|
||||
{
|
||||
if (Guid.TryParse(id, out var guidId))
|
||||
{
|
||||
memberIds.Add(guidId);
|
||||
}
|
||||
}
|
||||
|
||||
if (!memberIds.Any() && skipIfEmpty)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await _groupRepository.UpdateUsersAsync(group.Id, memberIds);
|
||||
}
|
||||
}
|
||||
|
@ -1,15 +1,13 @@
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.Repositories;
|
||||
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.Users.Interfaces;
|
||||
using Bit.Scim.Utilities;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Bit.Scim.Controllers.v2;
|
||||
|
||||
@ -19,39 +17,43 @@ namespace Bit.Scim.Controllers.v2;
|
||||
public class UsersController : Controller
|
||||
{
|
||||
private readonly IUserService _userService;
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
private readonly IOrganizationService _organizationService;
|
||||
private readonly IScimContext _scimContext;
|
||||
private readonly ScimSettings _scimSettings;
|
||||
private readonly IGetUserQuery _getUserQuery;
|
||||
private readonly IGetUsersListQuery _getUsersListQuery;
|
||||
private readonly IDeleteOrganizationUserCommand _deleteOrganizationUserCommand;
|
||||
private readonly IPatchUserCommand _patchUserCommand;
|
||||
private readonly IPostUserCommand _postUserCommand;
|
||||
private readonly ILogger<UsersController> _logger;
|
||||
|
||||
public UsersController(
|
||||
IUserService userService,
|
||||
IUserRepository userRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IOrganizationService organizationService,
|
||||
IScimContext scimContext,
|
||||
IOptions<ScimSettings> scimSettings,
|
||||
IGetUserQuery getUserQuery,
|
||||
IGetUsersListQuery getUsersListQuery,
|
||||
IDeleteOrganizationUserCommand deleteOrganizationUserCommand,
|
||||
IPatchUserCommand patchUserCommand,
|
||||
IPostUserCommand postUserCommand,
|
||||
ILogger<UsersController> logger)
|
||||
{
|
||||
_userService = userService;
|
||||
_userRepository = userRepository;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
_organizationService = organizationService;
|
||||
_scimContext = scimContext;
|
||||
_scimSettings = scimSettings?.Value;
|
||||
_getUserQuery = getUserQuery;
|
||||
_getUsersListQuery = getUsersListQuery;
|
||||
_deleteOrganizationUserCommand = deleteOrganizationUserCommand;
|
||||
_patchUserCommand = patchUserCommand;
|
||||
_postUserCommand = postUserCommand;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
public async Task<IActionResult> Get(Guid organizationId, Guid id)
|
||||
{
|
||||
var scimUserResponseModel = await _getUserQuery.GetUserAsync(organizationId, id);
|
||||
return Ok(scimUserResponseModel);
|
||||
var orgUser = await _organizationUserRepository.GetDetailsByIdAsync(id);
|
||||
if (orgUser == null || orgUser.OrganizationId != organizationId)
|
||||
{
|
||||
throw new NotFoundException("User not found.");
|
||||
}
|
||||
return Ok(new ScimUserResponseModel(orgUser));
|
||||
}
|
||||
|
||||
[HttpGet("")]
|
||||
@ -61,124 +63,23 @@ public class UsersController : Controller
|
||||
[FromQuery] int? count,
|
||||
[FromQuery] int? startIndex)
|
||||
{
|
||||
string emailFilter = null;
|
||||
string usernameFilter = null;
|
||||
string externalIdFilter = null;
|
||||
if (!string.IsNullOrWhiteSpace(filter))
|
||||
var usersListQueryResult = await _getUsersListQuery.GetUsersListAsync(organizationId, filter, count, startIndex);
|
||||
var scimListResponseModel = new ScimListResponseModel<ScimUserResponseModel>
|
||||
{
|
||||
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>
|
||||
{
|
||||
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("")]
|
||||
public async Task<IActionResult> Post(Guid organizationId, [FromBody] ScimUserRequestModel model)
|
||||
{
|
||||
var email = model.PrimaryEmail?.ToLowerInvariant();
|
||||
if (string.IsNullOrWhiteSpace(email))
|
||||
{
|
||||
switch (_scimContext.RequestScimProvider)
|
||||
{
|
||||
case ScimProviderType.AzureAd:
|
||||
email = model.UserName?.ToLowerInvariant();
|
||||
break;
|
||||
default:
|
||||
email = model.WorkEmail?.ToLowerInvariant();
|
||||
if (string.IsNullOrWhiteSpace(email))
|
||||
{
|
||||
email = model.Emails?.FirstOrDefault()?.Value?.ToLowerInvariant();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(email) || !model.Active)
|
||||
{
|
||||
return new BadRequestResult();
|
||||
}
|
||||
|
||||
var orgUsers = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(organizationId);
|
||||
var orgUserByEmail = orgUsers.FirstOrDefault(ou => ou.Email?.ToLowerInvariant() == email);
|
||||
if (orgUserByEmail != null)
|
||||
{
|
||||
return new ConflictResult();
|
||||
}
|
||||
|
||||
string externalId = null;
|
||||
if (!string.IsNullOrWhiteSpace(model.ExternalId))
|
||||
{
|
||||
externalId = model.ExternalId;
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(model.UserName))
|
||||
{
|
||||
externalId = model.UserName;
|
||||
}
|
||||
else
|
||||
{
|
||||
externalId = CoreHelpers.RandomString(15);
|
||||
}
|
||||
|
||||
var orgUserByExternalId = orgUsers.FirstOrDefault(ou => ou.ExternalId == externalId);
|
||||
if (orgUserByExternalId != null)
|
||||
{
|
||||
return new ConflictResult();
|
||||
}
|
||||
|
||||
var invitedOrgUser = await _organizationService.InviteUserAsync(organizationId, null, email,
|
||||
OrganizationUserType.User, false, externalId, new List<SelectionReadOnly>());
|
||||
var orgUser = await _organizationUserRepository.GetDetailsByIdAsync(invitedOrgUser.Id);
|
||||
var response = new ScimUserResponseModel(orgUser);
|
||||
return new CreatedResult(Url.Action(nameof(Get), new { orgUser.OrganizationId, orgUser.Id }), response);
|
||||
var orgUser = await _postUserCommand.PostUserAsync(organizationId, model);
|
||||
var scimUserResponseModel = new ScimUserResponseModel(orgUser);
|
||||
return new CreatedResult(Url.Action(nameof(Get), new { orgUser.OrganizationId, orgUser.Id }), scimUserResponseModel);
|
||||
}
|
||||
|
||||
[HttpPut("{id}")]
|
||||
@ -211,82 +112,14 @@ public class UsersController : Controller
|
||||
[HttpPatch("{id}")]
|
||||
public async Task<IActionResult> Patch(Guid organizationId, Guid id, [FromBody] ScimPatchModel model)
|
||||
{
|
||||
var orgUser = await _organizationUserRepository.GetByIdAsync(id);
|
||||
if (orgUser == null || orgUser.OrganizationId != organizationId)
|
||||
{
|
||||
return new NotFoundObjectResult(new ScimErrorResponseModel
|
||||
{
|
||||
Status = 404,
|
||||
Detail = "User not found."
|
||||
});
|
||||
}
|
||||
|
||||
var operationHandled = false;
|
||||
foreach (var operation in model.Operations)
|
||||
{
|
||||
// Replace operations
|
||||
if (operation.Op?.ToLowerInvariant() == "replace")
|
||||
{
|
||||
// Active from path
|
||||
if (operation.Path?.ToLowerInvariant() == "active")
|
||||
{
|
||||
var active = operation.Value.ToString()?.ToLowerInvariant();
|
||||
var handled = await HandleActiveOperationAsync(orgUser, active == "true");
|
||||
if (!operationHandled)
|
||||
{
|
||||
operationHandled = handled;
|
||||
}
|
||||
}
|
||||
// Active from value object
|
||||
else if (string.IsNullOrWhiteSpace(operation.Path) &&
|
||||
operation.Value.TryGetProperty("active", out var activeProperty))
|
||||
{
|
||||
var handled = await HandleActiveOperationAsync(orgUser, activeProperty.GetBoolean());
|
||||
if (!operationHandled)
|
||||
{
|
||||
operationHandled = handled;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!operationHandled)
|
||||
{
|
||||
_logger.LogWarning("User patch operation not handled: {operation} : ",
|
||||
string.Join(", ", model.Operations.Select(o => $"{o.Op}:{o.Path}")));
|
||||
}
|
||||
|
||||
await _patchUserCommand.PatchUserAsync(organizationId, id, model);
|
||||
return new NoContentResult();
|
||||
}
|
||||
|
||||
[HttpDelete("{id}")]
|
||||
public async Task<IActionResult> Delete(Guid organizationId, Guid id)
|
||||
{
|
||||
var orgUser = await _organizationUserRepository.GetByIdAsync(id);
|
||||
if (orgUser == null || orgUser.OrganizationId != organizationId)
|
||||
{
|
||||
return new NotFoundObjectResult(new ScimErrorResponseModel
|
||||
{
|
||||
Status = 404,
|
||||
Detail = "User not found."
|
||||
});
|
||||
}
|
||||
await _organizationService.DeleteUserAsync(organizationId, id, null);
|
||||
await _deleteOrganizationUserCommand.DeleteUserAsync(organizationId, id, null);
|
||||
return new NoContentResult();
|
||||
}
|
||||
|
||||
private async Task<bool> HandleActiveOperationAsync(Core.Entities.OrganizationUser orgUser, bool active)
|
||||
{
|
||||
if (active && orgUser.Status == OrganizationUserStatusType.Revoked)
|
||||
{
|
||||
await _organizationService.RestoreUserAsync(orgUser, null, _userService);
|
||||
return true;
|
||||
}
|
||||
else if (!active && orgUser.Status != OrganizationUserStatusType.Revoked)
|
||||
{
|
||||
await _organizationService.RevokeUserAsync(orgUser, null);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
64
bitwarden_license/src/Scim/Groups/GetGroupsListQuery.cs
Normal file
64
bitwarden_license/src/Scim/Groups/GetGroupsListQuery.cs
Normal file
@ -0,0 +1,64 @@
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Scim.Groups.Interfaces;
|
||||
|
||||
namespace Bit.Scim.Groups;
|
||||
|
||||
public class GetGroupsListQuery : IGetGroupsListQuery
|
||||
{
|
||||
private readonly IGroupRepository _groupRepository;
|
||||
|
||||
public GetGroupsListQuery(IGroupRepository groupRepository)
|
||||
{
|
||||
_groupRepository = groupRepository;
|
||||
}
|
||||
|
||||
public async Task<(IEnumerable<Group> groupList, int totalResults)> GetGroupsListAsync(Guid organizationId, string filter, int? count, int? startIndex)
|
||||
{
|
||||
string nameFilter = null;
|
||||
string externalIdFilter = null;
|
||||
if (!string.IsNullOrWhiteSpace(filter))
|
||||
{
|
||||
if (filter.StartsWith("displayName eq "))
|
||||
{
|
||||
nameFilter = filter.Substring(15).Trim('"');
|
||||
}
|
||||
else if (filter.StartsWith("externalId eq "))
|
||||
{
|
||||
externalIdFilter = filter.Substring(14).Trim('"');
|
||||
}
|
||||
}
|
||||
|
||||
var groupList = new List<Group>();
|
||||
var groups = await _groupRepository.GetManyByOrganizationIdAsync(organizationId);
|
||||
var totalResults = 0;
|
||||
if (!string.IsNullOrWhiteSpace(nameFilter))
|
||||
{
|
||||
var group = groups.FirstOrDefault(g => g.Name == nameFilter);
|
||||
if (group != null)
|
||||
{
|
||||
groupList.Add(group);
|
||||
}
|
||||
totalResults = groupList.Count;
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(externalIdFilter))
|
||||
{
|
||||
var group = groups.FirstOrDefault(ou => ou.ExternalId == externalIdFilter);
|
||||
if (group != null)
|
||||
{
|
||||
groupList.Add(group);
|
||||
}
|
||||
totalResults = groupList.Count;
|
||||
}
|
||||
else if (string.IsNullOrWhiteSpace(filter) && startIndex.HasValue && count.HasValue)
|
||||
{
|
||||
groupList = groups.OrderBy(g => g.Name)
|
||||
.Skip(startIndex.Value - 1)
|
||||
.Take(count.Value)
|
||||
.ToList();
|
||||
totalResults = groups.Count;
|
||||
}
|
||||
|
||||
return (groupList, totalResults);
|
||||
}
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
using Bit.Core.Entities;
|
||||
|
||||
namespace Bit.Scim.Groups.Interfaces;
|
||||
|
||||
public interface IGetGroupsListQuery
|
||||
{
|
||||
Task<(IEnumerable<Group> groupList, int totalResults)> GetGroupsListAsync(Guid organizationId, string filter, int? count, int? startIndex);
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
using Bit.Scim.Models;
|
||||
|
||||
namespace Bit.Scim.Groups.Interfaces;
|
||||
|
||||
public interface IPatchGroupCommand
|
||||
{
|
||||
Task PatchGroupAsync(Guid organizationId, Guid id, ScimPatchModel model);
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Scim.Models;
|
||||
|
||||
namespace Bit.Scim.Groups.Interfaces;
|
||||
|
||||
public interface IPostGroupCommand
|
||||
{
|
||||
Task<Group> PostGroupAsync(Guid organizationId, ScimGroupRequestModel model);
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Scim.Models;
|
||||
|
||||
namespace Bit.Scim.Groups.Interfaces;
|
||||
|
||||
public interface IPutGroupCommand
|
||||
{
|
||||
Task<Group> PutGroupAsync(Guid organizationId, Guid id, ScimGroupRequestModel model);
|
||||
}
|
147
bitwarden_license/src/Scim/Groups/PatchGroupCommand.cs
Normal file
147
bitwarden_license/src/Scim/Groups/PatchGroupCommand.cs
Normal file
@ -0,0 +1,147 @@
|
||||
using System.Text.Json;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Scim.Groups.Interfaces;
|
||||
using Bit.Scim.Models;
|
||||
|
||||
namespace Bit.Scim.Groups;
|
||||
|
||||
public class PatchGroupCommand : IPatchGroupCommand
|
||||
{
|
||||
private readonly IGroupRepository _groupRepository;
|
||||
private readonly IGroupService _groupService;
|
||||
private readonly ILogger<PatchGroupCommand> _logger;
|
||||
|
||||
public PatchGroupCommand(
|
||||
IGroupRepository groupRepository,
|
||||
IGroupService groupService,
|
||||
ILogger<PatchGroupCommand> logger)
|
||||
{
|
||||
_groupRepository = groupRepository;
|
||||
_groupService = groupService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task PatchGroupAsync(Guid organizationId, Guid id, ScimPatchModel model)
|
||||
{
|
||||
var group = await _groupRepository.GetByIdAsync(id);
|
||||
if (group == null || group.OrganizationId != organizationId)
|
||||
{
|
||||
throw new NotFoundException("Group not found.");
|
||||
}
|
||||
|
||||
var operationHandled = false;
|
||||
foreach (var operation in model.Operations)
|
||||
{
|
||||
// Replace operations
|
||||
if (operation.Op?.ToLowerInvariant() == "replace")
|
||||
{
|
||||
// Replace a list of members
|
||||
if (operation.Path?.ToLowerInvariant() == "members")
|
||||
{
|
||||
var ids = GetOperationValueIds(operation.Value);
|
||||
await _groupRepository.UpdateUsersAsync(group.Id, ids);
|
||||
operationHandled = true;
|
||||
}
|
||||
// Replace group name from path
|
||||
else if (operation.Path?.ToLowerInvariant() == "displayname")
|
||||
{
|
||||
group.Name = operation.Value.GetString();
|
||||
await _groupService.SaveAsync(group);
|
||||
operationHandled = true;
|
||||
}
|
||||
// Replace group name from value object
|
||||
else if (string.IsNullOrWhiteSpace(operation.Path) &&
|
||||
operation.Value.TryGetProperty("displayName", out var displayNameProperty))
|
||||
{
|
||||
group.Name = displayNameProperty.GetString();
|
||||
await _groupService.SaveAsync(group);
|
||||
operationHandled = true;
|
||||
}
|
||||
}
|
||||
// Add a single member
|
||||
else if (operation.Op?.ToLowerInvariant() == "add" &&
|
||||
!string.IsNullOrWhiteSpace(operation.Path) &&
|
||||
operation.Path.ToLowerInvariant().StartsWith("members[value eq "))
|
||||
{
|
||||
var addId = GetOperationPathId(operation.Path);
|
||||
if (addId.HasValue)
|
||||
{
|
||||
var orgUserIds = (await _groupRepository.GetManyUserIdsByIdAsync(group.Id)).ToHashSet();
|
||||
orgUserIds.Add(addId.Value);
|
||||
await _groupRepository.UpdateUsersAsync(group.Id, orgUserIds);
|
||||
operationHandled = true;
|
||||
}
|
||||
}
|
||||
// Add a list of members
|
||||
else if (operation.Op?.ToLowerInvariant() == "add" &&
|
||||
operation.Path?.ToLowerInvariant() == "members")
|
||||
{
|
||||
var orgUserIds = (await _groupRepository.GetManyUserIdsByIdAsync(group.Id)).ToHashSet();
|
||||
foreach (var v in GetOperationValueIds(operation.Value))
|
||||
{
|
||||
orgUserIds.Add(v);
|
||||
}
|
||||
await _groupRepository.UpdateUsersAsync(group.Id, orgUserIds);
|
||||
operationHandled = true;
|
||||
}
|
||||
// Remove a single member
|
||||
else if (operation.Op?.ToLowerInvariant() == "remove" &&
|
||||
!string.IsNullOrWhiteSpace(operation.Path) &&
|
||||
operation.Path.ToLowerInvariant().StartsWith("members[value eq "))
|
||||
{
|
||||
var removeId = GetOperationPathId(operation.Path);
|
||||
if (removeId.HasValue)
|
||||
{
|
||||
await _groupService.DeleteUserAsync(group, removeId.Value);
|
||||
operationHandled = true;
|
||||
}
|
||||
}
|
||||
// Remove a list of members
|
||||
else if (operation.Op?.ToLowerInvariant() == "remove" &&
|
||||
operation.Path?.ToLowerInvariant() == "members")
|
||||
{
|
||||
var orgUserIds = (await _groupRepository.GetManyUserIdsByIdAsync(group.Id)).ToHashSet();
|
||||
foreach (var v in GetOperationValueIds(operation.Value))
|
||||
{
|
||||
orgUserIds.Remove(v);
|
||||
}
|
||||
await _groupRepository.UpdateUsersAsync(group.Id, orgUserIds);
|
||||
operationHandled = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!operationHandled)
|
||||
{
|
||||
_logger.LogWarning("Group patch operation not handled: {0} : ",
|
||||
string.Join(", ", model.Operations.Select(o => $"{o.Op}:{o.Path}")));
|
||||
}
|
||||
}
|
||||
|
||||
private List<Guid> GetOperationValueIds(JsonElement objArray)
|
||||
{
|
||||
var ids = new List<Guid>();
|
||||
foreach (var obj in objArray.EnumerateArray())
|
||||
{
|
||||
if (obj.TryGetProperty("value", out var valueProperty))
|
||||
{
|
||||
if (valueProperty.TryGetGuid(out var guid))
|
||||
{
|
||||
ids.Add(guid);
|
||||
}
|
||||
}
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
private Guid? GetOperationPathId(string path)
|
||||
{
|
||||
// Parse Guid from string like: members[value eq "{GUID}"}]
|
||||
if (Guid.TryParse(path.Substring(18).Replace("\"]", string.Empty), out var id))
|
||||
{
|
||||
return id;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
75
bitwarden_license/src/Scim/Groups/PostGroupCommand.cs
Normal file
75
bitwarden_license/src/Scim/Groups/PostGroupCommand.cs
Normal file
@ -0,0 +1,75 @@
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Scim.Context;
|
||||
using Bit.Scim.Groups.Interfaces;
|
||||
using Bit.Scim.Models;
|
||||
|
||||
namespace Bit.Scim.Groups;
|
||||
|
||||
public class PostGroupCommand : IPostGroupCommand
|
||||
{
|
||||
private readonly IGroupRepository _groupRepository;
|
||||
private readonly IGroupService _groupService;
|
||||
private readonly IScimContext _scimContext;
|
||||
|
||||
public PostGroupCommand(
|
||||
IGroupRepository groupRepository,
|
||||
IGroupService groupService,
|
||||
IScimContext scimContext)
|
||||
{
|
||||
_groupRepository = groupRepository;
|
||||
_groupService = groupService;
|
||||
_scimContext = scimContext;
|
||||
}
|
||||
|
||||
public async Task<Group> PostGroupAsync(Guid organizationId, ScimGroupRequestModel model)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(model.DisplayName))
|
||||
{
|
||||
throw new BadRequestException();
|
||||
}
|
||||
|
||||
var groups = await _groupRepository.GetManyByOrganizationIdAsync(organizationId);
|
||||
if (!string.IsNullOrWhiteSpace(model.ExternalId) && groups.Any(g => g.ExternalId == model.ExternalId))
|
||||
{
|
||||
throw new ConflictException();
|
||||
}
|
||||
|
||||
var group = model.ToGroup(organizationId);
|
||||
await _groupService.SaveAsync(group, null);
|
||||
await UpdateGroupMembersAsync(group, model);
|
||||
|
||||
return group;
|
||||
}
|
||||
|
||||
private async Task UpdateGroupMembersAsync(Group group, ScimGroupRequestModel model)
|
||||
{
|
||||
if (_scimContext.RequestScimProvider != Core.Enums.ScimProviderType.Okta)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (model.Members == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var memberIds = new List<Guid>();
|
||||
foreach (var id in model.Members.Select(i => i.Value))
|
||||
{
|
||||
if (Guid.TryParse(id, out var guidId))
|
||||
{
|
||||
memberIds.Add(guidId);
|
||||
}
|
||||
}
|
||||
|
||||
if (!memberIds.Any())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await _groupRepository.UpdateUsersAsync(group.Id, memberIds);
|
||||
}
|
||||
}
|
65
bitwarden_license/src/Scim/Groups/PutGroupCommand.cs
Normal file
65
bitwarden_license/src/Scim/Groups/PutGroupCommand.cs
Normal file
@ -0,0 +1,65 @@
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Scim.Context;
|
||||
using Bit.Scim.Groups.Interfaces;
|
||||
using Bit.Scim.Models;
|
||||
|
||||
namespace Bit.Scim.Groups;
|
||||
|
||||
public class PutGroupCommand : IPutGroupCommand
|
||||
{
|
||||
private readonly IGroupRepository _groupRepository;
|
||||
private readonly IGroupService _groupService;
|
||||
private readonly IScimContext _scimContext;
|
||||
|
||||
public PutGroupCommand(
|
||||
IGroupRepository groupRepository,
|
||||
IGroupService groupService,
|
||||
IScimContext scimContext)
|
||||
{
|
||||
_groupRepository = groupRepository;
|
||||
_groupService = groupService;
|
||||
_scimContext = scimContext;
|
||||
}
|
||||
|
||||
public async Task<Group> PutGroupAsync(Guid organizationId, Guid id, ScimGroupRequestModel model)
|
||||
{
|
||||
var group = await _groupRepository.GetByIdAsync(id);
|
||||
if (group == null || group.OrganizationId != organizationId)
|
||||
{
|
||||
throw new NotFoundException("Group not found.");
|
||||
}
|
||||
|
||||
group.Name = model.DisplayName;
|
||||
await _groupService.SaveAsync(group);
|
||||
await UpdateGroupMembersAsync(group, model);
|
||||
|
||||
return group;
|
||||
}
|
||||
|
||||
private async Task UpdateGroupMembersAsync(Group group, ScimGroupRequestModel model)
|
||||
{
|
||||
if (_scimContext.RequestScimProvider != Core.Enums.ScimProviderType.Okta)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (model.Members == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var memberIds = new List<Guid>();
|
||||
foreach (var id in model.Members.Select(i => i.Value))
|
||||
{
|
||||
if (Guid.TryParse(id, out var guidId))
|
||||
{
|
||||
memberIds.Add(guidId);
|
||||
}
|
||||
}
|
||||
|
||||
await _groupRepository.UpdateUsersAsync(group.Id, memberIds);
|
||||
}
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
using Bit.Scim.Models;
|
||||
|
||||
namespace Bit.Scim.Queries.Users.Interfaces;
|
||||
|
||||
public interface IGetUserQuery
|
||||
{
|
||||
Task<ScimUserResponseModel> GetUserAsync(Guid organizationId, Guid id);
|
||||
}
|
@ -76,7 +76,10 @@ public class Startup
|
||||
});
|
||||
services.Configure<RouteOptions>(options => options.LowercaseUrls = true);
|
||||
|
||||
services.AddScimGroupCommands();
|
||||
services.AddScimGroupQueries();
|
||||
services.AddScimUserQueries();
|
||||
services.AddScimUserCommands();
|
||||
}
|
||||
|
||||
public void Configure(
|
||||
|
69
bitwarden_license/src/Scim/Users/GetUsersListQuery.cs
Normal file
69
bitwarden_license/src/Scim/Users/GetUsersListQuery.cs
Normal file
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
using Bit.Scim.Models;
|
||||
|
||||
namespace Bit.Scim.Users.Interfaces;
|
||||
|
||||
public interface IPatchUserCommand
|
||||
{
|
||||
Task PatchUserAsync(Guid organizationId, Guid id, ScimPatchModel model);
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
using Bit.Scim.Models;
|
||||
|
||||
namespace Bit.Scim.Users.Interfaces;
|
||||
|
||||
public interface IPostUserCommand
|
||||
{
|
||||
Task<OrganizationUserUserDetails> PostUserAsync(Guid organizationId, ScimUserRequestModel model);
|
||||
}
|
87
bitwarden_license/src/Scim/Users/PatchUserCommand.cs
Normal file
87
bitwarden_license/src/Scim/Users/PatchUserCommand.cs
Normal file
@ -0,0 +1,87 @@
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Scim.Models;
|
||||
using Bit.Scim.Users.Interfaces;
|
||||
|
||||
namespace Bit.Scim.Users;
|
||||
|
||||
public class PatchUserCommand : IPatchUserCommand
|
||||
{
|
||||
private readonly IUserService _userService;
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
private readonly IOrganizationService _organizationService;
|
||||
private readonly ILogger<PatchUserCommand> _logger;
|
||||
|
||||
public PatchUserCommand(
|
||||
IUserService userService,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IOrganizationService organizationService,
|
||||
ILogger<PatchUserCommand> logger)
|
||||
{
|
||||
_userService = userService;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
_organizationService = organizationService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task PatchUserAsync(Guid organizationId, Guid id, ScimPatchModel model)
|
||||
{
|
||||
var orgUser = await _organizationUserRepository.GetByIdAsync(id);
|
||||
if (orgUser == null || orgUser.OrganizationId != organizationId)
|
||||
{
|
||||
throw new NotFoundException("User not found.");
|
||||
}
|
||||
|
||||
var operationHandled = false;
|
||||
foreach (var operation in model.Operations)
|
||||
{
|
||||
// Replace operations
|
||||
if (operation.Op?.ToLowerInvariant() == "replace")
|
||||
{
|
||||
// Active from path
|
||||
if (operation.Path?.ToLowerInvariant() == "active")
|
||||
{
|
||||
var active = operation.Value.ToString()?.ToLowerInvariant();
|
||||
var handled = await HandleActiveOperationAsync(orgUser, active == "true");
|
||||
if (!operationHandled)
|
||||
{
|
||||
operationHandled = handled;
|
||||
}
|
||||
}
|
||||
// Active from value object
|
||||
else if (string.IsNullOrWhiteSpace(operation.Path) &&
|
||||
operation.Value.TryGetProperty("active", out var activeProperty))
|
||||
{
|
||||
var handled = await HandleActiveOperationAsync(orgUser, activeProperty.GetBoolean());
|
||||
if (!operationHandled)
|
||||
{
|
||||
operationHandled = handled;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!operationHandled)
|
||||
{
|
||||
_logger.LogWarning("User patch operation not handled: {operation} : ",
|
||||
string.Join(", ", model.Operations.Select(o => $"{o.Op}:{o.Path}")));
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> HandleActiveOperationAsync(Core.Entities.OrganizationUser orgUser, bool active)
|
||||
{
|
||||
if (active && orgUser.Status == OrganizationUserStatusType.Revoked)
|
||||
{
|
||||
await _organizationService.RestoreUserAsync(orgUser, null, _userService);
|
||||
return true;
|
||||
}
|
||||
else if (!active && orgUser.Status != OrganizationUserStatusType.Revoked)
|
||||
{
|
||||
await _organizationService.RevokeUserAsync(orgUser, null);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
88
bitwarden_license/src/Scim/Users/PostUserCommand.cs
Normal file
88
bitwarden_license/src/Scim/Users/PostUserCommand.cs
Normal file
@ -0,0 +1,88 @@
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Scim.Context;
|
||||
using Bit.Scim.Models;
|
||||
using Bit.Scim.Users.Interfaces;
|
||||
|
||||
namespace Bit.Scim.Users;
|
||||
|
||||
public class PostUserCommand : IPostUserCommand
|
||||
{
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
private readonly IOrganizationService _organizationService;
|
||||
private readonly IScimContext _scimContext;
|
||||
|
||||
public PostUserCommand(
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IOrganizationService organizationService,
|
||||
IScimContext scimContext)
|
||||
{
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
_organizationService = organizationService;
|
||||
_scimContext = scimContext;
|
||||
}
|
||||
|
||||
public async Task<OrganizationUserUserDetails> PostUserAsync(Guid organizationId, ScimUserRequestModel model)
|
||||
{
|
||||
var email = model.PrimaryEmail?.ToLowerInvariant();
|
||||
if (string.IsNullOrWhiteSpace(email))
|
||||
{
|
||||
switch (_scimContext.RequestScimProvider)
|
||||
{
|
||||
case ScimProviderType.AzureAd:
|
||||
email = model.UserName?.ToLowerInvariant();
|
||||
break;
|
||||
default:
|
||||
email = model.WorkEmail?.ToLowerInvariant();
|
||||
if (string.IsNullOrWhiteSpace(email))
|
||||
{
|
||||
email = model.Emails?.FirstOrDefault()?.Value?.ToLowerInvariant();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(email) || !model.Active)
|
||||
{
|
||||
throw new BadRequestException();
|
||||
}
|
||||
|
||||
var orgUsers = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(organizationId);
|
||||
var orgUserByEmail = orgUsers.FirstOrDefault(ou => ou.Email?.ToLowerInvariant() == email);
|
||||
if (orgUserByEmail != null)
|
||||
{
|
||||
throw new ConflictException();
|
||||
}
|
||||
|
||||
string externalId = null;
|
||||
if (!string.IsNullOrWhiteSpace(model.ExternalId))
|
||||
{
|
||||
externalId = model.ExternalId;
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(model.UserName))
|
||||
{
|
||||
externalId = model.UserName;
|
||||
}
|
||||
else
|
||||
{
|
||||
externalId = CoreHelpers.RandomString(15);
|
||||
}
|
||||
|
||||
var orgUserByExternalId = orgUsers.FirstOrDefault(ou => ou.ExternalId == externalId);
|
||||
if (orgUserByExternalId != null)
|
||||
{
|
||||
throw new ConflictException();
|
||||
}
|
||||
|
||||
var invitedOrgUser = await _organizationService.InviteUserAsync(organizationId, null, email,
|
||||
OrganizationUserType.User, false, externalId, new List<SelectionReadOnly>());
|
||||
var orgUser = await _organizationUserRepository.GetDetailsByIdAsync(invitedOrgUser.Id);
|
||||
|
||||
return orgUser;
|
||||
}
|
||||
}
|
@ -26,6 +26,14 @@ public class ExceptionHandlerFilterAttribute : ExceptionFilterAttribute
|
||||
{
|
||||
statusCode = StatusCodes.Status404NotFound;
|
||||
}
|
||||
else if (exception is BadRequestException)
|
||||
{
|
||||
statusCode = StatusCodes.Status400BadRequest;
|
||||
}
|
||||
else if (exception is ConflictException)
|
||||
{
|
||||
statusCode = StatusCodes.Status409Conflict;
|
||||
}
|
||||
|
||||
scimErrorResponseModel.Status = statusCode;
|
||||
|
||||
|
@ -1,12 +1,38 @@
|
||||
using Bit.Scim.Queries.Users;
|
||||
using Bit.Scim.Queries.Users.Interfaces;
|
||||
using Bit.Core.OrganizationFeatures.Groups;
|
||||
using Bit.Core.OrganizationFeatures.Groups.Interfaces;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationUsers;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Scim.Groups;
|
||||
using Bit.Scim.Groups.Interfaces;
|
||||
using Bit.Scim.Users;
|
||||
using Bit.Scim.Users.Interfaces;
|
||||
|
||||
namespace Bit.Scim.Utilities;
|
||||
|
||||
public static class ScimServiceCollectionExtensions
|
||||
{
|
||||
public static void AddScimGroupCommands(this IServiceCollection services)
|
||||
{
|
||||
services.AddScoped<IDeleteGroupCommand, DeleteGroupCommand>();
|
||||
services.AddScoped<IPatchGroupCommand, PatchGroupCommand>();
|
||||
services.AddScoped<IPostGroupCommand, PostGroupCommand>();
|
||||
services.AddScoped<IPutGroupCommand, PutGroupCommand>();
|
||||
}
|
||||
|
||||
public static void AddScimGroupQueries(this IServiceCollection services)
|
||||
{
|
||||
services.AddScoped<IGetGroupsListQuery, GetGroupsListQuery>();
|
||||
}
|
||||
|
||||
public static void AddScimUserCommands(this IServiceCollection services)
|
||||
{
|
||||
services.AddScoped<IDeleteOrganizationUserCommand, DeleteOrganizationUserCommand>();
|
||||
services.AddScoped<IPatchUserCommand, PatchUserCommand>();
|
||||
services.AddScoped<IPostUserCommand, PostUserCommand>();
|
||||
}
|
||||
|
||||
public static void AddScimUserQueries(this IServiceCollection services)
|
||||
{
|
||||
services.AddScoped<IGetUserQuery, GetUserQuery>();
|
||||
services.AddScoped<IGetUsersListQuery, GetUsersListQuery>();
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,129 @@
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Scim.Groups;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Bit.Test.Common.Helpers;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Scim.Test.Groups;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class GetGroupsListCommandTests
|
||||
{
|
||||
[Theory]
|
||||
[BitAutoData(10, 1)]
|
||||
[BitAutoData(2, 1)]
|
||||
[BitAutoData(1, 3)]
|
||||
public async Task GetGroupsList_Success(int count, int startIndex, SutProvider<GetGroupsListQuery> sutProvider, Guid organizationId, IList<Group> groups)
|
||||
{
|
||||
groups = SetGroupsOrganizationId(groups, organizationId);
|
||||
|
||||
sutProvider.GetDependency<IGroupRepository>()
|
||||
.GetManyByOrganizationIdAsync(organizationId)
|
||||
.Returns(groups);
|
||||
|
||||
var result = await sutProvider.Sut.GetGroupsListAsync(organizationId, null, count, startIndex);
|
||||
|
||||
AssertHelper.AssertPropertyEqual(groups.Skip(startIndex - 1).Take(count).ToList(), result.groupList);
|
||||
AssertHelper.AssertPropertyEqual(groups.Count, result.totalResults);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetGroupsList_FilterDisplayName_Success(SutProvider<GetGroupsListQuery> sutProvider, Guid organizationId, IList<Group> groups)
|
||||
{
|
||||
groups = SetGroupsOrganizationId(groups, organizationId);
|
||||
string name = groups.First().Name;
|
||||
string filter = $"displayName eq {name}";
|
||||
|
||||
var expectedGroupList = groups
|
||||
.Where(g => g.Name == name)
|
||||
.ToList();
|
||||
var expectedTotalResults = expectedGroupList.Count;
|
||||
|
||||
sutProvider.GetDependency<IGroupRepository>()
|
||||
.GetManyByOrganizationIdAsync(organizationId)
|
||||
.Returns(groups);
|
||||
|
||||
var result = await sutProvider.Sut.GetGroupsListAsync(organizationId, filter, null, null);
|
||||
|
||||
AssertHelper.AssertPropertyEqual(expectedGroupList, result.groupList);
|
||||
AssertHelper.AssertPropertyEqual(expectedTotalResults, result.totalResults);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetGroupsList_FilterDisplayName_Empty(string name, SutProvider<GetGroupsListQuery> sutProvider, Guid organizationId, IList<Group> groups)
|
||||
{
|
||||
groups = SetGroupsOrganizationId(groups, organizationId);
|
||||
string filter = $"displayName eq {name}";
|
||||
|
||||
var expectedGroupList = new List<Group>();
|
||||
var expectedTotalResults = expectedGroupList.Count;
|
||||
|
||||
sutProvider.GetDependency<IGroupRepository>()
|
||||
.GetManyByOrganizationIdAsync(organizationId)
|
||||
.Returns(groups);
|
||||
|
||||
var result = await sutProvider.Sut.GetGroupsListAsync(organizationId, filter, null, null);
|
||||
|
||||
AssertHelper.AssertPropertyEqual(expectedGroupList, result.groupList);
|
||||
AssertHelper.AssertPropertyEqual(expectedTotalResults, result.totalResults);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetGroupsList_FilterExternalId_Success(SutProvider<GetGroupsListQuery> sutProvider, Guid organizationId, IList<Group> groups)
|
||||
{
|
||||
groups = SetGroupsOrganizationId(groups, organizationId);
|
||||
string externalId = groups.First().ExternalId;
|
||||
string filter = $"externalId eq {externalId}";
|
||||
|
||||
var expectedGroupList = groups
|
||||
.Where(ou => ou.ExternalId == externalId)
|
||||
.ToList();
|
||||
var expectedTotalResults = expectedGroupList.Count;
|
||||
|
||||
sutProvider.GetDependency<IGroupRepository>()
|
||||
.GetManyByOrganizationIdAsync(organizationId)
|
||||
.Returns(groups);
|
||||
|
||||
var result = await sutProvider.Sut.GetGroupsListAsync(organizationId, filter, null, null);
|
||||
|
||||
AssertHelper.AssertPropertyEqual(expectedGroupList, result.groupList);
|
||||
AssertHelper.AssertPropertyEqual(expectedTotalResults, result.totalResults);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetGroupsList_FilterExternalId_Empty(string externalId, SutProvider<GetGroupsListQuery> sutProvider, Guid organizationId, IList<Group> groups)
|
||||
{
|
||||
groups = SetGroupsOrganizationId(groups, organizationId);
|
||||
string filter = $"externalId eq {externalId}";
|
||||
|
||||
var expectedGroupList = groups
|
||||
.Where(ou => ou.ExternalId == externalId)
|
||||
.ToList();
|
||||
var expectedTotalResults = expectedGroupList.Count;
|
||||
|
||||
sutProvider.GetDependency<IGroupRepository>()
|
||||
.GetManyByOrganizationIdAsync(organizationId)
|
||||
.Returns(groups);
|
||||
|
||||
var result = await sutProvider.Sut.GetGroupsListAsync(organizationId, filter, null, null);
|
||||
|
||||
AssertHelper.AssertPropertyEqual(expectedGroupList, result.groupList);
|
||||
AssertHelper.AssertPropertyEqual(expectedTotalResults, result.totalResults);
|
||||
}
|
||||
|
||||
private IList<Group> SetGroupsOrganizationId(IList<Group> groups, Guid organizationId)
|
||||
{
|
||||
return groups.Select(g =>
|
||||
{
|
||||
g.OrganizationId = organizationId;
|
||||
return g;
|
||||
}).ToList();
|
||||
}
|
||||
}
|
@ -0,0 +1,274 @@
|
||||
using System.Text.Json;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Scim.Groups;
|
||||
using Bit.Scim.Models;
|
||||
using Bit.Scim.Utilities;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Scim.Test.Groups;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class PatchGroupCommandTests
|
||||
{
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PatchGroup_ReplaceListMembers_Success(SutProvider<PatchGroupCommand> sutProvider, Group group, IEnumerable<Guid> userIds)
|
||||
{
|
||||
sutProvider.GetDependency<IGroupRepository>()
|
||||
.GetByIdAsync(group.Id)
|
||||
.Returns(group);
|
||||
|
||||
var scimPatchModel = new Models.ScimPatchModel
|
||||
{
|
||||
Operations = new List<ScimPatchModel.OperationModel>
|
||||
{
|
||||
new ScimPatchModel.OperationModel
|
||||
{
|
||||
Op = "replace",
|
||||
Path = "members",
|
||||
Value = JsonDocument.Parse(JsonSerializer.Serialize(userIds.Select(uid => new { value = uid }).ToArray())).RootElement
|
||||
}
|
||||
},
|
||||
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
|
||||
};
|
||||
|
||||
await sutProvider.Sut.PatchGroupAsync(group.OrganizationId, group.Id, scimPatchModel);
|
||||
|
||||
await sutProvider.GetDependency<IGroupRepository>().Received(1).UpdateUsersAsync(group.Id, Arg.Is<IEnumerable<Guid>>(arg => arg.All(id => userIds.Contains(id))));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PatchGroup_ReplaceDisplayNameFromPath_Success(SutProvider<PatchGroupCommand> sutProvider, Group group, string displayName)
|
||||
{
|
||||
sutProvider.GetDependency<IGroupRepository>()
|
||||
.GetByIdAsync(group.Id)
|
||||
.Returns(group);
|
||||
|
||||
var scimPatchModel = new Models.ScimPatchModel
|
||||
{
|
||||
Operations = new List<ScimPatchModel.OperationModel>
|
||||
{
|
||||
new ScimPatchModel.OperationModel
|
||||
{
|
||||
Op = "replace",
|
||||
Path = "displayname",
|
||||
Value = JsonDocument.Parse($"\"{displayName}\"").RootElement
|
||||
}
|
||||
},
|
||||
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
|
||||
};
|
||||
|
||||
await sutProvider.Sut.PatchGroupAsync(group.OrganizationId, group.Id, scimPatchModel);
|
||||
|
||||
await sutProvider.GetDependency<IGroupService>().Received(1).SaveAsync(group);
|
||||
Assert.Equal(displayName, group.Name);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PatchGroup_ReplaceDisplayNameFromValueObject_Success(SutProvider<PatchGroupCommand> sutProvider, Group group, string displayName)
|
||||
{
|
||||
sutProvider.GetDependency<IGroupRepository>()
|
||||
.GetByIdAsync(group.Id)
|
||||
.Returns(group);
|
||||
|
||||
var scimPatchModel = new Models.ScimPatchModel
|
||||
{
|
||||
Operations = new List<ScimPatchModel.OperationModel>
|
||||
{
|
||||
new ScimPatchModel.OperationModel
|
||||
{
|
||||
Op = "replace",
|
||||
Value = JsonDocument.Parse($"{{\"displayName\":\"{displayName}\"}}").RootElement
|
||||
}
|
||||
},
|
||||
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
|
||||
};
|
||||
|
||||
await sutProvider.Sut.PatchGroupAsync(group.OrganizationId, group.Id, scimPatchModel);
|
||||
|
||||
await sutProvider.GetDependency<IGroupService>().Received(1).SaveAsync(group);
|
||||
Assert.Equal(displayName, group.Name);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PatchGroup_AddSingleMember_Success(SutProvider<PatchGroupCommand> sutProvider, Group group, ICollection<Guid> existingMembers, Guid userId)
|
||||
{
|
||||
sutProvider.GetDependency<IGroupRepository>()
|
||||
.GetByIdAsync(group.Id)
|
||||
.Returns(group);
|
||||
|
||||
sutProvider.GetDependency<IGroupRepository>()
|
||||
.GetManyUserIdsByIdAsync(group.Id)
|
||||
.Returns(existingMembers);
|
||||
|
||||
var scimPatchModel = new Models.ScimPatchModel
|
||||
{
|
||||
Operations = new List<ScimPatchModel.OperationModel>
|
||||
{
|
||||
new ScimPatchModel.OperationModel
|
||||
{
|
||||
Op = "add",
|
||||
Path = $"members[value eq \"{userId}\"]",
|
||||
}
|
||||
},
|
||||
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
|
||||
};
|
||||
|
||||
await sutProvider.Sut.PatchGroupAsync(group.OrganizationId, group.Id, scimPatchModel);
|
||||
|
||||
await sutProvider.GetDependency<IGroupRepository>().Received(1).UpdateUsersAsync(group.Id, Arg.Is<IEnumerable<Guid>>(arg => arg.All(id => existingMembers.Append(userId).Contains(id))));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PatchGroup_AddListMembers_Success(SutProvider<PatchGroupCommand> sutProvider, Group group, ICollection<Guid> existingMembers, ICollection<Guid> userIds)
|
||||
{
|
||||
sutProvider.GetDependency<IGroupRepository>()
|
||||
.GetByIdAsync(group.Id)
|
||||
.Returns(group);
|
||||
|
||||
sutProvider.GetDependency<IGroupRepository>()
|
||||
.GetManyUserIdsByIdAsync(group.Id)
|
||||
.Returns(existingMembers);
|
||||
|
||||
var scimPatchModel = new Models.ScimPatchModel
|
||||
{
|
||||
Operations = new List<ScimPatchModel.OperationModel>
|
||||
{
|
||||
new ScimPatchModel.OperationModel
|
||||
{
|
||||
Op = "add",
|
||||
Path = $"members",
|
||||
Value = JsonDocument.Parse(JsonSerializer.Serialize(userIds.Select(uid => new { value = uid }).ToArray())).RootElement
|
||||
}
|
||||
},
|
||||
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
|
||||
};
|
||||
|
||||
await sutProvider.Sut.PatchGroupAsync(group.OrganizationId, group.Id, scimPatchModel);
|
||||
|
||||
await sutProvider.GetDependency<IGroupRepository>().Received(1).UpdateUsersAsync(group.Id, Arg.Is<IEnumerable<Guid>>(arg => arg.All(id => existingMembers.Concat(userIds).Contains(id))));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PatchGroup_RemoveSingleMember_Success(SutProvider<PatchGroupCommand> sutProvider, Group group, Guid userId)
|
||||
{
|
||||
sutProvider.GetDependency<IGroupRepository>()
|
||||
.GetByIdAsync(group.Id)
|
||||
.Returns(group);
|
||||
|
||||
var scimPatchModel = new Models.ScimPatchModel
|
||||
{
|
||||
Operations = new List<ScimPatchModel.OperationModel>
|
||||
{
|
||||
new ScimPatchModel.OperationModel
|
||||
{
|
||||
Op = "remove",
|
||||
Path = $"members[value eq \"{userId}\"]",
|
||||
}
|
||||
},
|
||||
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
|
||||
};
|
||||
|
||||
await sutProvider.Sut.PatchGroupAsync(group.OrganizationId, group.Id, scimPatchModel);
|
||||
|
||||
await sutProvider.GetDependency<IGroupService>().Received(1).DeleteUserAsync(group, userId);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PatchGroup_RemoveListMembers_Success(SutProvider<PatchGroupCommand> sutProvider, Group group, ICollection<Guid> existingMembers)
|
||||
{
|
||||
sutProvider.GetDependency<IGroupRepository>()
|
||||
.GetByIdAsync(group.Id)
|
||||
.Returns(group);
|
||||
|
||||
sutProvider.GetDependency<IGroupRepository>()
|
||||
.GetManyUserIdsByIdAsync(group.Id)
|
||||
.Returns(existingMembers);
|
||||
|
||||
var scimPatchModel = new Models.ScimPatchModel
|
||||
{
|
||||
Operations = new List<ScimPatchModel.OperationModel>
|
||||
{
|
||||
new ScimPatchModel.OperationModel
|
||||
{
|
||||
Op = "remove",
|
||||
Path = $"members",
|
||||
Value = JsonDocument.Parse(JsonSerializer.Serialize(existingMembers.Select(uid => new { value = uid }).ToArray())).RootElement
|
||||
}
|
||||
},
|
||||
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
|
||||
};
|
||||
|
||||
await sutProvider.Sut.PatchGroupAsync(group.OrganizationId, group.Id, scimPatchModel);
|
||||
|
||||
await sutProvider.GetDependency<IGroupRepository>().Received(1).UpdateUsersAsync(group.Id, Arg.Is<IEnumerable<Guid>>(arg => arg.All(id => existingMembers.Contains(id))));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PatchGroup_NoAction_Success(SutProvider<PatchGroupCommand> sutProvider, Group group)
|
||||
{
|
||||
sutProvider.GetDependency<IGroupRepository>()
|
||||
.GetByIdAsync(group.Id)
|
||||
.Returns(group);
|
||||
|
||||
var scimPatchModel = new Models.ScimPatchModel
|
||||
{
|
||||
Operations = new List<ScimPatchModel.OperationModel>(),
|
||||
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
|
||||
};
|
||||
|
||||
await sutProvider.Sut.PatchGroupAsync(group.OrganizationId, group.Id, scimPatchModel);
|
||||
|
||||
await sutProvider.GetDependency<IGroupRepository>().Received(0).UpdateUsersAsync(group.Id, Arg.Any<IEnumerable<Guid>>());
|
||||
await sutProvider.GetDependency<IGroupRepository>().Received(0).GetManyUserIdsByIdAsync(group.Id);
|
||||
await sutProvider.GetDependency<IGroupService>().Received(0).SaveAsync(group);
|
||||
await sutProvider.GetDependency<IGroupService>().Received(0).DeleteUserAsync(group, Arg.Any<Guid>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PatchGroup_NotFound_Throws(SutProvider<PatchGroupCommand> sutProvider, Guid organizationId, Guid groupId)
|
||||
{
|
||||
var scimPatchModel = new Models.ScimPatchModel
|
||||
{
|
||||
Operations = new List<ScimPatchModel.OperationModel>(),
|
||||
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
|
||||
};
|
||||
|
||||
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.PatchGroupAsync(organizationId, groupId, scimPatchModel));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PatchGroup_MismatchingOrganizationId_Throws(SutProvider<PatchGroupCommand> sutProvider, Guid organizationId, Guid groupId)
|
||||
{
|
||||
var scimPatchModel = new Models.ScimPatchModel
|
||||
{
|
||||
Operations = new List<ScimPatchModel.OperationModel>(),
|
||||
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IGroupRepository>()
|
||||
.GetByIdAsync(groupId)
|
||||
.Returns(new Group
|
||||
{
|
||||
Id = groupId,
|
||||
OrganizationId = Guid.NewGuid()
|
||||
});
|
||||
|
||||
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.PatchGroupAsync(organizationId, groupId, scimPatchModel));
|
||||
}
|
||||
}
|
121
bitwarden_license/test/Scim.Test/Groups/PostGroupCommandTests.cs
Normal file
121
bitwarden_license/test/Scim.Test/Groups/PostGroupCommandTests.cs
Normal file
@ -0,0 +1,121 @@
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Scim.Context;
|
||||
using Bit.Scim.Groups;
|
||||
using Bit.Scim.Models;
|
||||
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.Groups;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class PostGroupCommandTests
|
||||
{
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PostGroup_Success(SutProvider<PostGroupCommand> sutProvider, string displayName, string externalId, Guid organizationId, ICollection<Group> groups)
|
||||
{
|
||||
var scimGroupRequestModel = new ScimGroupRequestModel
|
||||
{
|
||||
DisplayName = displayName,
|
||||
ExternalId = externalId,
|
||||
Members = new List<ScimGroupRequestModel.GroupMembersModel>(),
|
||||
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
|
||||
};
|
||||
|
||||
var expectedResult = new Group
|
||||
{
|
||||
OrganizationId = organizationId,
|
||||
Name = displayName,
|
||||
ExternalId = externalId,
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IGroupRepository>()
|
||||
.GetManyByOrganizationIdAsync(organizationId)
|
||||
.Returns(groups);
|
||||
|
||||
var group = await sutProvider.Sut.PostGroupAsync(organizationId, scimGroupRequestModel);
|
||||
|
||||
await sutProvider.GetDependency<IGroupService>().Received(1).SaveAsync(group, null);
|
||||
await sutProvider.GetDependency<IGroupRepository>().Received(0).UpdateUsersAsync(Arg.Any<Guid>(), Arg.Any<IEnumerable<Guid>>());
|
||||
|
||||
AssertHelper.AssertPropertyEqual(expectedResult, group, "Id", "CreationDate", "RevisionDate");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PostGroup_WithMembers_Success(SutProvider<PostGroupCommand> sutProvider, string displayName, string externalId, Guid organizationId, ICollection<Group> groups, IEnumerable<Guid> membersUserIds)
|
||||
{
|
||||
var scimGroupRequestModel = new ScimGroupRequestModel
|
||||
{
|
||||
DisplayName = displayName,
|
||||
ExternalId = externalId,
|
||||
Members = membersUserIds.Select(uid => new ScimGroupRequestModel.GroupMembersModel { Value = uid.ToString() }).ToList(),
|
||||
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
|
||||
};
|
||||
|
||||
var expectedResult = new Group
|
||||
{
|
||||
OrganizationId = organizationId,
|
||||
Name = displayName,
|
||||
ExternalId = externalId
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IGroupRepository>()
|
||||
.GetManyByOrganizationIdAsync(organizationId)
|
||||
.Returns(groups);
|
||||
|
||||
sutProvider.GetDependency<IScimContext>()
|
||||
.RequestScimProvider
|
||||
.Returns(Core.Enums.ScimProviderType.Okta);
|
||||
|
||||
var group = await sutProvider.Sut.PostGroupAsync(organizationId, scimGroupRequestModel);
|
||||
|
||||
await sutProvider.GetDependency<IGroupService>().Received(1).SaveAsync(group, null);
|
||||
await sutProvider.GetDependency<IGroupRepository>().Received(1).UpdateUsersAsync(Arg.Any<Guid>(), Arg.Is<IEnumerable<Guid>>(arg => arg.All(id => membersUserIds.Contains(id))));
|
||||
|
||||
AssertHelper.AssertPropertyEqual(expectedResult, group, "Id", "CreationDate", "RevisionDate");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData((string)null)]
|
||||
[BitAutoData("")]
|
||||
[BitAutoData(" ")]
|
||||
public async Task PostGroup_NullDisplayName_Throws(string displayName, SutProvider<PostGroupCommand> sutProvider, Guid organizationId)
|
||||
{
|
||||
var scimGroupRequestModel = new ScimGroupRequestModel
|
||||
{
|
||||
DisplayName = displayName,
|
||||
ExternalId = Guid.NewGuid().ToString(),
|
||||
Members = new List<ScimGroupRequestModel.GroupMembersModel>(),
|
||||
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
|
||||
};
|
||||
|
||||
await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.PostGroupAsync(organizationId, scimGroupRequestModel));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PostGroup_ExistingExternalId_Throws(string displayName, SutProvider<PostGroupCommand> sutProvider, Guid organizationId, ICollection<Group> groups)
|
||||
{
|
||||
var scimGroupRequestModel = new ScimGroupRequestModel
|
||||
{
|
||||
DisplayName = displayName,
|
||||
ExternalId = groups.First().ExternalId,
|
||||
Members = new List<ScimGroupRequestModel.GroupMembersModel>(),
|
||||
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IGroupRepository>()
|
||||
.GetManyByOrganizationIdAsync(organizationId)
|
||||
.Returns(groups);
|
||||
|
||||
await Assert.ThrowsAsync<ConflictException>(async () => await sutProvider.Sut.PostGroupAsync(organizationId, scimGroupRequestModel));
|
||||
}
|
||||
}
|
122
bitwarden_license/test/Scim.Test/Groups/PutGroupCommandTests.cs
Normal file
122
bitwarden_license/test/Scim.Test/Groups/PutGroupCommandTests.cs
Normal file
@ -0,0 +1,122 @@
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Scim.Context;
|
||||
using Bit.Scim.Groups;
|
||||
using Bit.Scim.Models;
|
||||
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.Groups;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class PutGroupCommandTests
|
||||
{
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PutGroup_Success(SutProvider<PutGroupCommand> sutProvider, Group group, string displayName)
|
||||
{
|
||||
sutProvider.GetDependency<IGroupRepository>()
|
||||
.GetByIdAsync(group.Id)
|
||||
.Returns(group);
|
||||
|
||||
var inputModel = new ScimGroupRequestModel
|
||||
{
|
||||
DisplayName = displayName,
|
||||
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
|
||||
};
|
||||
|
||||
var expectedResult = new Group
|
||||
{
|
||||
Id = group.Id,
|
||||
AccessAll = group.AccessAll,
|
||||
ExternalId = group.ExternalId,
|
||||
Name = displayName,
|
||||
OrganizationId = group.OrganizationId
|
||||
};
|
||||
|
||||
var result = await sutProvider.Sut.PutGroupAsync(group.OrganizationId, group.Id, inputModel);
|
||||
|
||||
AssertHelper.AssertPropertyEqual(expectedResult, result, "CreationDate", "RevisionDate");
|
||||
Assert.Equal(displayName, group.Name);
|
||||
|
||||
await sutProvider.GetDependency<IGroupService>().Received(1).SaveAsync(group);
|
||||
await sutProvider.GetDependency<IGroupRepository>().Received(0).UpdateUsersAsync(group.Id, Arg.Any<IEnumerable<Guid>>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PutGroup_ChangeMembers_Success(SutProvider<PutGroupCommand> sutProvider, Group group, string displayName, IEnumerable<Guid> membersUserIds)
|
||||
{
|
||||
sutProvider.GetDependency<IGroupRepository>()
|
||||
.GetByIdAsync(group.Id)
|
||||
.Returns(group);
|
||||
|
||||
sutProvider.GetDependency<IScimContext>()
|
||||
.RequestScimProvider
|
||||
.Returns(Core.Enums.ScimProviderType.Okta);
|
||||
|
||||
var inputModel = new ScimGroupRequestModel
|
||||
{
|
||||
DisplayName = displayName,
|
||||
Members = membersUserIds.Select(uid => new ScimGroupRequestModel.GroupMembersModel { Value = uid.ToString() }).ToList(),
|
||||
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
|
||||
};
|
||||
|
||||
var expectedResult = new Group
|
||||
{
|
||||
Id = group.Id,
|
||||
AccessAll = group.AccessAll,
|
||||
ExternalId = group.ExternalId,
|
||||
Name = displayName,
|
||||
OrganizationId = group.OrganizationId
|
||||
};
|
||||
|
||||
var result = await sutProvider.Sut.PutGroupAsync(group.OrganizationId, group.Id, inputModel);
|
||||
|
||||
AssertHelper.AssertPropertyEqual(expectedResult, result, "CreationDate", "RevisionDate");
|
||||
Assert.Equal(displayName, group.Name);
|
||||
|
||||
await sutProvider.GetDependency<IGroupService>().Received(1).SaveAsync(group);
|
||||
await sutProvider.GetDependency<IGroupRepository>().Received(1).UpdateUsersAsync(group.Id, Arg.Is<IEnumerable<Guid>>(arg => arg.All(id => membersUserIds.Contains(id))));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PutGroup_NotFound_Throws(SutProvider<PutGroupCommand> sutProvider, Guid organizationId, Guid groupId, string displayName)
|
||||
{
|
||||
var scimGroupRequestModel = new ScimGroupRequestModel
|
||||
{
|
||||
DisplayName = displayName,
|
||||
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
|
||||
};
|
||||
|
||||
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.PutGroupAsync(organizationId, groupId, scimGroupRequestModel));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PutGroup_MismatchingOrganizationId_Throws(SutProvider<PutGroupCommand> sutProvider, Guid organizationId, Guid groupId, string displayName)
|
||||
{
|
||||
var scimGroupRequestModel = new ScimGroupRequestModel
|
||||
{
|
||||
DisplayName = displayName,
|
||||
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IGroupRepository>()
|
||||
.GetByIdAsync(groupId)
|
||||
.Returns(new Group
|
||||
{
|
||||
Id = groupId,
|
||||
OrganizationId = Guid.NewGuid()
|
||||
});
|
||||
|
||||
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.PutGroupAsync(organizationId, groupId, scimGroupRequestModel));
|
||||
}
|
||||
}
|
@ -1,66 +0,0 @@
|
||||
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));
|
||||
}
|
||||
}
|
139
bitwarden_license/test/Scim.Test/Users/GetUsersListQueryTests.cs
Normal file
139
bitwarden_license/test/Scim.Test/Users/GetUsersListQueryTests.cs
Normal file
@ -0,0 +1,139 @@
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Scim.Users;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Bit.Test.Common.Helpers;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Scim.Test.Users;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class GetUsersListQueryTests
|
||||
{
|
||||
[Theory]
|
||||
[BitAutoData(10, 1)]
|
||||
[BitAutoData(2, 1)]
|
||||
[BitAutoData(1, 3)]
|
||||
public async Task GetUsersList_Success(int count, int startIndex, SutProvider<GetUsersListQuery> sutProvider, Guid organizationId, IList<OrganizationUserUserDetails> organizationUserUserDetails)
|
||||
{
|
||||
organizationUserUserDetails = SetUsersOrganizationId(organizationUserUserDetails, organizationId);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyDetailsByOrganizationAsync(organizationId)
|
||||
.Returns(organizationUserUserDetails);
|
||||
|
||||
var result = await sutProvider.Sut.GetUsersListAsync(organizationId, null, count, startIndex);
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1).GetManyDetailsByOrganizationAsync(organizationId);
|
||||
|
||||
AssertHelper.AssertPropertyEqual(organizationUserUserDetails.Skip(startIndex - 1).Take(count).ToList(), result.userList);
|
||||
AssertHelper.AssertPropertyEqual(organizationUserUserDetails.Count, result.totalResults);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData("user1@example.com")]
|
||||
public async Task GetUsersList_FilterUserName_Success(string email, SutProvider<GetUsersListQuery> sutProvider, Guid organizationId, IList<OrganizationUserUserDetails> organizationUserUserDetails)
|
||||
{
|
||||
organizationUserUserDetails = SetUsersOrganizationId(organizationUserUserDetails, organizationId);
|
||||
organizationUserUserDetails.First().Email = email;
|
||||
string filter = $"userName eq {email}";
|
||||
|
||||
var expectedUserList = organizationUserUserDetails
|
||||
.Where(u => u.Email == email)
|
||||
.ToList();
|
||||
var expectedTotalResults = expectedUserList.Count;
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyDetailsByOrganizationAsync(organizationId)
|
||||
.Returns(organizationUserUserDetails);
|
||||
|
||||
var result = await sutProvider.Sut.GetUsersListAsync(organizationId, filter, null, null);
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1).GetManyDetailsByOrganizationAsync(organizationId);
|
||||
|
||||
AssertHelper.AssertPropertyEqual(expectedUserList, result.userList);
|
||||
AssertHelper.AssertPropertyEqual(expectedTotalResults, result.totalResults);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData("user1@example.com")]
|
||||
public async Task GetUsersList_FilterUserName_Empty(string email, SutProvider<GetUsersListQuery> sutProvider, Guid organizationId, IList<OrganizationUserUserDetails> organizationUserUserDetails)
|
||||
{
|
||||
organizationUserUserDetails = SetUsersOrganizationId(organizationUserUserDetails, organizationId);
|
||||
string filter = $"userName eq {email}";
|
||||
|
||||
var expectedUserList = new List<OrganizationUserUserDetails>();
|
||||
var expectedTotalResults = expectedUserList.Count;
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyDetailsByOrganizationAsync(organizationId)
|
||||
.Returns(organizationUserUserDetails);
|
||||
|
||||
var result = await sutProvider.Sut.GetUsersListAsync(organizationId, filter, null, null);
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1).GetManyDetailsByOrganizationAsync(organizationId);
|
||||
|
||||
AssertHelper.AssertPropertyEqual(expectedUserList, result.userList);
|
||||
AssertHelper.AssertPropertyEqual(expectedTotalResults, result.totalResults);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetUsersList_FilterExternalId_Success(SutProvider<GetUsersListQuery> sutProvider, Guid organizationId, IList<OrganizationUserUserDetails> organizationUserUserDetails)
|
||||
{
|
||||
organizationUserUserDetails = SetUsersOrganizationId(organizationUserUserDetails, organizationId);
|
||||
string externalId = organizationUserUserDetails.First().ExternalId;
|
||||
string filter = $"externalId eq {externalId}";
|
||||
|
||||
var expectedUserList = organizationUserUserDetails
|
||||
.Where(u => u.ExternalId == externalId)
|
||||
.ToList();
|
||||
var expectedTotalResults = expectedUserList.Count;
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyDetailsByOrganizationAsync(organizationId)
|
||||
.Returns(organizationUserUserDetails);
|
||||
|
||||
var result = await sutProvider.Sut.GetUsersListAsync(organizationId, filter, null, null);
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1).GetManyDetailsByOrganizationAsync(organizationId);
|
||||
|
||||
AssertHelper.AssertPropertyEqual(expectedUserList, result.userList);
|
||||
AssertHelper.AssertPropertyEqual(expectedTotalResults, result.totalResults);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetUsersList_FilterExternalId_Empty(string externalId, SutProvider<GetUsersListQuery> sutProvider, Guid organizationId, IList<OrganizationUserUserDetails> organizationUserUserDetails)
|
||||
{
|
||||
organizationUserUserDetails = SetUsersOrganizationId(organizationUserUserDetails, organizationId);
|
||||
string filter = $"externalId eq {externalId}";
|
||||
|
||||
var expectedUserList = organizationUserUserDetails
|
||||
.Where(u => u.ExternalId == externalId)
|
||||
.ToList();
|
||||
var expectedTotalResults = expectedUserList.Count;
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyDetailsByOrganizationAsync(organizationId)
|
||||
.Returns(organizationUserUserDetails);
|
||||
|
||||
var result = await sutProvider.Sut.GetUsersListAsync(organizationId, filter, null, null);
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1).GetManyDetailsByOrganizationAsync(organizationId);
|
||||
|
||||
AssertHelper.AssertPropertyEqual(expectedUserList, result.userList);
|
||||
AssertHelper.AssertPropertyEqual(expectedTotalResults, result.totalResults);
|
||||
}
|
||||
|
||||
private IList<OrganizationUserUserDetails> SetUsersOrganizationId(IList<OrganizationUserUserDetails> organizationUserUserDetails, Guid organizationId)
|
||||
{
|
||||
return organizationUserUserDetails.Select(ouud =>
|
||||
{
|
||||
ouud.OrganizationId = organizationId;
|
||||
return ouud;
|
||||
}).ToList();
|
||||
}
|
||||
}
|
186
bitwarden_license/test/Scim.Test/Users/PatchUserCommandTests.cs
Normal file
186
bitwarden_license/test/Scim.Test/Users/PatchUserCommandTests.cs
Normal file
@ -0,0 +1,186 @@
|
||||
using System.Text.Json;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Scim.Models;
|
||||
using Bit.Scim.Users;
|
||||
using Bit.Scim.Utilities;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Scim.Test.Users;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class PatchUserCommandTests
|
||||
{
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PatchUser_RestorePath_Success(SutProvider<PatchUserCommand> sutProvider, OrganizationUser organizationUser)
|
||||
{
|
||||
organizationUser.Status = Core.Enums.OrganizationUserStatusType.Revoked;
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetByIdAsync(organizationUser.Id)
|
||||
.Returns(organizationUser);
|
||||
|
||||
var scimPatchModel = new Models.ScimPatchModel
|
||||
{
|
||||
Operations = new List<ScimPatchModel.OperationModel>
|
||||
{
|
||||
new ScimPatchModel.OperationModel
|
||||
{
|
||||
Op = "replace",
|
||||
Path = "active",
|
||||
Value = JsonDocument.Parse("true").RootElement
|
||||
}
|
||||
},
|
||||
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
|
||||
};
|
||||
|
||||
await sutProvider.Sut.PatchUserAsync(organizationUser.OrganizationId, organizationUser.Id, scimPatchModel);
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationService>().Received(1).RestoreUserAsync(organizationUser, null, Arg.Any<IUserService>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PatchUser_RestoreValue_Success(SutProvider<PatchUserCommand> sutProvider, OrganizationUser organizationUser)
|
||||
{
|
||||
organizationUser.Status = Core.Enums.OrganizationUserStatusType.Revoked;
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetByIdAsync(organizationUser.Id)
|
||||
.Returns(organizationUser);
|
||||
|
||||
var scimPatchModel = new Models.ScimPatchModel
|
||||
{
|
||||
Operations = new List<ScimPatchModel.OperationModel>
|
||||
{
|
||||
new ScimPatchModel.OperationModel
|
||||
{
|
||||
Op = "replace",
|
||||
Value = JsonDocument.Parse("{\"active\":true}").RootElement
|
||||
}
|
||||
},
|
||||
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
|
||||
};
|
||||
|
||||
await sutProvider.Sut.PatchUserAsync(organizationUser.OrganizationId, organizationUser.Id, scimPatchModel);
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationService>().Received(1).RestoreUserAsync(organizationUser, null, Arg.Any<IUserService>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PatchUser_RevokePath_Success(SutProvider<PatchUserCommand> sutProvider, OrganizationUser organizationUser)
|
||||
{
|
||||
organizationUser.Status = Core.Enums.OrganizationUserStatusType.Confirmed;
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetByIdAsync(organizationUser.Id)
|
||||
.Returns(organizationUser);
|
||||
|
||||
var scimPatchModel = new Models.ScimPatchModel
|
||||
{
|
||||
Operations = new List<ScimPatchModel.OperationModel>
|
||||
{
|
||||
new ScimPatchModel.OperationModel
|
||||
{
|
||||
Op = "replace",
|
||||
Path = "active",
|
||||
Value = JsonDocument.Parse("false").RootElement
|
||||
}
|
||||
},
|
||||
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
|
||||
};
|
||||
|
||||
await sutProvider.Sut.PatchUserAsync(organizationUser.OrganizationId, organizationUser.Id, scimPatchModel);
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationService>().Received(1).RevokeUserAsync(organizationUser, null);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PatchUser_RevokeValue_Success(SutProvider<PatchUserCommand> sutProvider, OrganizationUser organizationUser)
|
||||
{
|
||||
organizationUser.Status = Core.Enums.OrganizationUserStatusType.Confirmed;
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetByIdAsync(organizationUser.Id)
|
||||
.Returns(organizationUser);
|
||||
|
||||
var scimPatchModel = new Models.ScimPatchModel
|
||||
{
|
||||
Operations = new List<ScimPatchModel.OperationModel>
|
||||
{
|
||||
new ScimPatchModel.OperationModel
|
||||
{
|
||||
Op = "replace",
|
||||
Value = JsonDocument.Parse("{\"active\":false}").RootElement
|
||||
}
|
||||
},
|
||||
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
|
||||
};
|
||||
|
||||
await sutProvider.Sut.PatchUserAsync(organizationUser.OrganizationId, organizationUser.Id, scimPatchModel);
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationService>().Received(1).RevokeUserAsync(organizationUser, null);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PatchUser_NoAction_Success(SutProvider<PatchUserCommand> sutProvider, OrganizationUser organizationUser)
|
||||
{
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetByIdAsync(organizationUser.Id)
|
||||
.Returns(organizationUser);
|
||||
|
||||
var scimPatchModel = new Models.ScimPatchModel
|
||||
{
|
||||
Operations = new List<ScimPatchModel.OperationModel>(),
|
||||
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
|
||||
};
|
||||
|
||||
await sutProvider.Sut.PatchUserAsync(organizationUser.OrganizationId, organizationUser.Id, scimPatchModel);
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationService>().Received(0).RestoreUserAsync(organizationUser, null, Arg.Any<IUserService>());
|
||||
await sutProvider.GetDependency<IOrganizationService>().Received(0).RevokeUserAsync(organizationUser, null);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PatchUser_NotFound_Throws(SutProvider<PatchUserCommand> sutProvider, Guid organizationId, Guid organizationUserId)
|
||||
{
|
||||
var scimPatchModel = new Models.ScimPatchModel
|
||||
{
|
||||
Operations = new List<ScimPatchModel.OperationModel>(),
|
||||
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
|
||||
};
|
||||
|
||||
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.PatchUserAsync(organizationId, organizationUserId, scimPatchModel));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PatchUser_MismatchingOrganizationId_Throws(SutProvider<PatchUserCommand> sutProvider, Guid organizationId, Guid organizationUserId)
|
||||
{
|
||||
var scimPatchModel = new Models.ScimPatchModel
|
||||
{
|
||||
Operations = new List<ScimPatchModel.OperationModel>(),
|
||||
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetByIdAsync(organizationUserId)
|
||||
.Returns(new OrganizationUser
|
||||
{
|
||||
Id = organizationUserId,
|
||||
OrganizationId = Guid.NewGuid()
|
||||
});
|
||||
|
||||
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.PatchUserAsync(organizationId, organizationUserId, scimPatchModel));
|
||||
}
|
||||
}
|
112
bitwarden_license/test/Scim.Test/Users/PostUserCommandTests.cs
Normal file
112
bitwarden_license/test/Scim.Test/Users/PostUserCommandTests.cs
Normal file
@ -0,0 +1,112 @@
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Scim.Models;
|
||||
using Bit.Scim.Users;
|
||||
using Bit.Scim.Utilities;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Scim.Test.Users;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class PostUserCommandTests
|
||||
{
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PostUser_Success(SutProvider<PostUserCommand> sutProvider, string externalId, Guid organizationId, List<BaseScimUserModel.EmailModel> emails, ICollection<OrganizationUserUserDetails> organizationUsers, Core.Entities.OrganizationUser newUser)
|
||||
{
|
||||
var scimUserRequestModel = new ScimUserRequestModel
|
||||
{
|
||||
ExternalId = externalId,
|
||||
Emails = emails,
|
||||
Active = true,
|
||||
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyDetailsByOrganizationAsync(organizationId)
|
||||
.Returns(organizationUsers);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationService>()
|
||||
.InviteUserAsync(organizationId, null, scimUserRequestModel.PrimaryEmail.ToLowerInvariant(), OrganizationUserType.User, false, externalId, Arg.Any<List<SelectionReadOnly>>())
|
||||
.Returns(newUser);
|
||||
|
||||
var user = await sutProvider.Sut.PostUserAsync(organizationId, scimUserRequestModel);
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationService>().Received(1).InviteUserAsync(organizationId, null, scimUserRequestModel.PrimaryEmail.ToLowerInvariant(),
|
||||
OrganizationUserType.User, false, scimUserRequestModel.ExternalId, Arg.Any<List<SelectionReadOnly>>());
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1).GetDetailsByIdAsync(newUser.Id);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PostUser_NullEmail_Throws(SutProvider<PostUserCommand> sutProvider, Guid organizationId)
|
||||
{
|
||||
var scimUserRequestModel = new ScimUserRequestModel
|
||||
{
|
||||
Emails = new List<BaseScimUserModel.EmailModel>(),
|
||||
Active = true,
|
||||
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
|
||||
};
|
||||
|
||||
await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.PostUserAsync(organizationId, scimUserRequestModel));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PostUser_Inactive_Throws(SutProvider<PostUserCommand> sutProvider, Guid organizationId, List<BaseScimUserModel.EmailModel> emails)
|
||||
{
|
||||
var scimUserRequestModel = new ScimUserRequestModel
|
||||
{
|
||||
Emails = emails,
|
||||
Active = false,
|
||||
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
|
||||
};
|
||||
|
||||
await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.PostUserAsync(organizationId, scimUserRequestModel));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PostUser_DuplicateExternalId_Throws(SutProvider<PostUserCommand> sutProvider, Guid organizationId, List<BaseScimUserModel.EmailModel> emails, ICollection<OrganizationUserUserDetails> organizationUsers)
|
||||
{
|
||||
var scimUserRequestModel = new ScimUserRequestModel
|
||||
{
|
||||
ExternalId = organizationUsers.First().ExternalId,
|
||||
Emails = emails,
|
||||
Active = true,
|
||||
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyDetailsByOrganizationAsync(organizationId)
|
||||
.Returns(organizationUsers);
|
||||
|
||||
await Assert.ThrowsAsync<ConflictException>(async () => await sutProvider.Sut.PostUserAsync(organizationId, scimUserRequestModel));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PostUser_DuplicateUserName_Throws(SutProvider<PostUserCommand> sutProvider, Guid organizationId, List<BaseScimUserModel.EmailModel> emails, ICollection<OrganizationUserUserDetails> organizationUsers)
|
||||
{
|
||||
var scimUserRequestModel = new ScimUserRequestModel
|
||||
{
|
||||
UserName = organizationUsers.First().ExternalId,
|
||||
Emails = emails,
|
||||
Active = true,
|
||||
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyDetailsByOrganizationAsync(organizationId)
|
||||
.Returns(organizationUsers);
|
||||
|
||||
await Assert.ThrowsAsync<ConflictException>(async () => await sutProvider.Sut.PostUserAsync(organizationId, scimUserRequestModel));
|
||||
}
|
||||
}
|
30
src/Core/OrganizationFeatures/Groups/DeleteGroupCommand.cs
Normal file
30
src/Core/OrganizationFeatures/Groups/DeleteGroupCommand.cs
Normal file
@ -0,0 +1,30 @@
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.OrganizationFeatures.Groups.Interfaces;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
|
||||
namespace Bit.Core.OrganizationFeatures.Groups;
|
||||
|
||||
public class DeleteGroupCommand : IDeleteGroupCommand
|
||||
{
|
||||
private readonly IEventService _eventService;
|
||||
private readonly IGroupRepository _groupRepository;
|
||||
|
||||
public DeleteGroupCommand(IEventService eventService, IGroupRepository groupRepository)
|
||||
{
|
||||
_eventService = eventService;
|
||||
_groupRepository = groupRepository;
|
||||
}
|
||||
|
||||
public async Task DeleteGroupAsync(Guid organizationId, Guid id)
|
||||
{
|
||||
var group = await _groupRepository.GetByIdAsync(id);
|
||||
if (group == null || group.OrganizationId != organizationId)
|
||||
{
|
||||
throw new NotFoundException("Group not found.");
|
||||
}
|
||||
|
||||
await _groupRepository.DeleteAsync(group);
|
||||
await _eventService.LogGroupEventAsync(group, Core.Enums.EventType.Group_Deleted);
|
||||
}
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
namespace Bit.Core.OrganizationFeatures.Groups.Interfaces;
|
||||
|
||||
public interface IDeleteGroupCommand
|
||||
{
|
||||
Task DeleteGroupAsync(Guid organizationId, Guid id);
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
|
||||
namespace Bit.Core.OrganizationFeatures.OrganizationUsers;
|
||||
|
||||
public class DeleteOrganizationUserCommand : IDeleteOrganizationUserCommand
|
||||
{
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
private readonly IOrganizationService _organizationService;
|
||||
|
||||
public DeleteOrganizationUserCommand(
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IOrganizationService organizationService
|
||||
)
|
||||
{
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
_organizationService = organizationService;
|
||||
}
|
||||
|
||||
public async Task DeleteUserAsync(Guid organizationId, Guid organizationUserId, Guid? deletingUserId)
|
||||
{
|
||||
var orgUser = await _organizationUserRepository.GetByIdAsync(organizationUserId);
|
||||
if (orgUser == null || orgUser.OrganizationId != organizationId)
|
||||
{
|
||||
throw new NotFoundException("User not found.");
|
||||
}
|
||||
|
||||
await _organizationService.DeleteUserAsync(organizationId, organizationUserId, deletingUserId);
|
||||
}
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
namespace Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
|
||||
public interface IDeleteOrganizationUserCommand
|
||||
{
|
||||
Task DeleteUserAsync(Guid organizationId, Guid organizationUserId, Guid? deletingUserId);
|
||||
}
|
@ -6,6 +6,7 @@ namespace Bit.Core.Services;
|
||||
public interface IGroupService
|
||||
{
|
||||
Task SaveAsync(Group group, IEnumerable<SelectionReadOnly> collections = null);
|
||||
[Obsolete("IDeleteGroupCommand should be used instead. To be removed by EC-608.")]
|
||||
Task DeleteAsync(Group group);
|
||||
Task DeleteUserAsync(Group group, Guid organizationUserId);
|
||||
}
|
||||
|
@ -43,6 +43,7 @@ public interface IOrganizationService
|
||||
Task<List<Tuple<OrganizationUser, string>>> ConfirmUsersAsync(Guid organizationId, Dictionary<Guid, string> keys,
|
||||
Guid confirmingUserId, IUserService userService);
|
||||
Task SaveUserAsync(OrganizationUser user, Guid? savingUserId, IEnumerable<SelectionReadOnly> collections);
|
||||
[Obsolete("IDeleteOrganizationUserCommand should be used instead. To be removed by EC-607.")]
|
||||
Task DeleteUserAsync(Guid organizationId, Guid organizationUserId, Guid? deletingUserId);
|
||||
Task DeleteUserAsync(Guid organizationId, Guid userId);
|
||||
Task<List<Tuple<OrganizationUser, string>>> DeleteUsersAsync(Guid organizationId,
|
||||
|
@ -75,6 +75,7 @@ public class GroupService : IGroupService
|
||||
}
|
||||
}
|
||||
|
||||
[Obsolete("IDeleteGroupCommand should be used instead. To be removed by EC-608.")]
|
||||
public async Task DeleteAsync(Group group)
|
||||
{
|
||||
await _groupRepository.DeleteAsync(group);
|
||||
|
@ -1645,6 +1645,7 @@ public class OrganizationService : IOrganizationService
|
||||
await _eventService.LogOrganizationUserEventAsync(user, EventType.OrganizationUser_Updated);
|
||||
}
|
||||
|
||||
[Obsolete("IDeleteOrganizationUserCommand should be used instead. To be removed by EC-607.")]
|
||||
public async Task DeleteUserAsync(Guid organizationId, Guid organizationUserId, Guid? deletingUserId)
|
||||
{
|
||||
var orgUser = await _organizationUserRepository.GetByIdAsync(organizationUserId);
|
||||
|
@ -0,0 +1,51 @@
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.OrganizationFeatures.Groups;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.OrganizationFeatures.Groups;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class DeleteGroupCommandTests
|
||||
{
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task DeleteGroup_Success(SutProvider<DeleteGroupCommand> sutProvider, Group group)
|
||||
{
|
||||
sutProvider.GetDependency<IGroupRepository>()
|
||||
.GetByIdAsync(group.Id)
|
||||
.Returns(group);
|
||||
|
||||
await sutProvider.Sut.DeleteGroupAsync(group.OrganizationId, group.Id);
|
||||
|
||||
await sutProvider.GetDependency<IGroupRepository>().Received(1).DeleteAsync(group);
|
||||
await sutProvider.GetDependency<IEventService>().Received(1).LogGroupEventAsync(group, Core.Enums.EventType.Group_Deleted);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task DeleteGroup_NotFound_Throws(SutProvider<DeleteGroupCommand> sutProvider, Guid organizationId, Guid groupId)
|
||||
{
|
||||
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.DeleteGroupAsync(organizationId, groupId));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task DeleteGroup_MismatchingOrganizationId_Throws(SutProvider<DeleteGroupCommand> sutProvider, Guid organizationId, Guid groupId)
|
||||
{
|
||||
sutProvider.GetDependency<IGroupRepository>()
|
||||
.GetByIdAsync(groupId)
|
||||
.Returns(new Core.Entities.Group
|
||||
{
|
||||
Id = groupId,
|
||||
OrganizationId = Guid.NewGuid()
|
||||
});
|
||||
|
||||
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.DeleteGroupAsync(organizationId, groupId));
|
||||
}
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationUsers;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.OrganizationFeatures.OrganizationUsers;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class DeleteOrganizationUserCommandTests
|
||||
{
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task DeleteUser_Success(SutProvider<DeleteOrganizationUserCommand> sutProvider, Guid organizationId, Guid organizationUserId)
|
||||
{
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetByIdAsync(organizationUserId)
|
||||
.Returns(new OrganizationUser
|
||||
{
|
||||
Id = organizationUserId,
|
||||
OrganizationId = organizationId
|
||||
});
|
||||
|
||||
await sutProvider.Sut.DeleteUserAsync(organizationId, organizationUserId, null);
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationService>().Received(1).DeleteUserAsync(organizationId, organizationUserId, null);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task DeleteUser_NotFound_Throws(SutProvider<DeleteOrganizationUserCommand> sutProvider, Guid organizationId, Guid organizationUserId)
|
||||
{
|
||||
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.DeleteUserAsync(organizationId, organizationUserId, null));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task DeleteUser_MismatchingOrganizationId_Throws(SutProvider<DeleteOrganizationUserCommand> 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.DeleteUserAsync(organizationId, organizationUserId, null));
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user