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

[PM-3175] Invite Organization Users Command Refactor #5394

Draft
wants to merge 19 commits into
base: main
Choose a base branch
from
Draft
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
76 changes: 40 additions & 36 deletions bitwarden_license/src/Scim/Users/PostUserCommand.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
๏ปฟusing Bit.Core.Enums;
๏ปฟusing Bit.Core;
using Bit.Core.AdminConsole.Models.Business;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.Repositories;
Expand All @@ -9,31 +13,20 @@

namespace Bit.Scim.Users;

public class PostUserCommand : IPostUserCommand
public class PostUserCommand(
IOrganizationRepository organizationRepository,
IOrganizationUserRepository organizationUserRepository,
IOrganizationService organizationService,
IPaymentService paymentService,
IScimContext scimContext,
IFeatureService featureService,
IInviteOrganizationUsersCommand inviteOrganizationUsersCommand,
TimeProvider timeProvider)
: IPostUserCommand
{
private readonly IOrganizationRepository _organizationRepository;
private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly IOrganizationService _organizationService;
private readonly IPaymentService _paymentService;
private readonly IScimContext _scimContext;

public PostUserCommand(
IOrganizationRepository organizationRepository,
IOrganizationUserRepository organizationUserRepository,
IOrganizationService organizationService,
IPaymentService paymentService,
IScimContext scimContext)
{
_organizationRepository = organizationRepository;
_organizationUserRepository = organizationUserRepository;
_organizationService = organizationService;
_paymentService = paymentService;
_scimContext = scimContext;
}

public async Task<OrganizationUserUserDetails> PostUserAsync(Guid organizationId, ScimUserRequestModel model)
{
var scimProvider = _scimContext.RequestScimProvider;
var scimProvider = scimContext.RequestScimProvider;
var invite = model.ToOrganizationUserInvite(scimProvider);

var email = invite.Emails.Single();
Expand All @@ -44,27 +37,38 @@
throw new BadRequestException();
}

var orgUsers = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(organizationId);
var orgUserByEmail = orgUsers.FirstOrDefault(ou => ou.Email?.ToLowerInvariant() == email);
if (orgUserByEmail != null)
var existingUsers = await organizationUserRepository.GetManyDetailsByOrganizationAsync(organizationId);

if (existingUsers.Any(ou => ou.Email?.ToLowerInvariant() == email || ou.ExternalId == externalId))
{
throw new ConflictException();
}

var orgUserByExternalId = orgUsers.FirstOrDefault(ou => ou.ExternalId == externalId);
if (orgUserByExternalId != null)
var organization = await organizationRepository.GetByIdAsync(organizationId);

var hasStandaloneSecretsManager = await paymentService.HasSecretsManagerStandalone(organization);
invite.AccessSecretsManager = hasStandaloneSecretsManager;

if (featureService.IsEnabled(FeatureFlagKeys.ScimCreateUserRefactor))
{
throw new ConflictException();
var organizationUser = (await inviteOrganizationUsersCommand.InviteScimOrganizationUserAsync(
InviteScimOrganizationUserRequest.Create(
OrganizationUserSingleEmailInvite.Create(
email,
invite.Collections,
externalId, invite.Type ?? OrganizationUserType.User,
invite.Permissions,
invite.AccessSecretsManager),
OrganizationDto.FromOrganization(organization),
timeProvider.GetUtcNow()))).Data;

Check warning on line 63 in bitwarden_license/src/Scim/Users/PostUserCommand.cs

View check run for this annotation

Codecov / codecov/patch

bitwarden_license/src/Scim/Users/PostUserCommand.cs#L54-L63

Added lines #L54 - L63 were not covered by tests

return await organizationUserRepository.GetDetailsByIdAsync(organizationUser.Id);

Check warning on line 65 in bitwarden_license/src/Scim/Users/PostUserCommand.cs

View check run for this annotation

Codecov / codecov/patch

bitwarden_license/src/Scim/Users/PostUserCommand.cs#L65

Added line #L65 was not covered by tests
}

var organization = await _organizationRepository.GetByIdAsync(organizationId);
var hasStandaloneSecretsManager = await _paymentService.HasSecretsManagerStandalone(organization);
invite.AccessSecretsManager = hasStandaloneSecretsManager;
var orgUser = await organizationService.InviteUserAsync(organizationId, null, EventSystemUser.SCIM, invite,
externalId);

var invitedOrgUser = await _organizationService.InviteUserAsync(organizationId, invitingUserId: null, EventSystemUser.SCIM,
invite, externalId);
var orgUser = await _organizationUserRepository.GetDetailsByIdAsync(invitedOrgUser.Id);
return await organizationUserRepository.GetDetailsByIdAsync(orgUser.Id);

return orgUser;
}
}
34 changes: 33 additions & 1 deletion src/Api/AdminConsole/Public/Controllers/MembersController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@
using Bit.Api.AdminConsole.Public.Models.Request;
using Bit.Api.AdminConsole.Public.Models.Response;
using Bit.Api.Models.Public.Response;
using Bit.Core;
using Bit.Core.AdminConsole.Models.Business;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
using Bit.Core.Context;
Expand All @@ -29,6 +33,9 @@
private readonly IOrganizationRepository _organizationRepository;
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
private readonly IInviteOrganizationUsersCommand _inviteOrganizationUsersCommand;
private readonly IFeatureService _featureService;
private readonly TimeProvider _timeProvider;

