mirror of
https://github.com/bitwarden/server
synced 2026-02-19 10:53:34 +00:00
[PM-18715] - SCIM Revoke User v2 (#7024)
* Migrated SCIM revoke user call to the v2 implementation. * Correcting feature string
This commit is contained in:
@@ -1,17 +1,22 @@
|
||||
// FIXME: Update this file to be null safe and then delete the line below
|
||||
#nullable disable
|
||||
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Models.Data;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RestoreUser.v1;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RevokeUser.v1;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RevokeUser.v2;
|
||||
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;
|
||||
using Bit.Scim.Utilities;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using IRevokeOrganizationUserCommand = Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RevokeUser.v1.IRevokeOrganizationUserCommand;
|
||||
using IRevokeOrganizationUserCommandV2 = Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RevokeUser.v2.IRevokeOrganizationUserCommand;
|
||||
|
||||
namespace Bit.Scim.Controllers.v2;
|
||||
|
||||
@@ -28,6 +33,8 @@ public class UsersController : Controller
|
||||
private readonly IPostUserCommand _postUserCommand;
|
||||
private readonly IRestoreOrganizationUserCommand _restoreOrganizationUserCommand;
|
||||
private readonly IRevokeOrganizationUserCommand _revokeOrganizationUserCommand;
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly IRevokeOrganizationUserCommandV2 _revokeOrganizationUserCommandV2;
|
||||
|
||||
public UsersController(IOrganizationUserRepository organizationUserRepository,
|
||||
IGetUsersListQuery getUsersListQuery,
|
||||
@@ -35,7 +42,9 @@ public class UsersController : Controller
|
||||
IPatchUserCommand patchUserCommand,
|
||||
IPostUserCommand postUserCommand,
|
||||
IRestoreOrganizationUserCommand restoreOrganizationUserCommand,
|
||||
IRevokeOrganizationUserCommand revokeOrganizationUserCommand)
|
||||
IRevokeOrganizationUserCommand revokeOrganizationUserCommand,
|
||||
IFeatureService featureService,
|
||||
IRevokeOrganizationUserCommandV2 revokeOrganizationUserCommandV2)
|
||||
{
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
_getUsersListQuery = getUsersListQuery;
|
||||
@@ -44,6 +53,8 @@ public class UsersController : Controller
|
||||
_postUserCommand = postUserCommand;
|
||||
_restoreOrganizationUserCommand = restoreOrganizationUserCommand;
|
||||
_revokeOrganizationUserCommand = revokeOrganizationUserCommand;
|
||||
_featureService = featureService;
|
||||
_revokeOrganizationUserCommandV2 = revokeOrganizationUserCommandV2;
|
||||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
@@ -100,7 +111,33 @@ public class UsersController : Controller
|
||||
}
|
||||
else if (!model.Active && orgUser.Status != OrganizationUserStatusType.Revoked)
|
||||
{
|
||||
await _revokeOrganizationUserCommand.RevokeUserAsync(orgUser, EventSystemUser.SCIM);
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.ScimRevokeV2))
|
||||
{
|
||||
var results = await _revokeOrganizationUserCommandV2.RevokeUsersAsync(
|
||||
new RevokeOrganizationUsersRequest(
|
||||
organizationId,
|
||||
[id],
|
||||
new SystemUser(EventSystemUser.SCIM)));
|
||||
|
||||
var errors = results.Select(x => x.Result.Match(
|
||||
y => $"{y.Message} for user {x.Id}",
|
||||
_ => null))
|
||||
.Where(x => !string.IsNullOrWhiteSpace(x))
|
||||
.ToList();
|
||||
|
||||
if (errors.Count != 0)
|
||||
{
|
||||
return new BadRequestObjectResult(new ScimErrorResponseModel
|
||||
{
|
||||
Status = 400,
|
||||
Detail = string.Join(", ", errors)
|
||||
});
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
await _revokeOrganizationUserCommand.RevokeUserAsync(orgUser, EventSystemUser.SCIM);
|
||||
}
|
||||
}
|
||||
|
||||
// Have to get full details object for response model
|
||||
|
||||
@@ -394,9 +394,18 @@ public class UsersControllerTests : IClassFixture<ScimApplicationFactory>, IAsyn
|
||||
Assert.Equal(_initialUserCount, databaseContext.OrganizationUsers.Count());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Put_RevokeUser_Success()
|
||||
[Theory]
|
||||
[InlineData(true)]
|
||||
[InlineData(false)]
|
||||
public async Task Put_RevokeUser_Success(bool scimRevokeV2Enabled)
|
||||
{
|
||||
var localFactory = new ScimApplicationFactory();
|
||||
localFactory.SubstituteService((IFeatureService featureService)
|
||||
=> featureService.IsEnabled(FeatureFlagKeys.ScimRevokeV2)
|
||||
.Returns(scimRevokeV2Enabled));
|
||||
|
||||
localFactory.ReinitializeDbForTests(localFactory.GetDatabaseContext());
|
||||
|
||||
var organizationUserId = ScimApplicationFactory.TestOrganizationUserId2;
|
||||
var inputModel = new ScimUserRequestModel
|
||||
{
|
||||
@@ -418,13 +427,13 @@ public class UsersControllerTests : IClassFixture<ScimApplicationFactory>, IAsyn
|
||||
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
|
||||
};
|
||||
|
||||
var context = await _factory.UsersPutAsync(ScimApplicationFactory.TestOrganizationId1, organizationUserId, inputModel);
|
||||
var context = await localFactory.UsersPutAsync(ScimApplicationFactory.TestOrganizationId1, organizationUserId, inputModel);
|
||||
|
||||
var responseModel = JsonSerializer.Deserialize<ScimUserResponseModel>(context.Response.Body, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
|
||||
Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode);
|
||||
AssertHelper.AssertPropertyEqual(expectedResponse, responseModel);
|
||||
|
||||
var databaseContext = _factory.GetDatabaseContext();
|
||||
var databaseContext = localFactory.GetDatabaseContext();
|
||||
var revokedUser = databaseContext.OrganizationUsers.FirstOrDefault(g => g.Id == organizationUserId);
|
||||
Assert.Equal(OrganizationUserStatusType.Revoked, revokedUser.Status);
|
||||
}
|
||||
|
||||
@@ -29,7 +29,8 @@ public class RevokeOrganizationUsersValidator(IHasConfirmedOwnersExceptQuery has
|
||||
Invalid(x, new UserAlreadyRevoked()),
|
||||
{ Type: OrganizationUserType.Owner } when !hasRemainingOwner =>
|
||||
Invalid(x, new MustHaveConfirmedOwner()),
|
||||
{ Type: OrganizationUserType.Owner } when !request.PerformedBy.IsOrganizationOwnerOrProvider =>
|
||||
{ Type: OrganizationUserType.Owner } when request.PerformedBy is not SystemUser
|
||||
&& !request.PerformedBy.IsOrganizationOwnerOrProvider =>
|
||||
Invalid(x, new OnlyOwnersCanRevokeOwners()),
|
||||
|
||||
_ => Valid(x)
|
||||
|
||||
@@ -139,6 +139,7 @@ public static class FeatureFlagKeys
|
||||
public const string ScimInviteUserOptimization = "pm-16811-optimize-invite-user-flow-to-fail-fast";
|
||||
public const string CreateDefaultLocation = "pm-19467-create-default-location";
|
||||
public const string AutomaticConfirmUsers = "pm-19934-auto-confirm-organization-users";
|
||||
public const string ScimRevokeV2 = "pm-32394-scim-revoke-put-v2";
|
||||
public const string PM23845_VNextApplicationCache = "pm-24957-refactor-memory-application-cache";
|
||||
public const string DefaultUserCollectionRestore = "pm-30883-my-items-restored-users";
|
||||
public const string PremiumAccessQuery = "pm-29495-refactor-premium-interface";
|
||||
|
||||
@@ -236,6 +236,35 @@ public class RevokeOrganizationUsersValidatorTests
|
||||
Assert.True(results.First().IsValid);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task ValidateAsync_WithSystemUser_RevokingOwner_ReturnsSuccess(
|
||||
SutProvider<RevokeOrganizationUsersValidator> sutProvider,
|
||||
Guid organizationId,
|
||||
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser ownerUser)
|
||||
{
|
||||
// Arrange
|
||||
ownerUser.OrganizationId = organizationId;
|
||||
ownerUser.UserId = Guid.NewGuid();
|
||||
|
||||
var actingUser = CreateActingUser(null, false, EventSystemUser.SCIM);
|
||||
var request = CreateValidationRequest(
|
||||
organizationId,
|
||||
[ownerUser],
|
||||
actingUser);
|
||||
|
||||
sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()
|
||||
.HasConfirmedOwnersExceptAsync(organizationId, Arg.Any<IEnumerable<Guid>>())
|
||||
.Returns(true);
|
||||
|
||||
// Act
|
||||
var results = (await sutProvider.Sut.ValidateAsync(request)).ToList();
|
||||
|
||||
// Assert
|
||||
Assert.Single(results);
|
||||
Assert.True(results.First().IsValid);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task ValidateAsync_WhenRevokingLastOwner_ReturnsErrorForThatUser(
|
||||
|
||||
Reference in New Issue
Block a user