1
0
mirror of https://github.com/bitwarden/server.git synced 2025-05-20 11:04:31 -05:00

Moved to private method. Made ScimInvite inherit the single invite base model. Moved create methods to constructors. A few more CR changes included.

This commit is contained in:
jrmccannon 2025-03-05 14:55:41 -06:00
parent 085dbffed1
commit 611ad8c343
No known key found for this signature in database
GPG Key ID: CF03F3DB01CE96A6
16 changed files with 114 additions and 130 deletions

View File

@ -1,4 +1,5 @@
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Models.Business;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
using Bit.Core.Enums;
using Bit.Core.Models.Data;
@ -26,13 +27,17 @@ public class ScimUserRequestModel : BaseScimUserModel
};
}
public OrganizationUserSingleEmailInvite ToInvite(ScimProviderType scimProvider, bool hasSecretsManager) =>
OrganizationUserSingleEmailInvite.Create(
EmailForInvite(scimProvider),
[],
OrganizationUserType.User,
new Permissions(),
hasSecretsManager);
public InviteScimOrganizationUserRequest ToRequest(
ScimProviderType scimProvider,
bool hasSecretsManager,
OrganizationDto organization,
DateTimeOffset performedAt) =>
new(
email: EmailForInvite(scimProvider),
hasSecretsManager: hasSecretsManager,
organization: organization,
performedAt: performedAt,
externalId: ExternalIdForInvite());
private string EmailForInvite(ScimProviderType scimProvider)
{

View File

@ -1,4 +1,6 @@
using Bit.Core;
#nullable enable
using Bit.Core;
using Bit.Core.AdminConsole.Models.Business;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
@ -25,7 +27,7 @@ public class PostUserCommand(
TimeProvider timeProvider)
: IPostUserCommand
{
public async Task<OrganizationUserUserDetails> PostUserAsync(Guid organizationId, ScimUserRequestModel model)
public async Task<OrganizationUserUserDetails?> PostUserAsync(Guid organizationId, ScimUserRequestModel model)
{
var scimProvider = scimContext.RequestScimProvider;
var invite = model.ToOrganizationUserInvite(scimProvider);
@ -57,23 +59,11 @@ public class PostUserCommand(
if (featureService.IsEnabled(FeatureFlagKeys.ScimInviteUserOptimization))
{
var request = InviteScimOrganizationUserRequest.Create(
model.ToInvite(scimProvider, hasStandaloneSecretsManager),
OrganizationDto.FromOrganization(organization),
timeProvider.GetUtcNow(),
model.ExternalIdForInvite()
);
var result = await inviteOrganizationUsersCommand.InviteScimOrganizationUserAsync(request);
if (result is Success<ScimInviteOrganizationUsersResponse> successfulResponse)
{
var invitedUser = await organizationUserRepository.GetDetailsByIdAsync(successfulResponse.Value.InvitedUser.Id);
return invitedUser;
}
return null;
return await InviteScimOrganizationUserAsync(model.ToRequest(
scimProvider: scimProvider,
hasSecretsManager: hasStandaloneSecretsManager,
organization: OrganizationDto.FromOrganization(organization),
performedAt: timeProvider.GetUtcNow()));
}
var invitedOrgUser = await organizationService.InviteUserAsync(organizationId, invitingUserId: null, EventSystemUser.SCIM,
@ -82,4 +72,19 @@ public class PostUserCommand(
return orgUser;
}
private async Task<OrganizationUserUserDetails?> InviteScimOrganizationUserAsync(InviteScimOrganizationUserRequest request)
{
var result = await inviteOrganizationUsersCommand.InviteScimOrganizationUserAsync(request);
if (result is not Success<ScimInviteOrganizationUsersResponse> successfulResponse)
{
return null;
}
var invitedUser = await organizationUserRepository.GetDetailsByIdAsync(successfulResponse.Value.InvitedUser.Id);
return invitedUser;
}
}

View File

@ -38,7 +38,7 @@ public class InviteOrganizationUsersCommand(IEventService eventService,
public async Task<CommandResult<ScimInviteOrganizationUsersResponse>> InviteScimOrganizationUserAsync(InviteScimOrganizationUserRequest request)
{
var result = await InviteOrganizationUsersAsync(InviteOrganizationUsersRequest.Create(request));
var result = await InviteOrganizationUsersAsync(new InviteOrganizationUsersRequest(request));
if (result is Failure<IEnumerable<OrganizationUser>> failure)
{
@ -47,7 +47,7 @@ public class InviteOrganizationUsersCommand(IEventService eventService,
if (result.Value.Any())
{
await eventService.LogOrganizationUserEventAsync((IOrganizationUser)result.Value.First(), EventType.OrganizationUser_Invited, EventSystemUser.SCIM, request.PerformedAt.UtcDateTime);
await eventService.LogOrganizationUserEventAsync<IOrganizationUser>(result.Value.First(), EventType.OrganizationUser_Invited, EventSystemUser.SCIM, request.PerformedAt.UtcDateTime);
}
return new Success<ScimInviteOrganizationUsersResponse>(new ScimInviteOrganizationUsersResponse
@ -113,7 +113,7 @@ public class InviteOrganizationUsersCommand(IEventService eventService,
{
logger.LogError(ex, FailedToInviteUsers);
await organizationUserRepository.DeleteManyAsync(organizationUserCollection.Select(x => x.User.Id));
await organizationUserRepository.DeleteManyAsync(organizationUserCollection.Select(x => x.OrganizationUser.Id));
await RevertSecretsManagerChangesAsync(validatedRequest, organization);
@ -122,7 +122,7 @@ public class InviteOrganizationUsersCommand(IEventService eventService,
return new Failure<IEnumerable<OrganizationUser>>(FailedToInviteUsers);
}
return new Success<IEnumerable<OrganizationUser>>(organizationUserCollection.Select(x => x.User));
return new Success<IEnumerable<OrganizationUser>>(organizationUserCollection.Select(x => x.OrganizationUser));
}
private async Task RevertPasswordManagerChangesAsync(Valid<InviteUserOrganizationValidationRequest> valid, Organization organization)
@ -162,7 +162,7 @@ public class InviteOrganizationUsersCommand(IEventService eventService,
private async Task SendInvitesAsync(IEnumerable<CreateOrganizationUser> users, Organization organization) =>
await sendOrganizationInvitesCommand.SendInvitesAsync(
new SendInvitesRequest(
users.Select(x => x.User),
users.Select(x => x.OrganizationUser),
organization));
private async Task SendAdditionalEmailsAsync(Valid<InviteUserOrganizationValidationRequest> valid, Organization organization)

View File

@ -7,7 +7,7 @@ namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUse
public class CreateOrganizationUser
{
public OrganizationUser User { get; set; }
public OrganizationUser OrganizationUser { get; set; }
public CollectionAccessSelection[] Collections { get; set; } = [];
public Guid[] Groups { get; set; } = [];
}
@ -17,7 +17,7 @@ public static class CreateOrganizationUserExtensions
public static Func<OrganizationUserInviteDto, CreateOrganizationUser> MapToDataModel(DateTimeOffset performedAt) =>
o => new CreateOrganizationUser
{
User = new OrganizationUser
OrganizationUser = new OrganizationUser
{
Id = CoreHelpers.GenerateComb(),
OrganizationId = o.OrganizationId,

View File

@ -9,20 +9,21 @@ public class InviteOrganizationUsersRequest
public Guid PerformedBy { get; }
public DateTimeOffset PerformedAt { get; }
public InviteOrganizationUsersRequest(OrganizationUserInvite[] Invites,
OrganizationDto Organization,
Guid PerformedBy,
DateTimeOffset PerformedAt)
public InviteOrganizationUsersRequest(OrganizationUserInvite[] invites,
OrganizationDto organization,
Guid performedBy,
DateTimeOffset performedAt)
{
this.Invites = Invites;
this.Organization = Organization;
this.PerformedBy = PerformedBy;
this.PerformedAt = PerformedAt;
Invites = invites;
Organization = organization;
PerformedBy = performedBy;
PerformedAt = performedAt;
}
public static InviteOrganizationUsersRequest Create(InviteScimOrganizationUserRequest request) =>
new([OrganizationUserInvite.Create(request.Invite, request.ExternalId)],
public InviteOrganizationUsersRequest(InviteScimOrganizationUserRequest request) :
this([OrganizationUserInvite.Create(request, request.ExternalId)],
request.Organization,
Guid.Empty,
request.PerformedAt);
request.PerformedAt)
{ }
}

View File

@ -11,11 +11,4 @@ public class ScimInviteOrganizationUsersResponse
{
public OrganizationUser InvitedUser { get; init; }
public static ScimInviteOrganizationUsersResponse Create(OrganizationUser invitedUser)
{
return new ScimInviteOrganizationUsersResponse
{
InvitedUser = invitedUser
};
}
}

View File

@ -1,26 +1,28 @@
using Bit.Core.AdminConsole.Models.Business;
using Bit.Core.Enums;
using Bit.Core.Models.Data;
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
public class InviteScimOrganizationUserRequest
public record InviteScimOrganizationUserRequest : OrganizationUserSingleEmailInvite
{
public OrganizationUserSingleEmailInvite Invite { get; }
public OrganizationDto Organization { get; }
public DateTimeOffset PerformedAt { get; }
public string ExternalId { get; } = string.Empty;
public OrganizationDto Organization { get; private init; }
public DateTimeOffset PerformedAt { get; private init; }
public string ExternalId { get; private init; } = string.Empty;
private InviteScimOrganizationUserRequest(OrganizationUserSingleEmailInvite invite,
public InviteScimOrganizationUserRequest(string email,
bool hasSecretsManager,
OrganizationDto organization,
DateTimeOffset performedAt,
string externalId)
string externalId) : base(
email: email,
accessibleCollections: [],
type: OrganizationUserType.User,
permissions: new Permissions(),
accessSecretsManager: hasSecretsManager)
{
Invite = invite;
Organization = organization;
PerformedAt = performedAt;
ExternalId = externalId;
}
public static InviteScimOrganizationUserRequest Create(OrganizationUserSingleEmailInvite invite,
OrganizationDto organization, DateTimeOffset performedAt, string externalId) =>
new(invite, organization, performedAt, externalId);
}

View File

@ -12,7 +12,6 @@ public class OrganizationUserInvite
public string[] Emails { get; private init; } = [];
public CollectionAccessSelection[] AccessibleCollections { get; private init; } = [];
public OrganizationUserType Type { get; private init; } = OrganizationUserType.User;
public Permissions Permissions { get; private init; } = new();
public string ExternalId { get; private init; } = string.Empty;
public bool AccessSecretsManager { get; private init; }

View File

@ -7,15 +7,15 @@ using static Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Invite
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
public class OrganizationUserSingleEmailInvite
public record OrganizationUserSingleEmailInvite
{
public string Email { get; private init; } = string.Empty;
public CollectionAccessSelection[] AccessibleCollections { get; private init; } = [];
public Permissions Permissions { get; private init; } = new();
public OrganizationUserType Type { get; private init; } = OrganizationUserType.User;
public bool AccessSecretsManager { get; private init; }
public string Email { get; init; } = string.Empty;
public CollectionAccessSelection[] AccessibleCollections { get; init; } = [];
public Permissions Permissions { get; init; } = new();
public OrganizationUserType Type { get; init; } = OrganizationUserType.User;
public bool AccessSecretsManager { get; init; }
public static OrganizationUserSingleEmailInvite Create(string email,
public OrganizationUserSingleEmailInvite(string email,
IEnumerable<CollectionAccessSelection> accessibleCollections,
OrganizationUserType type,
Permissions permissions,
@ -31,13 +31,10 @@ public class OrganizationUserSingleEmailInvite
throw new BadRequestException(InvalidCollectionConfigurationErrorMessage);
}
return new OrganizationUserSingleEmailInvite
{
Email = email,
AccessibleCollections = accessibleCollections.ToArray(),
Type = type,
Permissions = permissions,
AccessSecretsManager = accessSecretsManager
};
Email = email;
AccessibleCollections = accessibleCollections.ToArray();
Type = type;
Permissions = permissions;
AccessSecretsManager = accessSecretsManager;
}
}

View File

@ -590,12 +590,12 @@ public class OrganizationUserRepository : Repository<OrganizationUser, Guid>, IO
$"[{Schema}].[OrganizationUser_CreateManyWithCollectionsAndGroups]",
new
{
OrganizationUserData = JsonSerializer.Serialize(organizationUserCollection.Select(x => x.User)),
OrganizationUserData = JsonSerializer.Serialize(organizationUserCollection.Select(x => x.OrganizationUser)),
CollectionData = JsonSerializer.Serialize(organizationUserCollection
.SelectMany(x => x.Collections, (user, collection) => new CollectionUser
{
CollectionId = collection.Id,
OrganizationUserId = user.User.Id,
OrganizationUserId = user.OrganizationUser.Id,
ReadOnly = collection.ReadOnly,
HidePasswords = collection.HidePasswords,
Manage = collection.Manage
@ -604,7 +604,7 @@ public class OrganizationUserRepository : Repository<OrganizationUser, Guid>, IO
.SelectMany(x => x.Groups, (user, group) => new GroupUser
{
GroupId = group,
OrganizationUserId = user.User.Id
OrganizationUserId = user.OrganizationUser.Id
}))
},
commandType: CommandType.StoredProcedure);

View File

@ -762,19 +762,19 @@ public class OrganizationUserRepository : Repository<Core.Entities.OrganizationU
await using var dbContext = GetDatabaseContext(scope);
dbContext.OrganizationUsers.AddRange(Mapper.Map<List<OrganizationUser>>(organizationUserCollection.Select(x => x.User)));
dbContext.OrganizationUsers.AddRange(Mapper.Map<List<OrganizationUser>>(organizationUserCollection.Select(x => x.OrganizationUser)));
dbContext.CollectionUsers.AddRange(organizationUserCollection.SelectMany(x => x.Collections, (user, collection) => new CollectionUser
{
CollectionId = collection.Id,
HidePasswords = collection.HidePasswords,
OrganizationUserId = user.User.Id,
OrganizationUserId = user.OrganizationUser.Id,
Manage = collection.Manage,
ReadOnly = collection.ReadOnly
}));
dbContext.GroupUsers.AddRange(organizationUserCollection.SelectMany(x => x.Groups, (user, group) => new GroupUser
{
GroupId = group,
OrganizationUserId = user.User.Id
OrganizationUserId = user.OrganizationUser.Id
}));
await dbContext.SaveChangesAsync();

View File

@ -15,7 +15,7 @@ public class InviteOrganizationUserRequestTests
public void Create_WhenPassedInvalidEmail_ThrowsException(string email,
OrganizationUserType type, Permissions permissions, bool accessSecretsManager)
{
var action = () => OrganizationUserSingleEmailInvite.Create(email, [], type, permissions, accessSecretsManager);
var action = () => new OrganizationUserSingleEmailInvite(email, [], type, permissions, accessSecretsManager);
var exception = Assert.Throws<BadRequestException>(action);
@ -30,8 +30,7 @@ public class InviteOrganizationUserRequestTests
var invalidCollectionConfiguration = new CollectionAccessSelection { Manage = true, HidePasswords = true };
var action = () =>
OrganizationUserSingleEmailInvite.Create(validEmail, [invalidCollectionConfiguration], type, permissions, accessSecretsManager);
var action = () => new OrganizationUserSingleEmailInvite(validEmail, [invalidCollectionConfiguration], type, permissions, accessSecretsManager);
var exception = Assert.Throws<BadRequestException>(action);
@ -45,7 +44,7 @@ public class InviteOrganizationUserRequestTests
const string validEmail = "test@email.com";
var validCollectionConfiguration = new CollectionAccessSelection { Id = Guid.NewGuid(), Manage = true };
var invite = OrganizationUserSingleEmailInvite.Create(validEmail, [validCollectionConfiguration], type, permissions, accessSecretsManager);
var invite = new OrganizationUserSingleEmailInvite(validEmail, [validCollectionConfiguration], type, permissions, accessSecretsManager);
Assert.NotNull(invite);
Assert.Equal(validEmail, invite.Email);

View File

@ -1,23 +0,0 @@
using Bit.Core.AdminConsole.Models.Business;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
using Bit.Core.Enums;
using Bit.Core.Models.Data;
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Helpers;
public static class InviteScimOrganizationUserRequestHelpers
{
public static InviteScimOrganizationUserRequest GetInviteScimOrganizationUserRequestDefault(string email,
OrganizationDto organizationDto, DateTimeOffset performedAt, string externalId) =>
InviteScimOrganizationUserRequest.Create(
OrganizationUserSingleEmailInvite.Create(
email,
[],
OrganizationUserType.User,
new Permissions(),
false),
organizationDto,
performedAt,
externalId
);
}

View File

@ -12,8 +12,8 @@ public static class InviteUserOrganizationValidationRequestHelpers
{
Invites =
[
OrganizationUserInviteDto.Create(request.Invite.Email,
OrganizationUserInvite.Create(request.Invite, request.ExternalId), organizationDto.OrganizationId)
OrganizationUserInviteDto.Create(request.Email,
OrganizationUserInvite.Create(request, request.ExternalId), organizationDto.OrganizationId)
],
Organization = organizationDto,
PerformedBy = Guid.Empty,

View File

@ -18,7 +18,6 @@ using Bit.Test.Common.AutoFixture.Attributes;
using Microsoft.Extensions.Time.Testing;
using NSubstitute;
using Xunit;
using static Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Helpers.InviteScimOrganizationUserRequestHelpers;
using static Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Helpers.InviteUserOrganizationValidationRequestHelpers;
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;
@ -41,7 +40,8 @@ public class InviteOrganizationUserCommandTests
var organizationDto = OrganizationDto.FromOrganization(organization);
var request = GetInviteScimOrganizationUserRequestDefault(user.Email,
var request = new InviteScimOrganizationUserRequest(user.Email,
true,
organizationDto,
timeProvider.GetUtcNow(),
externalId);
@ -58,7 +58,7 @@ public class InviteOrganizationUserCommandTests
var result = await sutProvider.Sut.InviteScimOrganizationUserAsync(request);
// Assert
Assert.IsType<Success<OrganizationUser>>(result);
Assert.IsType<Success<ScimInviteOrganizationUsersResponse>>(result);
sutProvider.GetDependency<IPaymentService>()
.DidNotReceiveWithAnyArgs()
@ -88,7 +88,8 @@ public class InviteOrganizationUserCommandTests
var organizationDto = OrganizationDto.FromOrganization(organization);
var request = GetInviteScimOrganizationUserRequestDefault(user.Email,
var request = new InviteScimOrganizationUserRequest(user.Email,
true,
organizationDto,
timeProvider.GetUtcNow(),
externalId);
@ -109,12 +110,12 @@ public class InviteOrganizationUserCommandTests
var result = await sutProvider.Sut.InviteScimOrganizationUserAsync(request);
// Assert
Assert.IsType<Success<OrganizationUser>>(result);
Assert.IsType<Success<ScimInviteOrganizationUsersResponse>>(result);
sutProvider.GetDependency<IOrganizationUserRepository>()
.Received(1)
.CreateManyAsync(Arg.Is<IEnumerable<CreateOrganizationUser>>(users =>
users.Any(user => user.User.Email == request.Invite.Email)));
users.Any(user => user.OrganizationUser.Email == request.Email)));
sutProvider.GetDependency<ISendOrganizationInvitesCommand>()
.Received(1)
@ -140,7 +141,8 @@ public class InviteOrganizationUserCommandTests
var organizationDto = OrganizationDto.FromOrganization(organization);
var request = GetInviteScimOrganizationUserRequestDefault(user.Email,
var request = new InviteScimOrganizationUserRequest(user.Email,
true,
organizationDto,
timeProvider.GetUtcNow(),
externalId);
@ -194,7 +196,8 @@ public class InviteOrganizationUserCommandTests
var organizationDto = OrganizationDto.FromOrganization(organization);
var request = GetInviteScimOrganizationUserRequestDefault(user.Email,
var request = new InviteScimOrganizationUserRequest(user.Email,
true,
organizationDto,
timeProvider.GetUtcNow(),
externalId);
@ -221,7 +224,7 @@ public class InviteOrganizationUserCommandTests
var result = await sutProvider.Sut.InviteScimOrganizationUserAsync(request);
// Assert
Assert.IsType<Success<OrganizationUser>>(result);
Assert.IsType<Success<ScimInviteOrganizationUsersResponse>>(result);
sutProvider.GetDependency<IMailService>()
.Received(1)
@ -249,7 +252,8 @@ public class InviteOrganizationUserCommandTests
var organizationDto = OrganizationDto.FromOrganization(organization);
var request = GetInviteScimOrganizationUserRequestDefault(user.Email,
var request = new InviteScimOrganizationUserRequest(user.Email,
true,
organizationDto,
timeProvider.GetUtcNow(),
externalId);
@ -279,7 +283,7 @@ public class InviteOrganizationUserCommandTests
var result = await sutProvider.Sut.InviteScimOrganizationUserAsync(request);
// Assert
Assert.IsType<Success<OrganizationUser>>(result);
Assert.IsType<Success<ScimInviteOrganizationUsersResponse>>(result);
sutProvider.GetDependency<IPaymentService>()
.AdjustSeatsAsync(organization, organizationDto.Plan, passwordManagerUpdate.SeatsRequiredToAdd);
@ -311,7 +315,8 @@ public class InviteOrganizationUserCommandTests
var organizationDto = OrganizationDto.FromOrganization(organization);
var request = GetInviteScimOrganizationUserRequestDefault(user.Email,
var request = new InviteScimOrganizationUserRequest(user.Email,
true,
organizationDto,
timeProvider.GetUtcNow(),
externalId);
@ -345,7 +350,7 @@ public class InviteOrganizationUserCommandTests
var result = await sutProvider.Sut.InviteScimOrganizationUserAsync(request);
// Assert
Assert.IsType<Success<OrganizationUser>>(result);
Assert.IsType<Success<ScimInviteOrganizationUsersResponse>>(result);
sutProvider.GetDependency<IUpdateSecretsManagerSubscriptionCommand>()
.Received(1)

View File

@ -396,9 +396,9 @@ public class OrganizationUserRepositoryTests
var orgUserCollection = new List<CreateOrganizationUser>
{
new CreateOrganizationUser
new()
{
User = new OrganizationUser
OrganizationUser = new OrganizationUser
{
Id = CoreHelpers.GenerateComb(),
OrganizationId = organization.Id,
@ -425,6 +425,7 @@ public class OrganizationUserRepositoryTests
await organizationUserRepository.CreateManyAsync(orgUserCollection);
var orgUser = await organizationUserRepository.GetDetailsByIdAsync(orgUserCollection.First().User.Id);
var orgUser = await organizationUserRepository.GetDetailsByIdAsync(orgUserCollection.First().OrganizationUser.Id);
Assert.Equal(orgUserCollection.First().OrganizationUser.Id, orgUser.Id);
}
}