public MembersController(
IOrganizationUserRepository organizationUserRepository,
Expand All @@ -42,7 +49,10 @@
IPaymentService paymentService,
IOrganizationRepository organizationRepository,
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
IRemoveOrganizationUserCommand removeOrganizationUserCommand)
IRemoveOrganizationUserCommand removeOrganizationUserCommand,
IInviteOrganizationUsersCommand inviteOrganizationUsersCommand,
IFeatureService featureService,
TimeProvider timeProvider)
{
_organizationUserRepository = organizationUserRepository;
_groupRepository = groupRepository;
Expand All @@ -56,6 +66,9 @@
_organizationRepository = organizationRepository;
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
_removeOrganizationUserCommand = removeOrganizationUserCommand;
_inviteOrganizationUsersCommand = inviteOrganizationUsersCommand;
_featureService = featureService;
_timeProvider = timeProvider;
}

/// <summary>
Expand Down Expand Up @@ -151,8 +164,27 @@

invite.AccessSecretsManager = hasStandaloneSecretsManager;

if (_featureService.IsEnabled(FeatureFlagKeys.ScimCreateUserRefactor))
{
var invitedUserResult = await _inviteOrganizationUsersCommand.InvitePublicApiOrganizationUserAsync(
InviteOrganizationUserRequest.Create(
model.ToOrganizationUserSingleEmailInvite(hasStandaloneSecretsManager),
OrganizationDto.FromOrganization(organization),
Guid.Empty,
_timeProvider.GetUtcNow()
));

Check warning on line 175 in src/Api/AdminConsole/Public/Controllers/MembersController.cs

View check run for this annotation

Codecov / codecov/patch

src/Api/AdminConsole/Public/Controllers/MembersController.cs#L168-L175

Added lines #L168 - L175 were not covered by tests

if (invitedUserResult is { Success: true })
{
return new JsonResult(new MemberResponseModel(invitedUserResult.Data, invite.Collections));

Check warning on line 179 in src/Api/AdminConsole/Public/Controllers/MembersController.cs

View check run for this annotation

Codecov / codecov/patch

src/Api/AdminConsole/Public/Controllers/MembersController.cs#L178-L179

Added lines #L178 - L179 were not covered by tests
}

return new EmptyResult(); // TODO figure out something better to put here

Check warning on line 182 in src/Api/AdminConsole/Public/Controllers/MembersController.cs

View check run for this annotation

Codecov / codecov/patch

src/Api/AdminConsole/Public/Controllers/MembersController.cs#L182

Added line #L182 was not covered by tests
}

var user = await _organizationService.InviteUserAsync(_currentContext.OrganizationId.Value, null,
systemUser: null, invite, model.ExternalId);

var response = new MemberResponseModel(user, invite.Collections);
return new JsonResult(response);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
๏ปฟusing System.ComponentModel.DataAnnotations;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Models.Business;
using Bit.Core.Models.Data;
using Bit.Core.Utilities;
using OrganizationUserInvite = Bit.Core.Models.Business.OrganizationUserInvite;

namespace Bit.Api.AdminConsole.Public.Models.Request;

Expand Down Expand Up @@ -40,4 +42,13 @@

return invite;
}

public OrganizationUserSingleEmailInvite ToOrganizationUserSingleEmailInvite(bool hasSecretsManager) =>
OrganizationUserSingleEmailInvite.Create(
Email,
Collections.Select(c => c.ToCollectionAccessSelection()).ToArray(),
string.Empty,
Type.Value,
Type is OrganizationUserType.Custom && Permissions is not null ? Permissions.ToData() : new Permissions(),
hasSecretsManager);

Check warning on line 53 in src/Api/AdminConsole/Public/Models/Request/MemberCreateRequestModel.cs

View check run for this annotation

Codecov / codecov/patch

src/Api/AdminConsole/Public/Models/Request/MemberCreateRequestModel.cs#L48-L53

Added lines #L48 - L53 were not covered by tests
}
57 changes: 57 additions & 0 deletions src/Core/AdminConsole/Models/Business/OrganizationDto.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
๏ปฟusing Bit.Core.AdminConsole.Entities;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Models.StaticStore;
using Bit.Core.Utilities;

namespace Bit.Core.AdminConsole.Models.Business;

