1
0
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:
Rui Tomé 2022-10-31 09:58:21 +00:00 committed by GitHub
parent 9703fb6874
commit 0a01051d83
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 2028 additions and 564 deletions

View File

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

View File

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

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

View File

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

View File

@ -0,0 +1,8 @@
using Bit.Scim.Models;
namespace Bit.Scim.Groups.Interfaces;
public interface IPatchGroupCommand
{
Task PatchGroupAsync(Guid organizationId, Guid id, ScimPatchModel model);
}

View File

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

View File

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

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

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

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

View File

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

View File

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

View File

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

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

View File

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

View File

@ -0,0 +1,8 @@
using Bit.Scim.Models;
namespace Bit.Scim.Users.Interfaces;
public interface IPatchUserCommand
{
Task PatchUserAsync(Guid organizationId, Guid id, ScimPatchModel model);
}

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

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

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

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

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

View File

@ -0,0 +1,6 @@
namespace Bit.Core.OrganizationFeatures.Groups.Interfaces;
public interface IDeleteGroupCommand
{
Task DeleteGroupAsync(Guid organizationId, Guid id);
}

View File

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

View File

@ -0,0 +1,6 @@
namespace Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces;
public interface IDeleteOrganizationUserCommand
{
Task DeleteUserAsync(Guid organizationId, Guid organizationUserId, Guid? deletingUserId);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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