Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

[EC-449] Event log user for SCIM events #2306

Merged
merged 56 commits into from
Nov 9, 2022
Merged
Show file tree
Hide file tree
Changes from 45 commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
1c1e193
[EC-449] Added new Enum EventSystemUser
r-tome Sep 27, 2022
df5db9f
[EC-449] Added SystemUser property to Event model
r-tome Sep 27, 2022
1fa9405
[EC-449] Added SQL migration to add new column 'SystemUserType' to Event
r-tome Sep 27, 2022
2fc8544
[EC-449] EF migrations
r-tome Sep 27, 2022
2870069
[EC-449] Added EventSystemUser to EventResponseModel
r-tome Sep 27, 2022
043ebb3
[EC-449] Saving EventSystemUser.SCIM on SCIM controller actions
r-tome Sep 27, 2022
3f6b2a9
Merge branch 'master' into EC-449-event-log-user-for-scim-events
r-tome Sep 28, 2022
b6f09de
[EC-449] Updated Event_Create stored procedure on Sql project
r-tome Sep 28, 2022
5e526ed
Merge branch 'EC-449-event-log-user-for-scim-events' of https://githu…
r-tome Sep 28, 2022
e1a1c8c
[EC-449] Fixed SystemUser column name on Event table
r-tome Sep 28, 2022
52d22d0
[EC-507] SCIM CQRS Refactor - Groups/Put (#2269)
r-tome Oct 17, 2022
94ff4f8
[EC-507] SCIM CQRS Refactor - Groups/GetList (#2272)
r-tome Oct 17, 2022
7f181e2
[EC-507] SCIM CQRS Refactor - Groups/Get (#2271)
r-tome Oct 17, 2022
0605a31
[EC-507] SCIM CQRS Refactor - Groups/Patch (#2268)
r-tome Oct 17, 2022
520b2ab
[EC-508] SCIM CQRS Refactor - Users/Delete (#2261)
r-tome Oct 17, 2022
f56e425
[EC-508] SCIM CQRS Refactor - Users/Post (#2264)
r-tome Oct 17, 2022
a6013c7
[EC-508] SCIM CQRS Refactor - Users/Patch (#2262)
r-tome Oct 18, 2022
4f10d81
[EC-507] SCIM CQRS Refactor - Groups/Post (#2270)
r-tome Oct 18, 2022
0a0a8eb
[EC-508] SCIM CQRS Refactor - Users/GetList (#2265)
r-tome Oct 18, 2022
2fa17f4
Merge branch 'master' into EC-449-event-log-user-for-scim-events
r-tome Oct 18, 2022
48cc8be
[EC-507] SCIM CQRS Refactor - Groups/Delete (#2267)
r-tome Oct 18, 2022
e0ff808
[EC-449] Add overloads for EventService and GroupService methods that…
r-tome Oct 18, 2022
659feb5
Merge branch 'master' into feature/scim-cqrs
r-tome Oct 18, 2022
4936012
[EC-507] Move IDeleteGroupCommand to Groups folder
r-tome Oct 19, 2022
8f37a51
Merge branch 'master' into feature/scim-cqrs
r-tome Oct 19, 2022
2e6bf33
Merge branch 'master' into EC-449-event-log-user-for-scim-events
r-tome Oct 19, 2022
03e88c5
[EC-449] Add method overloads in IOrganizationService without EventSy…
r-tome Oct 19, 2022
e118bbb
[EC-449] Add RevokeUserAsync overload without EventSystemUser
r-tome Oct 19, 2022
cf01034
[EC-449] Reverted OrganizationUsersController to not pass EventSystem…
r-tome Oct 19, 2022
f582c7a
[EC-449] Uncomment assertion in GroupServiceTests
r-tome Oct 19, 2022
fec3441
[EC-449] Update method overloads to not have nullable EventSystemUser
r-tome Oct 21, 2022
1fe4e69
[EC-449] Add unit tests around events that can store EventSystemUser
r-tome Oct 21, 2022
4ce7623
Merge branch 'master' into EC-449-event-log-user-for-scim-events
r-tome Oct 28, 2022
4109099
[EC-449] Deleted private method GroupService.GroupRepositoryDeleteAsync
r-tome Oct 28, 2022
efdd4e7
[EC-449] Move Event log call to public DeleteUserAsync methods
r-tome Oct 28, 2022
1f81565
[EC-449] Move call to EventService log to public OrganizationService.…
r-tome Oct 28, 2022
572ae0d
[EC-449] Move EventService call to public OrganizationService.DeleteU…
r-tome Oct 28, 2022
7af2c4d
[EC-449] Move EventService call to OrganizationService.RevokeUserAsyn…
r-tome Oct 28, 2022
4329048
[EC-449] Move EventService call to OrganizationService.RestoreUserAsy…
r-tome Oct 28, 2022
313f1b9
[EC-449] Add missing comma in SQL script for new SystemUser column on…
r-tome Oct 28, 2022
07682de
[EC-449] Remove Autofixture hack from OrganizationServiceTests
r-tome Oct 28, 2022
cd6f7c0
[EC-449] Remove invitingUser param when methods expect an EventSystem…
r-tome Oct 28, 2022
d3c6999
Merge branch 'feature/scim-cqrs' into EC-449-event-log-user-for-scim-…
r-tome Oct 28, 2022
fc6adb7
Merge remote-tracking branch 'origin/master' into feature/scim-cqrs
eliykat Oct 30, 2022
0107416
Merge branch 'feature/scim-cqrs' into EC-449-event-log-user-for-scim-…
eliykat Oct 30, 2022
c69bfbd
[EC-449] Move DeleteUserAsync validation to private method
r-tome Oct 31, 2022
7f8db4b
Merge branch 'master' into EC-449-event-log-user-for-scim-events
r-tome Oct 31, 2022
caeb333
[EC-449] Move revokingUserId from RevokeUserAsync private method
r-tome Oct 31, 2022
bac325c
[EC-449] Move restoringUserId to RestoreUserAsync public method
r-tome Oct 31, 2022
c66e305
[EC-449] Set up OrganizationServiceTest Restore and Revoke tests on a…
r-tome Oct 31, 2022
aad961b
[EC-449] SaveUsersSendInvitesAsync to return both OrganizationUsers a…
r-tome Oct 31, 2022
a3a5092
Merge branch 'master' into EC-449-event-log-user-for-scim-events
r-tome Oct 31, 2022
0dc6ffb
[EC-449] Undo unintended change on CipherRepository
r-tome Nov 1, 2022
9760044
[EC-449] Add SystemUser value to EventTableEntity
r-tome Nov 2, 2022
93ac9d4
Merge branch 'master' into EC-449-event-log-user-for-scim-events
r-tome Nov 2, 2022
f4ab101
Merge branch 'master' into EC-449-event-log-user-for-scim-events
r-tome Nov 9, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
298 changes: 37 additions & 261 deletions bitwarden_license/src/Scim/Controllers/v2/GroupsController.cs
Original file line number Diff line number Diff line change
@@ -1,36 +1,43 @@
using System.Text.Json;
using Bit.Core.Entities;
using Bit.Core.Enums;
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;
}

Expand All @@ -40,13 +47,9 @@ public async Task<IActionResult> Get(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."
});
throw new NotFoundException("Group not found.");
}
return new ObjectResult(new ScimGroupResponseModel(group));
return Ok(new ScimGroupResponseModel(group));
}

[HttpGet("")]
Expand All @@ -56,272 +59,45 @@ public async Task<IActionResult> Get(
[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, EventSystemUser.SCIM);
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);
}
}
Loading