From a2b155e81c02665bfb2197c88989056d69c57e1d Mon Sep 17 00:00:00 2001 From: jrmccannon Date: Thu, 20 Mar 2025 09:06:34 -0500 Subject: [PATCH] Made HasSecretsManagerStandalone return if org doesn't have sm. Added overload for lighter weight model and moved common code to private method. --- .../src/Scim/Models/ScimUserRequestModel.cs | 2 -- .../src/Scim/Users/PostUserCommand.cs | 11 ++++----- .../InviteOrganizationUsersCommand.cs | 4 +++- .../Models/InviteOrganizationUsersRequest.cs | 4 ++-- .../Models/OrganizationUserInvite.cs | 4 ++-- .../OrganizationUserSingleEmailInvite.cs | 9 ++----- src/Core/Services/IPaymentService.cs | 24 +++++++++++++++++-- .../Implementations/StripePaymentService.cs | 20 +++++++++++++--- 8 files changed, 53 insertions(+), 25 deletions(-) diff --git a/bitwarden_license/src/Scim/Models/ScimUserRequestModel.cs b/bitwarden_license/src/Scim/Models/ScimUserRequestModel.cs index b82b4c299f..1ba66983b8 100644 --- a/bitwarden_license/src/Scim/Models/ScimUserRequestModel.cs +++ b/bitwarden_license/src/Scim/Models/ScimUserRequestModel.cs @@ -29,12 +29,10 @@ public class ScimUserRequestModel : BaseScimUserModel public OrganizationUserSingleEmailInvite ToRequest( ScimProviderType scimProvider, - bool hasSecretsManager, InviteOrganization inviteOrganization, DateTimeOffset performedAt) => new( email: EmailForInvite(scimProvider), - hasSecretsManager: hasSecretsManager, inviteOrganization: inviteOrganization, performedAt: performedAt, externalId: ExternalIdForInvite()); diff --git a/bitwarden_license/src/Scim/Users/PostUserCommand.cs b/bitwarden_license/src/Scim/Users/PostUserCommand.cs index ce26bf08be..b947797516 100644 --- a/bitwarden_license/src/Scim/Users/PostUserCommand.cs +++ b/bitwarden_license/src/Scim/Users/PostUserCommand.cs @@ -65,15 +65,15 @@ public class PostUserCommand( throw new NotFoundException(); } - var hasStandaloneSecretsManager = await paymentService.HasSecretsManagerStandalone(organization); - invite.AccessSecretsManager = hasStandaloneSecretsManager; if (featureService.IsEnabled(FeatureFlagKeys.ScimInviteUserOptimization)) { - return await InviteScimOrganizationUserAsync(model, organization, scimProvider, - hasStandaloneSecretsManager); + return await InviteScimOrganizationUserAsync(model, organization, scimProvider); } + var hasStandaloneSecretsManager = await paymentService.HasSecretsManagerStandalone(organization); + invite.AccessSecretsManager = hasStandaloneSecretsManager; + var invitedOrgUser = await organizationService.InviteUserAsync(organizationId, invitingUserId: null, EventSystemUser.SCIM, invite, externalId); @@ -83,7 +83,7 @@ public class PostUserCommand( } private async Task InviteScimOrganizationUserAsync(ScimUserRequestModel model, - Organization organization, ScimProviderType scimProvider, bool hasStandaloneSecretsManager) + Organization organization, ScimProviderType scimProvider) { var plan = await pricingClient.GetPlan(organization.PlanType); @@ -96,7 +96,6 @@ public class PostUserCommand( var request = model.ToRequest( scimProvider: scimProvider, - hasSecretsManager: hasStandaloneSecretsManager, inviteOrganization: new InviteOrganization(organization, plan), performedAt: timeProvider.GetUtcNow()); diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUsersCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUsersCommand.cs index 94392cdb34..b15face7a4 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUsersCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUsersCommand.cs @@ -41,7 +41,9 @@ public class InviteOrganizationUsersCommand(IEventService eventService, public async Task> InviteScimOrganizationUserAsync(OrganizationUserSingleEmailInvite request) { - var result = await InviteOrganizationUsersAsync(new InviteOrganizationUsersRequest(request)); + var hasSecretsManager = await paymentService.HasSecretsManagerStandalone(request.InviteOrganization); + + var result = await InviteOrganizationUsersAsync(new InviteOrganizationUsersRequest(request, hasSecretsManager)); switch (result) { diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Models/InviteOrganizationUsersRequest.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Models/InviteOrganizationUsersRequest.cs index 05819c25d1..bdca690c2e 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Models/InviteOrganizationUsersRequest.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Models/InviteOrganizationUsersRequest.cs @@ -20,8 +20,8 @@ public class InviteOrganizationUsersRequest PerformedAt = performedAt; } - public InviteOrganizationUsersRequest(OrganizationUserSingleEmailInvite request) : - this([OrganizationUserInvite.Create(request)], + public InviteOrganizationUsersRequest(OrganizationUserSingleEmailInvite request, bool hasStandaloneSecretsManager) : + this([OrganizationUserInvite.Create(request, hasStandaloneSecretsManager)], request.InviteOrganization, Guid.Empty, request.PerformedAt) diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Models/OrganizationUserInvite.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Models/OrganizationUserInvite.cs index 7ce791be59..550c0be635 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Models/OrganizationUserInvite.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Models/OrganizationUserInvite.cs @@ -42,13 +42,13 @@ public class OrganizationUserInvite }; } - public static OrganizationUserInvite Create(OrganizationUserSingleEmailInvite invite) => + public static OrganizationUserInvite Create(OrganizationUserSingleEmailInvite invite, bool hasStandaloneSecretsManager) => Create([invite.Email], invite.AccessibleCollections, invite.Type, invite.Permissions, invite.ExternalId, - invite.AccessSecretsManager); + hasStandaloneSecretsManager); private static void ValidateEmailAddresses(string[] emails) { diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Models/OrganizationUserSingleEmailInvite.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Models/OrganizationUserSingleEmailInvite.cs index f52f5353ce..c2ff0add59 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Models/OrganizationUserSingleEmailInvite.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Models/OrganizationUserSingleEmailInvite.cs @@ -14,21 +14,18 @@ public record OrganizationUserSingleEmailInvite public CollectionAccessSelection[] AccessibleCollections { get; init; } = []; public Permissions Permissions { get; init; } = new(); public OrganizationUserType Type { get; init; } = OrganizationUserType.User; - public bool AccessSecretsManager { get; init; } public InviteOrganization InviteOrganization { get; private init; } public DateTimeOffset PerformedAt { get; private init; } public string ExternalId { get; private init; } = string.Empty; public OrganizationUserSingleEmailInvite(string email, - bool hasSecretsManager, InviteOrganization inviteOrganization, DateTimeOffset performedAt, string externalId) : this( email: email, accessibleCollections: [], type: OrganizationUserType.User, - permissions: new Permissions(), - accessSecretsManager: hasSecretsManager) + permissions: new Permissions()) { InviteOrganization = inviteOrganization; PerformedAt = performedAt; @@ -38,8 +35,7 @@ public record OrganizationUserSingleEmailInvite public OrganizationUserSingleEmailInvite(string email, IEnumerable accessibleCollections, OrganizationUserType type, - Permissions permissions, - bool accessSecretsManager) + Permissions permissions) { if (!email.IsValidEmail()) { @@ -55,6 +51,5 @@ public record OrganizationUserSingleEmailInvite AccessibleCollections = accessibleCollections.ToArray(); Type = type; Permissions = permissions; - AccessSecretsManager = accessSecretsManager; } } diff --git a/src/Core/Services/IPaymentService.cs b/src/Core/Services/IPaymentService.cs index e3495c0e65..c42a186009 100644 --- a/src/Core/Services/IPaymentService.cs +++ b/src/Core/Services/IPaymentService.cs @@ -1,5 +1,6 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities.Provider; +using Bit.Core.AdminConsole.Models.Business; using Bit.Core.Billing.Models; using Bit.Core.Billing.Models.Api.Requests.Accounts; using Bit.Core.Billing.Models.Api.Requests.Organizations; @@ -44,9 +45,28 @@ public interface IPaymentService Task GetSubscriptionAsync(ISubscriber subscriber); Task GetTaxInfoAsync(ISubscriber subscriber); Task SaveTaxInfoAsync(ISubscriber subscriber, TaxInfo taxInfo); - Task AddSecretsManagerToSubscription(Organization org, Plan plan, int additionalSmSeats, - int additionalServiceAccount); + Task AddSecretsManagerToSubscription(Organization org, Plan plan, int additionalSmSeats, int additionalServiceAccount); + /// + /// Secrets Manager Standalone is a discount in Stripe that is used to give an organization access to Secrets Manager. + /// Usually, this also implies that when they invite a user to their organization, they are doing so for both Password + /// Manager and Secrets Manger. + /// + /// This will not call out to Stripe if they don't have a GatewayId or if they don't have Secrets Manager. + /// + /// Organization Entity + /// If the organization has Secrets Manager and has the Standalone Stripe Discount Task HasSecretsManagerStandalone(Organization organization); + + /// + /// Secrets Manager Standalone is a discount in Stripe that is used to give an organization access to Secrets Manager. + /// Usually, this also implies that when they invite a user to their organization, they are doing so for both Password + /// Manager and Secrets Manger. + /// + /// This will not call out to Stripe if they don't have a GatewayId or if they don't have Secrets Manager. + /// + /// Organization Representation used for Inviting Organization Users + /// If the organization has Secrets Manager and has the Standalone Stripe Discount + Task HasSecretsManagerStandalone(InviteOrganization organization); Task PreviewInvoiceAsync(PreviewIndividualInvoiceRequestBody parameters, string gatewayCustomerId, string gatewaySubscriptionId); Task PreviewInvoiceAsync(PreviewOrganizationInvoiceRequestBody parameters, string gatewayCustomerId, string gatewaySubscriptionId); diff --git a/src/Core/Services/Implementations/StripePaymentService.cs b/src/Core/Services/Implementations/StripePaymentService.cs index ca377407f4..c257655120 100644 --- a/src/Core/Services/Implementations/StripePaymentService.cs +++ b/src/Core/Services/Implementations/StripePaymentService.cs @@ -1,5 +1,6 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities.Provider; +using Bit.Core.AdminConsole.Models.Business; using Bit.Core.Billing.Constants; using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Models; @@ -1079,14 +1080,27 @@ public class StripePaymentService : IPaymentService new SecretsManagerSubscribeUpdate(org, plan, additionalSmSeats, additionalServiceAccount), true); - public async Task HasSecretsManagerStandalone(Organization organization) + public async Task HasSecretsManagerStandalone(Organization organization) => + await HasSecretsManagerAsync(gatewayCustomerId: organization.GatewayCustomerId, + organizationHasSecretsManager: organization.UseSecretsManager); + + public async Task HasSecretsManagerStandalone(InviteOrganization organization) => + await HasSecretsManagerAsync(gatewayCustomerId: organization.GatewayCustomerId, + organizationHasSecretsManager: organization.UseSecretsManager); + + private async Task HasSecretsManagerAsync(string gatewayCustomerId, bool organizationHasSecretsManager) { - if (string.IsNullOrEmpty(organization.GatewayCustomerId)) + if (string.IsNullOrEmpty(gatewayCustomerId)) { return false; } - var customer = await _stripeAdapter.CustomerGetAsync(organization.GatewayCustomerId); + if (organizationHasSecretsManager is false) + { + return false; + } + + var customer = await _stripeAdapter.CustomerGetAsync(gatewayCustomerId); return customer?.Discount?.Coupon?.Id == SecretsManagerStandaloneDiscountId; }