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:
parent
febc696c80
commit
0be40d1bd9
@ -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;
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
@ -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));
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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(
|
||||||
|
Loading…
Reference in New Issue
Block a user