public record OrganizationDto(
Guid OrganizationId,
bool UseCustomPermissions,
int? Seats,
int? MaxAutoScaleSeats,
int? SmSeats,
int? SmMaxAutoScaleSeats,

Check warning on line 15 in src/Core/AdminConsole/Models/Business/OrganizationDto.cs

View check run for this annotation

Codecov / codecov/patch

src/Core/AdminConsole/Models/Business/OrganizationDto.cs#L14-L15

Added lines #L14 - L15 were not covered by tests
Plan Plan,
string GatewayCustomerId,
string GatewaySubscriptionId,
bool UseSecretsManager

Check warning on line 19 in src/Core/AdminConsole/Models/Business/OrganizationDto.cs

View check run for this annotation

Codecov / codecov/patch

src/Core/AdminConsole/Models/Business/OrganizationDto.cs#L19

Added line #L19 was not covered by tests
) : ISubscriber
{
public Guid Id => OrganizationId;
public GatewayType? Gateway { get; set; }

Check warning on line 23 in src/Core/AdminConsole/Models/Business/OrganizationDto.cs

View check run for this annotation

Codecov / codecov/patch

src/Core/AdminConsole/Models/Business/OrganizationDto.cs#L22-L23

Added lines #L22 - L23 were not covered by tests
public string GatewayCustomerId { get; set; } = GatewayCustomerId;
public string GatewaySubscriptionId { get; set; } = GatewaySubscriptionId;
public string BillingEmailAddress() => throw new NotImplementedException();

Check warning on line 26 in src/Core/AdminConsole/Models/Business/OrganizationDto.cs

View check run for this annotation

Codecov / codecov/patch

src/Core/AdminConsole/Models/Business/OrganizationDto.cs#L26

Added line #L26 was not covered by tests

public string BillingName() => throw new NotImplementedException();

Check warning on line 28 in src/Core/AdminConsole/Models/Business/OrganizationDto.cs

View check run for this annotation

Codecov / codecov/patch

src/Core/AdminConsole/Models/Business/OrganizationDto.cs#L28

Added line #L28 was not covered by tests

public string SubscriberName() => throw new NotImplementedException();

Check warning on line 30 in src/Core/AdminConsole/Models/Business/OrganizationDto.cs

View check run for this annotation

Codecov / codecov/patch

src/Core/AdminConsole/Models/Business/OrganizationDto.cs#L30

Added line #L30 was not covered by tests

public string BraintreeCustomerIdPrefix() => throw new NotImplementedException();

Check warning on line 32 in src/Core/AdminConsole/Models/Business/OrganizationDto.cs

View check run for this annotation

Codecov / codecov/patch

src/Core/AdminConsole/Models/Business/OrganizationDto.cs#L32

Added line #L32 was not covered by tests

public string BraintreeIdField() => throw new NotImplementedException();

Check warning on line 34 in src/Core/AdminConsole/Models/Business/OrganizationDto.cs

View check run for this annotation

Codecov / codecov/patch

src/Core/AdminConsole/Models/Business/OrganizationDto.cs#L34

Added line #L34 was not covered by tests

public string BraintreeCloudRegionField() => throw new NotImplementedException();

Check warning on line 36 in src/Core/AdminConsole/Models/Business/OrganizationDto.cs

View check run for this annotation

Codecov / codecov/patch

src/Core/AdminConsole/Models/Business/OrganizationDto.cs#L36

Added line #L36 was not covered by tests

public bool IsOrganization() => throw new NotImplementedException();

Check warning on line 38 in src/Core/AdminConsole/Models/Business/OrganizationDto.cs

View check run for this annotation

Codecov / codecov/patch

src/Core/AdminConsole/Models/Business/OrganizationDto.cs#L38

Added line #L38 was not covered by tests

public bool IsUser() => throw new NotImplementedException();

Check warning on line 40 in src/Core/AdminConsole/Models/Business/OrganizationDto.cs

View check run for this annotation

Codecov / codecov/patch

src/Core/AdminConsole/Models/Business/OrganizationDto.cs#L40

Added line #L40 was not covered by tests

public string SubscriberType() => throw new NotImplementedException();

Check warning on line 42 in src/Core/AdminConsole/Models/Business/OrganizationDto.cs

View check run for this annotation

Codecov / codecov/patch

src/Core/AdminConsole/Models/Business/OrganizationDto.cs#L42

Added line #L42 was not covered by tests

public bool IsExpired() => throw new NotImplementedException();

Check warning on line 44 in src/Core/AdminConsole/Models/Business/OrganizationDto.cs

View check run for this annotation

Codecov / codecov/patch

src/Core/AdminConsole/Models/Business/OrganizationDto.cs#L44

Added line #L44 was not covered by tests

public static OrganizationDto FromOrganization(Organization organization) =>
new(organization.Id,
organization.UseCustomPermissions,
organization.Seats,
organization.MaxAutoscaleSeats,
organization.SmSeats,
organization.MaxAutoscaleSmSeats,
StaticStore.GetPlan(organization.PlanType),
organization.GatewayCustomerId,
organization.GatewaySubscriptionId,
organization.UseSecretsManager);
}
Loading
Loading