mirror of
https://github.com/bitwarden/server.git
synced 2025-04-05 13:08:17 -05:00
Separated old and new code explicitly. Moved old code checks down into new code as well. Added error and mapper to Failure<T>
This commit is contained in:
parent
7be2e2bd07
commit
44b817ad03
@ -2,6 +2,7 @@
|
||||
using Bit.Core.AdminConsole.Models.Business;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Utilities;
|
||||
using OrganizationUserInvite = Bit.Core.Models.Business.OrganizationUserInvite;
|
||||
@ -30,12 +31,21 @@ public class ScimUserRequestModel : BaseScimUserModel
|
||||
public OrganizationUserSingleEmailInvite ToRequest(
|
||||
ScimProviderType scimProvider,
|
||||
InviteOrganization inviteOrganization,
|
||||
DateTimeOffset performedAt) =>
|
||||
new(
|
||||
email: EmailForInvite(scimProvider),
|
||||
inviteOrganization: inviteOrganization,
|
||||
performedAt: performedAt,
|
||||
externalId: ExternalIdForInvite());
|
||||
DateTimeOffset performedAt)
|
||||
{
|
||||
var email = EmailForInvite(scimProvider);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(email) || !Active)
|
||||
{
|
||||
throw new BadRequestException();
|
||||
}
|
||||
|
||||
return new(
|
||||
email: email,
|
||||
inviteOrganization: inviteOrganization,
|
||||
performedAt: performedAt,
|
||||
externalId: ExternalIdForInvite());
|
||||
}
|
||||
|
||||
private string EmailForInvite(ScimProviderType scimProvider)
|
||||
{
|
||||
|
@ -1,7 +1,6 @@
|
||||
#nullable enable
|
||||
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Business;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;
|
||||
@ -16,6 +15,7 @@ using Bit.Core.Services;
|
||||
using Bit.Scim.Context;
|
||||
using Bit.Scim.Models;
|
||||
using Bit.Scim.Users.Interfaces;
|
||||
using static Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Errors.ErrorMapper;
|
||||
|
||||
namespace Bit.Scim.Users;
|
||||
|
||||
@ -34,7 +34,63 @@ public class PostUserCommand(
|
||||
{
|
||||
public async Task<OrganizationUserUserDetails?> PostUserAsync(Guid organizationId, ScimUserRequestModel model)
|
||||
{
|
||||
var scimProvider = scimContext.RequestScimProvider;
|
||||
if (featureService.IsEnabled(FeatureFlagKeys.ScimInviteUserOptimization) is false)
|
||||
{
|
||||
return await InviteScimOrganizationUserAsync(model, organizationId, scimContext.RequestScimProvider);
|
||||
}
|
||||
|
||||
return await InviteScimOrganizationUserAsync_vNext(model, organizationId, scimContext.RequestScimProvider);
|
||||
}
|
||||
|
||||
private async Task<OrganizationUserUserDetails?> InviteScimOrganizationUserAsync_vNext(
|
||||
ScimUserRequestModel model,
|
||||
Guid organizationId,
|
||||
ScimProviderType scimProvider)
|
||||
{
|
||||
var organization = await organizationRepository.GetByIdAsync(organizationId);
|
||||
|
||||
if (organization is null)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var plan = await pricingClient.GetPlan(organization.PlanType);
|
||||
|
||||
if (plan == null)
|
||||
{
|
||||
logger.LogError("Plan {planType} not found for organization {organizationId}",
|
||||
organization.PlanType, organization.Id);
|
||||
return null;
|
||||
}
|
||||
|
||||
var request = model.ToRequest(
|
||||
scimProvider: scimProvider,
|
||||
inviteOrganization: new InviteOrganization(organization, plan),
|
||||
performedAt: timeProvider.GetUtcNow());
|
||||
|
||||
var result = await inviteOrganizationUsersCommand.InviteScimOrganizationUserAsync(request);
|
||||
|
||||
var invitedOrganizationUserId = result switch
|
||||
{
|
||||
Success<ScimInviteOrganizationUsersResponse> success => success.Value.InvitedUser.Id,
|
||||
Failure<ScimInviteOrganizationUsersResponse> { ErrorMessage: InviteOrganizationUsersCommand.NoUsersToInvite } => (Guid?)null,
|
||||
Failure<ScimInviteOrganizationUsersResponse> failure when failure.Errors.Length != 0 => throw MapToBitException(failure.Errors),
|
||||
Failure<ScimInviteOrganizationUsersResponse> failure when failure.ErrorMessages.Count != 0 => throw new BadRequestException(failure.ErrorMessage),
|
||||
_ => throw new InvalidOperationException()
|
||||
};
|
||||
|
||||
var organizationUser = invitedOrganizationUserId.HasValue
|
||||
? await organizationUserRepository.GetDetailsByIdAsync(invitedOrganizationUserId.Value)
|
||||
: null;
|
||||
|
||||
return organizationUser;
|
||||
}
|
||||
|
||||
private async Task<OrganizationUserUserDetails?> InviteScimOrganizationUserAsync(
|
||||
ScimUserRequestModel model,
|
||||
Guid organizationId,
|
||||
ScimProviderType scimProvider)
|
||||
{
|
||||
var invite = model.ToOrganizationUserInvite(scimProvider);
|
||||
|
||||
var email = invite.Emails.Single();
|
||||
@ -65,56 +121,15 @@ public class PostUserCommand(
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
|
||||
if (featureService.IsEnabled(FeatureFlagKeys.ScimInviteUserOptimization))
|
||||
{
|
||||
return await InviteScimOrganizationUserAsync(model, organization, scimProvider);
|
||||
}
|
||||
|
||||
var hasStandaloneSecretsManager = await paymentService.HasSecretsManagerStandalone(organization);
|
||||
invite.AccessSecretsManager = hasStandaloneSecretsManager;
|
||||
|
||||
var invitedOrgUser = await organizationService.InviteUserAsync(organizationId, invitingUserId: null,
|
||||
EventSystemUser.SCIM,
|
||||
invite, externalId);
|
||||
invite,
|
||||
externalId);
|
||||
var orgUser = await organizationUserRepository.GetDetailsByIdAsync(invitedOrgUser.Id);
|
||||
|
||||
return orgUser;
|
||||
}
|
||||
|
||||
private async Task<OrganizationUserUserDetails?> InviteScimOrganizationUserAsync(ScimUserRequestModel model,
|
||||
Organization organization, ScimProviderType scimProvider)
|
||||
{
|
||||
var plan = await pricingClient.GetPlan(organization.PlanType);
|
||||
|
||||
if (plan == null)
|
||||
{
|
||||
logger.LogError("Plan {planType} not found for organization {organizationId}", organization.PlanType,
|
||||
organization.Id);
|
||||
return null;
|
||||
}
|
||||
|
||||
var request = model.ToRequest(
|
||||
scimProvider: scimProvider,
|
||||
inviteOrganization: new InviteOrganization(organization, plan),
|
||||
performedAt: timeProvider.GetUtcNow());
|
||||
|
||||
var result = await inviteOrganizationUsersCommand.InviteScimOrganizationUserAsync(request);
|
||||
|
||||
var invitedOrganizationUserId = result switch
|
||||
{
|
||||
Success<ScimInviteOrganizationUsersResponse> success => success.Value.InvitedUser.Id,
|
||||
Failure<ScimInviteOrganizationUsersResponse> { ErrorMessage: InviteOrganizationUsersCommand.NoUsersToInvite } => (Guid?)null,
|
||||
Failure<ScimInviteOrganizationUsersResponse> failure when failure.ErrorMessages.Count != 0 => throw new BadRequestException(failure.ErrorMessage),
|
||||
_ => throw new InvalidOperationException()
|
||||
};
|
||||
|
||||
var organizationUser = invitedOrganizationUserId.HasValue
|
||||
? await organizationUserRepository.GetDetailsByIdAsync(invitedOrganizationUserId.Value)
|
||||
: null;
|
||||
|
||||
return organizationUser;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -27,7 +27,7 @@ public class PostUserCommandTests
|
||||
ExternalId = externalId,
|
||||
Emails = emails,
|
||||
Active = true,
|
||||
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
|
||||
Schemas = [ScimConstants.Scim2SchemaUser]
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
@ -39,13 +39,16 @@ public class PostUserCommandTests
|
||||
sutProvider.GetDependency<IPaymentService>().HasSecretsManagerStandalone(organization).Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationService>()
|
||||
.InviteUserAsync(organizationId, invitingUserId: null, EventSystemUser.SCIM,
|
||||
.InviteUserAsync(organizationId,
|
||||
invitingUserId: null,
|
||||
EventSystemUser.SCIM,
|
||||
Arg.Is<OrganizationUserInvite>(i =>
|
||||
i.Emails.Single().Equals(scimUserRequestModel.PrimaryEmail.ToLowerInvariant()) &&
|
||||
i.Type == OrganizationUserType.User &&
|
||||
!i.Collections.Any() &&
|
||||
!i.Groups.Any() &&
|
||||
i.AccessSecretsManager), externalId)
|
||||
i.AccessSecretsManager),
|
||||
externalId)
|
||||
.Returns(newUser);
|
||||
|
||||
var user = await sutProvider.Sut.PostUserAsync(organizationId, scimUserRequestModel);
|
||||
|
@ -0,0 +1,37 @@
|
||||
using Bit.Core.AdminConsole.Errors;
|
||||
using Bit.Core.Exceptions;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Errors;
|
||||
|
||||
public static class ErrorMapper
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Maps the Error<T> to a Bit.Exception class.
|
||||
/// </summary>
|
||||
/// <param name="error"></param>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <returns></returns>
|
||||
public static Exception MapToBitException<T>(Error<T> error) =>
|
||||
error switch
|
||||
{
|
||||
UserAlreadyExistsError alreadyExistsError => new ConflictException(alreadyExistsError.Message),
|
||||
_ => new BadRequestException(error.Message)
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// This maps the Error<T> object to the Bit.Exception class.
|
||||
///
|
||||
/// This should be replaced by an IActionResult mapper when possible.
|
||||
/// </summary>
|
||||
/// <param name="errors"></param>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <returns></returns>
|
||||
public static Exception MapToBitException<T>(IEnumerable<Error<T>> errors) =>
|
||||
errors switch
|
||||
{
|
||||
not null when errors.Count() == 1 => MapToBitException(errors.First()),
|
||||
not null when errors.Count() > 1 => new BadRequestException(string.Join(' ', errors.Select(e => e.Message))),
|
||||
_ => new BadRequestException()
|
||||
};
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
using Bit.Core.AdminConsole.Errors;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Errors;
|
||||
|
||||
public record UserAlreadyExistsError(ScimInviteOrganizationUsersResponse Response) : Error<ScimInviteOrganizationUsersResponse>(Code, Response)
|
||||
{
|
||||
public const string Code = "User already exists";
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Errors;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation;
|
||||
using Bit.Core.AdminConsole.Shared.Validation;
|
||||
@ -39,10 +40,21 @@ public class InviteOrganizationUsersCommand(IEventService eventService,
|
||||
public const string NoUsersToInvite = "No users to invite.";
|
||||
public const string InvalidResultType = "Invalid result type.";
|
||||
|
||||
|
||||
public async Task<CommandResult<ScimInviteOrganizationUsersResponse>> InviteScimOrganizationUserAsync(OrganizationUserSingleEmailInvite request)
|
||||
{
|
||||
var hasSecretsManager = await paymentService.HasSecretsManagerStandalone(request.InviteOrganization);
|
||||
|
||||
var orgUsers = await organizationUserRepository.GetManyDetailsByOrganizationAsync(request.InviteOrganization.OrganizationId);
|
||||
|
||||
if (orgUsers.Any(existingUser =>
|
||||
request.Email.Equals(existingUser.Email, StringComparison.InvariantCultureIgnoreCase) ||
|
||||
request.ExternalId.Equals(existingUser.ExternalId, StringComparison.InvariantCultureIgnoreCase)))
|
||||
{
|
||||
return new Failure<ScimInviteOrganizationUsersResponse>(
|
||||
new UserAlreadyExistsError(new ScimInviteOrganizationUsersResponse(request)));
|
||||
}
|
||||
|
||||
var result = await InviteOrganizationUsersAsync(new InviteOrganizationUsersRequest(request, hasSecretsManager));
|
||||
|
||||
switch (result)
|
||||
|
@ -11,4 +11,17 @@ public class ScimInviteOrganizationUsersResponse
|
||||
{
|
||||
public OrganizationUser InvitedUser { get; init; }
|
||||
|
||||
public ScimInviteOrganizationUsersResponse()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public ScimInviteOrganizationUsersResponse(OrganizationUserSingleEmailInvite request)
|
||||
{
|
||||
InvitedUser = new OrganizationUser
|
||||
{
|
||||
Email = request.Email,
|
||||
ExternalId = request.ExternalId
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -5,5 +5,5 @@ namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUse
|
||||
|
||||
public record CannotAutoScaleOnSelfHostError(IGlobalSettings InvalidSettings) : Error<IGlobalSettings>(Code, InvalidSettings)
|
||||
{
|
||||
public const string Code = "CannotAutoScaleOnSelfHost";
|
||||
public const string Code = "Cannot auto scale self-host.";
|
||||
}
|
||||
|
@ -40,10 +40,18 @@ public class Success<T>(T value) : CommandResult<T>
|
||||
public class Failure<T>(IEnumerable<string> errorMessages) : CommandResult<T>
|
||||
{
|
||||
public List<string> ErrorMessages { get; } = errorMessages.ToList();
|
||||
public Error<T>[] Errors { get; set; } = [];
|
||||
|
||||
public string ErrorMessage => string.Join(" ", ErrorMessages);
|
||||
|
||||
public Failure(string error) : this([error]) { }
|
||||
public Failure(string error) : this([error])
|
||||
{
|
||||
}
|
||||
|
||||
public Failure(Error<T> error) : this([error.Message])
|
||||
{
|
||||
Errors = [error];
|
||||
}
|
||||
}
|
||||
|
||||
public class Partial<T> : CommandResult<T>
|
||||
|
Loading…
x
Reference in New Issue
Block a user