1
0
mirror of https://github.com/bitwarden/server.git synced 2024-11-22 12:15:36 +01:00

[AC-2489] Resolve SM Standalone issues with SCIM & Directory Connector (#4011)

* Add auto-scale support to standalone SM for SCIM

* Mark users for SM when using SM Stadalone with Directory Connector
This commit is contained in:
Alex Morask 2024-05-20 10:22:16 -04:00 committed by GitHub
parent febc696c80
commit 0be40d1bd9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 43 additions and 10 deletions

View File

@ -14,17 +14,23 @@ namespace Bit.Scim.Users;
public class PostUserCommand : IPostUserCommand public class PostUserCommand : IPostUserCommand
{ {
private readonly IOrganizationRepository _organizationRepository;
private readonly IOrganizationUserRepository _organizationUserRepository; private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly IOrganizationService _organizationService; private readonly IOrganizationService _organizationService;
private readonly IPaymentService _paymentService;
private readonly IScimContext _scimContext; private readonly IScimContext _scimContext;
public PostUserCommand( public PostUserCommand(
IOrganizationRepository organizationRepository,
IOrganizationUserRepository organizationUserRepository, IOrganizationUserRepository organizationUserRepository,
IOrganizationService organizationService, IOrganizationService organizationService,
IPaymentService paymentService,
IScimContext scimContext) IScimContext scimContext)
{ {
_organizationRepository = organizationRepository;
_organizationUserRepository = organizationUserRepository; _organizationUserRepository = organizationUserRepository;
_organizationService = organizationService; _organizationService = organizationService;
_paymentService = paymentService;
_scimContext = scimContext; _scimContext = scimContext;
} }
@ -80,8 +86,13 @@ public class PostUserCommand : IPostUserCommand
throw new ConflictException(); throw new ConflictException();
} }
var organization = await _organizationRepository.GetByIdAsync(organizationId);
var hasStandaloneSecretsManager = await _paymentService.HasSecretsManagerStandalone(organization);
var invitedOrgUser = await _organizationService.InviteUserAsync(organizationId, EventSystemUser.SCIM, email, var invitedOrgUser = await _organizationService.InviteUserAsync(organizationId, EventSystemUser.SCIM, email,
OrganizationUserType.User, false, externalId, new List<CollectionAccessSelection>(), new List<Guid>()); OrganizationUserType.User, false, externalId, new List<CollectionAccessSelection>(), new List<Guid>(), hasStandaloneSecretsManager);
var orgUser = await _organizationUserRepository.GetDetailsByIdAsync(invitedOrgUser.Id); var orgUser = await _organizationUserRepository.GetDetailsByIdAsync(invitedOrgUser.Id);
return orgUser; return orgUser;

View File

@ -1,4 +1,5 @@
using Bit.Core.Enums; using Bit.Core.AdminConsole.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.Models.Data; using Bit.Core.Models.Data;
using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Models.Data.Organizations.OrganizationUsers;
@ -19,7 +20,7 @@ public class PostUserCommandTests
{ {
[Theory] [Theory]
[BitAutoData] [BitAutoData]
public async Task PostUser_Success(SutProvider<PostUserCommand> sutProvider, string externalId, Guid organizationId, List<BaseScimUserModel.EmailModel> emails, ICollection<OrganizationUserUserDetails> organizationUsers, Core.Entities.OrganizationUser newUser) public async Task PostUser_Success(SutProvider<PostUserCommand> sutProvider, string externalId, Guid organizationId, List<BaseScimUserModel.EmailModel> emails, ICollection<OrganizationUserUserDetails> organizationUsers, Core.Entities.OrganizationUser newUser, Organization organization)
{ {
var scimUserRequestModel = new ScimUserRequestModel var scimUserRequestModel = new ScimUserRequestModel
{ {
@ -33,16 +34,20 @@ public class PostUserCommandTests
.GetManyDetailsByOrganizationAsync(organizationId) .GetManyDetailsByOrganizationAsync(organizationId)
.Returns(organizationUsers); .Returns(organizationUsers);
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organizationId).Returns(organization);
sutProvider.GetDependency<IPaymentService>().HasSecretsManagerStandalone(organization).Returns(true);
sutProvider.GetDependency<IOrganizationService>() sutProvider.GetDependency<IOrganizationService>()
.InviteUserAsync(organizationId, EventSystemUser.SCIM, scimUserRequestModel.PrimaryEmail.ToLowerInvariant(), .InviteUserAsync(organizationId, EventSystemUser.SCIM, scimUserRequestModel.PrimaryEmail.ToLowerInvariant(),
OrganizationUserType.User, false, externalId, Arg.Any<List<CollectionAccessSelection>>(), OrganizationUserType.User, false, externalId, Arg.Any<List<CollectionAccessSelection>>(),
Arg.Any<List<Guid>>()) Arg.Any<List<Guid>>(), true)
.Returns(newUser); .Returns(newUser);
var user = await sutProvider.Sut.PostUserAsync(organizationId, scimUserRequestModel); var user = await sutProvider.Sut.PostUserAsync(organizationId, scimUserRequestModel);
await sutProvider.GetDependency<IOrganizationService>().Received(1).InviteUserAsync(organizationId, EventSystemUser.SCIM, scimUserRequestModel.PrimaryEmail.ToLowerInvariant(), await sutProvider.GetDependency<IOrganizationService>().Received(1).InviteUserAsync(organizationId, EventSystemUser.SCIM, scimUserRequestModel.PrimaryEmail.ToLowerInvariant(),
OrganizationUserType.User, false, scimUserRequestModel.ExternalId, Arg.Any<List<CollectionAccessSelection>>(), Arg.Any<List<Guid>>()); OrganizationUserType.User, false, scimUserRequestModel.ExternalId, Arg.Any<List<CollectionAccessSelection>>(), Arg.Any<List<Guid>>(), true);
await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1).GetDetailsByIdAsync(newUser.Id); await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1).GetDetailsByIdAsync(newUser.Id);
} }

View File

@ -49,7 +49,7 @@ public interface IOrganizationService
Task<OrganizationUser> InviteUserAsync(Guid organizationId, Guid? invitingUserId, string email, Task<OrganizationUser> InviteUserAsync(Guid organizationId, Guid? invitingUserId, string email,
OrganizationUserType type, bool accessAll, string externalId, ICollection<CollectionAccessSelection> collections, IEnumerable<Guid> groups); OrganizationUserType type, bool accessAll, string externalId, ICollection<CollectionAccessSelection> collections, IEnumerable<Guid> groups);
Task<OrganizationUser> InviteUserAsync(Guid organizationId, EventSystemUser systemUser, string email, Task<OrganizationUser> InviteUserAsync(Guid organizationId, EventSystemUser systemUser, string email,
OrganizationUserType type, bool accessAll, string externalId, IEnumerable<CollectionAccessSelection> collections, IEnumerable<Guid> groups); OrganizationUserType type, bool accessAll, string externalId, IEnumerable<CollectionAccessSelection> collections, IEnumerable<Guid> groups, bool accessSecretsManager);
Task<IEnumerable<Tuple<OrganizationUser, string>>> ResendInvitesAsync(Guid organizationId, Guid? invitingUserId, IEnumerable<Guid> organizationUsersId); Task<IEnumerable<Tuple<OrganizationUser, string>>> ResendInvitesAsync(Guid organizationId, Guid? invitingUserId, IEnumerable<Guid> organizationUsersId);
Task ResendInviteAsync(Guid organizationId, Guid? invitingUserId, Guid organizationUserId, bool initOrganization = false); Task ResendInviteAsync(Guid organizationId, Guid? invitingUserId, Guid organizationUserId, bool initOrganization = false);
Task<OrganizationUser> ConfirmUserAsync(Guid organizationId, Guid organizationUserId, string key, Task<OrganizationUser> ConfirmUserAsync(Guid organizationId, Guid organizationUserId, string key,

View File

@ -1679,14 +1679,14 @@ public class OrganizationService : IOrganizationService
public async Task<OrganizationUser> InviteUserAsync(Guid organizationId, EventSystemUser systemUser, string email, public async Task<OrganizationUser> InviteUserAsync(Guid organizationId, EventSystemUser systemUser, string email,
OrganizationUserType type, bool accessAll, string externalId, IEnumerable<CollectionAccessSelection> collections, OrganizationUserType type, bool accessAll, string externalId, IEnumerable<CollectionAccessSelection> collections,
IEnumerable<Guid> groups) IEnumerable<Guid> groups, bool accessSecretsManager)
{ {
// Collection associations validation not required as they are always an empty list - created via system user (scim) // Collection associations validation not required as they are always an empty list - created via system user (scim)
return await SaveUserSendInviteAsync(organizationId, invitingUserId: null, systemUser, email, type, accessAll, externalId, collections, groups); return await SaveUserSendInviteAsync(organizationId, invitingUserId: null, systemUser, email, type, accessAll, externalId, collections, groups, accessSecretsManager);
} }
private async Task<OrganizationUser> SaveUserSendInviteAsync(Guid organizationId, Guid? invitingUserId, EventSystemUser? systemUser, string email, private async Task<OrganizationUser> SaveUserSendInviteAsync(Guid organizationId, Guid? invitingUserId, EventSystemUser? systemUser, string email,
OrganizationUserType type, bool accessAll, string externalId, IEnumerable<CollectionAccessSelection> collections, IEnumerable<Guid> groups) OrganizationUserType type, bool accessAll, string externalId, IEnumerable<CollectionAccessSelection> collections, IEnumerable<Guid> groups, bool accessSecretsManager = false)
{ {
var invite = new OrganizationUserInvite() var invite = new OrganizationUserInvite()
{ {
@ -1694,7 +1694,8 @@ public class OrganizationService : IOrganizationService
Type = type, Type = type,
AccessAll = accessAll, AccessAll = accessAll,
Collections = collections, Collections = collections,
Groups = groups Groups = groups,
AccessSecretsManager = accessSecretsManager
}; };
var results = systemUser.HasValue ? await InviteUsersAsync(organizationId, systemUser.Value, var results = systemUser.HasValue ? await InviteUsersAsync(organizationId, systemUser.Value,
new (OrganizationUserInvite, string)[] { (invite, externalId) }) : await InviteUsersAsync(organizationId, invitingUserId, new (OrganizationUserInvite, string)[] { (invite, externalId) }) : await InviteUsersAsync(organizationId, invitingUserId,
@ -1793,6 +1794,8 @@ public class OrganizationService : IOrganizationService
enoughSeatsAvailable = seatsAvailable >= usersToAdd.Count; enoughSeatsAvailable = seatsAvailable >= usersToAdd.Count;
} }
var hasStandaloneSecretsManager = await _paymentService.HasSecretsManagerStandalone(organization);
var userInvites = new List<(OrganizationUserInvite, string)>(); var userInvites = new List<(OrganizationUserInvite, string)>();
foreach (var user in newUsers) foreach (var user in newUsers)
{ {
@ -1809,6 +1812,7 @@ public class OrganizationService : IOrganizationService
Type = OrganizationUserType.User, Type = OrganizationUserType.User,
AccessAll = false, AccessAll = false,
Collections = new List<CollectionAccessSelection>(), Collections = new List<CollectionAccessSelection>(),
AccessSecretsManager = hasStandaloneSecretsManager
}; };
userInvites.Add((invite, user.ExternalId)); userInvites.Add((invite, user.ExternalId));
} }

View File

@ -57,4 +57,5 @@ public interface IPaymentService
Task<string> AddSecretsManagerToSubscription(Organization org, Plan plan, int additionalSmSeats, Task<string> AddSecretsManagerToSubscription(Organization org, Plan plan, int additionalSmSeats,
int additionalServiceAccount, DateTime? prorationDate = null); int additionalServiceAccount, DateTime? prorationDate = null);
Task<bool> RisksSubscriptionFailure(Organization organization); Task<bool> RisksSubscriptionFailure(Organization organization);
Task<bool> HasSecretsManagerStandalone(Organization organization);
} }

View File

@ -1800,6 +1800,18 @@ public class StripePaymentService : IPaymentService
return paymentSource == null; return paymentSource == null;
} }
public async Task<bool> HasSecretsManagerStandalone(Organization organization)
{
if (string.IsNullOrEmpty(organization.GatewayCustomerId))
{
return false;
}
var customer = await _stripeAdapter.CustomerGetAsync(organization.GatewayCustomerId);
return customer?.Discount?.Coupon?.Id == SecretsManagerStandaloneDiscountId;
}
private PaymentMethod GetLatestCardPaymentMethod(string customerId) private PaymentMethod GetLatestCardPaymentMethod(string customerId)
{ {
var cardPaymentMethods = _stripeAdapter.PaymentMethodListAutoPaging( var cardPaymentMethods = _stripeAdapter.PaymentMethodListAutoPaging(