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-507] SCIM CQRS Refactor - Groups/Put #2269

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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 9 additions & 17 deletions bitwarden_license/src/Scim/Controllers/v2/GroupsController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,34 +3,36 @@
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 ILogger<GroupsController> _logger;
private readonly IPutGroupCommand _putGroupCommand;

public GroupsController(
IGroupRepository groupRepository,
IGroupService groupService,
IOptions<ScimSettings> scimSettings,
IScimContext scimContext,
IPutGroupCommand putGroupCommand,
ILogger<GroupsController> logger)
{
_scimSettings = scimSettings?.Value;
_groupRepository = groupRepository;
_groupService = groupService;
_scimContext = scimContext;
_putGroupCommand = putGroupCommand;
_logger = logger;
}

Expand Down Expand Up @@ -135,20 +137,10 @@ public async Task<IActionResult> Post(Guid organizationId, [FromBody] ScimGroupR
[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}")]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
65 changes: 65 additions & 0 deletions bitwarden_license/src/Scim/Groups/PutGroupCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
using Bit.Core.Entities;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Scim.Context;
using Bit.Scim.Groups.Interfaces;
using Bit.Scim.Models;

namespace Bit.Scim.Groups;

public class PutGroupCommand : IPutGroupCommand
{
private readonly IGroupRepository _groupRepository;
private readonly IGroupService _groupService;
private readonly IScimContext _scimContext;

public PutGroupCommand(
IGroupRepository groupRepository,
IGroupService groupService,
IScimContext scimContext)
{
_groupRepository = groupRepository;
_groupService = groupService;
_scimContext = scimContext;
}

public async Task<Group> PutGroupAsync(Guid organizationId, Guid id, ScimGroupRequestModel model)
{
var group = await _groupRepository.GetByIdAsync(id);
if (group == null || group.OrganizationId != organizationId)
{
throw new NotFoundException("Group not found.");
}

group.Name = model.DisplayName;
await _groupService.SaveAsync(group);
await UpdateGroupMembersAsync(group, model);

return group;
}

private async Task UpdateGroupMembersAsync(Group group, ScimGroupRequestModel model)
{
if (_scimContext.RequestScimProvider != Core.Enums.ScimProviderType.Okta)
{
return;
}

if (model.Members == null)
{
return;
}

var memberIds = new List<Guid>();
foreach (var id in model.Members.Select(i => i.Value))
{
if (Guid.TryParse(id, out var guidId))
{
memberIds.Add(guidId);
}
}

await _groupRepository.UpdateUsersAsync(group.Id, memberIds);
}
}
1 change: 1 addition & 0 deletions bitwarden_license/src/Scim/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ public void ConfigureServices(IServiceCollection services)
});
services.Configure<RouteOptions>(options => options.LowercaseUrls = true);

services.AddScimGroupCommands();
services.AddScimUserQueries();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Scim.Models;
using Bit.Scim.Queries.Users.Interfaces;
using Bit.Scim.Users.Interfaces;

namespace Bit.Scim.Queries.Users;
namespace Bit.Scim.Users;

public class GetUserQuery : IGetUserQuery
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using Bit.Scim.Models;

namespace Bit.Scim.Queries.Users.Interfaces;
namespace Bit.Scim.Users.Interfaces;

public interface IGetUserQuery
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
using Bit.Scim.Queries.Users;
using Bit.Scim.Queries.Users.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<IPutGroupCommand, PutGroupCommand>();
}

public static void AddScimUserQueries(this IServiceCollection services)
{
services.AddScoped<IGetUserQuery, GetUserQuery>();
Expand Down
122 changes: 122 additions & 0 deletions bitwarden_license/test/Scim.Test/Groups/PutGroupCommandTests.cs
Original file line number Diff line number Diff line change
@@ -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));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@
using Bit.Core.Exceptions;
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.Repositories;
using Bit.Scim.Queries.Users;
using Bit.Scim.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;
namespace Bit.Scim.Test.Users;

[SutProviderCustomize]
public class GetUserQueryTests
Expand Down