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.Models.Business;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Models.Data;
|
using Bit.Core.Models.Data;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using OrganizationUserInvite = Bit.Core.Models.Business.OrganizationUserInvite;
|
using OrganizationUserInvite = Bit.Core.Models.Business.OrganizationUserInvite;
|
||||||
@ -30,12 +31,21 @@ public class ScimUserRequestModel : BaseScimUserModel
|
|||||||
public OrganizationUserSingleEmailInvite ToRequest(
|
public OrganizationUserSingleEmailInvite ToRequest(
|
||||||
ScimProviderType scimProvider,
|
ScimProviderType scimProvider,
|
||||||
InviteOrganization inviteOrganization,
|
InviteOrganization inviteOrganization,
|
||||||
DateTimeOffset performedAt) =>
|
DateTimeOffset performedAt)
|
||||||
new(
|
{
|
||||||
email: EmailForInvite(scimProvider),
|
var email = EmailForInvite(scimProvider);
|
||||||
inviteOrganization: inviteOrganization,
|
|
||||||
performedAt: performedAt,
|
if (string.IsNullOrWhiteSpace(email) || !Active)
|
||||||
externalId: ExternalIdForInvite());
|
{
|
||||||
|
throw new BadRequestException();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new(
|
||||||
|
email: email,
|
||||||
|
inviteOrganization: inviteOrganization,
|
||||||
|
performedAt: performedAt,
|
||||||
|
externalId: ExternalIdForInvite());
|
||||||
|
}
|
||||||
|
|
||||||
private string EmailForInvite(ScimProviderType scimProvider)
|
private string EmailForInvite(ScimProviderType scimProvider)
|
||||||
{
|
{
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
#nullable enable
|
#nullable enable
|
||||||
|
|
||||||
using Bit.Core;
|
using Bit.Core;
|
||||||
using Bit.Core.AdminConsole.Entities;
|
|
||||||
using Bit.Core.AdminConsole.Enums;
|
using Bit.Core.AdminConsole.Enums;
|
||||||
using Bit.Core.AdminConsole.Models.Business;
|
using Bit.Core.AdminConsole.Models.Business;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;
|
||||||
@ -16,6 +15,7 @@ using Bit.Core.Services;
|
|||||||
using Bit.Scim.Context;
|
using Bit.Scim.Context;
|
||||||
using Bit.Scim.Models;
|
using Bit.Scim.Models;
|
||||||
using Bit.Scim.Users.Interfaces;
|
using Bit.Scim.Users.Interfaces;
|
||||||
|
using static Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Errors.ErrorMapper;
|
||||||
|
|
||||||
namespace Bit.Scim.Users;
|
namespace Bit.Scim.Users;
|
||||||
|
|
||||||
@ -34,7 +34,63 @@ public class PostUserCommand(
|
|||||||
{
|
{
|
||||||
public async Task<OrganizationUserUserDetails?> PostUserAsync(Guid organizationId, ScimUserRequestModel model)
|
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 invite = model.ToOrganizationUserInvite(scimProvider);
|
||||||
|
|
||||||
var email = invite.Emails.Single();
|
var email = invite.Emails.Single();
|
||||||
@ -65,56 +121,15 @@ public class PostUserCommand(
|
|||||||
throw new NotFoundException();
|
throw new NotFoundException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (featureService.IsEnabled(FeatureFlagKeys.ScimInviteUserOptimization))
|
|
||||||
{
|
|
||||||
return await InviteScimOrganizationUserAsync(model, organization, scimProvider);
|
|
||||||
}
|
|
||||||
|
|
||||||
var hasStandaloneSecretsManager = await paymentService.HasSecretsManagerStandalone(organization);
|
var hasStandaloneSecretsManager = await paymentService.HasSecretsManagerStandalone(organization);
|
||||||
invite.AccessSecretsManager = hasStandaloneSecretsManager;
|
invite.AccessSecretsManager = hasStandaloneSecretsManager;
|
||||||
|
|
||||||
var invitedOrgUser = await organizationService.InviteUserAsync(organizationId, invitingUserId: null,
|
var invitedOrgUser = await organizationService.InviteUserAsync(organizationId, invitingUserId: null,
|
||||||
EventSystemUser.SCIM,
|
EventSystemUser.SCIM,
|
||||||
invite, externalId);
|
invite,
|
||||||
|
externalId);
|
||||||
var orgUser = await organizationUserRepository.GetDetailsByIdAsync(invitedOrgUser.Id);
|
var orgUser = await organizationUserRepository.GetDetailsByIdAsync(invitedOrgUser.Id);
|
||||||
|
|
||||||
return orgUser;
|
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,
|
ExternalId = externalId,
|
||||||
Emails = emails,
|
Emails = emails,
|
||||||
Active = true,
|
Active = true,
|
||||||
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
|
Schemas = [ScimConstants.Scim2SchemaUser]
|
||||||
};
|
};
|
||||||
|
|
||||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||||
@ -39,13 +39,16 @@ public class PostUserCommandTests
|
|||||||
sutProvider.GetDependency<IPaymentService>().HasSecretsManagerStandalone(organization).Returns(true);
|
sutProvider.GetDependency<IPaymentService>().HasSecretsManagerStandalone(organization).Returns(true);
|
||||||
|
|
||||||
sutProvider.GetDependency<IOrganizationService>()
|
sutProvider.GetDependency<IOrganizationService>()
|
||||||
.InviteUserAsync(organizationId, invitingUserId: null, EventSystemUser.SCIM,
|
.InviteUserAsync(organizationId,
|
||||||
|
invitingUserId: null,
|
||||||
|
EventSystemUser.SCIM,
|
||||||
Arg.Is<OrganizationUserInvite>(i =>
|
Arg.Is<OrganizationUserInvite>(i =>
|
||||||
i.Emails.Single().Equals(scimUserRequestModel.PrimaryEmail.ToLowerInvariant()) &&
|
i.Emails.Single().Equals(scimUserRequestModel.PrimaryEmail.ToLowerInvariant()) &&
|
||||||
i.Type == OrganizationUserType.User &&
|
i.Type == OrganizationUserType.User &&
|
||||||
!i.Collections.Any() &&
|
!i.Collections.Any() &&
|
||||||
!i.Groups.Any() &&
|
!i.Groups.Any() &&
|
||||||
i.AccessSecretsManager), externalId)
|
i.AccessSecretsManager),
|
||||||
|
externalId)
|
||||||
.Returns(newUser);
|
.Returns(newUser);
|
||||||
|
|
||||||
var user = await sutProvider.Sut.PostUserAsync(organizationId, scimUserRequestModel);
|
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.Entities;
|
||||||
using Bit.Core.AdminConsole.Interfaces;
|
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.Models;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation;
|
||||||
using Bit.Core.AdminConsole.Shared.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 NoUsersToInvite = "No users to invite.";
|
||||||
public const string InvalidResultType = "Invalid result type.";
|
public const string InvalidResultType = "Invalid result type.";
|
||||||
|
|
||||||
|
|
||||||
public async Task<CommandResult<ScimInviteOrganizationUsersResponse>> InviteScimOrganizationUserAsync(OrganizationUserSingleEmailInvite request)
|
public async Task<CommandResult<ScimInviteOrganizationUsersResponse>> InviteScimOrganizationUserAsync(OrganizationUserSingleEmailInvite request)
|
||||||
{
|
{
|
||||||
var hasSecretsManager = await paymentService.HasSecretsManagerStandalone(request.InviteOrganization);
|
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));
|
var result = await InviteOrganizationUsersAsync(new InviteOrganizationUsersRequest(request, hasSecretsManager));
|
||||||
|
|
||||||
switch (result)
|
switch (result)
|
||||||
|
@ -11,4 +11,17 @@ public class ScimInviteOrganizationUsersResponse
|
|||||||
{
|
{
|
||||||
public OrganizationUser InvitedUser { get; init; }
|
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 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 class Failure<T>(IEnumerable<string> errorMessages) : CommandResult<T>
|
||||||
{
|
{
|
||||||
public List<string> ErrorMessages { get; } = errorMessages.ToList();
|
public List<string> ErrorMessages { get; } = errorMessages.ToList();
|
||||||
|
public Error<T>[] Errors { get; set; } = [];
|
||||||
|
|
||||||
public string ErrorMessage => string.Join(" ", ErrorMessages);
|
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>
|
public class Partial<T> : CommandResult<T>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user