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

Merge remote-tracking branch 'origin/main' into user-service-test-tweaks

This commit is contained in:
Thomas Rittson 2025-05-26 15:30:55 +10:00
commit dadc227ee3
No known key found for this signature in database
GPG Key ID: CDDDA03861C35E27
331 changed files with 17489 additions and 4730 deletions

3
.github/CODEOWNERS vendored
View File

@ -90,6 +90,9 @@ src/Admin/Views/Tools @bitwarden/team-billing-dev
.github/workflows/test-database.yml @bitwarden/team-platform-dev .github/workflows/test-database.yml @bitwarden/team-platform-dev
.github/workflows/test.yml @bitwarden/team-platform-dev .github/workflows/test.yml @bitwarden/team-platform-dev
**/*Platform* @bitwarden/team-platform-dev **/*Platform* @bitwarden/team-platform-dev
**/.dockerignore @bitwarden/team-platform-dev
**/Dockerfile @bitwarden/team-platform-dev
**/entrypoint.sh @bitwarden/team-platform-dev
# Multiple owners - DO NOT REMOVE (BRE) # Multiple owners - DO NOT REMOVE (BRE)
**/packages.lock.json **/packages.lock.json

View File

@ -636,7 +636,9 @@ jobs:
setup-ephemeral-environment: setup-ephemeral-environment:
name: Setup Ephemeral Environment name: Setup Ephemeral Environment
needs: build-docker needs:
- build-artifacts
- build-docker
if: | if: |
needs.build-artifacts.outputs.has_secrets == 'true' needs.build-artifacts.outputs.has_secrets == 'true'
&& github.event_name == 'pull_request' && github.event_name == 'pull_request'

View File

@ -2,7 +2,9 @@ name: Build on PR Target
on: on:
pull_request_target: pull_request_target:
types: [opened, synchronize] types: [opened, synchronize, reopened]
branches:
- "main"
defaults: defaults:
run: run:

View File

@ -7,8 +7,14 @@ on:
- "main" - "main"
- "rc" - "rc"
- "hotfix-rc" - "hotfix-rc"
pull_request:
types: [opened, synchronize, reopened]
branches-ignore:
- main
pull_request_target: pull_request_target:
types: [opened, synchronize] types: [opened, synchronize, reopened]
branches:
- "main"
jobs: jobs:
check-run: check-run:

View File

@ -3,7 +3,7 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<Version>2025.5.0</Version> <Version>2025.5.1</Version>
<RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace> <RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>

View File

@ -3,9 +3,9 @@ using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.Providers.Interfaces; using Bit.Core.AdminConsole.Providers.Interfaces;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Services; using Bit.Core.AdminConsole.Services;
using Bit.Core.Billing.Entities;
using Bit.Core.Billing.Enums; using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Repositories; using Bit.Core.Billing.Providers.Entities;
using Bit.Core.Billing.Providers.Repositories;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.Repositories; using Bit.Core.Repositories;

View File

@ -7,13 +7,12 @@ using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Constants; using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Extensions;
using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Providers.Services;
using Bit.Core.Billing.Services; using Bit.Core.Billing.Services;
using Bit.Core.Billing.Services.Implementations.AutomaticTax;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
using Microsoft.Extensions.DependencyInjection;
using Stripe; using Stripe;
namespace Bit.Commercial.Core.AdminConsole.Providers; namespace Bit.Commercial.Core.AdminConsole.Providers;
@ -23,7 +22,6 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
private readonly IEventService _eventService; private readonly IEventService _eventService;
private readonly IMailService _mailService; private readonly IMailService _mailService;
private readonly IOrganizationRepository _organizationRepository; private readonly IOrganizationRepository _organizationRepository;
private readonly IOrganizationService _organizationService;
private readonly IProviderOrganizationRepository _providerOrganizationRepository; private readonly IProviderOrganizationRepository _providerOrganizationRepository;
private readonly IStripeAdapter _stripeAdapter; private readonly IStripeAdapter _stripeAdapter;
private readonly IFeatureService _featureService; private readonly IFeatureService _featureService;
@ -31,26 +29,22 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
private readonly ISubscriberService _subscriberService; private readonly ISubscriberService _subscriberService;
private readonly IHasConfirmedOwnersExceptQuery _hasConfirmedOwnersExceptQuery; private readonly IHasConfirmedOwnersExceptQuery _hasConfirmedOwnersExceptQuery;
private readonly IPricingClient _pricingClient; private readonly IPricingClient _pricingClient;
private readonly IAutomaticTaxStrategy _automaticTaxStrategy;
public RemoveOrganizationFromProviderCommand( public RemoveOrganizationFromProviderCommand(
IEventService eventService, IEventService eventService,
IMailService mailService, IMailService mailService,
IOrganizationRepository organizationRepository, IOrganizationRepository organizationRepository,
IOrganizationService organizationService,
IProviderOrganizationRepository providerOrganizationRepository, IProviderOrganizationRepository providerOrganizationRepository,
IStripeAdapter stripeAdapter, IStripeAdapter stripeAdapter,
IFeatureService featureService, IFeatureService featureService,
IProviderBillingService providerBillingService, IProviderBillingService providerBillingService,
ISubscriberService subscriberService, ISubscriberService subscriberService,
IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery, IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery,
IPricingClient pricingClient, IPricingClient pricingClient)
[FromKeyedServices(AutomaticTaxFactory.BusinessUse)] IAutomaticTaxStrategy automaticTaxStrategy)
{ {
_eventService = eventService; _eventService = eventService;
_mailService = mailService; _mailService = mailService;
_organizationRepository = organizationRepository; _organizationRepository = organizationRepository;
_organizationService = organizationService;
_providerOrganizationRepository = providerOrganizationRepository; _providerOrganizationRepository = providerOrganizationRepository;
_stripeAdapter = stripeAdapter; _stripeAdapter = stripeAdapter;
_featureService = featureService; _featureService = featureService;
@ -58,7 +52,6 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
_subscriberService = subscriberService; _subscriberService = subscriberService;
_hasConfirmedOwnersExceptQuery = hasConfirmedOwnersExceptQuery; _hasConfirmedOwnersExceptQuery = hasConfirmedOwnersExceptQuery;
_pricingClient = pricingClient; _pricingClient = pricingClient;
_automaticTaxStrategy = automaticTaxStrategy;
} }
public async Task RemoveOrganizationFromProvider( public async Task RemoveOrganizationFromProvider(
@ -76,7 +69,7 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
if (!await _hasConfirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync( if (!await _hasConfirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync(
providerOrganization.OrganizationId, providerOrganization.OrganizationId,
Array.Empty<Guid>(), [],
includeProvider: false)) includeProvider: false))
{ {
throw new BadRequestException("Organization must have at least one confirmed owner."); throw new BadRequestException("Organization must have at least one confirmed owner.");
@ -101,7 +94,7 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
/// <summary> /// <summary>
/// When a client organization is unlinked from a provider, we have to check if they're Stripe-enabled /// When a client organization is unlinked from a provider, we have to check if they're Stripe-enabled
/// and, if they are, we remove their MSP discount and set their Subscription to `send_invoice`. This is because /// and, if they are, we remove their MSP discount and set their Subscription to `send_invoice`. This is because
/// the provider's payment method will be removed from their Stripe customer causing ensuing charges to fail. Lastly, /// the provider's payment method will be removed from their Stripe customer, causing ensuing charges to fail. Lastly,
/// we email the organization owners letting them know they need to add a new payment method. /// we email the organization owners letting them know they need to add a new payment method.
/// </summary> /// </summary>
private async Task ResetOrganizationBillingAsync( private async Task ResetOrganizationBillingAsync(
@ -141,15 +134,18 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
Items = [new SubscriptionItemOptions { Price = plan.PasswordManager.StripeSeatPlanId, Quantity = organization.Seats }] Items = [new SubscriptionItemOptions { Price = plan.PasswordManager.StripeSeatPlanId, Quantity = organization.Seats }]
}; };
if (_featureService.IsEnabled(FeatureFlagKeys.PM19147_AutomaticTaxImprovements)) var setNonUSBusinessUseToReverseCharge = _featureService.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge);
if (setNonUSBusinessUseToReverseCharge)
{ {
_automaticTaxStrategy.SetCreateOptions(subscriptionCreateOptions, customer); subscriptionCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true };
} }
else else if (customer.HasRecognizedTaxLocation())
{ {
subscriptionCreateOptions.AutomaticTax ??= new SubscriptionAutomaticTaxOptions subscriptionCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions
{ {
Enabled = true Enabled = customer.Address.Country == "US" ||
customer.TaxIds.Any()
}; };
} }
@ -186,7 +182,7 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
await _mailService.SendProviderUpdatePaymentMethod( await _mailService.SendProviderUpdatePaymentMethod(
organization.Id, organization.Id,
organization.Name, organization.Name,
provider.Name, provider.Name!,
organizationOwnerEmails); organizationOwnerEmails);
} }
} }

View File

@ -5,12 +5,13 @@ using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.Models.Business.Provider; using Bit.Core.AdminConsole.Models.Business.Provider;
using Bit.Core.AdminConsole.Models.Business.Tokenables; using Bit.Core.AdminConsole.Models.Business.Tokenables;
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Services; using Bit.Core.AdminConsole.Services;
using Bit.Core.Billing.Enums; using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Models; using Bit.Core.Billing.Models;
using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Services; using Bit.Core.Billing.Providers.Services;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
@ -53,6 +54,7 @@ public class ProviderService : IProviderService
private readonly IApplicationCacheService _applicationCacheService; private readonly IApplicationCacheService _applicationCacheService;
private readonly IProviderBillingService _providerBillingService; private readonly IProviderBillingService _providerBillingService;
private readonly IPricingClient _pricingClient; private readonly IPricingClient _pricingClient;
private readonly IProviderClientOrganizationSignUpCommand _providerClientOrganizationSignUpCommand;
public ProviderService(IProviderRepository providerRepository, IProviderUserRepository providerUserRepository, public ProviderService(IProviderRepository providerRepository, IProviderUserRepository providerUserRepository,
IProviderOrganizationRepository providerOrganizationRepository, IUserRepository userRepository, IProviderOrganizationRepository providerOrganizationRepository, IUserRepository userRepository,
@ -61,7 +63,8 @@ public class ProviderService : IProviderService
IOrganizationRepository organizationRepository, GlobalSettings globalSettings, IOrganizationRepository organizationRepository, GlobalSettings globalSettings,
ICurrentContext currentContext, IStripeAdapter stripeAdapter, IFeatureService featureService, ICurrentContext currentContext, IStripeAdapter stripeAdapter, IFeatureService featureService,
IDataProtectorTokenFactory<ProviderDeleteTokenable> providerDeleteTokenDataFactory, IDataProtectorTokenFactory<ProviderDeleteTokenable> providerDeleteTokenDataFactory,
IApplicationCacheService applicationCacheService, IProviderBillingService providerBillingService, IPricingClient pricingClient) IApplicationCacheService applicationCacheService, IProviderBillingService providerBillingService, IPricingClient pricingClient,
IProviderClientOrganizationSignUpCommand providerClientOrganizationSignUpCommand)
{ {
_providerRepository = providerRepository; _providerRepository = providerRepository;
_providerUserRepository = providerUserRepository; _providerUserRepository = providerUserRepository;
@ -81,6 +84,7 @@ public class ProviderService : IProviderService
_applicationCacheService = applicationCacheService; _applicationCacheService = applicationCacheService;
_providerBillingService = providerBillingService; _providerBillingService = providerBillingService;
_pricingClient = pricingClient; _pricingClient = pricingClient;
_providerClientOrganizationSignUpCommand = providerClientOrganizationSignUpCommand;
} }
public async Task<Provider> CompleteSetupAsync(Provider provider, Guid ownerUserId, string token, string key, TaxInfo taxInfo, TokenizedPaymentSource tokenizedPaymentSource = null) public async Task<Provider> CompleteSetupAsync(Provider provider, Guid ownerUserId, string token, string key, TaxInfo taxInfo, TokenizedPaymentSource tokenizedPaymentSource = null)
@ -560,12 +564,12 @@ public class ProviderService : IProviderService
ThrowOnInvalidPlanType(provider.Type, organizationSignup.Plan); ThrowOnInvalidPlanType(provider.Type, organizationSignup.Plan);
var (organization, _, defaultCollection) = await _organizationService.SignupClientAsync(organizationSignup); var signUpResponse = await _providerClientOrganizationSignUpCommand.SignUpClientOrganizationAsync(organizationSignup);
var providerOrganization = new ProviderOrganization var providerOrganization = new ProviderOrganization
{ {
ProviderId = providerId, ProviderId = providerId,
OrganizationId = organization.Id, OrganizationId = signUpResponse.Organization.Id,
Key = organizationSignup.OwnerKey, Key = organizationSignup.OwnerKey,
}; };
@ -574,12 +578,12 @@ public class ProviderService : IProviderService
// Give the owner Can Manage access over the default collection // Give the owner Can Manage access over the default collection
// The orgUser is not available when the org is created so we have to do it here as part of the invite // The orgUser is not available when the org is created so we have to do it here as part of the invite
var defaultOwnerAccess = defaultCollection != null var defaultOwnerAccess = signUpResponse.DefaultCollection != null
? ?
[ [
new CollectionAccessSelection new CollectionAccessSelection
{ {
Id = defaultCollection.Id, Id = signUpResponse.DefaultCollection.Id,
HidePasswords = false, HidePasswords = false,
ReadOnly = false, ReadOnly = false,
Manage = true Manage = true
@ -587,7 +591,7 @@ public class ProviderService : IProviderService
] ]
: Array.Empty<CollectionAccessSelection>(); : Array.Empty<CollectionAccessSelection>();
await _organizationService.InviteUsersAsync(organization.Id, user.Id, systemUser: null, await _organizationService.InviteUsersAsync(signUpResponse.Organization.Id, user.Id, systemUser: null,
new (OrganizationUserInvite, string)[] new (OrganizationUserInvite, string)[]
{ {
( (

View File

@ -1,8 +1,8 @@
using System.Globalization; using System.Globalization;
using Bit.Core.Billing.Entities; using Bit.Core.Billing.Providers.Entities;
using CsvHelper.Configuration.Attributes; using CsvHelper.Configuration.Attributes;
namespace Bit.Commercial.Core.Billing.Models; namespace Bit.Commercial.Core.Billing.Providers.Models;
public class ProviderClientInvoiceReportRow public class ProviderClientInvoiceReportRow
{ {

View File

@ -7,11 +7,12 @@ using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing; using Bit.Core.Billing;
using Bit.Core.Billing.Constants; using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Entities;
using Bit.Core.Billing.Enums; using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Extensions;
using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Repositories; using Bit.Core.Billing.Providers.Entities;
using Bit.Core.Billing.Providers.Repositories;
using Bit.Core.Billing.Providers.Services;
using Bit.Core.Billing.Services; using Bit.Core.Billing.Services;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
@ -24,7 +25,7 @@ using Microsoft.Extensions.Logging;
using OneOf; using OneOf;
using Stripe; using Stripe;
namespace Bit.Commercial.Core.Billing; namespace Bit.Commercial.Core.Billing.Providers.Services;
[RequireFeature(FeatureFlagKeys.PM18770_EnableOrganizationBusinessUnitConversion)] [RequireFeature(FeatureFlagKeys.PM18770_EnableOrganizationBusinessUnitConversion)]
public class BusinessUnitConverter( public class BusinessUnitConverter(
@ -67,6 +68,7 @@ public class BusinessUnitConverter(
organization.MaxStorageGb = updatedPlan.PasswordManager.BaseStorageGb; organization.MaxStorageGb = updatedPlan.PasswordManager.BaseStorageGb;
organization.UsePolicies = updatedPlan.HasPolicies; organization.UsePolicies = updatedPlan.HasPolicies;
organization.UseSso = updatedPlan.HasSso; organization.UseSso = updatedPlan.HasSso;
organization.UseOrganizationDomains = updatedPlan.HasOrganizationDomains;
organization.UseGroups = updatedPlan.HasGroups; organization.UseGroups = updatedPlan.HasGroups;
organization.UseEvents = updatedPlan.HasEvents; organization.UseEvents = updatedPlan.HasEvents;
organization.UseDirectory = updatedPlan.HasDirectory; organization.UseDirectory = updatedPlan.HasDirectory;

View File

@ -1,5 +1,5 @@
using System.Globalization; using System.Globalization;
using Bit.Commercial.Core.Billing.Models; using Bit.Commercial.Core.Billing.Providers.Models;
using Bit.Core; using Bit.Core;
using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Entities.Provider;
@ -8,15 +8,17 @@ using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing; using Bit.Core.Billing;
using Bit.Core.Billing.Caches; using Bit.Core.Billing.Caches;
using Bit.Core.Billing.Constants; using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Entities;
using Bit.Core.Billing.Enums; using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Extensions;
using Bit.Core.Billing.Models; using Bit.Core.Billing.Models;
using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Repositories; using Bit.Core.Billing.Providers.Entities;
using Bit.Core.Billing.Providers.Models;
using Bit.Core.Billing.Providers.Repositories;
using Bit.Core.Billing.Providers.Services;
using Bit.Core.Billing.Services; using Bit.Core.Billing.Services;
using Bit.Core.Billing.Services.Contracts; using Bit.Core.Billing.Tax.Models;
using Bit.Core.Billing.Services.Implementations.AutomaticTax; using Bit.Core.Billing.Tax.Services;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.Models.Business; using Bit.Core.Models.Business;
@ -25,15 +27,13 @@ using Bit.Core.Services;
using Bit.Core.Settings; using Bit.Core.Settings;
using Braintree; using Braintree;
using CsvHelper; using CsvHelper;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Stripe; using Stripe;
using static Bit.Core.Billing.Utilities; using static Bit.Core.Billing.Utilities;
using Customer = Stripe.Customer; using Customer = Stripe.Customer;
using Subscription = Stripe.Subscription; using Subscription = Stripe.Subscription;
namespace Bit.Commercial.Core.Billing; namespace Bit.Commercial.Core.Billing.Providers.Services;
public class ProviderBillingService( public class ProviderBillingService(
IBraintreeGateway braintreeGateway, IBraintreeGateway braintreeGateway,
@ -50,8 +50,7 @@ public class ProviderBillingService(
ISetupIntentCache setupIntentCache, ISetupIntentCache setupIntentCache,
IStripeAdapter stripeAdapter, IStripeAdapter stripeAdapter,
ISubscriberService subscriberService, ISubscriberService subscriberService,
ITaxService taxService, ITaxService taxService)
[FromKeyedServices(AutomaticTaxFactory.BusinessUse)] IAutomaticTaxStrategy automaticTaxStrategy)
: IProviderBillingService : IProviderBillingService
{ {
public async Task AddExistingOrganization( public async Task AddExistingOrganization(
@ -97,6 +96,7 @@ public class ProviderBillingService(
organization.MaxStorageGb = plan.PasswordManager.BaseStorageGb; organization.MaxStorageGb = plan.PasswordManager.BaseStorageGb;
organization.UsePolicies = plan.HasPolicies; organization.UsePolicies = plan.HasPolicies;
organization.UseSso = plan.HasSso; organization.UseSso = plan.HasSso;
organization.UseOrganizationDomains = plan.HasOrganizationDomains;
organization.UseGroups = plan.HasGroups; organization.UseGroups = plan.HasGroups;
organization.UseEvents = plan.HasEvents; organization.UseEvents = plan.HasEvents;
organization.UseDirectory = plan.HasDirectory; organization.UseDirectory = plan.HasDirectory;
@ -125,7 +125,7 @@ public class ProviderBillingService(
/* /*
* We have to scale the provider's seats before the ProviderOrganization * We have to scale the provider's seats before the ProviderOrganization
* row is inserted so the added organization's seats don't get double counted. * row is inserted so the added organization's seats don't get double-counted.
*/ */
await ScaleSeats(provider, organization.PlanType, organization.Seats!.Value); await ScaleSeats(provider, organization.PlanType, organization.Seats!.Value);
@ -233,7 +233,7 @@ public class ProviderBillingService(
var providerCustomer = await subscriberService.GetCustomerOrThrow(provider, new CustomerGetOptions var providerCustomer = await subscriberService.GetCustomerOrThrow(provider, new CustomerGetOptions
{ {
Expand = ["tax_ids"] Expand = ["tax", "tax_ids"]
}); });
var providerTaxId = providerCustomer.TaxIds.FirstOrDefault(); var providerTaxId = providerCustomer.TaxIds.FirstOrDefault();
@ -281,6 +281,13 @@ public class ProviderBillingService(
] ]
}; };
var setNonUSBusinessUseToReverseCharge = featureService.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge);
if (setNonUSBusinessUseToReverseCharge && providerCustomer.Address is not { Country: "US" })
{
customerCreateOptions.TaxExempt = StripeConstants.TaxExempt.Reverse;
}
var customer = await stripeAdapter.CustomerCreateAsync(customerCreateOptions); var customer = await stripeAdapter.CustomerCreateAsync(customerCreateOptions);
organization.GatewayCustomerId = customer.Id; organization.GatewayCustomerId = customer.Id;
@ -517,6 +524,13 @@ public class ProviderBillingService(
} }
}; };
var setNonUSBusinessUseToReverseCharge = featureService.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge);
if (setNonUSBusinessUseToReverseCharge && taxInfo.BillingAddressCountry != "US")
{
options.TaxExempt = StripeConstants.TaxExempt.Reverse;
}
if (!string.IsNullOrEmpty(taxInfo.TaxIdNumber)) if (!string.IsNullOrEmpty(taxInfo.TaxIdNumber))
{ {
var taxIdType = taxService.GetStripeTaxCode( var taxIdType = taxService.GetStripeTaxCode(
@ -528,6 +542,7 @@ public class ProviderBillingService(
logger.LogWarning("Could not infer tax ID type in country '{Country}' with tax ID '{TaxID}'.", logger.LogWarning("Could not infer tax ID type in country '{Country}' with tax ID '{TaxID}'.",
taxInfo.BillingAddressCountry, taxInfo.BillingAddressCountry,
taxInfo.TaxIdNumber); taxInfo.TaxIdNumber);
throw new BadRequestException("billingTaxIdTypeInferenceError"); throw new BadRequestException("billingTaxIdTypeInferenceError");
} }
@ -692,6 +707,13 @@ public class ProviderBillingService(
customer.Metadata.ContainsKey(BraintreeCustomerIdKey) || customer.Metadata.ContainsKey(BraintreeCustomerIdKey) ||
setupIntent.IsUnverifiedBankAccount()); setupIntent.IsUnverifiedBankAccount());
int? trialPeriodDays = provider.Type switch
{
ProviderType.Msp when usePaymentMethod => 14,
ProviderType.BusinessUnit when usePaymentMethod => 4,
_ => null
};
var subscriptionCreateOptions = new SubscriptionCreateOptions var subscriptionCreateOptions = new SubscriptionCreateOptions
{ {
CollectionMethod = usePaymentMethod ? CollectionMethod = usePaymentMethod ?
@ -705,17 +727,24 @@ public class ProviderBillingService(
}, },
OffSession = true, OffSession = true,
ProrationBehavior = StripeConstants.ProrationBehavior.CreateProrations, ProrationBehavior = StripeConstants.ProrationBehavior.CreateProrations,
TrialPeriodDays = usePaymentMethod ? 14 : null TrialPeriodDays = trialPeriodDays
}; };
if (featureService.IsEnabled(FeatureFlagKeys.PM19147_AutomaticTaxImprovements)) var setNonUSBusinessUseToReverseCharge =
{ featureService.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge);
automaticTaxStrategy.SetCreateOptions(subscriptionCreateOptions, customer);
} if (setNonUSBusinessUseToReverseCharge)
else
{ {
subscriptionCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }; subscriptionCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true };
} }
else if (customer.HasRecognizedTaxLocation())
{
subscriptionCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions
{
Enabled = customer.Address.Country == "US" ||
customer.TaxIds.Any()
};
}
try try
{ {

View File

@ -6,7 +6,7 @@ using Bit.Core.Billing;
using Bit.Core.Billing.Enums; using Bit.Core.Billing.Enums;
using Stripe; using Stripe;
namespace Bit.Commercial.Core.Billing; namespace Bit.Commercial.Core.Billing.Providers.Services;
public static class ProviderPriceAdapter public static class ProviderPriceAdapter
{ {

View File

@ -1,13 +1,9 @@
using Bit.Core.AdminConsole.Entities; using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Licenses;
using Bit.Core.Billing.Licenses.Extensions;
using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Pricing;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.SecretsManager.Queries.Projects.Interfaces; using Bit.Core.SecretsManager.Queries.Projects.Interfaces;
using Bit.Core.SecretsManager.Repositories; using Bit.Core.SecretsManager.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings; using Bit.Core.Settings;
namespace Bit.Commercial.Core.SecretsManager.Queries.Projects; namespace Bit.Commercial.Core.SecretsManager.Queries.Projects;
@ -17,72 +13,42 @@ public class MaxProjectsQuery : IMaxProjectsQuery
private readonly IOrganizationRepository _organizationRepository; private readonly IOrganizationRepository _organizationRepository;
private readonly IProjectRepository _projectRepository; private readonly IProjectRepository _projectRepository;
private readonly IGlobalSettings _globalSettings; private readonly IGlobalSettings _globalSettings;
private readonly ILicensingService _licensingService;
private readonly IPricingClient _pricingClient; private readonly IPricingClient _pricingClient;
public MaxProjectsQuery( public MaxProjectsQuery(
IOrganizationRepository organizationRepository, IOrganizationRepository organizationRepository,
IProjectRepository projectRepository, IProjectRepository projectRepository,
IGlobalSettings globalSettings, IGlobalSettings globalSettings,
ILicensingService licensingService,
IPricingClient pricingClient) IPricingClient pricingClient)
{ {
_organizationRepository = organizationRepository; _organizationRepository = organizationRepository;
_projectRepository = projectRepository; _projectRepository = projectRepository;
_globalSettings = globalSettings; _globalSettings = globalSettings;
_licensingService = licensingService;
_pricingClient = pricingClient; _pricingClient = pricingClient;
} }
public async Task<(short? max, bool? overMax)> GetByOrgIdAsync(Guid organizationId, int projectsToAdd) public async Task<(short? max, bool? overMax)> GetByOrgIdAsync(Guid organizationId, int projectsToAdd)
{ {
// "MaxProjects" only applies to free 2-person organizations, which can't be self-hosted.
if (_globalSettings.SelfHosted)
{
return (null, null);
}
var org = await _organizationRepository.GetByIdAsync(organizationId); var org = await _organizationRepository.GetByIdAsync(organizationId);
if (org == null) if (org == null)
{ {
throw new NotFoundException(); throw new NotFoundException();
} }
var (planType, maxProjects) = await GetPlanTypeAndMaxProjectsAsync(org); var plan = await _pricingClient.GetPlan(org.PlanType);
if (planType != PlanType.Free) if (plan is not { SecretsManager: not null, Type: PlanType.Free })
{ {
return (null, null); return (null, null);
} }
var projects = await _projectRepository.GetProjectCountByOrganizationIdAsync(organizationId); var projects = await _projectRepository.GetProjectCountByOrganizationIdAsync(organizationId);
return ((short? max, bool? overMax))(projects + projectsToAdd > maxProjects ? (maxProjects, true) : (maxProjects, false)); return ((short? max, bool? overMax))(projects + projectsToAdd > plan.SecretsManager.MaxProjects ? (plan.SecretsManager.MaxProjects, true) : (plan.SecretsManager.MaxProjects, false));
}
private async Task<(PlanType planType, int maxProjects)> GetPlanTypeAndMaxProjectsAsync(Organization organization)
{
if (_globalSettings.SelfHosted)
{
var license = await _licensingService.ReadOrganizationLicenseAsync(organization);
if (license == null)
{
throw new BadRequestException("License not found.");
}
var claimsPrincipal = _licensingService.GetClaimsPrincipalFromLicense(license);
var maxProjects = claimsPrincipal.GetValue<int?>(OrganizationLicenseConstants.SmMaxProjects);
if (!maxProjects.HasValue)
{
throw new BadRequestException("License does not contain a value for max Secrets Manager projects");
}
var planType = claimsPrincipal.GetValue<PlanType>(OrganizationLicenseConstants.PlanType);
return (planType, maxProjects.Value);
}
var plan = await _pricingClient.GetPlan(organization.PlanType);
if (plan is { SupportsSecretsManager: true })
{
return (plan.Type, plan.SecretsManager.MaxProjects);
}
throw new BadRequestException("Existing plan not found.");
} }
} }

View File

@ -1,9 +1,9 @@
using Bit.Commercial.Core.AdminConsole.Providers; using Bit.Commercial.Core.AdminConsole.Providers;
using Bit.Commercial.Core.AdminConsole.Services; using Bit.Commercial.Core.AdminConsole.Services;
using Bit.Commercial.Core.Billing; using Bit.Commercial.Core.Billing.Providers.Services;
using Bit.Core.AdminConsole.Providers.Interfaces; using Bit.Core.AdminConsole.Providers.Interfaces;
using Bit.Core.AdminConsole.Services; using Bit.Core.AdminConsole.Services;
using Bit.Core.Billing.Services; using Bit.Core.Billing.Providers.Services;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
namespace Bit.Commercial.Core.Utilities; namespace Bit.Commercial.Core.Utilities;

View File

@ -9,17 +9,17 @@
"version": "0.0.0", "version": "0.0.0",
"license": "-", "license": "-",
"dependencies": { "dependencies": {
"bootstrap": "5.3.3", "bootstrap": "5.3.6",
"font-awesome": "4.7.0", "font-awesome": "4.7.0",
"jquery": "3.7.1" "jquery": "3.7.1"
}, },
"devDependencies": { "devDependencies": {
"css-loader": "7.1.2", "css-loader": "7.1.2",
"expose-loader": "5.0.0", "expose-loader": "5.0.1",
"mini-css-extract-plugin": "2.9.2", "mini-css-extract-plugin": "2.9.2",
"sass": "1.85.0", "sass": "1.88.0",
"sass-loader": "16.0.4", "sass-loader": "16.0.5",
"webpack": "5.97.1", "webpack": "5.99.8",
"webpack-cli": "5.1.4" "webpack-cli": "5.1.4"
} }
}, },
@ -455,13 +455,13 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "22.13.14", "version": "22.15.21",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.14.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.21.tgz",
"integrity": "sha512-Zs/Ollc1SJ8nKUAgc7ivOEdIBM8JAKgrqqUYi2J997JuKO7/tpQC+WCetQ1sypiKCQWHdvdg9wBNpUPEWZae7w==", "integrity": "sha512-EV/37Td6c+MgKAbkcLG6vqZ2zEYHD7bvSrzqqs2RIhbA6w3x+Dqz8MZM3sP6kGTeLrdoOgKZe+Xja7tUB2DNkQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"undici-types": "~6.20.0" "undici-types": "~6.21.0"
} }
}, },
"node_modules/@webassemblyjs/ast": { "node_modules/@webassemblyjs/ast": {
@ -748,9 +748,9 @@
} }
}, },
"node_modules/bootstrap": { "node_modules/bootstrap": {
"version": "5.3.3", "version": "5.3.6",
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.3.tgz", "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.6.tgz",
"integrity": "sha512-8HLCdWgyoMguSO9o+aH+iuZ+aht+mzW0u3HIMzVu7Srrpv7EBBxTnrFlSCskwdY1+EOFQSm7uMJhNQHkdPcmjg==", "integrity": "sha512-jX0GAcRzvdwISuvArXn3m7KZscWWFAf1MKBcnzaN02qWMb3jpMoUX4/qgeiGzqyIb4ojulRzs89UCUmGcFSzTA==",
"funding": [ "funding": [
{ {
"type": "github", "type": "github",
@ -781,9 +781,9 @@
} }
}, },
"node_modules/browserslist": { "node_modules/browserslist": {
"version": "4.24.4", "version": "4.24.5",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.5.tgz",
"integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", "integrity": "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@ -801,10 +801,10 @@
], ],
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"caniuse-lite": "^1.0.30001688", "caniuse-lite": "^1.0.30001716",
"electron-to-chromium": "^1.5.73", "electron-to-chromium": "^1.5.149",
"node-releases": "^2.0.19", "node-releases": "^2.0.19",
"update-browserslist-db": "^1.1.1" "update-browserslist-db": "^1.1.3"
}, },
"bin": { "bin": {
"browserslist": "cli.js" "browserslist": "cli.js"
@ -821,9 +821,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/caniuse-lite": { "node_modules/caniuse-lite": {
"version": "1.0.30001707", "version": "1.0.30001718",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001707.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001718.tgz",
"integrity": "sha512-3qtRjw/HQSMlDWf+X79N206fepf4SOOU6SQLMaq/0KkZLmSjPxAkBOQQ+FxbHKfHmYLZFfdWsO3KA90ceHPSnw==", "integrity": "sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@ -975,9 +975,9 @@
} }
}, },
"node_modules/electron-to-chromium": { "node_modules/electron-to-chromium": {
"version": "1.5.128", "version": "1.5.155",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.128.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.155.tgz",
"integrity": "sha512-bo1A4HH/NS522Ws0QNFIzyPcyUUNV/yyy70Ho1xqfGYzPUme2F/xr4tlEOuM6/A538U1vDA7a4XfCd1CKRegKQ==", "integrity": "sha512-ps5KcGGmwL8VaeJlvlDlu4fORQpv3+GIcF5I3f9tUKUlJ/wsysh6HU8P5L1XWRYeXfA0oJd4PyM8ds8zTFf6Ng==",
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
@ -1009,9 +1009,9 @@
} }
}, },
"node_modules/es-module-lexer": { "node_modules/es-module-lexer": {
"version": "1.6.0", "version": "1.7.0",
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.6.0.tgz", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
"integrity": "sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==", "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
@ -1083,9 +1083,9 @@
} }
}, },
"node_modules/expose-loader": { "node_modules/expose-loader": {
"version": "5.0.0", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/expose-loader/-/expose-loader-5.0.0.tgz", "resolved": "https://registry.npmjs.org/expose-loader/-/expose-loader-5.0.1.tgz",
"integrity": "sha512-BtUqYRmvx1bEY5HN6eK2I9URUZgNmN0x5UANuocaNjXSgfoDlkXt+wyEMe7i5DzDNh2BKJHPc5F4rBwEdSQX6w==", "integrity": "sha512-5YPZuszN/eWND/B+xuq5nIpb/l5TV1HYmdO6SubYtHv+HenVw9/6bn33Mm5reY8DNid7AVtbARvyUD34edfCtg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@ -1106,13 +1106,6 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/fast-json-stable-stringify": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
"dev": true,
"license": "MIT"
},
"node_modules/fast-uri": { "node_modules/fast-uri": {
"version": "3.0.6", "version": "3.0.6",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz",
@ -1248,9 +1241,9 @@
} }
}, },
"node_modules/immutable": { "node_modules/immutable": {
"version": "5.1.1", "version": "5.1.2",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.1.tgz", "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.2.tgz",
"integrity": "sha512-3jatXi9ObIsPGr3N5hGw/vWWcTkq6hUYhpQz4k0wLC+owqWi/LiugIw9x0EdNZ2yGedKN/HzePiBvaJRXa0Ujg==", "integrity": "sha512-qHKXW1q6liAk1Oys6umoaZbDRqjcjgSrbnrifHsfsttza7zcvRAsL7mMV6xWcyhwQy7Xj5v4hhbr6b+iDYwlmQ==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
@ -1754,16 +1747,6 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/randombytes": { "node_modules/randombytes": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
@ -1877,9 +1860,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/sass": { "node_modules/sass": {
"version": "1.85.0", "version": "1.88.0",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.85.0.tgz", "resolved": "https://registry.npmjs.org/sass/-/sass-1.88.0.tgz",
"integrity": "sha512-3ToiC1xZ1Y8aU7+CkgCI/tqyuPXEmYGJXO7H4uqp0xkLXUqp88rQQ4j1HmP37xSJLbCJPaIiv+cT1y+grssrww==", "integrity": "sha512-sF6TWQqjFvr4JILXzG4ucGOLELkESHL+I5QJhh7CNaE+Yge0SI+ehCatsXhJ7ymU1hAFcIS3/PBpjdIbXoyVbg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -1898,9 +1881,9 @@
} }
}, },
"node_modules/sass-loader": { "node_modules/sass-loader": {
"version": "16.0.4", "version": "16.0.5",
"resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.4.tgz", "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.5.tgz",
"integrity": "sha512-LavLbgbBGUt3wCiYzhuLLu65+fWXaXLmq7YxivLhEqmiupCFZ5sKUAipK3do6V80YSU0jvSxNhEdT13IXNr3rg==", "integrity": "sha512-oL+CMBXrj6BZ/zOq4os+UECPL+bWqt6OAC6DWS8Ln8GZRcMDjlJ4JC3FBDuHJdYaFWIdKNIBYmtZtK2MaMkNIw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -1939,9 +1922,9 @@
} }
}, },
"node_modules/schema-utils": { "node_modules/schema-utils": {
"version": "4.3.0", "version": "4.3.2",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.0.tgz", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz",
"integrity": "sha512-Gf9qqc58SpCA/xdziiHz35F4GNIWYWZrEshUc/G/r5BnLph6xpKuLeoJoQuj5WfBIx/eQLf+hmVPYHaxJu7V2g==", "integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -1959,9 +1942,9 @@
} }
}, },
"node_modules/semver": { "node_modules/semver": {
"version": "7.7.1", "version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
"integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
"dev": true, "dev": true,
"license": "ISC", "license": "ISC",
"bin": { "bin": {
@ -2078,9 +2061,9 @@
} }
}, },
"node_modules/tapable": { "node_modules/tapable": {
"version": "2.2.1", "version": "2.2.2",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz",
"integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", "integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@ -2088,14 +2071,14 @@
} }
}, },
"node_modules/terser": { "node_modules/terser": {
"version": "5.39.0", "version": "5.39.2",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.39.0.tgz", "resolved": "https://registry.npmjs.org/terser/-/terser-5.39.2.tgz",
"integrity": "sha512-LBAhFyLho16harJoWMg/nZsQYgTrg5jXOn2nCYjRUcZZEdE3qa2zb8QEDRUGVZBW4rlazf2fxkg8tztybTaqWw==", "integrity": "sha512-yEPUmWve+VA78bI71BW70Dh0TuV4HHd+I5SHOAfS1+QBOmvmCiiffgjR8ryyEd3KIfvPGFqoADt8LdQ6XpXIvg==",
"dev": true, "dev": true,
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"dependencies": { "dependencies": {
"@jridgewell/source-map": "^0.3.3", "@jridgewell/source-map": "^0.3.3",
"acorn": "^8.8.2", "acorn": "^8.14.0",
"commander": "^2.20.0", "commander": "^2.20.0",
"source-map-support": "~0.5.20" "source-map-support": "~0.5.20"
}, },
@ -2156,9 +2139,9 @@
} }
}, },
"node_modules/undici-types": { "node_modules/undici-types": {
"version": "6.20.0", "version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
@ -2193,16 +2176,6 @@
"browserslist": ">= 4.21.0" "browserslist": ">= 4.21.0"
} }
}, },
"node_modules/uri-js": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
"integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"punycode": "^2.1.0"
}
},
"node_modules/util-deprecate": { "node_modules/util-deprecate": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@ -2211,9 +2184,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/watchpack": { "node_modules/watchpack": {
"version": "2.4.2", "version": "2.4.4",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz",
"integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==", "integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -2225,14 +2198,15 @@
} }
}, },
"node_modules/webpack": { "node_modules/webpack": {
"version": "5.97.1", "version": "5.99.8",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.97.1.tgz", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.99.8.tgz",
"integrity": "sha512-EksG6gFY3L1eFMROS/7Wzgrii5mBAFe4rIr3r2BTfo7bcc+DWwFZ4OJ/miOuHJO/A85HwyI4eQ0F6IKXesO7Fg==", "integrity": "sha512-lQ3CPiSTpfOnrEGeXDwoq5hIGzSjmwD72GdfVzF7CQAI7t47rJG9eDWvcEkEn3CUQymAElVvDg3YNTlCYj+qUQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@types/eslint-scope": "^3.7.7", "@types/eslint-scope": "^3.7.7",
"@types/estree": "^1.0.6", "@types/estree": "^1.0.6",
"@types/json-schema": "^7.0.15",
"@webassemblyjs/ast": "^1.14.1", "@webassemblyjs/ast": "^1.14.1",
"@webassemblyjs/wasm-edit": "^1.14.1", "@webassemblyjs/wasm-edit": "^1.14.1",
"@webassemblyjs/wasm-parser": "^1.14.1", "@webassemblyjs/wasm-parser": "^1.14.1",
@ -2249,9 +2223,9 @@
"loader-runner": "^4.2.0", "loader-runner": "^4.2.0",
"mime-types": "^2.1.27", "mime-types": "^2.1.27",
"neo-async": "^2.6.2", "neo-async": "^2.6.2",
"schema-utils": "^3.2.0", "schema-utils": "^4.3.2",
"tapable": "^2.1.1", "tapable": "^2.1.1",
"terser-webpack-plugin": "^5.3.10", "terser-webpack-plugin": "^5.3.11",
"watchpack": "^2.4.1", "watchpack": "^2.4.1",
"webpack-sources": "^3.2.3" "webpack-sources": "^3.2.3"
}, },
@ -2352,59 +2326,6 @@
"node": ">=10.13.0" "node": ">=10.13.0"
} }
}, },
"node_modules/webpack/node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true,
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
"json-schema-traverse": "^0.4.1",
"uri-js": "^4.2.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/webpack/node_modules/ajv-keywords": {
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
"integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"ajv": "^6.9.1"
}
},
"node_modules/webpack/node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
"dev": true,
"license": "MIT"
},
"node_modules/webpack/node_modules/schema-utils": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz",
"integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/json-schema": "^7.0.8",
"ajv": "^6.12.5",
"ajv-keywords": "^3.5.2"
},
"engines": {
"node": ">= 10.13.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/webpack"
}
},
"node_modules/which": { "node_modules/which": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",

View File

@ -8,17 +8,17 @@
"build": "webpack" "build": "webpack"
}, },
"dependencies": { "dependencies": {
"bootstrap": "5.3.3", "bootstrap": "5.3.6",
"font-awesome": "4.7.0", "font-awesome": "4.7.0",
"jquery": "3.7.1" "jquery": "3.7.1"
}, },
"devDependencies": { "devDependencies": {
"css-loader": "7.1.2", "css-loader": "7.1.2",
"expose-loader": "5.0.0", "expose-loader": "5.0.1",
"mini-css-extract-plugin": "2.9.2", "mini-css-extract-plugin": "2.9.2",
"sass": "1.85.0", "sass": "1.88.0",
"sass-loader": "16.0.4", "sass-loader": "16.0.5",
"webpack": "5.97.1", "webpack": "5.99.8",
"webpack-cli": "5.1.4" "webpack-cli": "5.1.4"
} }
} }

View File

@ -1,4 +1,5 @@
using Bit.Commercial.Core.AdminConsole.Providers; using Bit.Commercial.Core.AdminConsole.Providers;
using Bit.Core;
using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Enums.Provider;
@ -7,6 +8,7 @@ using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Constants; using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Enums; using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Providers.Services;
using Bit.Core.Billing.Services; using Bit.Core.Billing.Services;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
@ -223,31 +225,115 @@ public class RemoveOrganizationFromProviderCommandTests
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>(); var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
stripeAdapter.CustomerUpdateAsync(organization.GatewayCustomerId, Arg.Is<CustomerUpdateOptions>(options =>
options.Description == string.Empty &&
options.Email == organization.BillingEmail &&
options.Expand[0] == "tax" &&
options.Expand[1] == "tax_ids")).Returns(new Customer
{
Id = "customer_id",
Address = new Address
{
Country = "US"
}
});
stripeAdapter.SubscriptionCreateAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(new Subscription stripeAdapter.SubscriptionCreateAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(new Subscription
{ {
Id = "subscription_id" Id = "subscription_id"
}); });
sutProvider.GetDependency<IAutomaticTaxStrategy>() await sutProvider.Sut.RemoveOrganizationFromProvider(provider, providerOrganization, organization);
.When(x => x.SetCreateOptions(
Arg.Is<SubscriptionCreateOptions>(options => await stripeAdapter.Received(1).SubscriptionCreateAsync(Arg.Is<SubscriptionCreateOptions>(options =>
options.Customer == organization.GatewayCustomerId && options.Customer == organization.GatewayCustomerId &&
options.CollectionMethod == StripeConstants.CollectionMethod.SendInvoice && options.CollectionMethod == StripeConstants.CollectionMethod.SendInvoice &&
options.DaysUntilDue == 30 && options.DaysUntilDue == 30 &&
options.Metadata["organizationId"] == organization.Id.ToString() && options.AutomaticTax.Enabled == true &&
options.OffSession == true && options.Metadata["organizationId"] == organization.Id.ToString() &&
options.ProrationBehavior == StripeConstants.ProrationBehavior.CreateProrations && options.OffSession == true &&
options.Items.First().Price == teamsMonthlyPlan.PasswordManager.StripeSeatPlanId && options.ProrationBehavior == StripeConstants.ProrationBehavior.CreateProrations &&
options.Items.First().Quantity == organization.Seats) options.Items.First().Price == teamsMonthlyPlan.PasswordManager.StripeSeatPlanId &&
, Arg.Any<Customer>())) options.Items.First().Quantity == organization.Seats));
.Do(x =>
await sutProvider.GetDependency<IProviderBillingService>().Received(1)
.ScaleSeats(provider, organization.PlanType, -organization.Seats ?? 0);
await organizationRepository.Received(1).ReplaceAsync(Arg.Is<Organization>(
org =>
org.BillingEmail == "a@example.com" &&
org.GatewaySubscriptionId == "subscription_id" &&
org.Status == OrganizationStatusType.Created));
await sutProvider.GetDependency<IProviderOrganizationRepository>().Received(1)
.DeleteAsync(providerOrganization);
await sutProvider.GetDependency<IEventService>().Received(1)
.LogProviderOrganizationEventAsync(providerOrganization, EventType.ProviderOrganization_Removed);
await sutProvider.GetDependency<IMailService>().Received(1)
.SendProviderUpdatePaymentMethod(
organization.Id,
organization.Name,
provider.Name,
Arg.Is<IEnumerable<string>>(emails => emails.FirstOrDefault() == "a@example.com"));
}
[Theory, BitAutoData]
public async Task RemoveOrganizationFromProvider_OrganizationStripeEnabled_ConsolidatedBilling_ReverseCharge_MakesCorrectInvocations(
Provider provider,
ProviderOrganization providerOrganization,
Organization organization,
SutProvider<RemoveOrganizationFromProviderCommand> sutProvider)
{
provider.Status = ProviderStatusType.Billable;
providerOrganization.ProviderId = provider.Id;
organization.Status = OrganizationStatusType.Managed;
organization.PlanType = PlanType.TeamsMonthly;
var teamsMonthlyPlan = StaticStore.GetPlan(PlanType.TeamsMonthly);
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(PlanType.TeamsMonthly).Returns(teamsMonthlyPlan);
sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>().HasConfirmedOwnersExceptAsync(
providerOrganization.OrganizationId,
[],
includeProvider: false)
.Returns(true);
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
organizationRepository.GetOwnerEmailAddressesById(organization.Id).Returns([
"a@example.com",
"b@example.com"
]);
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
stripeAdapter.CustomerUpdateAsync(organization.GatewayCustomerId, Arg.Is<CustomerUpdateOptions>(options =>
options.Description == string.Empty &&
options.Email == organization.BillingEmail &&
options.Expand[0] == "tax" &&
options.Expand[1] == "tax_ids")).Returns(new Customer
{ {
x.Arg<SubscriptionCreateOptions>().AutomaticTax = new SubscriptionAutomaticTaxOptions Id = "customer_id",
Address = new Address
{ {
Enabled = true Country = "US"
}; }
}); });
stripeAdapter.SubscriptionCreateAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(new Subscription
{
Id = "subscription_id"
});
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge).Returns(true);
await sutProvider.Sut.RemoveOrganizationFromProvider(provider, providerOrganization, organization); await sutProvider.Sut.RemoveOrganizationFromProvider(provider, providerOrganization, organization);
await stripeAdapter.Received(1).SubscriptionCreateAsync(Arg.Is<SubscriptionCreateOptions>(options => await stripeAdapter.Received(1).SubscriptionCreateAsync(Arg.Is<SubscriptionCreateOptions>(options =>

View File

@ -6,11 +6,12 @@ using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.Models.Business.Provider; using Bit.Core.AdminConsole.Models.Business.Provider;
using Bit.Core.AdminConsole.Models.Business.Tokenables; using Bit.Core.AdminConsole.Models.Business.Tokenables;
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Enums; using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Models; using Bit.Core.Billing.Models;
using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Services; using Bit.Core.Billing.Providers.Services;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
@ -717,8 +718,8 @@ public class ProviderServiceTests
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider); sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>(); var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
sutProvider.GetDependency<IOrganizationService>().SignupClientAsync(organizationSignup) sutProvider.GetDependency<IProviderClientOrganizationSignUpCommand>().SignUpClientOrganizationAsync(organizationSignup)
.Returns((organization, null as OrganizationUser, new Collection())); .Returns(new ProviderClientOrganizationSignUpResponse(organization, new Collection()));
var providerOrganization = var providerOrganization =
await sutProvider.Sut.CreateOrganizationAsync(provider.Id, organizationSignup, clientOwnerEmail, user); await sutProvider.Sut.CreateOrganizationAsync(provider.Id, organizationSignup, clientOwnerEmail, user);
@ -755,8 +756,8 @@ public class ProviderServiceTests
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>(); var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
sutProvider.GetDependency<IOrganizationService>().SignupClientAsync(organizationSignup) sutProvider.GetDependency<IProviderClientOrganizationSignUpCommand>().SignUpClientOrganizationAsync(organizationSignup)
.Returns((organization, null as OrganizationUser, new Collection())); .Returns(new ProviderClientOrganizationSignUpResponse(organization, new Collection()));
await Assert.ThrowsAsync<BadRequestException>(() => await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.CreateOrganizationAsync(provider.Id, organizationSignup, clientOwnerEmail, user)); sutProvider.Sut.CreateOrganizationAsync(provider.Id, organizationSignup, clientOwnerEmail, user));
@ -782,8 +783,8 @@ public class ProviderServiceTests
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>(); var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
sutProvider.GetDependency<IOrganizationService>().SignupClientAsync(organizationSignup) sutProvider.GetDependency<IProviderClientOrganizationSignUpCommand>().SignUpClientOrganizationAsync(organizationSignup)
.Returns((organization, null as OrganizationUser, new Collection())); .Returns(new ProviderClientOrganizationSignUpResponse(organization, new Collection()));
var providerOrganization = await sutProvider.Sut.CreateOrganizationAsync(provider.Id, organizationSignup, clientOwnerEmail, user); var providerOrganization = await sutProvider.Sut.CreateOrganizationAsync(provider.Id, organizationSignup, clientOwnerEmail, user);
@ -821,8 +822,8 @@ public class ProviderServiceTests
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider); sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>(); var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
sutProvider.GetDependency<IOrganizationService>().SignupClientAsync(organizationSignup) sutProvider.GetDependency<IProviderClientOrganizationSignUpCommand>().SignUpClientOrganizationAsync(organizationSignup)
.Returns((organization, null as OrganizationUser, defaultCollection)); .Returns(new ProviderClientOrganizationSignUpResponse(organization, defaultCollection));
var providerOrganization = var providerOrganization =
await sutProvider.Sut.CreateOrganizationAsync(provider.Id, organizationSignup, clientOwnerEmail, user); await sutProvider.Sut.CreateOrganizationAsync(provider.Id, organizationSignup, clientOwnerEmail, user);

View File

@ -1,16 +1,16 @@
#nullable enable #nullable enable
using System.Text; using System.Text;
using Bit.Commercial.Core.Billing; using Bit.Commercial.Core.Billing.Providers.Services;
using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing; using Bit.Core.Billing;
using Bit.Core.Billing.Constants; using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Entities;
using Bit.Core.Billing.Enums; using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Repositories; using Bit.Core.Billing.Providers.Entities;
using Bit.Core.Billing.Providers.Repositories;
using Bit.Core.Billing.Services; using Bit.Core.Billing.Services;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
@ -25,7 +25,7 @@ using NSubstitute;
using Stripe; using Stripe;
using Xunit; using Xunit;
namespace Bit.Commercial.Core.Test.Billing; namespace Bit.Commercial.Core.Test.Billing.Providers;
public class BusinessUnitConverterTests public class BusinessUnitConverterTests
{ {

View File

@ -1,7 +1,7 @@
using System.Globalization; using System.Globalization;
using System.Net; using System.Net;
using Bit.Commercial.Core.Billing; using Bit.Commercial.Core.Billing.Providers.Models;
using Bit.Commercial.Core.Billing.Models; using Bit.Commercial.Core.Billing.Providers.Services;
using Bit.Core; using Bit.Core;
using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Entities.Provider;
@ -10,13 +10,14 @@ using Bit.Core.AdminConsole.Models.Data.Provider;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Caches; using Bit.Core.Billing.Caches;
using Bit.Core.Billing.Constants; using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Entities;
using Bit.Core.Billing.Enums; using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Models; using Bit.Core.Billing.Models;
using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Repositories; using Bit.Core.Billing.Providers.Entities;
using Bit.Core.Billing.Providers.Models;
using Bit.Core.Billing.Providers.Repositories;
using Bit.Core.Billing.Services; using Bit.Core.Billing.Services;
using Bit.Core.Billing.Services.Contracts; using Bit.Core.Billing.Tax.Services;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
@ -39,7 +40,7 @@ using Customer = Stripe.Customer;
using PaymentMethod = Stripe.PaymentMethod; using PaymentMethod = Stripe.PaymentMethod;
using Subscription = Stripe.Subscription; using Subscription = Stripe.Subscription;
namespace Bit.Commercial.Core.Test.Billing; namespace Bit.Commercial.Core.Test.Billing.Providers;
[SutProviderCustomize] [SutProviderCustomize]
public class ProviderBillingServiceTests public class ProviderBillingServiceTests
@ -261,7 +262,7 @@ public class ProviderBillingServiceTests
}; };
sutProvider.GetDependency<ISubscriberService>().GetCustomerOrThrow(provider, Arg.Is<CustomerGetOptions>( sutProvider.GetDependency<ISubscriberService>().GetCustomerOrThrow(provider, Arg.Is<CustomerGetOptions>(
options => options.Expand.FirstOrDefault() == "tax_ids")) options => options.Expand.Contains("tax") && options.Expand.Contains("tax_ids")))
.Returns(providerCustomer); .Returns(providerCustomer);
sutProvider.GetDependency<IGlobalSettings>().BaseServiceUri sutProvider.GetDependency<IGlobalSettings>().BaseServiceUri
@ -311,6 +312,91 @@ public class ProviderBillingServiceTests
org => org.GatewayCustomerId == "customer_id")); org => org.GatewayCustomerId == "customer_id"));
} }
[Theory, BitAutoData]
public async Task CreateCustomer_ForClientOrg_ReverseCharge_Succeeds(
Provider provider,
Organization organization,
SutProvider<ProviderBillingService> sutProvider)
{
organization.GatewayCustomerId = null;
organization.Name = "Name";
organization.BusinessName = "BusinessName";
var providerCustomer = new Customer
{
Address = new Address
{
Country = "CA",
PostalCode = "12345",
Line1 = "123 Main St.",
Line2 = "Unit 4",
City = "Fake Town",
State = "Fake State"
},
TaxIds = new StripeList<TaxId>
{
Data =
[
new TaxId { Type = "TYPE", Value = "VALUE" }
]
}
};
sutProvider.GetDependency<ISubscriberService>().GetCustomerOrThrow(provider, Arg.Is<CustomerGetOptions>(
options => options.Expand.Contains("tax") && options.Expand.Contains("tax_ids")))
.Returns(providerCustomer);
sutProvider.GetDependency<IGlobalSettings>().BaseServiceUri
.Returns(new Bit.Core.Settings.GlobalSettings.BaseServiceUriSettings(new Bit.Core.Settings.GlobalSettings())
{
CloudRegion = "US"
});
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge).Returns(true);
sutProvider.GetDependency<IStripeAdapter>().CustomerCreateAsync(Arg.Is<CustomerCreateOptions>(
options =>
options.Address.Country == providerCustomer.Address.Country &&
options.Address.PostalCode == providerCustomer.Address.PostalCode &&
options.Address.Line1 == providerCustomer.Address.Line1 &&
options.Address.Line2 == providerCustomer.Address.Line2 &&
options.Address.City == providerCustomer.Address.City &&
options.Address.State == providerCustomer.Address.State &&
options.Name == organization.DisplayName() &&
options.Description == $"{provider.Name} Client Organization" &&
options.Email == provider.BillingEmail &&
options.InvoiceSettings.CustomFields.FirstOrDefault().Name == "Organization" &&
options.InvoiceSettings.CustomFields.FirstOrDefault().Value == "Name" &&
options.Metadata["region"] == "US" &&
options.TaxIdData.FirstOrDefault().Type == providerCustomer.TaxIds.FirstOrDefault().Type &&
options.TaxIdData.FirstOrDefault().Value == providerCustomer.TaxIds.FirstOrDefault().Value &&
options.TaxExempt == StripeConstants.TaxExempt.Reverse))
.Returns(new Customer { Id = "customer_id" });
await sutProvider.Sut.CreateCustomerForClientOrganization(provider, organization);
await sutProvider.GetDependency<IStripeAdapter>().Received(1).CustomerCreateAsync(Arg.Is<CustomerCreateOptions>(
options =>
options.Address.Country == providerCustomer.Address.Country &&
options.Address.PostalCode == providerCustomer.Address.PostalCode &&
options.Address.Line1 == providerCustomer.Address.Line1 &&
options.Address.Line2 == providerCustomer.Address.Line2 &&
options.Address.City == providerCustomer.Address.City &&
options.Address.State == providerCustomer.Address.State &&
options.Name == organization.DisplayName() &&
options.Description == $"{provider.Name} Client Organization" &&
options.Email == provider.BillingEmail &&
options.InvoiceSettings.CustomFields.FirstOrDefault().Name == "Organization" &&
options.InvoiceSettings.CustomFields.FirstOrDefault().Value == "Name" &&
options.Metadata["region"] == "US" &&
options.TaxIdData.FirstOrDefault().Type == providerCustomer.TaxIds.FirstOrDefault().Type &&
options.TaxIdData.FirstOrDefault().Value == providerCustomer.TaxIds.FirstOrDefault().Value));
await sutProvider.GetDependency<IOrganizationRepository>().Received(1).ReplaceAsync(Arg.Is<Organization>(
org => org.GatewayCustomerId == "customer_id"));
}
#endregion #endregion
#region GenerateClientInvoiceReport #region GenerateClientInvoiceReport
@ -1181,6 +1267,62 @@ public class ProviderBillingServiceTests
Assert.Equivalent(expected, actual); Assert.Equivalent(expected, actual);
} }
[Theory, BitAutoData]
public async Task SetupCustomer_WithCard_ReverseCharge_Success(
SutProvider<ProviderBillingService> sutProvider,
Provider provider,
TaxInfo taxInfo)
{
provider.Name = "MSP";
sutProvider.GetDependency<ITaxService>()
.GetStripeTaxCode(Arg.Is<string>(
p => p == taxInfo.BillingAddressCountry),
Arg.Is<string>(p => p == taxInfo.TaxIdNumber))
.Returns(taxInfo.TaxIdType);
taxInfo.BillingAddressCountry = "AD";
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
var expected = new Customer
{
Id = "customer_id",
Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported }
};
var tokenizedPaymentSource = new TokenizedPaymentSource(PaymentMethodType.Card, "token");
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true);
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge).Returns(true);
stripeAdapter.CustomerCreateAsync(Arg.Is<CustomerCreateOptions>(o =>
o.Address.Country == taxInfo.BillingAddressCountry &&
o.Address.PostalCode == taxInfo.BillingAddressPostalCode &&
o.Address.Line1 == taxInfo.BillingAddressLine1 &&
o.Address.Line2 == taxInfo.BillingAddressLine2 &&
o.Address.City == taxInfo.BillingAddressCity &&
o.Address.State == taxInfo.BillingAddressState &&
o.Description == WebUtility.HtmlDecode(provider.BusinessName) &&
o.Email == provider.BillingEmail &&
o.PaymentMethod == tokenizedPaymentSource.Token &&
o.InvoiceSettings.DefaultPaymentMethod == tokenizedPaymentSource.Token &&
o.InvoiceSettings.CustomFields.FirstOrDefault().Name == "Provider" &&
o.InvoiceSettings.CustomFields.FirstOrDefault().Value == "MSP" &&
o.Metadata["region"] == "" &&
o.TaxIdData.FirstOrDefault().Type == taxInfo.TaxIdType &&
o.TaxIdData.FirstOrDefault().Value == taxInfo.TaxIdNumber &&
o.TaxExempt == StripeConstants.TaxExempt.Reverse))
.Returns(expected);
var actual = await sutProvider.Sut.SetupCustomer(provider, taxInfo, tokenizedPaymentSource);
Assert.Equivalent(expected, actual);
}
[Theory, BitAutoData] [Theory, BitAutoData]
public async Task SetupCustomer_Throws_BadRequestException_WhenTaxIdIsInvalid( public async Task SetupCustomer_Throws_BadRequestException_WhenTaxIdIsInvalid(
SutProvider<ProviderBillingService> sutProvider, SutProvider<ProviderBillingService> sutProvider,
@ -1306,7 +1448,7 @@ public class ProviderBillingServiceTests
.Returns(new Customer .Returns(new Customer
{ {
Id = "customer_id", Id = "customer_id",
Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported } Address = new Address { Country = "US" }
}); });
var providerPlans = new List<ProviderPlan> var providerPlans = new List<ProviderPlan>
@ -1358,7 +1500,7 @@ public class ProviderBillingServiceTests
var customer = new Customer var customer = new Customer
{ {
Id = "customer_id", Id = "customer_id",
Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported } Address = new Address { Country = "US" }
}; };
sutProvider.GetDependency<ISubscriberService>() sutProvider.GetDependency<ISubscriberService>()
.GetCustomerOrThrow( .GetCustomerOrThrow(
@ -1398,19 +1540,6 @@ public class ProviderBillingServiceTests
var expected = new Subscription { Id = "subscription_id", Status = StripeConstants.SubscriptionStatus.Active }; var expected = new Subscription { Id = "subscription_id", Status = StripeConstants.SubscriptionStatus.Active };
sutProvider.GetDependency<IAutomaticTaxStrategy>()
.When(x => x.SetCreateOptions(
Arg.Is<SubscriptionCreateOptions>(options =>
options.Customer == "customer_id")
, Arg.Is<Customer>(p => p == customer)))
.Do(x =>
{
x.Arg<SubscriptionCreateOptions>().AutomaticTax = new SubscriptionAutomaticTaxOptions
{
Enabled = true
};
});
sutProvider.GetDependency<IStripeAdapter>().SubscriptionCreateAsync(Arg.Is<SubscriptionCreateOptions>( sutProvider.GetDependency<IStripeAdapter>().SubscriptionCreateAsync(Arg.Is<SubscriptionCreateOptions>(
sub => sub =>
sub.AutomaticTax.Enabled == true && sub.AutomaticTax.Enabled == true &&
@ -1442,11 +1571,11 @@ public class ProviderBillingServiceTests
var customer = new Customer var customer = new Customer
{ {
Id = "customer_id", Id = "customer_id",
Address = new Address { Country = "US" },
InvoiceSettings = new CustomerInvoiceSettings InvoiceSettings = new CustomerInvoiceSettings
{ {
DefaultPaymentMethodId = "pm_123" DefaultPaymentMethodId = "pm_123"
}, }
Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported }
}; };
sutProvider.GetDependency<ISubscriberService>() sutProvider.GetDependency<ISubscriberService>()
@ -1487,19 +1616,6 @@ public class ProviderBillingServiceTests
var expected = new Subscription { Id = "subscription_id", Status = StripeConstants.SubscriptionStatus.Active }; var expected = new Subscription { Id = "subscription_id", Status = StripeConstants.SubscriptionStatus.Active };
sutProvider.GetDependency<IAutomaticTaxStrategy>()
.When(x => x.SetCreateOptions(
Arg.Is<SubscriptionCreateOptions>(options =>
options.Customer == "customer_id")
, Arg.Is<Customer>(p => p == customer)))
.Do(x =>
{
x.Arg<SubscriptionCreateOptions>().AutomaticTax = new SubscriptionAutomaticTaxOptions
{
Enabled = true
};
});
sutProvider.GetDependency<IFeatureService>() sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true); .IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true);
@ -1535,9 +1651,9 @@ public class ProviderBillingServiceTests
var customer = new Customer var customer = new Customer
{ {
Id = "customer_id", Id = "customer_id",
Address = new Address { Country = "US" },
InvoiceSettings = new CustomerInvoiceSettings(), InvoiceSettings = new CustomerInvoiceSettings(),
Metadata = new Dictionary<string, string>(), Metadata = new Dictionary<string, string>()
Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported }
}; };
sutProvider.GetDependency<ISubscriberService>() sutProvider.GetDependency<ISubscriberService>()
@ -1578,19 +1694,6 @@ public class ProviderBillingServiceTests
var expected = new Subscription { Id = "subscription_id", Status = StripeConstants.SubscriptionStatus.Active }; var expected = new Subscription { Id = "subscription_id", Status = StripeConstants.SubscriptionStatus.Active };
sutProvider.GetDependency<IAutomaticTaxStrategy>()
.When(x => x.SetCreateOptions(
Arg.Is<SubscriptionCreateOptions>(options =>
options.Customer == "customer_id")
, Arg.Is<Customer>(p => p == customer)))
.Do(x =>
{
x.Arg<SubscriptionCreateOptions>().AutomaticTax = new SubscriptionAutomaticTaxOptions
{
Enabled = true
};
});
sutProvider.GetDependency<IFeatureService>() sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true); .IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true);
@ -1645,12 +1748,15 @@ public class ProviderBillingServiceTests
var customer = new Customer var customer = new Customer
{ {
Id = "customer_id", Id = "customer_id",
Address = new Address
{
Country = "US"
},
InvoiceSettings = new CustomerInvoiceSettings(), InvoiceSettings = new CustomerInvoiceSettings(),
Metadata = new Dictionary<string, string> Metadata = new Dictionary<string, string>
{ {
["btCustomerId"] = "braintree_customer_id" ["btCustomerId"] = "braintree_customer_id"
}, }
Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported }
}; };
sutProvider.GetDependency<ISubscriberService>() sutProvider.GetDependency<ISubscriberService>()
@ -1691,22 +1797,92 @@ public class ProviderBillingServiceTests
var expected = new Subscription { Id = "subscription_id", Status = StripeConstants.SubscriptionStatus.Active }; var expected = new Subscription { Id = "subscription_id", Status = StripeConstants.SubscriptionStatus.Active };
sutProvider.GetDependency<IAutomaticTaxStrategy>() sutProvider.GetDependency<IFeatureService>()
.When(x => x.SetCreateOptions( .IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true);
Arg.Is<SubscriptionCreateOptions>(options =>
options.Customer == "customer_id") sutProvider.GetDependency<IStripeAdapter>().SubscriptionCreateAsync(Arg.Is<SubscriptionCreateOptions>(
, Arg.Is<Customer>(p => p == customer))) sub =>
.Do(x => sub.AutomaticTax.Enabled == true &&
sub.CollectionMethod == StripeConstants.CollectionMethod.ChargeAutomatically &&
sub.Customer == "customer_id" &&
sub.DaysUntilDue == null &&
sub.Items.Count == 2 &&
sub.Items.ElementAt(0).Price == ProviderPriceAdapter.MSP.Active.Teams &&
sub.Items.ElementAt(0).Quantity == 100 &&
sub.Items.ElementAt(1).Price == ProviderPriceAdapter.MSP.Active.Enterprise &&
sub.Items.ElementAt(1).Quantity == 100 &&
sub.Metadata["providerId"] == provider.Id.ToString() &&
sub.OffSession == true &&
sub.ProrationBehavior == StripeConstants.ProrationBehavior.CreateProrations &&
sub.TrialPeriodDays == 14)).Returns(expected);
var actual = await sutProvider.Sut.SetupSubscription(provider);
Assert.Equivalent(expected, actual);
}
[Theory, BitAutoData]
public async Task SetupSubscription_ReverseCharge_Succeeds(
SutProvider<ProviderBillingService> sutProvider,
Provider provider)
{
provider.Type = ProviderType.Msp;
provider.GatewaySubscriptionId = null;
var customer = new Customer
{
Id = "customer_id",
Address = new Address { Country = "CA" },
InvoiceSettings = new CustomerInvoiceSettings
{ {
x.Arg<SubscriptionCreateOptions>().AutomaticTax = new SubscriptionAutomaticTaxOptions DefaultPaymentMethodId = "pm_123"
{ }
Enabled = true };
};
}); sutProvider.GetDependency<ISubscriberService>()
.GetCustomerOrThrow(
provider,
Arg.Is<CustomerGetOptions>(p => p.Expand.Contains("tax") || p.Expand.Contains("tax_ids"))).Returns(customer);
var providerPlans = new List<ProviderPlan>
{
new()
{
Id = Guid.NewGuid(),
ProviderId = provider.Id,
PlanType = PlanType.TeamsMonthly,
SeatMinimum = 100,
PurchasedSeats = 0,
AllocatedSeats = 0
},
new()
{
Id = Guid.NewGuid(),
ProviderId = provider.Id,
PlanType = PlanType.EnterpriseMonthly,
SeatMinimum = 100,
PurchasedSeats = 0,
AllocatedSeats = 0
}
};
foreach (var plan in providerPlans)
{
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(plan.PlanType)
.Returns(StaticStore.GetPlan(plan.PlanType));
}
sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id)
.Returns(providerPlans);
var expected = new Subscription { Id = "subscription_id", Status = StripeConstants.SubscriptionStatus.Active };
sutProvider.GetDependency<IFeatureService>() sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true); .IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true);
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge).Returns(true);
sutProvider.GetDependency<IStripeAdapter>().SubscriptionCreateAsync(Arg.Is<SubscriptionCreateOptions>( sutProvider.GetDependency<IStripeAdapter>().SubscriptionCreateAsync(Arg.Is<SubscriptionCreateOptions>(
sub => sub =>
sub.AutomaticTax.Enabled == true && sub.AutomaticTax.Enabled == true &&

View File

@ -1,11 +1,11 @@
using Bit.Commercial.Core.Billing; using Bit.Commercial.Core.Billing.Providers.Services;
using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.Billing.Enums; using Bit.Core.Billing.Enums;
using Stripe; using Stripe;
using Xunit; using Xunit;
namespace Bit.Commercial.Core.Test.Billing; namespace Bit.Commercial.Core.Test.Billing.Providers;
public class ProviderPriceAdapterTests public class ProviderPriceAdapterTests
{ {

View File

@ -1,9 +1,9 @@
using Bit.Core.Billing.Services; using Bit.Core.Billing.Tax.Services.Implementations;
using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.AutoFixture.Attributes;
using Xunit; using Xunit;
namespace Bit.Commercial.Core.Test.Billing; namespace Bit.Commercial.Core.Test.Billing.Tax;
[SutProviderCustomize] [SutProviderCustomize]
public class TaxServiceTests public class TaxServiceTests

View File

@ -1,14 +1,10 @@
using System.Security.Claims; using Bit.Commercial.Core.SecretsManager.Queries.Projects;
using Bit.Commercial.Core.SecretsManager.Queries.Projects;
using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities;
using Bit.Core.Billing.Enums; using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Licenses;
using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Pricing;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.Models.Business;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.SecretsManager.Repositories; using Bit.Core.SecretsManager.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings; using Bit.Core.Settings;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture;
@ -22,11 +18,26 @@ namespace Bit.Commercial.Core.Test.SecretsManager.Queries.Projects;
[SutProviderCustomize] [SutProviderCustomize]
public class MaxProjectsQueryTests public class MaxProjectsQueryTests
{ {
[Theory]
[BitAutoData]
public async Task GetByOrgIdAsync_SelfHosted_ReturnsNulls(SutProvider<MaxProjectsQuery> sutProvider,
Guid organizationId)
{
sutProvider.GetDependency<IGlobalSettings>().SelfHosted.Returns(true);
var (max, overMax) = await sutProvider.Sut.GetByOrgIdAsync(organizationId, 1);
Assert.Null(max);
Assert.Null(overMax);
}
[Theory] [Theory]
[BitAutoData] [BitAutoData]
public async Task GetByOrgIdAsync_OrganizationIsNull_ThrowsNotFound(SutProvider<MaxProjectsQuery> sutProvider, public async Task GetByOrgIdAsync_OrganizationIsNull_ThrowsNotFound(SutProvider<MaxProjectsQuery> sutProvider,
Guid organizationId) Guid organizationId)
{ {
sutProvider.GetDependency<IGlobalSettings>().SelfHosted.Returns(false);
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(default).ReturnsNull(); sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(default).ReturnsNull();
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.GetByOrgIdAsync(organizationId, 1)); await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.GetByOrgIdAsync(organizationId, 1));
@ -35,54 +46,6 @@ public class MaxProjectsQueryTests
.GetProjectCountByOrganizationIdAsync(organizationId); .GetProjectCountByOrganizationIdAsync(organizationId);
} }
[Theory]
[BitAutoData(PlanType.FamiliesAnnually2019)]
[BitAutoData(PlanType.Custom)]
[BitAutoData(PlanType.FamiliesAnnually)]
public async Task GetByOrgIdAsync_Cloud_SmPlanIsNull_ThrowsBadRequest(PlanType planType,
SutProvider<MaxProjectsQuery> sutProvider, Organization organization)
{
organization.PlanType = planType;
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(organization.Id)
.Returns(organization);
sutProvider.GetDependency<IGlobalSettings>().SelfHosted.Returns(false);
var plan = StaticStore.GetPlan(planType);
sutProvider.GetDependency<IPricingClient>().GetPlan(organization.PlanType).Returns(plan);
await Assert.ThrowsAsync<BadRequestException>(
async () => await sutProvider.Sut.GetByOrgIdAsync(organization.Id, 1));
await sutProvider.GetDependency<IProjectRepository>()
.DidNotReceiveWithAnyArgs()
.GetProjectCountByOrganizationIdAsync(organization.Id);
}
[Theory]
[BitAutoData]
public async Task GetByOrgIdAsync_SelfHosted_NoMaxProjectsClaim_ThrowsBadRequest(
SutProvider<MaxProjectsQuery> sutProvider, Organization organization)
{
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(organization.Id)
.Returns(organization);
sutProvider.GetDependency<IGlobalSettings>().SelfHosted.Returns(true);
var license = new OrganizationLicense();
var claimsPrincipal = new ClaimsPrincipal();
sutProvider.GetDependency<ILicensingService>().ReadOrganizationLicenseAsync(organization).Returns(license);
sutProvider.GetDependency<ILicensingService>().GetClaimsPrincipalFromLicense(license).Returns(claimsPrincipal);
await Assert.ThrowsAsync<BadRequestException>(
async () => await sutProvider.Sut.GetByOrgIdAsync(organization.Id, 1));
await sutProvider.GetDependency<IProjectRepository>()
.DidNotReceiveWithAnyArgs()
.GetProjectCountByOrganizationIdAsync(organization.Id);
}
[Theory] [Theory]
[BitAutoData(PlanType.TeamsMonthly2019)] [BitAutoData(PlanType.TeamsMonthly2019)]
[BitAutoData(PlanType.TeamsMonthly2020)] [BitAutoData(PlanType.TeamsMonthly2020)]
@ -97,57 +60,16 @@ public class MaxProjectsQueryTests
[BitAutoData(PlanType.EnterpriseAnnually2019)] [BitAutoData(PlanType.EnterpriseAnnually2019)]
[BitAutoData(PlanType.EnterpriseAnnually2020)] [BitAutoData(PlanType.EnterpriseAnnually2020)]
[BitAutoData(PlanType.EnterpriseAnnually)] [BitAutoData(PlanType.EnterpriseAnnually)]
public async Task GetByOrgIdAsync_Cloud_SmNoneFreePlans_ReturnsNull(PlanType planType, public async Task GetByOrgIdAsync_SmNoneFreePlans_ReturnsNull(PlanType planType,
SutProvider<MaxProjectsQuery> sutProvider, Organization organization) SutProvider<MaxProjectsQuery> sutProvider, Organization organization)
{ {
organization.PlanType = planType;
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
sutProvider.GetDependency<IGlobalSettings>().SelfHosted.Returns(false); sutProvider.GetDependency<IGlobalSettings>().SelfHosted.Returns(false);
var plan = StaticStore.GetPlan(planType);
sutProvider.GetDependency<IPricingClient>().GetPlan(organization.PlanType).Returns(plan);
var (limit, overLimit) = await sutProvider.Sut.GetByOrgIdAsync(organization.Id, 1);
Assert.Null(limit);
Assert.Null(overLimit);
await sutProvider.GetDependency<IProjectRepository>().DidNotReceiveWithAnyArgs()
.GetProjectCountByOrganizationIdAsync(organization.Id);
}
[Theory]
[BitAutoData(PlanType.TeamsMonthly2019)]
[BitAutoData(PlanType.TeamsMonthly2020)]
[BitAutoData(PlanType.TeamsMonthly)]
[BitAutoData(PlanType.TeamsAnnually2019)]
[BitAutoData(PlanType.TeamsAnnually2020)]
[BitAutoData(PlanType.TeamsAnnually)]
[BitAutoData(PlanType.TeamsStarter)]
[BitAutoData(PlanType.EnterpriseMonthly2019)]
[BitAutoData(PlanType.EnterpriseMonthly2020)]
[BitAutoData(PlanType.EnterpriseMonthly)]
[BitAutoData(PlanType.EnterpriseAnnually2019)]
[BitAutoData(PlanType.EnterpriseAnnually2020)]
[BitAutoData(PlanType.EnterpriseAnnually)]
public async Task GetByOrgIdAsync_SelfHosted_SmNoneFreePlans_ReturnsNull(PlanType planType,
SutProvider<MaxProjectsQuery> sutProvider, Organization organization)
{
organization.PlanType = planType; organization.PlanType = planType;
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization); sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
sutProvider.GetDependency<IGlobalSettings>().SelfHosted.Returns(true);
var license = new OrganizationLicense(); sutProvider.GetDependency<IPricingClient>().GetPlan(organization.PlanType)
var plan = StaticStore.GetPlan(planType); .Returns(StaticStore.GetPlan(organization.PlanType));
var claims = new List<Claim>
{
new (nameof(OrganizationLicenseConstants.PlanType), organization.PlanType.ToString()),
new (nameof(OrganizationLicenseConstants.SmMaxProjects), plan.SecretsManager.MaxProjects.ToString())
};
var identity = new ClaimsIdentity(claims, "TestAuthenticationType");
var claimsPrincipal = new ClaimsPrincipal(identity);
sutProvider.GetDependency<ILicensingService>().ReadOrganizationLicenseAsync(organization).Returns(license);
sutProvider.GetDependency<ILicensingService>().GetClaimsPrincipalFromLicense(license).Returns(claimsPrincipal);
var (limit, overLimit) = await sutProvider.Sut.GetByOrgIdAsync(organization.Id, 1); var (limit, overLimit) = await sutProvider.Sut.GetByOrgIdAsync(organization.Id, 1);
@ -183,7 +105,7 @@ public class MaxProjectsQueryTests
[BitAutoData(PlanType.Free, 3, 4, true)] [BitAutoData(PlanType.Free, 3, 4, true)]
[BitAutoData(PlanType.Free, 4, 4, true)] [BitAutoData(PlanType.Free, 4, 4, true)]
[BitAutoData(PlanType.Free, 40, 4, true)] [BitAutoData(PlanType.Free, 40, 4, true)]
public async Task GetByOrgIdAsync_Cloud_SmFreePlan__Success(PlanType planType, int projects, int projectsToAdd, bool expectedOverMax, public async Task GetByOrgIdAsync_SmFreePlan__Success(PlanType planType, int projects, int projectsToAdd, bool expectedOverMax,
SutProvider<MaxProjectsQuery> sutProvider, Organization organization) SutProvider<MaxProjectsQuery> sutProvider, Organization organization)
{ {
organization.PlanType = planType; organization.PlanType = planType;
@ -191,66 +113,8 @@ public class MaxProjectsQueryTests
sutProvider.GetDependency<IProjectRepository>().GetProjectCountByOrganizationIdAsync(organization.Id) sutProvider.GetDependency<IProjectRepository>().GetProjectCountByOrganizationIdAsync(organization.Id)
.Returns(projects); .Returns(projects);
sutProvider.GetDependency<IGlobalSettings>().SelfHosted.Returns(false); sutProvider.GetDependency<IPricingClient>().GetPlan(organization.PlanType)
var plan = StaticStore.GetPlan(planType); .Returns(StaticStore.GetPlan(organization.PlanType));
sutProvider.GetDependency<IPricingClient>().GetPlan(organization.PlanType).Returns(plan);
var (max, overMax) = await sutProvider.Sut.GetByOrgIdAsync(organization.Id, projectsToAdd);
Assert.NotNull(max);
Assert.NotNull(overMax);
Assert.Equal(3, max.Value);
Assert.Equal(expectedOverMax, overMax);
await sutProvider.GetDependency<IProjectRepository>().Received(1)
.GetProjectCountByOrganizationIdAsync(organization.Id);
}
[Theory]
[BitAutoData(PlanType.Free, 0, 1, false)]
[BitAutoData(PlanType.Free, 1, 1, false)]
[BitAutoData(PlanType.Free, 2, 1, false)]
[BitAutoData(PlanType.Free, 3, 1, true)]
[BitAutoData(PlanType.Free, 4, 1, true)]
[BitAutoData(PlanType.Free, 40, 1, true)]
[BitAutoData(PlanType.Free, 0, 2, false)]
[BitAutoData(PlanType.Free, 1, 2, false)]
[BitAutoData(PlanType.Free, 2, 2, true)]
[BitAutoData(PlanType.Free, 3, 2, true)]
[BitAutoData(PlanType.Free, 4, 2, true)]
[BitAutoData(PlanType.Free, 40, 2, true)]
[BitAutoData(PlanType.Free, 0, 3, false)]
[BitAutoData(PlanType.Free, 1, 3, true)]
[BitAutoData(PlanType.Free, 2, 3, true)]
[BitAutoData(PlanType.Free, 3, 3, true)]
[BitAutoData(PlanType.Free, 4, 3, true)]
[BitAutoData(PlanType.Free, 40, 3, true)]
[BitAutoData(PlanType.Free, 0, 4, true)]
[BitAutoData(PlanType.Free, 1, 4, true)]
[BitAutoData(PlanType.Free, 2, 4, true)]
[BitAutoData(PlanType.Free, 3, 4, true)]
[BitAutoData(PlanType.Free, 4, 4, true)]
[BitAutoData(PlanType.Free, 40, 4, true)]
public async Task GetByOrgIdAsync_SelfHosted_SmFreePlan__Success(PlanType planType, int projects, int projectsToAdd, bool expectedOverMax,
SutProvider<MaxProjectsQuery> sutProvider, Organization organization)
{
organization.PlanType = planType;
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
sutProvider.GetDependency<IProjectRepository>().GetProjectCountByOrganizationIdAsync(organization.Id)
.Returns(projects);
sutProvider.GetDependency<IGlobalSettings>().SelfHosted.Returns(true);
var license = new OrganizationLicense();
var plan = StaticStore.GetPlan(planType);
var claims = new List<Claim>
{
new (nameof(OrganizationLicenseConstants.PlanType), organization.PlanType.ToString()),
new (nameof(OrganizationLicenseConstants.SmMaxProjects), plan.SecretsManager.MaxProjects.ToString())
};
var identity = new ClaimsIdentity(claims, "TestAuthenticationType");
var claimsPrincipal = new ClaimsPrincipal(identity);
sutProvider.GetDependency<ILicensingService>().ReadOrganizationLicenseAsync(organization).Returns(license);
sutProvider.GetDependency<ILicensingService>().GetClaimsPrincipalFromLicense(license).Returns(claimsPrincipal);
var (max, overMax) = await sutProvider.Sut.GetByOrgIdAsync(organization.Id, projectsToAdd); var (max, overMax) = await sutProvider.Sut.GetByOrgIdAsync(organization.Id, projectsToAdd);

View File

@ -11,6 +11,7 @@ MAILCATCHER_PORT=1080
# Alternative databases # Alternative databases
POSTGRES_PASSWORD=SET_A_PASSWORD_HERE_123 POSTGRES_PASSWORD=SET_A_PASSWORD_HERE_123
MYSQL_ROOT_PASSWORD=SET_A_PASSWORD_HERE_123 MYSQL_ROOT_PASSWORD=SET_A_PASSWORD_HERE_123
MARIADB_ROOT_PASSWORD=SET_A_PASSWORD_HERE_123
# IdP configuration # IdP configuration
# Complete using the values from the Manage SSO page in the web vault # Complete using the values from the Manage SSO page in the web vault

View File

@ -70,6 +70,20 @@ services:
profiles: profiles:
- mysql - mysql
mariadb:
image: mariadb:10
ports:
- 4306:3306
environment:
MARIADB_USER: maria
MARIADB_PASSWORD: ${MARIADB_ROOT_PASSWORD}
MARIADB_DATABASE: vault_dev
MARIADB_RANDOM_ROOT_PASSWORD: "true"
volumes:
- mariadb_dev_data:/var/lib/mysql
profiles:
- mariadb
idp: idp:
image: kenchan0130/simplesamlphp:1.19.8 image: kenchan0130/simplesamlphp:1.19.8
container_name: idp container_name: idp

View File

@ -5,6 +5,7 @@ param(
[switch]$all, [switch]$all,
[switch]$postgres, [switch]$postgres,
[switch]$mysql, [switch]$mysql,
[switch]$mariadb,
[switch]$mssql, [switch]$mssql,
[switch]$sqlite, [switch]$sqlite,
[switch]$selfhost, [switch]$selfhost,
@ -15,11 +16,15 @@ param(
$ErrorActionPreference = "Stop" $ErrorActionPreference = "Stop"
$currentDir = Get-Location $currentDir = Get-Location
if (!$all -and !$postgres -and !$mysql -and !$sqlite) { function Get-IsEFDatabase {
return $postgres -or $mysql -or $mariadb -or $sqlite;
}
if (!$all -and !$(Get-IsEFDatabase)) {
$mssql = $true; $mssql = $true;
} }
if ($all -or $postgres -or $mysql -or $sqlite) { if ($all -or $(Get-IsEFDatabase)) {
dotnet ef *> $null dotnet ef *> $null
if ($LASTEXITCODE -ne 0) { if ($LASTEXITCODE -ne 0) {
Write-Host "Entity Framework Core tools were not found in the dotnet global tools. Attempting to install" Write-Host "Entity Framework Core tools were not found in the dotnet global tools. Attempting to install"
@ -60,9 +65,12 @@ if ($all -or $mssql) {
} }
Foreach ($item in @( Foreach ($item in @(
@($mysql, "MySQL", "MySqlMigrations", "mySql", 2),
@($postgres, "PostgreSQL", "PostgresMigrations", "postgreSql", 0), @($postgres, "PostgreSQL", "PostgresMigrations", "postgreSql", 0),
@($sqlite, "SQLite", "SqliteMigrations", "sqlite", 1) @($sqlite, "SQLite", "SqliteMigrations", "sqlite", 1),
@($mysql, "MySQL", "MySqlMigrations", "mySql", 2),
# MariaDB shares the MySQL connection string in the server config so they are mutually exclusive in that context.
# However they can still be run independently for integration tests.
@($mariadb, "MariaDB", "MySqlMigrations", "mySql", 3)
)) { )) {
if (!$item[0] -and !$all) { if (!$item[0] -and !$all) {
continue continue

View File

@ -4,6 +4,7 @@
"rollForward": "latestFeature" "rollForward": "latestFeature"
}, },
"msbuild-sdks": { "msbuild-sdks": {
"Microsoft.Build.Traversal": "4.1.0" "Microsoft.Build.Traversal": "4.1.0",
"Microsoft.Build.Sql": "0.1.9-preview"
} }
} }

View File

@ -40,8 +40,6 @@ export function authenticate(
payload["deviceName"] = "chrome"; payload["deviceName"] = "chrome";
payload["username"] = username; payload["username"] = username;
payload["password"] = password; payload["password"] = password;
params.headers["Auth-Email"] = encoding.b64encode(username);
} else { } else {
payload["scope"] = "api.organization"; payload["scope"] = "api.organization";
payload["grant_type"] = "client_credentials"; payload["grant_type"] = "client_credentials";

View File

@ -11,7 +11,7 @@ using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Enums; using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Extensions;
using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Services; using Bit.Core.Billing.Providers.Services;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Models.OrganizationConnectionConfigs; using Bit.Core.Models.OrganizationConnectionConfigs;
@ -462,6 +462,7 @@ public class OrganizationsController : Controller
organization.UsersGetPremium = model.UsersGetPremium; organization.UsersGetPremium = model.UsersGetPremium;
organization.UseSecretsManager = model.UseSecretsManager; organization.UseSecretsManager = model.UseSecretsManager;
organization.UseRiskInsights = model.UseRiskInsights; organization.UseRiskInsights = model.UseRiskInsights;
organization.UseOrganizationDomains = model.UseOrganizationDomains;
organization.UseAdminSponsoredFamilies = model.UseAdminSponsoredFamilies; organization.UseAdminSponsoredFamilies = model.UseAdminSponsoredFamilies;
//secrets //secrets

View File

@ -10,13 +10,13 @@ using Bit.Core.AdminConsole.Providers.Interfaces;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Services; using Bit.Core.AdminConsole.Services;
using Bit.Core.Billing.Constants; using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Entities;
using Bit.Core.Billing.Enums; using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Extensions;
using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Repositories; using Bit.Core.Billing.Providers.Entities;
using Bit.Core.Billing.Services; using Bit.Core.Billing.Providers.Models;
using Bit.Core.Billing.Services.Contracts; using Bit.Core.Billing.Providers.Repositories;
using Bit.Core.Billing.Providers.Services;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.Repositories; using Bit.Core.Repositories;

View File

@ -102,7 +102,7 @@ public class OrganizationEditModel : OrganizationViewModel
MaxAutoscaleSmSeats = org.MaxAutoscaleSmSeats; MaxAutoscaleSmSeats = org.MaxAutoscaleSmSeats;
SmServiceAccounts = org.SmServiceAccounts; SmServiceAccounts = org.SmServiceAccounts;
MaxAutoscaleSmServiceAccounts = org.MaxAutoscaleSmServiceAccounts; MaxAutoscaleSmServiceAccounts = org.MaxAutoscaleSmServiceAccounts;
UseOrganizationDomains = org.UseOrganizationDomains;
_plans = plans; _plans = plans;
} }
@ -186,6 +186,8 @@ public class OrganizationEditModel : OrganizationViewModel
public int? SmServiceAccounts { get; set; } public int? SmServiceAccounts { get; set; }
[Display(Name = "Max Autoscale Machine Accounts")] [Display(Name = "Max Autoscale Machine Accounts")]
public int? MaxAutoscaleSmServiceAccounts { get; set; } public int? MaxAutoscaleSmServiceAccounts { get; set; }
[Display(Name = "Use Organization Domains")]
public bool UseOrganizationDomains { get; set; }
/** /**
* Creates a Plan[] object for use in Javascript * Creates a Plan[] object for use in Javascript
@ -215,6 +217,7 @@ public class OrganizationEditModel : OrganizationViewModel
Has2fa = p.Has2fa, Has2fa = p.Has2fa,
HasApi = p.HasApi, HasApi = p.HasApi,
HasSso = p.HasSso, HasSso = p.HasSso,
HasOrganizationDomains = p.HasOrganizationDomains,
HasKeyConnector = p.HasKeyConnector, HasKeyConnector = p.HasKeyConnector,
HasScim = p.HasScim, HasScim = p.HasScim,
HasResetPassword = p.HasResetPassword, HasResetPassword = p.HasResetPassword,
@ -315,6 +318,7 @@ public class OrganizationEditModel : OrganizationViewModel
existingOrganization.MaxAutoscaleSmSeats = MaxAutoscaleSmSeats; existingOrganization.MaxAutoscaleSmSeats = MaxAutoscaleSmSeats;
existingOrganization.SmServiceAccounts = SmServiceAccounts; existingOrganization.SmServiceAccounts = SmServiceAccounts;
existingOrganization.MaxAutoscaleSmServiceAccounts = MaxAutoscaleSmServiceAccounts; existingOrganization.MaxAutoscaleSmServiceAccounts = MaxAutoscaleSmServiceAccounts;
existingOrganization.UseOrganizationDomains = UseOrganizationDomains;
return existingOrganization; return existingOrganization;
} }
} }

View File

@ -2,8 +2,8 @@
using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.Models.Data.Provider; using Bit.Core.AdminConsole.Models.Data.Provider;
using Bit.Core.Billing.Entities;
using Bit.Core.Billing.Enums; using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Providers.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.SharedWeb.Utilities; using Bit.SharedWeb.Utilities;

View File

@ -2,8 +2,8 @@
using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.Models.Data.Provider; using Bit.Core.AdminConsole.Models.Data.Provider;
using Bit.Core.Billing.Entities;
using Bit.Core.Billing.Enums; using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Providers.Entities;
namespace Bit.Admin.AdminConsole.Models; namespace Bit.Admin.AdminConsole.Models;

View File

@ -124,6 +124,10 @@
<input type="checkbox" class="form-check-input" asp-for="UseSso" disabled='@(canEditPlan ? null : "disabled")'> <input type="checkbox" class="form-check-input" asp-for="UseSso" disabled='@(canEditPlan ? null : "disabled")'>
<label class="form-check-label" asp-for="UseSso"></label> <label class="form-check-label" asp-for="UseSso"></label>
</div> </div>
<div class="form-check">
<input type="checkbox" class="form-check-input" asp-for="UseOrganizationDomains" disabled='@(canEditPlan ? null : "disabled")'>
<label class="form-check-label" asp-for="UseOrganizationDomains"></label>
</div>
<div class="form-check"> <div class="form-check">
<input type="checkbox" class="form-check-input" asp-for="UseKeyConnector" disabled='@(canEditPlan ? null : "disabled")'> <input type="checkbox" class="form-check-input" asp-for="UseKeyConnector" disabled='@(canEditPlan ? null : "disabled")'>
<label class="form-check-label" asp-for="UseKeyConnector"></label> <label class="form-check-label" asp-for="UseKeyConnector"></label>

View File

@ -69,6 +69,7 @@
document.getElementById('@(nameof(Model.UseGroups))').checked = plan.hasGroups; document.getElementById('@(nameof(Model.UseGroups))').checked = plan.hasGroups;
document.getElementById('@(nameof(Model.UsePolicies))').checked = plan.hasPolicies; document.getElementById('@(nameof(Model.UsePolicies))').checked = plan.hasPolicies;
document.getElementById('@(nameof(Model.UseSso))').checked = plan.hasSso; document.getElementById('@(nameof(Model.UseSso))').checked = plan.hasSso;
document.getElementById('@(nameof(Model.UseOrganizationDomains))').checked = plan.hasOrganizationDomains;
document.getElementById('@(nameof(Model.UseScim))').checked = plan.hasScim; document.getElementById('@(nameof(Model.UseScim))').checked = plan.hasScim;
document.getElementById('@(nameof(Model.UseDirectory))').checked = plan.hasDirectory; document.getElementById('@(nameof(Model.UseDirectory))').checked = plan.hasDirectory;
document.getElementById('@(nameof(Model.UseEvents))').checked = plan.hasEvents; document.getElementById('@(nameof(Model.UseEvents))').checked = plan.hasEvents;

View File

@ -7,7 +7,7 @@ using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Services; using Bit.Core.Billing.Providers.Services;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Utilities; using Bit.Core.Utilities;

View File

@ -1,8 +1,8 @@
using Bit.Admin.Billing.Models; using Bit.Admin.Billing.Models;
using Bit.Admin.Enums; using Bit.Admin.Enums;
using Bit.Admin.Utilities; using Bit.Admin.Utilities;
using Bit.Core.Billing.Migration.Models; using Bit.Core.Billing.Providers.Migration.Models;
using Bit.Core.Billing.Migration.Services; using Bit.Core.Billing.Providers.Migration.Services;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;

View File

@ -1,4 +1,4 @@
using Bit.Core.Billing.Entities; using Bit.Core.Billing.Providers.Entities;
namespace Bit.Admin.Billing.Models; namespace Bit.Admin.Billing.Models;

View File

@ -1,5 +1,5 @@
@using System.Text.Json @using System.Text.Json
@model Bit.Core.Billing.Migration.Models.ProviderMigrationResult @model Bit.Core.Billing.Providers.Migration.Models.ProviderMigrationResult
@{ @{
ViewData["Title"] = "Results"; ViewData["Title"] = "Results";
} }

View File

@ -1,4 +1,4 @@
@model Bit.Core.Billing.Migration.Models.ProviderMigrationResult[] @model Bit.Core.Billing.Providers.Migration.Models.ProviderMigrationResult[]
@{ @{
ViewData["Title"] = "Results"; ViewData["Title"] = "Results";
} }

View File

@ -4,7 +4,6 @@ using Bit.Admin.Enums;
using Bit.Admin.Models; using Bit.Admin.Models;
using Bit.Admin.Services; using Bit.Admin.Services;
using Bit.Admin.Utilities; using Bit.Admin.Utilities;
using Bit.Core;
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
@ -89,7 +88,7 @@ public class UsersController : Controller
var ciphers = await _cipherRepository.GetManyByUserIdAsync(id); var ciphers = await _cipherRepository.GetManyByUserIdAsync(id);
var isTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user); var isTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user);
var verifiedDomain = await AccountDeprovisioningEnabled(user.Id); var verifiedDomain = await _userService.IsClaimedByAnyOrganizationAsync(user.Id);
return View(UserViewModel.MapViewModel(user, isTwoFactorEnabled, ciphers, verifiedDomain)); return View(UserViewModel.MapViewModel(user, isTwoFactorEnabled, ciphers, verifiedDomain));
} }
@ -106,7 +105,7 @@ public class UsersController : Controller
var billingInfo = await _paymentService.GetBillingAsync(user); var billingInfo = await _paymentService.GetBillingAsync(user);
var billingHistoryInfo = await _paymentService.GetBillingHistoryAsync(user); var billingHistoryInfo = await _paymentService.GetBillingHistoryAsync(user);
var isTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user); var isTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user);
var verifiedDomain = await AccountDeprovisioningEnabled(user.Id); var verifiedDomain = await _userService.IsClaimedByAnyOrganizationAsync(user.Id);
var deviceVerificationRequired = await _userService.ActiveNewDeviceVerificationException(user.Id); var deviceVerificationRequired = await _userService.ActiveNewDeviceVerificationException(user.Id);
return View(new UserEditModel(user, isTwoFactorEnabled, ciphers, billingInfo, billingHistoryInfo, _globalSettings, verifiedDomain, deviceVerificationRequired)); return View(new UserEditModel(user, isTwoFactorEnabled, ciphers, billingInfo, billingHistoryInfo, _globalSettings, verifiedDomain, deviceVerificationRequired));
@ -178,12 +177,4 @@ public class UsersController : Controller
await _userService.ToggleNewDeviceVerificationException(user.Id); await _userService.ToggleNewDeviceVerificationException(user.Id);
return RedirectToAction("Edit", new { id }); return RedirectToAction("Edit", new { id });
} }
// TODO: Feature flag to be removed in PM-14207
private async Task<bool?> AccountDeprovisioningEnabled(Guid userId)
{
return _featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
? await _userService.IsClaimedByAnyOrganizationAsync(userId)
: null;
}
} }

View File

@ -17,7 +17,7 @@ public class ChargeBraintreeModel : IValidatableObject
{ {
if (Id != null) if (Id != null)
{ {
if (Id.Length != 36 || (Id[0] != 'o' && Id[0] != 'u') || if (Id.Length != 36 || (Id[0] != 'o' && Id[0] != 'u' && Id[0] != 'p') ||
!Guid.TryParse(Id.Substring(1, 32), out var guid)) !Guid.TryParse(Id.Substring(1, 32), out var guid))
{ {
yield return new ValidationResult("Customer Id is not a valid format."); yield return new ValidationResult("Customer Id is not a valid format.");

View File

@ -10,7 +10,7 @@ using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.DependencyInjection.Extensions;
using Bit.Admin.Services; using Bit.Admin.Services;
using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Extensions;
using Bit.Core.Billing.Migration; using Bit.Core.Billing.Providers.Migration;
#if !OSS #if !OSS
using Bit.Commercial.Core.Utilities; using Bit.Commercial.Core.Utilities;

View File

@ -2,7 +2,7 @@
using Bit.Core; using Bit.Core;
using Bit.Core.Jobs; using Bit.Core.Jobs;
using Bit.Core.Tools.Repositories; using Bit.Core.Tools.Repositories;
using Bit.Core.Tools.Services; using Bit.Core.Tools.SendFeatures.Commands.Interfaces;
using Quartz; using Quartz;
namespace Bit.Admin.Tools.Jobs; namespace Bit.Admin.Tools.Jobs;
@ -32,10 +32,10 @@ public class DeleteSendsJob : BaseJob
} }
using (var scope = _serviceProvider.CreateScope()) using (var scope = _serviceProvider.CreateScope())
{ {
var sendService = scope.ServiceProvider.GetRequiredService<ISendService>(); var nonAnonymousSendCommand = scope.ServiceProvider.GetRequiredService<INonAnonymousSendCommand>();
foreach (var send in sends) foreach (var send in sends)
{ {
await sendService.DeleteSendAsync(send); await nonAnonymousSendCommand.DeleteSendAsync(send);
} }
} }
} }

View File

@ -9,18 +9,18 @@
"version": "0.0.0", "version": "0.0.0",
"license": "GPL-3.0", "license": "GPL-3.0",
"dependencies": { "dependencies": {
"bootstrap": "5.3.3", "bootstrap": "5.3.6",
"font-awesome": "4.7.0", "font-awesome": "4.7.0",
"jquery": "3.7.1", "jquery": "3.7.1",
"toastr": "2.1.4" "toastr": "2.1.4"
}, },
"devDependencies": { "devDependencies": {
"css-loader": "7.1.2", "css-loader": "7.1.2",
"expose-loader": "5.0.0", "expose-loader": "5.0.1",
"mini-css-extract-plugin": "2.9.2", "mini-css-extract-plugin": "2.9.2",
"sass": "1.85.0", "sass": "1.88.0",
"sass-loader": "16.0.4", "sass-loader": "16.0.5",
"webpack": "5.97.1", "webpack": "5.99.8",
"webpack-cli": "5.1.4" "webpack-cli": "5.1.4"
} }
}, },
@ -456,13 +456,13 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "22.13.14", "version": "22.15.21",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.14.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.21.tgz",
"integrity": "sha512-Zs/Ollc1SJ8nKUAgc7ivOEdIBM8JAKgrqqUYi2J997JuKO7/tpQC+WCetQ1sypiKCQWHdvdg9wBNpUPEWZae7w==", "integrity": "sha512-EV/37Td6c+MgKAbkcLG6vqZ2zEYHD7bvSrzqqs2RIhbA6w3x+Dqz8MZM3sP6kGTeLrdoOgKZe+Xja7tUB2DNkQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"undici-types": "~6.20.0" "undici-types": "~6.21.0"
} }
}, },
"node_modules/@webassemblyjs/ast": { "node_modules/@webassemblyjs/ast": {
@ -749,9 +749,9 @@
} }
}, },
"node_modules/bootstrap": { "node_modules/bootstrap": {
"version": "5.3.3", "version": "5.3.6",
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.3.tgz", "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.6.tgz",
"integrity": "sha512-8HLCdWgyoMguSO9o+aH+iuZ+aht+mzW0u3HIMzVu7Srrpv7EBBxTnrFlSCskwdY1+EOFQSm7uMJhNQHkdPcmjg==", "integrity": "sha512-jX0GAcRzvdwISuvArXn3m7KZscWWFAf1MKBcnzaN02qWMb3jpMoUX4/qgeiGzqyIb4ojulRzs89UCUmGcFSzTA==",
"funding": [ "funding": [
{ {
"type": "github", "type": "github",
@ -782,9 +782,9 @@
} }
}, },
"node_modules/browserslist": { "node_modules/browserslist": {
"version": "4.24.4", "version": "4.24.5",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.5.tgz",
"integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", "integrity": "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@ -802,10 +802,10 @@
], ],
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"caniuse-lite": "^1.0.30001688", "caniuse-lite": "^1.0.30001716",
"electron-to-chromium": "^1.5.73", "electron-to-chromium": "^1.5.149",
"node-releases": "^2.0.19", "node-releases": "^2.0.19",
"update-browserslist-db": "^1.1.1" "update-browserslist-db": "^1.1.3"
}, },
"bin": { "bin": {
"browserslist": "cli.js" "browserslist": "cli.js"
@ -822,9 +822,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/caniuse-lite": { "node_modules/caniuse-lite": {
"version": "1.0.30001707", "version": "1.0.30001718",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001707.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001718.tgz",
"integrity": "sha512-3qtRjw/HQSMlDWf+X79N206fepf4SOOU6SQLMaq/0KkZLmSjPxAkBOQQ+FxbHKfHmYLZFfdWsO3KA90ceHPSnw==", "integrity": "sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@ -976,9 +976,9 @@
} }
}, },
"node_modules/electron-to-chromium": { "node_modules/electron-to-chromium": {
"version": "1.5.128", "version": "1.5.155",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.128.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.155.tgz",
"integrity": "sha512-bo1A4HH/NS522Ws0QNFIzyPcyUUNV/yyy70Ho1xqfGYzPUme2F/xr4tlEOuM6/A538U1vDA7a4XfCd1CKRegKQ==", "integrity": "sha512-ps5KcGGmwL8VaeJlvlDlu4fORQpv3+GIcF5I3f9tUKUlJ/wsysh6HU8P5L1XWRYeXfA0oJd4PyM8ds8zTFf6Ng==",
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
@ -1010,9 +1010,9 @@
} }
}, },
"node_modules/es-module-lexer": { "node_modules/es-module-lexer": {
"version": "1.6.0", "version": "1.7.0",
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.6.0.tgz", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
"integrity": "sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==", "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
@ -1084,9 +1084,9 @@
} }
}, },
"node_modules/expose-loader": { "node_modules/expose-loader": {
"version": "5.0.0", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/expose-loader/-/expose-loader-5.0.0.tgz", "resolved": "https://registry.npmjs.org/expose-loader/-/expose-loader-5.0.1.tgz",
"integrity": "sha512-BtUqYRmvx1bEY5HN6eK2I9URUZgNmN0x5UANuocaNjXSgfoDlkXt+wyEMe7i5DzDNh2BKJHPc5F4rBwEdSQX6w==", "integrity": "sha512-5YPZuszN/eWND/B+xuq5nIpb/l5TV1HYmdO6SubYtHv+HenVw9/6bn33Mm5reY8DNid7AVtbARvyUD34edfCtg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@ -1107,13 +1107,6 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/fast-json-stable-stringify": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
"dev": true,
"license": "MIT"
},
"node_modules/fast-uri": { "node_modules/fast-uri": {
"version": "3.0.6", "version": "3.0.6",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz",
@ -1249,9 +1242,9 @@
} }
}, },
"node_modules/immutable": { "node_modules/immutable": {
"version": "5.1.1", "version": "5.1.2",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.1.tgz", "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.2.tgz",
"integrity": "sha512-3jatXi9ObIsPGr3N5hGw/vWWcTkq6hUYhpQz4k0wLC+owqWi/LiugIw9x0EdNZ2yGedKN/HzePiBvaJRXa0Ujg==", "integrity": "sha512-qHKXW1q6liAk1Oys6umoaZbDRqjcjgSrbnrifHsfsttza7zcvRAsL7mMV6xWcyhwQy7Xj5v4hhbr6b+iDYwlmQ==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
@ -1755,16 +1748,6 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/randombytes": { "node_modules/randombytes": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
@ -1878,9 +1861,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/sass": { "node_modules/sass": {
"version": "1.85.0", "version": "1.88.0",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.85.0.tgz", "resolved": "https://registry.npmjs.org/sass/-/sass-1.88.0.tgz",
"integrity": "sha512-3ToiC1xZ1Y8aU7+CkgCI/tqyuPXEmYGJXO7H4uqp0xkLXUqp88rQQ4j1HmP37xSJLbCJPaIiv+cT1y+grssrww==", "integrity": "sha512-sF6TWQqjFvr4JILXzG4ucGOLELkESHL+I5QJhh7CNaE+Yge0SI+ehCatsXhJ7ymU1hAFcIS3/PBpjdIbXoyVbg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -1899,9 +1882,9 @@
} }
}, },
"node_modules/sass-loader": { "node_modules/sass-loader": {
"version": "16.0.4", "version": "16.0.5",
"resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.4.tgz", "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.5.tgz",
"integrity": "sha512-LavLbgbBGUt3wCiYzhuLLu65+fWXaXLmq7YxivLhEqmiupCFZ5sKUAipK3do6V80YSU0jvSxNhEdT13IXNr3rg==", "integrity": "sha512-oL+CMBXrj6BZ/zOq4os+UECPL+bWqt6OAC6DWS8Ln8GZRcMDjlJ4JC3FBDuHJdYaFWIdKNIBYmtZtK2MaMkNIw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -1940,9 +1923,9 @@
} }
}, },
"node_modules/schema-utils": { "node_modules/schema-utils": {
"version": "4.3.0", "version": "4.3.2",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.0.tgz", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz",
"integrity": "sha512-Gf9qqc58SpCA/xdziiHz35F4GNIWYWZrEshUc/G/r5BnLph6xpKuLeoJoQuj5WfBIx/eQLf+hmVPYHaxJu7V2g==", "integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -1960,9 +1943,9 @@
} }
}, },
"node_modules/semver": { "node_modules/semver": {
"version": "7.7.1", "version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
"integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
"dev": true, "dev": true,
"license": "ISC", "license": "ISC",
"bin": { "bin": {
@ -2079,9 +2062,9 @@
} }
}, },
"node_modules/tapable": { "node_modules/tapable": {
"version": "2.2.1", "version": "2.2.2",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz",
"integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", "integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@ -2089,14 +2072,14 @@
} }
}, },
"node_modules/terser": { "node_modules/terser": {
"version": "5.39.0", "version": "5.39.2",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.39.0.tgz", "resolved": "https://registry.npmjs.org/terser/-/terser-5.39.2.tgz",
"integrity": "sha512-LBAhFyLho16harJoWMg/nZsQYgTrg5jXOn2nCYjRUcZZEdE3qa2zb8QEDRUGVZBW4rlazf2fxkg8tztybTaqWw==", "integrity": "sha512-yEPUmWve+VA78bI71BW70Dh0TuV4HHd+I5SHOAfS1+QBOmvmCiiffgjR8ryyEd3KIfvPGFqoADt8LdQ6XpXIvg==",
"dev": true, "dev": true,
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"dependencies": { "dependencies": {
"@jridgewell/source-map": "^0.3.3", "@jridgewell/source-map": "^0.3.3",
"acorn": "^8.8.2", "acorn": "^8.14.0",
"commander": "^2.20.0", "commander": "^2.20.0",
"source-map-support": "~0.5.20" "source-map-support": "~0.5.20"
}, },
@ -2165,9 +2148,9 @@
} }
}, },
"node_modules/undici-types": { "node_modules/undici-types": {
"version": "6.20.0", "version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
@ -2202,16 +2185,6 @@
"browserslist": ">= 4.21.0" "browserslist": ">= 4.21.0"
} }
}, },
"node_modules/uri-js": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
"integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"punycode": "^2.1.0"
}
},
"node_modules/util-deprecate": { "node_modules/util-deprecate": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@ -2220,9 +2193,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/watchpack": { "node_modules/watchpack": {
"version": "2.4.2", "version": "2.4.4",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz",
"integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==", "integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -2234,14 +2207,15 @@
} }
}, },
"node_modules/webpack": { "node_modules/webpack": {
"version": "5.97.1", "version": "5.99.8",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.97.1.tgz", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.99.8.tgz",
"integrity": "sha512-EksG6gFY3L1eFMROS/7Wzgrii5mBAFe4rIr3r2BTfo7bcc+DWwFZ4OJ/miOuHJO/A85HwyI4eQ0F6IKXesO7Fg==", "integrity": "sha512-lQ3CPiSTpfOnrEGeXDwoq5hIGzSjmwD72GdfVzF7CQAI7t47rJG9eDWvcEkEn3CUQymAElVvDg3YNTlCYj+qUQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@types/eslint-scope": "^3.7.7", "@types/eslint-scope": "^3.7.7",
"@types/estree": "^1.0.6", "@types/estree": "^1.0.6",
"@types/json-schema": "^7.0.15",
"@webassemblyjs/ast": "^1.14.1", "@webassemblyjs/ast": "^1.14.1",
"@webassemblyjs/wasm-edit": "^1.14.1", "@webassemblyjs/wasm-edit": "^1.14.1",
"@webassemblyjs/wasm-parser": "^1.14.1", "@webassemblyjs/wasm-parser": "^1.14.1",
@ -2258,9 +2232,9 @@
"loader-runner": "^4.2.0", "loader-runner": "^4.2.0",
"mime-types": "^2.1.27", "mime-types": "^2.1.27",
"neo-async": "^2.6.2", "neo-async": "^2.6.2",
"schema-utils": "^3.2.0", "schema-utils": "^4.3.2",
"tapable": "^2.1.1", "tapable": "^2.1.1",
"terser-webpack-plugin": "^5.3.10", "terser-webpack-plugin": "^5.3.11",
"watchpack": "^2.4.1", "watchpack": "^2.4.1",
"webpack-sources": "^3.2.3" "webpack-sources": "^3.2.3"
}, },
@ -2361,59 +2335,6 @@
"node": ">=10.13.0" "node": ">=10.13.0"
} }
}, },
"node_modules/webpack/node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true,
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
"json-schema-traverse": "^0.4.1",
"uri-js": "^4.2.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/webpack/node_modules/ajv-keywords": {
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
"integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"ajv": "^6.9.1"
}
},
"node_modules/webpack/node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
"dev": true,
"license": "MIT"
},
"node_modules/webpack/node_modules/schema-utils": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz",
"integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/json-schema": "^7.0.8",
"ajv": "^6.12.5",
"ajv-keywords": "^3.5.2"
},
"engines": {
"node": ">= 10.13.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/webpack"
}
},
"node_modules/which": { "node_modules/which": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",

View File

@ -8,18 +8,18 @@
"build": "webpack" "build": "webpack"
}, },
"dependencies": { "dependencies": {
"bootstrap": "5.3.3", "bootstrap": "5.3.6",
"font-awesome": "4.7.0", "font-awesome": "4.7.0",
"jquery": "3.7.1", "jquery": "3.7.1",
"toastr": "2.1.4" "toastr": "2.1.4"
}, },
"devDependencies": { "devDependencies": {
"css-loader": "7.1.2", "css-loader": "7.1.2",
"expose-loader": "5.0.0", "expose-loader": "5.0.1",
"mini-css-extract-plugin": "2.9.2", "mini-css-extract-plugin": "2.9.2",
"sass": "1.85.0", "sass": "1.88.0",
"sass-loader": "16.0.4", "sass-loader": "16.0.5",
"webpack": "5.97.1", "webpack": "5.99.8",
"webpack-cli": "5.1.4" "webpack-cli": "5.1.4"
} }
} }

View File

@ -2,13 +2,11 @@
using Bit.Api.AdminConsole.Models.Request.Organizations; using Bit.Api.AdminConsole.Models.Request.Organizations;
using Bit.Api.AdminConsole.Models.Response.Organizations; using Bit.Api.AdminConsole.Models.Response.Organizations;
using Bit.Api.Models.Response; using Bit.Api.Models.Response;
using Bit.Core;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Utilities;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@ -137,7 +135,6 @@ public class OrganizationDomainController : Controller
[AllowAnonymous] [AllowAnonymous]
[HttpPost("domain/sso/verified")] [HttpPost("domain/sso/verified")]
[RequireFeature(FeatureFlagKeys.VerifiedSsoDomainEndpoint)]
public async Task<VerifiedOrganizationDomainSsoDetailsResponseModel> GetVerifiedOrgDomainSsoDetailsAsync( public async Task<VerifiedOrganizationDomainSsoDetailsResponseModel> GetVerifiedOrgDomainSsoDetailsAsync(
[FromBody] OrganizationDomainSsoDetailsRequestModel model) [FromBody] OrganizationDomainSsoDetailsRequestModel model)
{ {

View File

@ -616,7 +616,6 @@ public class OrganizationUsersController : Controller
new OrganizationUserBulkResponseModel(r.OrganizationUserId, r.ErrorMessage))); new OrganizationUserBulkResponseModel(r.OrganizationUserId, r.ErrorMessage)));
} }
[RequireFeature(FeatureFlagKeys.AccountDeprovisioning)]
[HttpDelete("{id}/delete-account")] [HttpDelete("{id}/delete-account")]
[HttpPost("{id}/delete-account")] [HttpPost("{id}/delete-account")]
public async Task DeleteAccount(Guid orgId, Guid id) public async Task DeleteAccount(Guid orgId, Guid id)
@ -635,7 +634,6 @@ public class OrganizationUsersController : Controller
await _deleteClaimedOrganizationUserAccountCommand.DeleteUserAsync(orgId, id, currentUser.Id); await _deleteClaimedOrganizationUserAccountCommand.DeleteUserAsync(orgId, id, currentUser.Id);
} }
[RequireFeature(FeatureFlagKeys.AccountDeprovisioning)]
[HttpDelete("delete-account")] [HttpDelete("delete-account")]
[HttpPost("delete-account")] [HttpPost("delete-account")]
public async Task<ListResponseModel<OrganizationUserBulkResponseModel>> BulkDeleteAccount(Guid orgId, [FromBody] OrganizationUserBulkRequestModel model) public async Task<ListResponseModel<OrganizationUserBulkResponseModel>> BulkDeleteAccount(Guid orgId, [FromBody] OrganizationUserBulkRequestModel model)
@ -760,11 +758,6 @@ public class OrganizationUsersController : Controller
private async Task<IDictionary<Guid, bool>> GetClaimedByOrganizationStatusAsync(Guid orgId, IEnumerable<Guid> userIds) private async Task<IDictionary<Guid, bool>> GetClaimedByOrganizationStatusAsync(Guid orgId, IEnumerable<Guid> userIds)
{ {
if (!_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning))
{
return userIds.ToDictionary(kvp => kvp, kvp => false);
}
var usersOrganizationClaimedStatus = await _getOrganizationUsersClaimedStatusQuery.GetUsersOrganizationClaimedStatusAsync(orgId, userIds); var usersOrganizationClaimedStatus = await _getOrganizationUsersClaimedStatusQuery.GetUsersOrganizationClaimedStatusAsync(orgId, userIds);
return usersOrganizationClaimedStatus; return usersOrganizationClaimedStatus;
} }

View File

@ -25,7 +25,7 @@ using Bit.Core.Auth.Services;
using Bit.Core.Billing.Enums; using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Extensions;
using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Services; using Bit.Core.Billing.Providers.Services;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
@ -279,8 +279,7 @@ public class OrganizationsController : Controller
throw new BadRequestException("Your organization's Single Sign-On settings prevent you from leaving."); throw new BadRequestException("Your organization's Single Sign-On settings prevent you from leaving.");
} }
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning) if ((await _userService.GetOrganizationsClaimingUserAsync(user.Id)).Any(x => x.Id == id))
&& (await _userService.GetOrganizationsClaimingUserAsync(user.Id)).Any(x => x.Id == id))
{ {
throw new BadRequestException("Claimed user account cannot leave claiming organization. Contact your organization administrator for additional details."); throw new BadRequestException("Claimed user account cannot leave claiming organization. Contact your organization administrator for additional details.");
} }

View File

@ -2,7 +2,6 @@
using Bit.Api.AdminConsole.Models.Response.Helpers; using Bit.Api.AdminConsole.Models.Response.Helpers;
using Bit.Api.AdminConsole.Models.Response.Organizations; using Bit.Api.AdminConsole.Models.Response.Organizations;
using Bit.Api.Models.Response; using Bit.Api.Models.Response;
using Bit.Core;
using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;
@ -79,7 +78,7 @@ public class PoliciesController : Controller
return new PolicyDetailResponseModel(new Policy { Type = (PolicyType)type }); return new PolicyDetailResponseModel(new Policy { Type = (PolicyType)type });
} }
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning) && policy.Type is PolicyType.SingleOrg) if (policy.Type is PolicyType.SingleOrg)
{ {
return await policy.GetSingleOrgPolicyDetailResponseAsync(_organizationHasVerifiedDomainsQuery); return await policy.GetSingleOrgPolicyDetailResponseAsync(_organizationHasVerifiedDomainsQuery);
} }

View File

@ -2,7 +2,7 @@
using Bit.Api.Billing.Models.Requests; using Bit.Api.Billing.Models.Requests;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Services; using Bit.Core.AdminConsole.Services;
using Bit.Core.Billing.Services; using Bit.Core.Billing.Providers.Services;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Models.Business; using Bit.Core.Models.Business;

View File

@ -75,6 +75,8 @@ public class OrganizationCreateRequestModel : IValidatableObject
public string InitiationPath { get; set; } public string InitiationPath { get; set; }
public bool SkipTrial { get; set; }
public virtual OrganizationSignup ToOrganizationSignup(User user) public virtual OrganizationSignup ToOrganizationSignup(User user)
{ {
var orgSignup = new OrganizationSignup var orgSignup = new OrganizationSignup
@ -107,6 +109,7 @@ public class OrganizationCreateRequestModel : IValidatableObject
BillingAddressCountry = BillingAddressCountry, BillingAddressCountry = BillingAddressCountry,
}, },
InitiationPath = InitiationPath, InitiationPath = InitiationPath,
SkipTrial = SkipTrial
}; };
Keys?.ToOrganizationSignup(orgSignup); Keys?.ToOrganizationSignup(orgSignup);

View File

@ -64,6 +64,7 @@ public class OrganizationResponseModel : ResponseModel
LimitItemDeletion = organization.LimitItemDeletion; LimitItemDeletion = organization.LimitItemDeletion;
AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems; AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems;
UseRiskInsights = organization.UseRiskInsights; UseRiskInsights = organization.UseRiskInsights;
UseOrganizationDomains = organization.UseOrganizationDomains;
UseAdminSponsoredFamilies = organization.UseAdminSponsoredFamilies; UseAdminSponsoredFamilies = organization.UseAdminSponsoredFamilies;
} }
@ -111,6 +112,7 @@ public class OrganizationResponseModel : ResponseModel
public bool LimitItemDeletion { get; set; } public bool LimitItemDeletion { get; set; }
public bool AllowAdminAccessToAllCollectionItems { get; set; } public bool AllowAdminAccessToAllCollectionItems { get; set; }
public bool UseRiskInsights { get; set; } public bool UseRiskInsights { get; set; }
public bool UseOrganizationDomains { get; set; }
public bool UseAdminSponsoredFamilies { get; set; } public bool UseAdminSponsoredFamilies { get; set; }
} }

View File

@ -58,7 +58,8 @@ public class ProfileOrganizationResponseModel : ResponseModel
ProviderName = organization.ProviderName; ProviderName = organization.ProviderName;
ProviderType = organization.ProviderType; ProviderType = organization.ProviderType;
FamilySponsorshipFriendlyName = organization.FamilySponsorshipFriendlyName; FamilySponsorshipFriendlyName = organization.FamilySponsorshipFriendlyName;
FamilySponsorshipAvailable = FamilySponsorshipFriendlyName == null && IsAdminInitiated = organization.IsAdminInitiated ?? false;
FamilySponsorshipAvailable = (FamilySponsorshipFriendlyName == null || IsAdminInitiated) &&
StaticStore.GetSponsoredPlan(PlanSponsorshipType.FamiliesForEnterprise) StaticStore.GetSponsoredPlan(PlanSponsorshipType.FamiliesForEnterprise)
.UsersCanSponsor(organization); .UsersCanSponsor(organization);
ProductTierType = organization.PlanType.GetProductTier(); ProductTierType = organization.PlanType.GetProductTier();
@ -72,6 +73,7 @@ public class ProfileOrganizationResponseModel : ResponseModel
AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems; AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems;
UserIsClaimedByOrganization = organizationIdsClaimingUser.Contains(organization.OrganizationId); UserIsClaimedByOrganization = organizationIdsClaimingUser.Contains(organization.OrganizationId);
UseRiskInsights = organization.UseRiskInsights; UseRiskInsights = organization.UseRiskInsights;
UseOrganizationDomains = organization.UseOrganizationDomains;
UseAdminSponsoredFamilies = organization.UseAdminSponsoredFamilies; UseAdminSponsoredFamilies = organization.UseAdminSponsoredFamilies;
if (organization.SsoConfig != null) if (organization.SsoConfig != null)
@ -135,7 +137,6 @@ public class ProfileOrganizationResponseModel : ResponseModel
public bool AllowAdminAccessToAllCollectionItems { get; set; } public bool AllowAdminAccessToAllCollectionItems { get; set; }
/// <summary> /// <summary>
/// Obsolete. /// Obsolete.
///
/// See <see cref="UserIsClaimedByOrganization"/> /// See <see cref="UserIsClaimedByOrganization"/>
/// </summary> /// </summary>
[Obsolete("Please use UserIsClaimedByOrganization instead. This property will be removed in a future version.")] [Obsolete("Please use UserIsClaimedByOrganization instead. This property will be removed in a future version.")]
@ -145,16 +146,15 @@ public class ProfileOrganizationResponseModel : ResponseModel
set => UserIsClaimedByOrganization = value; set => UserIsClaimedByOrganization = value;
} }
/// <summary> /// <summary>
/// Indicates if the organization claims the user. /// Indicates if the user is claimed by the organization.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// An organization claims a user if the user's email domain is verified by the organization and the user is a member of it. /// A user is claimed by an organization if the user's email domain is verified by the organization and the user is a member.
/// The organization must be enabled and able to have verified domains. /// The organization must be enabled and able to have verified domains.
/// </remarks> /// </remarks>
/// <returns>
/// False if the Account Deprovisioning feature flag is disabled.
/// </returns>
public bool UserIsClaimedByOrganization { get; set; } public bool UserIsClaimedByOrganization { get; set; }
public bool UseRiskInsights { get; set; } public bool UseRiskInsights { get; set; }
public bool UseOrganizationDomains { get; set; }
public bool UseAdminSponsoredFamilies { get; set; } public bool UseAdminSponsoredFamilies { get; set; }
public bool IsAdminInitiated { get; set; }
} }

View File

@ -50,6 +50,7 @@ public class ProfileProviderOrganizationResponseModel : ProfileOrganizationRespo
LimitItemDeletion = organization.LimitItemDeletion; LimitItemDeletion = organization.LimitItemDeletion;
AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems; AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems;
UseRiskInsights = organization.UseRiskInsights; UseRiskInsights = organization.UseRiskInsights;
UseOrganizationDomains = organization.UseOrganizationDomains;
UseAdminSponsoredFamilies = organization.UseAdminSponsoredFamilies; UseAdminSponsoredFamilies = organization.UseAdminSponsoredFamilies;
} }
} }

View File

@ -518,9 +518,8 @@ public class AccountsController : Controller
} }
else else
{ {
// If Account Deprovisioning is enabled, we need to check if the user is claimed by any organization. // Check if the user is claimed by any organization.
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning) if (await _userService.IsClaimedByAnyOrganizationAsync(user.Id))
&& await _userService.IsClaimedByAnyOrganizationAsync(user.Id))
{ {
throw new BadRequestException("Cannot delete accounts owned by an organization. Contact your organization administrator for additional details."); throw new BadRequestException("Cannot delete accounts owned by an organization. Contact your organization administrator for additional details.");
} }

View File

@ -1,7 +1,7 @@
#nullable enable #nullable enable
using Bit.Api.Billing.Models.Responses; using Bit.Api.Billing.Models.Responses;
using Bit.Core.Billing.Models.Api.Requests.Accounts;
using Bit.Core.Billing.Services; using Bit.Core.Billing.Services;
using Bit.Core.Billing.Tax.Requests;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;

View File

@ -1,5 +1,5 @@
using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities;
using Bit.Core.Billing.Models.Api.Requests.Organizations; using Bit.Core.Billing.Tax.Requests;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;

View File

@ -8,7 +8,9 @@ using Bit.Core;
using Bit.Core.Billing.Models; using Bit.Core.Billing.Models;
using Bit.Core.Billing.Models.Sales; using Bit.Core.Billing.Models.Sales;
using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Providers.Services;
using Bit.Core.Billing.Services; using Bit.Core.Billing.Services;
using Bit.Core.Billing.Tax.Models;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
@ -291,14 +293,20 @@ public class OrganizationBillingController(
sale.Organization.PlanType = plan.Type; sale.Organization.PlanType = plan.Type;
sale.Organization.Plan = plan.Name; sale.Organization.Plan = plan.Name;
sale.SubscriptionSetup.SkipTrial = true; sale.SubscriptionSetup.SkipTrial = true;
await organizationBillingService.Finalize(sale);
if (organizationSignup.PaymentMethodType == null || string.IsNullOrEmpty(organizationSignup.PaymentToken))
{
return Error.BadRequest("A payment method is required to restart the subscription.");
}
var org = await organizationRepository.GetByIdAsync(organizationId); var org = await organizationRepository.GetByIdAsync(organizationId);
Debug.Assert(org is not null, "This organization has already been found via this same ID, this should be fine."); Debug.Assert(org is not null, "This organization has already been found via this same ID, this should be fine.");
if (organizationSignup.PaymentMethodType != null) var paymentSource = new TokenizedPaymentSource(organizationSignup.PaymentMethodType.Value, organizationSignup.PaymentToken);
var taxInformation = TaxInformation.From(organizationSignup.TaxInfo);
await organizationBillingService.Finalize(sale);
var updatedOrg = await organizationRepository.GetByIdAsync(organizationId);
if (updatedOrg != null)
{ {
var paymentSource = new TokenizedPaymentSource(organizationSignup.PaymentMethodType.Value, organizationSignup.PaymentToken); await organizationBillingService.UpdatePaymentMethod(updatedOrg, paymentSource, taxInformation);
var taxInformation = TaxInformation.From(organizationSignup.TaxInfo);
await organizationBillingService.UpdatePaymentMethod(org, paymentSource, taxInformation);
} }
return TypedResults.Ok(); return TypedResults.Ok();

View File

@ -222,6 +222,20 @@ public class OrganizationSponsorshipsController : Controller
await _revokeSponsorshipCommand.RevokeSponsorshipAsync(existingOrgSponsorship); await _revokeSponsorshipCommand.RevokeSponsorshipAsync(existingOrgSponsorship);
} }
[Authorize("Application")]
[HttpDelete("{sponsoringOrgId}/{sponsoredFriendlyName}/revoke")]
[SelfHosted(NotSelfHostedOnly = true)]
public async Task AdminInitiatedRevokeSponsorshipAsync(Guid sponsoringOrgId, string sponsoredFriendlyName)
{
var sponsorships = await _organizationSponsorshipRepository.GetManyBySponsoringOrganizationAsync(sponsoringOrgId);
var existingOrgSponsorship = sponsorships.FirstOrDefault(s => s.FriendlyName != null && s.FriendlyName.Equals(sponsoredFriendlyName, StringComparison.OrdinalIgnoreCase));
if (existingOrgSponsorship == null)
{
throw new BadRequestException("The specified sponsored organization could not be found under the given sponsoring organization.");
}
await _revokeSponsorshipCommand.RevokeSponsorshipAsync(existingOrgSponsorship);
}
[Authorize("Application")] [Authorize("Application")]
[HttpDelete("sponsored/{sponsoredOrgId}")] [HttpDelete("sponsored/{sponsoredOrgId}")]
[HttpPost("sponsored/{sponsoredOrgId}/remove")] [HttpPost("sponsored/{sponsoredOrgId}/remove")]
@ -271,8 +285,11 @@ public class OrganizationSponsorshipsController : Controller
} }
var sponsorships = await _organizationSponsorshipRepository.GetManyBySponsoringOrganizationAsync(sponsoringOrgId); var sponsorships = await _organizationSponsorshipRepository.GetManyBySponsoringOrganizationAsync(sponsoringOrgId);
return new ListResponseModel<OrganizationSponsorshipInvitesResponseModel>(sponsorships.Select(s => return new ListResponseModel<OrganizationSponsorshipInvitesResponseModel>(
new OrganizationSponsorshipInvitesResponseModel(new OrganizationSponsorshipData(s)))); sponsorships
.Where(s => s.IsAdminInitiated)
.Select(s => new OrganizationSponsorshipInvitesResponseModel(new OrganizationSponsorshipData(s)))
);
} }

View File

@ -109,28 +109,6 @@ public class OrganizationsController(
return license; return license;
} }
[HttpPost("{id:guid}/payment")]
[SelfHosted(NotSelfHostedOnly = true)]
public async Task PostPayment(Guid id, [FromBody] PaymentRequestModel model)
{
if (!await currentContext.EditPaymentMethods(id))
{
throw new NotFoundException();
}
await organizationService.ReplacePaymentMethodAsync(id, model.PaymentToken,
model.PaymentMethodType.Value, new TaxInfo
{
BillingAddressLine1 = model.Line1,
BillingAddressLine2 = model.Line2,
BillingAddressState = model.State,
BillingAddressCity = model.City,
BillingAddressPostalCode = model.PostalCode,
BillingAddressCountry = model.Country,
TaxIdNumber = model.TaxId,
});
}
[HttpPost("{id:guid}/upgrade")] [HttpPost("{id:guid}/upgrade")]
[SelfHosted(NotSelfHostedOnly = true)] [SelfHosted(NotSelfHostedOnly = true)]
public async Task<PaymentResponseModel> PostUpgrade(Guid id, [FromBody] OrganizationUpgradeRequestModel model) public async Task<PaymentResponseModel> PostUpgrade(Guid id, [FromBody] OrganizationUpgradeRequestModel model)

View File

@ -1,11 +1,14 @@
using Bit.Api.Billing.Models.Requests; using Bit.Api.Billing.Models.Requests;
using Bit.Api.Billing.Models.Responses; using Bit.Api.Billing.Models.Responses;
using Bit.Commercial.Core.Billing.Providers.Services;
using Bit.Core; using Bit.Core;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Models;
using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Repositories; using Bit.Core.Billing.Providers.Models;
using Bit.Core.Billing.Providers.Repositories;
using Bit.Core.Billing.Providers.Services;
using Bit.Core.Billing.Services; using Bit.Core.Billing.Services;
using Bit.Core.Billing.Tax.Models;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Models.BitStripe; using Bit.Core.Models.BitStripe;
using Bit.Core.Services; using Bit.Core.Services;
@ -147,13 +150,33 @@ public class ProviderBillingController(
var providerPlans = await providerPlanRepository.GetByProviderId(provider.Id); var providerPlans = await providerPlanRepository.GetByProviderId(provider.Id);
var getProviderPriceFromStripe = featureService.IsEnabled(FeatureFlagKeys.PM21383_GetProviderPriceFromStripe);
var configuredProviderPlans = await Task.WhenAll(providerPlans.Select(async providerPlan => var configuredProviderPlans = await Task.WhenAll(providerPlans.Select(async providerPlan =>
{ {
var plan = await pricingClient.GetPlanOrThrow(providerPlan.PlanType); var plan = await pricingClient.GetPlanOrThrow(providerPlan.PlanType);
decimal unitAmount;
if (getProviderPriceFromStripe)
{
var priceId = ProviderPriceAdapter.GetPriceId(provider, subscription, plan.Type);
var price = await stripeAdapter.PriceGetAsync(priceId);
unitAmount = price.UnitAmountDecimal.HasValue
? price.UnitAmountDecimal.Value / 100M
: plan.PasswordManager.ProviderPortalSeatPrice;
}
else
{
unitAmount = plan.PasswordManager.ProviderPortalSeatPrice;
}
return new ConfiguredProviderPlan( return new ConfiguredProviderPlan(
providerPlan.Id, providerPlan.Id,
providerPlan.ProviderId, providerPlan.ProviderId,
plan, plan,
unitAmount,
providerPlan.SeatMinimum ?? 0, providerPlan.SeatMinimum ?? 0,
providerPlan.PurchasedSeats ?? 0, providerPlan.PurchasedSeats ?? 0,
providerPlan.AllocatedSeats ?? 0); providerPlan.AllocatedSeats ?? 0);

View File

@ -1,4 +1,4 @@
using Bit.Core.Billing.Services; using Bit.Core.Billing.Tax.Services;
using Bit.Core.Services; using Bit.Core.Services;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Http.HttpResults;

View File

@ -0,0 +1,36 @@
using Bit.Api.Billing.Models.Requests;
using Bit.Core.Billing.Tax.Commands;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Bit.Api.Billing.Controllers;
[Authorize("Application")]
[Route("tax")]
public class TaxController(
IPreviewTaxAmountCommand previewTaxAmountCommand) : BaseBillingController
{
[HttpPost("preview-amount/organization-trial")]
public async Task<IResult> PreviewTaxAmountForOrganizationTrialAsync(
[FromBody] PreviewTaxAmountForOrganizationTrialRequestBody requestBody)
{
var parameters = new OrganizationTrialParameters
{
PlanType = requestBody.PlanType,
ProductType = requestBody.ProductType,
TaxInformation = new OrganizationTrialParameters.TaxInformationDTO
{
Country = requestBody.TaxInformation.Country,
PostalCode = requestBody.TaxInformation.PostalCode,
TaxId = requestBody.TaxInformation.TaxId
}
};
var result = await previewTaxAmountCommand.Run(parameters);
return result.Match<IResult>(
taxAmount => TypedResults.Ok(new { TaxAmount = taxAmount }),
badRequest => Error.BadRequest(badRequest.TranslationKey),
unhandled => Error.ServerError(unhandled.TranslationKey));
}
}

View File

@ -0,0 +1,27 @@
#nullable enable
using System.ComponentModel.DataAnnotations;
using Bit.Core.Billing.Enums;
namespace Bit.Api.Billing.Models.Requests;
public class PreviewTaxAmountForOrganizationTrialRequestBody
{
[Required]
public PlanType PlanType { get; set; }
[Required]
public ProductType ProductType { get; set; }
[Required] public TaxInformationDTO TaxInformation { get; set; } = null!;
public class TaxInformationDTO
{
[Required]
public string Country { get; set; } = null!;
[Required]
public string PostalCode { get; set; } = null!;
public string? TaxId { get; set; }
}
}

View File

@ -1,5 +1,5 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Bit.Core.Billing.Models; using Bit.Core.Billing.Tax.Models;
namespace Bit.Api.Billing.Models.Requests; namespace Bit.Api.Billing.Models.Requests;

View File

@ -1,4 +1,5 @@
using Bit.Core.Billing.Models; using Bit.Core.Billing.Models;
using Bit.Core.Billing.Tax.Models;
namespace Bit.Api.Billing.Models.Responses; namespace Bit.Api.Billing.Models.Responses;

View File

@ -2,6 +2,8 @@
using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.Billing.Enums; using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Models; using Bit.Core.Billing.Models;
using Bit.Core.Billing.Providers.Models;
using Bit.Core.Billing.Tax.Models;
using Stripe; using Stripe;
namespace Bit.Api.Billing.Models.Responses; namespace Bit.Api.Billing.Models.Responses;
@ -34,7 +36,7 @@ public record ProviderSubscriptionResponse(
.Select(providerPlan => .Select(providerPlan =>
{ {
var plan = providerPlan.Plan; var plan = providerPlan.Plan;
var cost = (providerPlan.SeatMinimum + providerPlan.PurchasedSeats) * plan.PasswordManager.ProviderPortalSeatPrice; var cost = (providerPlan.SeatMinimum + providerPlan.PurchasedSeats) * providerPlan.Price;
var cadence = plan.IsAnnual ? _annualCadence : _monthlyCadence; var cadence = plan.IsAnnual ? _annualCadence : _monthlyCadence;
return new ProviderPlanResponse( return new ProviderPlanResponse(
plan.Name, plan.Name,

View File

@ -1,4 +1,4 @@
using Bit.Core.Billing.Models; using Bit.Core.Billing.Tax.Models;
namespace Bit.Api.Billing.Models.Responses; namespace Bit.Api.Billing.Models.Responses;

View File

@ -1,6 +1,10 @@
using Bit.Api.Models.Request.Organizations; using Bit.Api.AdminConsole.Authorization.Requirements;
using Bit.Api.Models.Request.Organizations;
using Bit.Api.Models.Response;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.Models.Api.Response.OrganizationSponsorships;
using Bit.Core.Models.Data.Organizations.OrganizationSponsorships;
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces; using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
@ -22,6 +26,7 @@ public class SelfHostedOrganizationSponsorshipsController : Controller
private readonly IRevokeSponsorshipCommand _revokeSponsorshipCommand; private readonly IRevokeSponsorshipCommand _revokeSponsorshipCommand;
private readonly ICurrentContext _currentContext; private readonly ICurrentContext _currentContext;
private readonly IFeatureService _featureService; private readonly IFeatureService _featureService;
private readonly IAuthorizationService _authorizationService;
public SelfHostedOrganizationSponsorshipsController( public SelfHostedOrganizationSponsorshipsController(
ICreateSponsorshipCommand offerSponsorshipCommand, ICreateSponsorshipCommand offerSponsorshipCommand,
@ -30,7 +35,8 @@ public class SelfHostedOrganizationSponsorshipsController : Controller
IOrganizationSponsorshipRepository organizationSponsorshipRepository, IOrganizationSponsorshipRepository organizationSponsorshipRepository,
IOrganizationUserRepository organizationUserRepository, IOrganizationUserRepository organizationUserRepository,
ICurrentContext currentContext, ICurrentContext currentContext,
IFeatureService featureService IFeatureService featureService,
IAuthorizationService authorizationService
) )
{ {
_offerSponsorshipCommand = offerSponsorshipCommand; _offerSponsorshipCommand = offerSponsorshipCommand;
@ -40,6 +46,7 @@ public class SelfHostedOrganizationSponsorshipsController : Controller
_organizationUserRepository = organizationUserRepository; _organizationUserRepository = organizationUserRepository;
_currentContext = currentContext; _currentContext = currentContext;
_featureService = featureService; _featureService = featureService;
_authorizationService = authorizationService;
} }
[HttpPost("{sponsoringOrgId}/families-for-enterprise")] [HttpPost("{sponsoringOrgId}/families-for-enterprise")]
@ -84,4 +91,41 @@ public class SelfHostedOrganizationSponsorshipsController : Controller
await _revokeSponsorshipCommand.RevokeSponsorshipAsync(existingOrgSponsorship); await _revokeSponsorshipCommand.RevokeSponsorshipAsync(existingOrgSponsorship);
} }
[HttpDelete("{sponsoringOrgId}/{sponsoredFriendlyName}/revoke")]
public async Task AdminInitiatedRevokeSponsorshipAsync(Guid sponsoringOrgId, string sponsoredFriendlyName)
{
var sponsorships = await _organizationSponsorshipRepository.GetManyBySponsoringOrganizationAsync(sponsoringOrgId);
var existingOrgSponsorship = sponsorships.FirstOrDefault(s => s.FriendlyName != null && s.FriendlyName.Equals(sponsoredFriendlyName, StringComparison.OrdinalIgnoreCase));
if (existingOrgSponsorship == null)
{
throw new BadRequestException("The specified sponsored organization could not be found under the given sponsoring organization.");
}
await _revokeSponsorshipCommand.RevokeSponsorshipAsync(existingOrgSponsorship);
}
[Authorize("Application")]
[HttpGet("{orgId}/sponsored")]
public async Task<ListResponseModel<OrganizationSponsorshipInvitesResponseModel>> GetSponsoredOrganizations(Guid orgId)
{
var sponsoringOrg = await _organizationRepository.GetByIdAsync(orgId);
if (sponsoringOrg == null)
{
throw new NotFoundException();
}
var authorizationResult = await _authorizationService.AuthorizeAsync(User, orgId, new ManageUsersRequirement());
if (!authorizationResult.Succeeded)
{
throw new UnauthorizedAccessException();
}
var sponsorships = await _organizationSponsorshipRepository.GetManyBySponsoringOrganizationAsync(orgId);
return new ListResponseModel<OrganizationSponsorshipInvitesResponseModel>(
sponsorships
.Where(s => s.IsAdminInitiated)
.Select(s => new OrganizationSponsorshipInvitesResponseModel(new OrganizationSponsorshipData(s)))
);
}
} }

View File

@ -4,18 +4,20 @@ namespace Bit.Api.Tools.Models.Response;
public class MemberCipherDetailsResponseModel public class MemberCipherDetailsResponseModel
{ {
public Guid? UserGuid { get; set; }
public string UserName { get; set; } public string UserName { get; set; }
public string Email { get; set; } public string Email { get; set; }
public bool UsesKeyConnector { get; set; } public bool UsesKeyConnector { get; set; }
/// <summary> /// <summary>
/// A distinct list of the cipher ids associated with /// A distinct list of the cipher ids associated with
/// the organization member /// the organization member
/// </summary> /// </summary>
public IEnumerable<string> CipherIds { get; set; } public IEnumerable<string> CipherIds { get; set; }
public MemberCipherDetailsResponseModel(MemberAccessCipherDetails memberAccessCipherDetails) public MemberCipherDetailsResponseModel(MemberAccessCipherDetails memberAccessCipherDetails)
{ {
this.UserGuid = memberAccessCipherDetails.UserGuid;
this.UserName = memberAccessCipherDetails.UserName; this.UserName = memberAccessCipherDetails.UserName;
this.Email = memberAccessCipherDetails.Email; this.Email = memberAccessCipherDetails.Email;
this.UsesKeyConnector = memberAccessCipherDetails.UsesKeyConnector; this.UsesKeyConnector = memberAccessCipherDetails.UsesKeyConnector;

View File

@ -12,17 +12,17 @@ namespace Bit.Api.KeyManagement.Validators;
/// </summary> /// </summary>
public class SendRotationValidator : IRotationValidator<IEnumerable<SendWithIdRequestModel>, IReadOnlyList<Send>> public class SendRotationValidator : IRotationValidator<IEnumerable<SendWithIdRequestModel>, IReadOnlyList<Send>>
{ {
private readonly ISendService _sendService; private readonly ISendAuthorizationService _sendAuthorizationService;
private readonly ISendRepository _sendRepository; private readonly ISendRepository _sendRepository;
/// <summary> /// <summary>
/// Instantiates a new <see cref="SendRotationValidator"/> /// Instantiates a new <see cref="SendRotationValidator"/>
/// </summary> /// </summary>
/// <param name="sendService">Enables conversion of <see cref="SendWithIdRequestModel"/> to <see cref="Send"/></param> /// <param name="sendAuthorizationService">Enables conversion of <see cref="SendWithIdRequestModel"/> to <see cref="Send"/></param>
/// <param name="sendRepository">Retrieves all user <see cref="Send"/>s</param> /// <param name="sendRepository">Retrieves all user <see cref="Send"/>s</param>
public SendRotationValidator(ISendService sendService, ISendRepository sendRepository) public SendRotationValidator(ISendAuthorizationService sendAuthorizationService, ISendRepository sendRepository)
{ {
_sendService = sendService; _sendAuthorizationService = sendAuthorizationService;
_sendRepository = sendRepository; _sendRepository = sendRepository;
} }
@ -44,7 +44,7 @@ public class SendRotationValidator : IRotationValidator<IEnumerable<SendWithIdRe
throw new BadRequestException("All existing sends must be included in the rotation."); throw new BadRequestException("All existing sends must be included in the rotation.");
} }
result.Add(send.ToSend(existing, _sendService)); result.Add(send.ToSend(existing, _sendAuthorizationService));
} }
return result; return result;

View File

@ -32,6 +32,7 @@ public class PlanResponseModel : ResponseModel
HasTotp = plan.HasTotp; HasTotp = plan.HasTotp;
Has2fa = plan.Has2fa; Has2fa = plan.Has2fa;
HasSso = plan.HasSso; HasSso = plan.HasSso;
HasOrganizationDomains = plan.HasOrganizationDomains;
HasResetPassword = plan.HasResetPassword; HasResetPassword = plan.HasResetPassword;
UsersGetPremium = plan.UsersGetPremium; UsersGetPremium = plan.UsersGetPremium;
UpgradeSortOrder = plan.UpgradeSortOrder; UpgradeSortOrder = plan.UpgradeSortOrder;
@ -71,6 +72,7 @@ public class PlanResponseModel : ResponseModel
public bool Has2fa { get; set; } public bool Has2fa { get; set; }
public bool HasApi { get; set; } public bool HasApi { get; set; }
public bool HasSso { get; set; } public bool HasSso { get; set; }
public bool HasOrganizationDomains { get; set; }
public bool HasResetPassword { get; set; } public bool HasResetPassword { get; set; }
public bool UsersGetPremium { get; set; } public bool UsersGetPremium { get; set; }

View File

@ -35,6 +35,7 @@ using Bit.Core.Services;
using Bit.Core.Tools.ImportFeatures; using Bit.Core.Tools.ImportFeatures;
using Bit.Core.Tools.ReportFeatures; using Bit.Core.Tools.ReportFeatures;
using Bit.Core.Auth.Models.Api.Request; using Bit.Core.Auth.Models.Api.Request;
using Bit.Core.Tools.SendFeatures;
#if !OSS #if !OSS
using Bit.Commercial.Core.SecretsManager; using Bit.Commercial.Core.SecretsManager;
@ -186,6 +187,7 @@ public class Startup
services.AddPhishingDomainServices(globalSettings); services.AddPhishingDomainServices(globalSettings);
services.AddBillingQueries(); services.AddBillingQueries();
services.AddSendServices();
// Authorization Handlers // Authorization Handlers
services.AddAuthorizationHandlers(); services.AddAuthorizationHandlers();

View File

@ -12,6 +12,8 @@ using Bit.Core.Settings;
using Bit.Core.Tools.Enums; using Bit.Core.Tools.Enums;
using Bit.Core.Tools.Models.Data; using Bit.Core.Tools.Models.Data;
using Bit.Core.Tools.Repositories; using Bit.Core.Tools.Repositories;
using Bit.Core.Tools.SendFeatures;
using Bit.Core.Tools.SendFeatures.Commands.Interfaces;
using Bit.Core.Tools.Services; using Bit.Core.Tools.Services;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
@ -25,8 +27,10 @@ public class SendsController : Controller
{ {
private readonly ISendRepository _sendRepository; private readonly ISendRepository _sendRepository;
private readonly IUserService _userService; private readonly IUserService _userService;
private readonly ISendService _sendService; private readonly ISendAuthorizationService _sendAuthorizationService;
private readonly ISendFileStorageService _sendFileStorageService; private readonly ISendFileStorageService _sendFileStorageService;
private readonly IAnonymousSendCommand _anonymousSendCommand;
private readonly INonAnonymousSendCommand _nonAnonymousSendCommand;
private readonly ILogger<SendsController> _logger; private readonly ILogger<SendsController> _logger;
private readonly GlobalSettings _globalSettings; private readonly GlobalSettings _globalSettings;
private readonly ICurrentContext _currentContext; private readonly ICurrentContext _currentContext;
@ -34,7 +38,9 @@ public class SendsController : Controller
public SendsController( public SendsController(
ISendRepository sendRepository, ISendRepository sendRepository,
IUserService userService, IUserService userService,
ISendService sendService, ISendAuthorizationService sendAuthorizationService,
IAnonymousSendCommand anonymousSendCommand,
INonAnonymousSendCommand nonAnonymousSendCommand,
ISendFileStorageService sendFileStorageService, ISendFileStorageService sendFileStorageService,
ILogger<SendsController> logger, ILogger<SendsController> logger,
GlobalSettings globalSettings, GlobalSettings globalSettings,
@ -42,13 +48,16 @@ public class SendsController : Controller
{ {
_sendRepository = sendRepository; _sendRepository = sendRepository;
_userService = userService; _userService = userService;
_sendService = sendService; _sendAuthorizationService = sendAuthorizationService;
_anonymousSendCommand = anonymousSendCommand;
_nonAnonymousSendCommand = nonAnonymousSendCommand;
_sendFileStorageService = sendFileStorageService; _sendFileStorageService = sendFileStorageService;
_logger = logger; _logger = logger;
_globalSettings = globalSettings; _globalSettings = globalSettings;
_currentContext = currentContext; _currentContext = currentContext;
} }
#region Anonymous endpoints
[AllowAnonymous] [AllowAnonymous]
[HttpPost("access/{id}")] [HttpPost("access/{id}")]
public async Task<IActionResult> Access(string id, [FromBody] SendAccessRequestModel model) public async Task<IActionResult> Access(string id, [FromBody] SendAccessRequestModel model)
@ -61,18 +70,19 @@ public class SendsController : Controller
//} //}
var guid = new Guid(CoreHelpers.Base64UrlDecode(id)); var guid = new Guid(CoreHelpers.Base64UrlDecode(id));
var (send, passwordRequired, passwordInvalid) = var send = await _sendRepository.GetByIdAsync(guid);
await _sendService.AccessAsync(guid, model.Password); SendAccessResult sendAuthResult =
if (passwordRequired) await _sendAuthorizationService.AccessAsync(send, model.Password);
if (sendAuthResult.Equals(SendAccessResult.PasswordRequired))
{ {
return new UnauthorizedResult(); return new UnauthorizedResult();
} }
if (passwordInvalid) if (sendAuthResult.Equals(SendAccessResult.PasswordInvalid))
{ {
await Task.Delay(2000); await Task.Delay(2000);
throw new BadRequestException("Invalid password."); throw new BadRequestException("Invalid password.");
} }
if (send == null) if (sendAuthResult.Equals(SendAccessResult.Denied))
{ {
throw new NotFoundException(); throw new NotFoundException();
} }
@ -106,19 +116,19 @@ public class SendsController : Controller
throw new BadRequestException("Could not locate send"); throw new BadRequestException("Could not locate send");
} }
var (url, passwordRequired, passwordInvalid) = await _sendService.GetSendFileDownloadUrlAsync(send, fileId, var (url, result) = await _anonymousSendCommand.GetSendFileDownloadUrlAsync(send, fileId,
model.Password); model.Password);
if (passwordRequired) if (result.Equals(SendAccessResult.PasswordRequired))
{ {
return new UnauthorizedResult(); return new UnauthorizedResult();
} }
if (passwordInvalid) if (result.Equals(SendAccessResult.PasswordInvalid))
{ {
await Task.Delay(2000); await Task.Delay(2000);
throw new BadRequestException("Invalid password."); throw new BadRequestException("Invalid password.");
} }
if (send == null) if (result.Equals(SendAccessResult.Denied))
{ {
throw new NotFoundException(); throw new NotFoundException();
} }
@ -130,6 +140,45 @@ public class SendsController : Controller
}); });
} }
[AllowAnonymous]
[HttpPost("file/validate/azure")]
public async Task<ObjectResult> AzureValidateFile()
{
return await ApiHelpers.HandleAzureEvents(Request, new Dictionary<string, Func<EventGridEvent, Task>>
{
{
"Microsoft.Storage.BlobCreated", async (eventGridEvent) =>
{
try
{
var blobName = eventGridEvent.Subject.Split($"{AzureSendFileStorageService.FilesContainerName}/blobs/")[1];
var sendId = AzureSendFileStorageService.SendIdFromBlobName(blobName);
var send = await _sendRepository.GetByIdAsync(new Guid(sendId));
if (send == null)
{
if (_sendFileStorageService is AzureSendFileStorageService azureSendFileStorageService)
{
await azureSendFileStorageService.DeleteBlobAsync(blobName);
}
return;
}
await _nonAnonymousSendCommand.ConfirmFileSize(send);
}
catch (Exception e)
{
_logger.LogError(e, $"Uncaught exception occurred while handling event grid event: {JsonSerializer.Serialize(eventGridEvent)}");
return;
}
}
}
});
}
#endregion
#region Non-anonymous endpoints
[HttpGet("{id}")] [HttpGet("{id}")]
public async Task<SendResponseModel> Get(string id) public async Task<SendResponseModel> Get(string id)
{ {
@ -157,8 +206,8 @@ public class SendsController : Controller
{ {
model.ValidateCreation(); model.ValidateCreation();
var userId = _userService.GetProperUserId(User).Value; var userId = _userService.GetProperUserId(User).Value;
var send = model.ToSend(userId, _sendService); var send = model.ToSend(userId, _sendAuthorizationService);
await _sendService.SaveSendAsync(send); await _nonAnonymousSendCommand.SaveSendAsync(send);
return new SendResponseModel(send, _globalSettings); return new SendResponseModel(send, _globalSettings);
} }
@ -175,15 +224,15 @@ public class SendsController : Controller
throw new BadRequestException("Invalid content. File size hint is required."); throw new BadRequestException("Invalid content. File size hint is required.");
} }
if (model.FileLength.Value > SendService.MAX_FILE_SIZE) if (model.FileLength.Value > Constants.FileSize501mb)
{ {
throw new BadRequestException($"Max file size is {SendService.MAX_FILE_SIZE_READABLE}."); throw new BadRequestException($"Max file size is {SendFileSettingHelper.MAX_FILE_SIZE_READABLE}.");
} }
model.ValidateCreation(); model.ValidateCreation();
var userId = _userService.GetProperUserId(User).Value; var userId = _userService.GetProperUserId(User).Value;
var (send, data) = model.ToSend(userId, model.File.FileName, _sendService); var (send, data) = model.ToSend(userId, model.File.FileName, _sendAuthorizationService);
var uploadUrl = await _sendService.SaveFileSendAsync(send, data, model.FileLength.Value); var uploadUrl = await _nonAnonymousSendCommand.SaveFileSendAsync(send, data, model.FileLength.Value);
return new SendFileUploadDataResponseModel return new SendFileUploadDataResponseModel
{ {
Url = uploadUrl, Url = uploadUrl,
@ -230,41 +279,7 @@ public class SendsController : Controller
var send = await _sendRepository.GetByIdAsync(new Guid(id)); var send = await _sendRepository.GetByIdAsync(new Guid(id));
await Request.GetFileAsync(async (stream) => await Request.GetFileAsync(async (stream) =>
{ {
await _sendService.UploadFileToExistingSendAsync(stream, send); await _nonAnonymousSendCommand.UploadFileToExistingSendAsync(stream, send);
});
}
[AllowAnonymous]
[HttpPost("file/validate/azure")]
public async Task<ObjectResult> AzureValidateFile()
{
return await ApiHelpers.HandleAzureEvents(Request, new Dictionary<string, Func<EventGridEvent, Task>>
{
{
"Microsoft.Storage.BlobCreated", async (eventGridEvent) =>
{
try
{
var blobName = eventGridEvent.Subject.Split($"{AzureSendFileStorageService.FilesContainerName}/blobs/")[1];
var sendId = AzureSendFileStorageService.SendIdFromBlobName(blobName);
var send = await _sendRepository.GetByIdAsync(new Guid(sendId));
if (send == null)
{
if (_sendFileStorageService is AzureSendFileStorageService azureSendFileStorageService)
{
await azureSendFileStorageService.DeleteBlobAsync(blobName);
}
return;
}
await _sendService.ValidateSendFile(send);
}
catch (Exception e)
{
_logger.LogError(e, $"Uncaught exception occurred while handling event grid event: {JsonSerializer.Serialize(eventGridEvent)}");
return;
}
}
}
}); });
} }
@ -279,7 +294,7 @@ public class SendsController : Controller
throw new NotFoundException(); throw new NotFoundException();
} }
await _sendService.SaveSendAsync(model.ToSend(send, _sendService)); await _nonAnonymousSendCommand.SaveSendAsync(model.ToSend(send, _sendAuthorizationService));
return new SendResponseModel(send, _globalSettings); return new SendResponseModel(send, _globalSettings);
} }
@ -294,7 +309,7 @@ public class SendsController : Controller
} }
send.Password = null; send.Password = null;
await _sendService.SaveSendAsync(send); await _nonAnonymousSendCommand.SaveSendAsync(send);
return new SendResponseModel(send, _globalSettings); return new SendResponseModel(send, _globalSettings);
} }
@ -308,6 +323,8 @@ public class SendsController : Controller
throw new NotFoundException(); throw new NotFoundException();
} }
await _sendService.DeleteSendAsync(send); await _nonAnonymousSendCommand.DeleteSendAsync(send);
} }
#endregion
} }

View File

@ -36,31 +36,31 @@ public class SendRequestModel
public bool? Disabled { get; set; } public bool? Disabled { get; set; }
public bool? HideEmail { get; set; } public bool? HideEmail { get; set; }
public Send ToSend(Guid userId, ISendService sendService) public Send ToSend(Guid userId, ISendAuthorizationService sendAuthorizationService)
{ {
var send = new Send var send = new Send
{ {
Type = Type, Type = Type,
UserId = (Guid?)userId UserId = (Guid?)userId
}; };
ToSend(send, sendService); ToSend(send, sendAuthorizationService);
return send; return send;
} }
public (Send, SendFileData) ToSend(Guid userId, string fileName, ISendService sendService) public (Send, SendFileData) ToSend(Guid userId, string fileName, ISendAuthorizationService sendAuthorizationService)
{ {
var send = ToSendBase(new Send var send = ToSendBase(new Send
{ {
Type = Type, Type = Type,
UserId = (Guid?)userId UserId = (Guid?)userId
}, sendService); }, sendAuthorizationService);
var data = new SendFileData(Name, Notes, fileName); var data = new SendFileData(Name, Notes, fileName);
return (send, data); return (send, data);
} }
public Send ToSend(Send existingSend, ISendService sendService) public Send ToSend(Send existingSend, ISendAuthorizationService sendAuthorizationService)
{ {
existingSend = ToSendBase(existingSend, sendService); existingSend = ToSendBase(existingSend, sendAuthorizationService);
switch (existingSend.Type) switch (existingSend.Type)
{ {
case SendType.File: case SendType.File:
@ -125,7 +125,7 @@ public class SendRequestModel
} }
} }
private Send ToSendBase(Send existingSend, ISendService sendService) private Send ToSendBase(Send existingSend, ISendAuthorizationService authorizationService)
{ {
existingSend.Key = Key; existingSend.Key = Key;
existingSend.ExpirationDate = ExpirationDate; existingSend.ExpirationDate = ExpirationDate;
@ -133,7 +133,7 @@ public class SendRequestModel
existingSend.MaxAccessCount = MaxAccessCount; existingSend.MaxAccessCount = MaxAccessCount;
if (!string.IsNullOrWhiteSpace(Password)) if (!string.IsNullOrWhiteSpace(Password))
{ {
existingSend.Password = sendService.HashPassword(Password); existingSend.Password = authorizationService.HashPassword(Password);
} }
existingSend.Disabled = Disabled.GetValueOrDefault(); existingSend.Disabled = Disabled.GetValueOrDefault();
existingSend.HideEmail = HideEmail.GetValueOrDefault(); existingSend.HideEmail = HideEmail.GetValueOrDefault();

View File

@ -151,6 +151,16 @@ public class CiphersController : Controller
public async Task<CipherResponseModel> Post([FromBody] CipherRequestModel model) public async Task<CipherResponseModel> Post([FromBody] CipherRequestModel model)
{ {
var user = await _userService.GetUserByPrincipalAsync(User); var user = await _userService.GetUserByPrincipalAsync(User);
// Validate the model was encrypted for the posting user
if (model.EncryptedFor != null)
{
if (model.EncryptedFor != user.Id)
{
throw new BadRequestException("Cipher was not encrypted for the current user. Please try again.");
}
}
var cipher = model.ToCipherDetails(user.Id); var cipher = model.ToCipherDetails(user.Id);
if (cipher.OrganizationId.HasValue && !await _currentContext.OrganizationUser(cipher.OrganizationId.Value)) if (cipher.OrganizationId.HasValue && !await _currentContext.OrganizationUser(cipher.OrganizationId.Value))
{ {
@ -170,6 +180,16 @@ public class CiphersController : Controller
public async Task<CipherResponseModel> PostCreate([FromBody] CipherCreateRequestModel model) public async Task<CipherResponseModel> PostCreate([FromBody] CipherCreateRequestModel model)
{ {
var user = await _userService.GetUserByPrincipalAsync(User); var user = await _userService.GetUserByPrincipalAsync(User);
// Validate the model was encrypted for the posting user
if (model.Cipher.EncryptedFor != null)
{
if (model.Cipher.EncryptedFor != user.Id)
{
throw new BadRequestException("Cipher was not encrypted for the current user. Please try again.");
}
}
var cipher = model.Cipher.ToCipherDetails(user.Id); var cipher = model.Cipher.ToCipherDetails(user.Id);
if (cipher.OrganizationId.HasValue && !await _currentContext.OrganizationUser(cipher.OrganizationId.Value)) if (cipher.OrganizationId.HasValue && !await _currentContext.OrganizationUser(cipher.OrganizationId.Value))
{ {
@ -192,6 +212,16 @@ public class CiphersController : Controller
} }
var userId = _userService.GetProperUserId(User).Value; var userId = _userService.GetProperUserId(User).Value;
// Validate the model was encrypted for the posting user
if (model.Cipher.EncryptedFor != null)
{
if (model.Cipher.EncryptedFor != userId)
{
throw new BadRequestException("Cipher was not encrypted for the current user. Please try again.");
}
}
await _cipherService.SaveAsync(cipher, userId, model.Cipher.LastKnownRevisionDate, model.CollectionIds, true, false); await _cipherService.SaveAsync(cipher, userId, model.Cipher.LastKnownRevisionDate, model.CollectionIds, true, false);
var response = new CipherMiniResponseModel(cipher, _globalSettings, false); var response = new CipherMiniResponseModel(cipher, _globalSettings, false);
@ -209,6 +239,15 @@ public class CiphersController : Controller
throw new NotFoundException(); throw new NotFoundException();
} }
// Validate the model was encrypted for the posting user
if (model.EncryptedFor != null)
{
if (model.EncryptedFor != user.Id)
{
throw new BadRequestException("Cipher was not encrypted for the current user. Please try again.");
}
}
ValidateClientVersionForFido2CredentialSupport(cipher); ValidateClientVersionForFido2CredentialSupport(cipher);
var collectionIds = (await _collectionCipherRepository.GetManyByUserIdCipherIdAsync(user.Id, id)).Select(c => c.CollectionId).ToList(); var collectionIds = (await _collectionCipherRepository.GetManyByUserIdCipherIdAsync(user.Id, id)).Select(c => c.CollectionId).ToList();
@ -237,6 +276,15 @@ public class CiphersController : Controller
var userId = _userService.GetProperUserId(User).Value; var userId = _userService.GetProperUserId(User).Value;
var cipher = await _cipherRepository.GetOrganizationDetailsByIdAsync(id); var cipher = await _cipherRepository.GetOrganizationDetailsByIdAsync(id);
// Validate the model was encrypted for the posting user
if (model.EncryptedFor != null)
{
if (model.EncryptedFor != userId)
{
throw new BadRequestException("Cipher was not encrypted for the current user. Please try again.");
}
}
ValidateClientVersionForFido2CredentialSupport(cipher); ValidateClientVersionForFido2CredentialSupport(cipher);
if (cipher == null || !cipher.OrganizationId.HasValue || if (cipher == null || !cipher.OrganizationId.HasValue ||
@ -315,26 +363,10 @@ public class CiphersController : Controller
{ {
var org = _currentContext.GetOrganization(organizationId); var org = _currentContext.GetOrganization(organizationId);
// If we're not an "admin", we don't need to check the ciphers // If we're not an "admin" or if we're not a provider user we don't need to check the ciphers
if (org is not ({ Type: OrganizationUserType.Owner or OrganizationUserType.Admin } or { Permissions.EditAnyCollection: true })) if (org is not ({ Type: OrganizationUserType.Owner or OrganizationUserType.Admin } or { Permissions.EditAnyCollection: true }) || await _currentContext.ProviderUserForOrgAsync(organizationId))
{ {
// Are we a provider user? If so, we need to be sure we're not restricted return false;
// Once the feature flag is removed, this check can be combined with the above
if (await _currentContext.ProviderUserForOrgAsync(organizationId))
{
// Provider is restricted from editing ciphers, so we're not an "admin"
if (_featureService.IsEnabled(FeatureFlagKeys.RestrictProviderAccess))
{
return false;
}
// Provider is unrestricted, so we're an "admin", don't return early
}
else
{
// Not a provider or admin
return false;
}
} }
// We know we're an "admin", now check the ciphers explicitly (in case admins are restricted) // We know we're an "admin", now check the ciphers explicitly (in case admins are restricted)
@ -350,26 +382,10 @@ public class CiphersController : Controller
var org = _currentContext.GetOrganization(organizationId); var org = _currentContext.GetOrganization(organizationId);
// If we're not an "admin", we don't need to check the ciphers // If we're not an "admin" or if we're a provider user we don't need to check the ciphers
if (org is not ({ Type: OrganizationUserType.Owner or OrganizationUserType.Admin } or { Permissions.EditAnyCollection: true })) if (org is not ({ Type: OrganizationUserType.Owner or OrganizationUserType.Admin } or { Permissions.EditAnyCollection: true }) || await _currentContext.ProviderUserForOrgAsync(organizationId))
{ {
// Are we a provider user? If so, we need to be sure we're not restricted return false;
// Once the feature flag is removed, this check can be combined with the above
if (await _currentContext.ProviderUserForOrgAsync(organizationId))
{
// Provider is restricted from editing ciphers, so we're not an "admin"
if (_featureService.IsEnabled(FeatureFlagKeys.RestrictProviderAccess))
{
return false;
}
// Provider is unrestricted, so we're an "admin", don't return early
}
else
{
// Not a provider or admin
return false;
}
} }
// If the user can edit all ciphers for the organization, just check they all belong to the org // If the user can edit all ciphers for the organization, just check they all belong to the org
@ -462,10 +478,10 @@ public class CiphersController : Controller
return true; return true;
} }
// Provider users can edit all ciphers if RestrictProviderAccess is disabled // Provider users cannot edit ciphers
if (await _currentContext.ProviderUserForOrgAsync(organizationId)) if (await _currentContext.ProviderUserForOrgAsync(organizationId))
{ {
return !_featureService.IsEnabled(FeatureFlagKeys.RestrictProviderAccess); return false;
} }
return false; return false;
@ -485,10 +501,10 @@ public class CiphersController : Controller
return true; return true;
} }
// Provider users can only access organization ciphers if RestrictProviderAccess is disabled // Provider users cannot access organization ciphers
if (await _currentContext.ProviderUserForOrgAsync(organizationId)) if (await _currentContext.ProviderUserForOrgAsync(organizationId))
{ {
return !_featureService.IsEnabled(FeatureFlagKeys.RestrictProviderAccess); return false;
} }
return false; return false;
@ -508,10 +524,10 @@ public class CiphersController : Controller
return true; return true;
} }
// Provider users can only access all ciphers if RestrictProviderAccess is disabled // Provider users cannot access ciphers
if (await _currentContext.ProviderUserForOrgAsync(organizationId)) if (await _currentContext.ProviderUserForOrgAsync(organizationId))
{ {
return !_featureService.IsEnabled(FeatureFlagKeys.RestrictProviderAccess); return false;
} }
return false; return false;
@ -690,6 +706,15 @@ public class CiphersController : Controller
throw new NotFoundException(); throw new NotFoundException();
} }
// Validate the model was encrypted for the posting user
if (model.Cipher.EncryptedFor != null)
{
if (model.Cipher.EncryptedFor != user.Id)
{
throw new BadRequestException("Cipher was not encrypted for the current user. Please try again.");
}
}
ValidateClientVersionForFido2CredentialSupport(cipher); ValidateClientVersionForFido2CredentialSupport(cipher);
var original = cipher.Clone(); var original = cipher.Clone();
@ -1051,6 +1076,18 @@ public class CiphersController : Controller
var ciphers = await _cipherRepository.GetManyByUserIdAsync(userId, withOrganizations: false); var ciphers = await _cipherRepository.GetManyByUserIdAsync(userId, withOrganizations: false);
var ciphersDict = ciphers.ToDictionary(c => c.Id); var ciphersDict = ciphers.ToDictionary(c => c.Id);
// Validate the model was encrypted for the posting user
foreach (var cipher in model.Ciphers)
{
if (cipher.EncryptedFor != null)
{
if (cipher.EncryptedFor != userId)
{
throw new BadRequestException("Cipher was not encrypted for the current user. Please try again.");
}
}
}
var shareCiphers = new List<(Cipher, DateTime?)>(); var shareCiphers = new List<(Cipher, DateTime?)>();
foreach (var cipher in model.Ciphers) foreach (var cipher in model.Ciphers)
{ {
@ -1086,9 +1123,8 @@ public class CiphersController : Controller
throw new BadRequestException(ModelState); throw new BadRequestException(ModelState);
} }
// If Account Deprovisioning is enabled, we need to check if the user is claimed by any organization. // Check if the user is claimed by any organization.
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning) if (await _userService.IsClaimedByAnyOrganizationAsync(user.Id))
&& await _userService.IsClaimedByAnyOrganizationAsync(user.Id))
{ {
throw new BadRequestException("Cannot purge accounts owned by an organization. Contact your organization administrator for additional details."); throw new BadRequestException("Cannot purge accounts owned by an organization. Contact your organization administrator for additional details.");
} }

View File

@ -11,6 +11,10 @@ namespace Bit.Api.Vault.Models.Request;
public class CipherRequestModel public class CipherRequestModel
{ {
/// <summary>
/// The Id of the user that encrypted the cipher. It should always represent a UserId.
/// </summary>
public Guid? EncryptedFor { get; set; }
public CipherType Type { get; set; } public CipherType Type { get; set; }
[StringLength(36)] [StringLength(36)]

View File

@ -63,6 +63,12 @@ public class FreshdeskController : Controller
note += $"<li>Region: {_billingSettings.FreshDesk.Region}</li>"; note += $"<li>Region: {_billingSettings.FreshDesk.Region}</li>";
var customFields = new Dictionary<string, object>(); var customFields = new Dictionary<string, object>();
var user = await _userRepository.GetByEmailAsync(ticketContactEmail); var user = await _userRepository.GetByEmailAsync(ticketContactEmail);
if (user == null)
{
note += $"<li>No user found: {ticketContactEmail}</li>";
await CreateNote(ticketId, note);
}
if (user != null) if (user != null)
{ {
var userLink = $"{_globalSettings.BaseServiceUri.Admin}/users/edit/{user.Id}"; var userLink = $"{_globalSettings.BaseServiceUri.Admin}/users/edit/{user.Id}";
@ -121,18 +127,7 @@ public class FreshdeskController : Controller
Content = JsonContent.Create(updateBody), Content = JsonContent.Create(updateBody),
}; };
await CallFreshdeskApiAsync(updateRequest); await CallFreshdeskApiAsync(updateRequest);
await CreateNote(ticketId, note);
var noteBody = new Dictionary<string, object>
{
{ "body", $"<ul>{note}</ul>" },
{ "private", true }
};
var noteRequest = new HttpRequestMessage(HttpMethod.Post,
string.Format("https://bitwarden.freshdesk.com/api/v2/tickets/{0}/notes", ticketId))
{
Content = JsonContent.Create(noteBody),
};
await CallFreshdeskApiAsync(noteRequest);
} }
return new OkResult(); return new OkResult();
@ -208,6 +203,21 @@ public class FreshdeskController : Controller
return true; return true;
} }
private async Task CreateNote(string ticketId, string note)
{
var noteBody = new Dictionary<string, object>
{
{ "body", $"<ul>{note}</ul>" },
{ "private", true }
};
var noteRequest = new HttpRequestMessage(HttpMethod.Post,
string.Format("https://bitwarden.freshdesk.com/api/v2/tickets/{0}/notes", ticketId))
{
Content = JsonContent.Create(noteBody),
};
await CallFreshdeskApiAsync(noteRequest);
}
private async Task AddAnswerNoteToTicketAsync(string note, string ticketId) private async Task AddAnswerNoteToTicketAsync(string note, string ticketId)
{ {
// if there is no content, then we don't need to add a note // if there is no content, then we don't need to add a note

View File

@ -1,8 +1,8 @@
using Bit.Billing.Constants; using Bit.Billing.Constants;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Entities;
using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Repositories; using Bit.Core.Billing.Providers.Entities;
using Bit.Core.Billing.Providers.Repositories;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Stripe; using Stripe;

View File

@ -1,11 +1,11 @@
using Bit.Core; using Bit.Core;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Constants; using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Enums; using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Extensions;
using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Services;
using Bit.Core.Billing.Services.Contracts;
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces; using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
@ -25,8 +25,7 @@ public class UpcomingInvoiceHandler(
IStripeEventService stripeEventService, IStripeEventService stripeEventService,
IStripeEventUtilityService stripeEventUtilityService, IStripeEventUtilityService stripeEventUtilityService,
IUserRepository userRepository, IUserRepository userRepository,
IValidateSponsorshipCommand validateSponsorshipCommand, IValidateSponsorshipCommand validateSponsorshipCommand)
IAutomaticTaxFactory automaticTaxFactory)
: IUpcomingInvoiceHandler : IUpcomingInvoiceHandler
{ {
public async Task HandleAsync(Event parsedEvent) public async Task HandleAsync(Event parsedEvent)
@ -46,6 +45,8 @@ public class UpcomingInvoiceHandler(
var (organizationId, userId, providerId) = stripeEventUtilityService.GetIdsFromMetadata(subscription.Metadata); var (organizationId, userId, providerId) = stripeEventUtilityService.GetIdsFromMetadata(subscription.Metadata);
var setNonUSBusinessUseToReverseCharge = featureService.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge);
if (organizationId.HasValue) if (organizationId.HasValue)
{ {
var organization = await organizationRepository.GetByIdAsync(organizationId.Value); var organization = await organizationRepository.GetByIdAsync(organizationId.Value);
@ -55,7 +56,7 @@ public class UpcomingInvoiceHandler(
return; return;
} }
await TryEnableAutomaticTaxAsync(subscription); await AlignOrganizationTaxConcernsAsync(organization, subscription, parsedEvent.Id, setNonUSBusinessUseToReverseCharge);
var plan = await pricingClient.GetPlanOrThrow(organization.PlanType); var plan = await pricingClient.GetPlanOrThrow(organization.PlanType);
@ -100,7 +101,25 @@ public class UpcomingInvoiceHandler(
return; return;
} }
await TryEnableAutomaticTaxAsync(subscription); if (!subscription.AutomaticTax.Enabled && subscription.Customer.HasRecognizedTaxLocation())
{
try
{
await stripeFacade.UpdateSubscription(subscription.Id,
new SubscriptionUpdateOptions
{
AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }
});
}
catch (Exception exception)
{
logger.LogError(
exception,
"Failed to set user's ({UserID}) subscription to automatic tax while processing event with ID {EventID}",
user.Id,
parsedEvent.Id);
}
}
if (user.Premium) if (user.Premium)
{ {
@ -116,7 +135,7 @@ public class UpcomingInvoiceHandler(
return; return;
} }
await TryEnableAutomaticTaxAsync(subscription); await AlignProviderTaxConcernsAsync(provider, subscription, parsedEvent.Id, setNonUSBusinessUseToReverseCharge);
await SendUpcomingInvoiceEmailsAsync(new List<string> { provider.BillingEmail }, invoice); await SendUpcomingInvoiceEmailsAsync(new List<string> { provider.BillingEmail }, invoice);
} }
@ -139,50 +158,123 @@ public class UpcomingInvoiceHandler(
} }
} }
private async Task TryEnableAutomaticTaxAsync(Subscription subscription) private async Task AlignOrganizationTaxConcernsAsync(
Organization organization,
Subscription subscription,
string eventId,
bool setNonUSBusinessUseToReverseCharge)
{ {
if (featureService.IsEnabled(FeatureFlagKeys.PM19147_AutomaticTaxImprovements)) var nonUSBusinessUse =
{ organization.PlanType.GetProductTier() != ProductTierType.Families &&
var automaticTaxParameters = new AutomaticTaxFactoryParameters(subscription.Items.Select(x => x.Price.Id)); subscription.Customer.Address.Country != "US";
var automaticTaxStrategy = await automaticTaxFactory.CreateAsync(automaticTaxParameters);
var updateOptions = automaticTaxStrategy.GetUpdateOptions(subscription);
if (updateOptions == null) bool setAutomaticTaxToEnabled;
if (setNonUSBusinessUseToReverseCharge)
{
if (nonUSBusinessUse && subscription.Customer.TaxExempt != StripeConstants.TaxExempt.Reverse)
{ {
return; try
{
await stripeFacade.UpdateCustomer(subscription.CustomerId,
new CustomerUpdateOptions { TaxExempt = StripeConstants.TaxExempt.Reverse });
}
catch (Exception exception)
{
logger.LogError(
exception,
"Failed to set organization's ({OrganizationID}) to reverse tax exemption while processing event with ID {EventID}",
organization.Id,
eventId);
}
} }
await stripeFacade.UpdateSubscription(subscription.Id, updateOptions); setAutomaticTaxToEnabled = true;
return;
} }
else
if (subscription.AutomaticTax.Enabled ||
!subscription.Customer.HasBillingLocation() ||
await IsNonTaxableNonUSBusinessUseSubscription(subscription))
{ {
return; setAutomaticTaxToEnabled =
subscription.Customer.HasRecognizedTaxLocation() &&
(subscription.Customer.Address.Country == "US" ||
(nonUSBusinessUse && subscription.Customer.TaxIds.Any()));
} }
await stripeFacade.UpdateSubscription(subscription.Id, if (!subscription.AutomaticTax.Enabled && setAutomaticTaxToEnabled)
new SubscriptionUpdateOptions {
try
{ {
DefaultTaxRates = [], await stripeFacade.UpdateSubscription(subscription.Id,
AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true } new SubscriptionUpdateOptions
}); {
AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }
});
}
catch (Exception exception)
{
logger.LogError(
exception,
"Failed to set organization's ({OrganizationID}) subscription to automatic tax while processing event with ID {EventID}",
organization.Id,
eventId);
}
}
}
return; private async Task AlignProviderTaxConcernsAsync(
Provider provider,
Subscription subscription,
string eventId,
bool setNonUSBusinessUseToReverseCharge)
{
bool setAutomaticTaxToEnabled;
async Task<bool> IsNonTaxableNonUSBusinessUseSubscription(Subscription localSubscription) if (setNonUSBusinessUseToReverseCharge)
{ {
var familyPriceIds = (await Task.WhenAll( if (subscription.Customer.Address.Country != "US" && subscription.Customer.TaxExempt != StripeConstants.TaxExempt.Reverse)
pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually2019), {
pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually))) try
.Select(plan => plan.PasswordManager.StripePlanId); {
await stripeFacade.UpdateCustomer(subscription.CustomerId,
new CustomerUpdateOptions { TaxExempt = StripeConstants.TaxExempt.Reverse });
}
catch (Exception exception)
{
logger.LogError(
exception,
"Failed to set provider's ({ProviderID}) to reverse tax exemption while processing event with ID {EventID}",
provider.Id,
eventId);
}
}
return localSubscription.Customer.Address.Country != "US" && setAutomaticTaxToEnabled = true;
localSubscription.Metadata.ContainsKey(StripeConstants.MetadataKeys.OrganizationId) && }
!localSubscription.Items.Select(item => item.Price.Id).Intersect(familyPriceIds).Any() && else
!localSubscription.Customer.TaxIds.Any(); {
setAutomaticTaxToEnabled =
subscription.Customer.HasRecognizedTaxLocation() &&
(subscription.Customer.Address.Country == "US" ||
subscription.Customer.TaxIds.Any());
}
if (!subscription.AutomaticTax.Enabled && setAutomaticTaxToEnabled)
{
try
{
await stripeFacade.UpdateSubscription(subscription.Id,
new SubscriptionUpdateOptions
{
AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }
});
}
catch (Exception exception)
{
logger.LogError(
exception,
"Failed to set provider's ({ProviderID}) subscription to automatic tax while processing event with ID {EventID}",
provider.Id,
eventId);
}
} }
} }
} }

View File

@ -114,6 +114,11 @@ public class Organization : ITableObject<Guid>, IStorableSubscriber, IRevisable,
/// </summary> /// </summary>
public bool UseRiskInsights { get; set; } public bool UseRiskInsights { get; set; }
/// <summary>
/// If true, the organization can claim domains, which unlocks additional enterprise features
/// </summary>
public bool UseOrganizationDomains { get; set; }
/// <summary> /// <summary>
/// If set to true, admins can initiate organization-issued sponsorships. /// If set to true, admins can initiate organization-issued sponsorships.
/// </summary> /// </summary>
@ -319,5 +324,7 @@ public class Organization : ITableObject<Guid>, IStorableSubscriber, IRevisable,
SmSeats = license.SmSeats; SmSeats = license.SmSeats;
SmServiceAccounts = license.SmServiceAccounts; SmServiceAccounts = license.SmServiceAccounts;
UseRiskInsights = license.UseRiskInsights; UseRiskInsights = license.UseRiskInsights;
UseOrganizationDomains = license.UseOrganizationDomains;
UseAdminSponsoredFamilies = license.UseAdminSponsoredFamilies;
} }
} }

View File

@ -26,6 +26,7 @@ public class OrganizationAbility
LimitItemDeletion = organization.LimitItemDeletion; LimitItemDeletion = organization.LimitItemDeletion;
AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems; AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems;
UseRiskInsights = organization.UseRiskInsights; UseRiskInsights = organization.UseRiskInsights;
UseOrganizationDomains = organization.UseOrganizationDomains;
UseAdminSponsoredFamilies = organization.UseAdminSponsoredFamilies; UseAdminSponsoredFamilies = organization.UseAdminSponsoredFamilies;
} }
@ -46,5 +47,6 @@ public class OrganizationAbility
public bool LimitItemDeletion { get; set; } public bool LimitItemDeletion { get; set; }
public bool AllowAdminAccessToAllCollectionItems { get; set; } public bool AllowAdminAccessToAllCollectionItems { get; set; }
public bool UseRiskInsights { get; set; } public bool UseRiskInsights { get; set; }
public bool UseOrganizationDomains { get; set; }
public bool UseAdminSponsoredFamilies { get; set; } public bool UseAdminSponsoredFamilies { get; set; }
} }

View File

@ -59,5 +59,7 @@ public class OrganizationUserOrganizationDetails
public bool LimitItemDeletion { get; set; } public bool LimitItemDeletion { get; set; }
public bool AllowAdminAccessToAllCollectionItems { get; set; } public bool AllowAdminAccessToAllCollectionItems { get; set; }
public bool UseRiskInsights { get; set; } public bool UseRiskInsights { get; set; }
public bool UseOrganizationDomains { get; set; }
public bool UseAdminSponsoredFamilies { get; set; } public bool UseAdminSponsoredFamilies { get; set; }
public bool? IsAdminInitiated { get; set; }
} }

View File

@ -150,6 +150,7 @@ public class SelfHostedOrganizationDetails : Organization
AllowAdminAccessToAllCollectionItems = AllowAdminAccessToAllCollectionItems, AllowAdminAccessToAllCollectionItems = AllowAdminAccessToAllCollectionItems,
Status = Status, Status = Status,
UseRiskInsights = UseRiskInsights, UseRiskInsights = UseRiskInsights,
UseAdminSponsoredFamilies = UseAdminSponsoredFamilies,
}; };
} }
} }

View File

@ -45,6 +45,7 @@ public class ProviderUserOrganizationDetails
public bool LimitItemDeletion { get; set; } public bool LimitItemDeletion { get; set; }
public bool AllowAdminAccessToAllCollectionItems { get; set; } public bool AllowAdminAccessToAllCollectionItems { get; set; }
public bool UseRiskInsights { get; set; } public bool UseRiskInsights { get; set; }
public bool UseOrganizationDomains { get; set; }
public bool UseAdminSponsoredFamilies { get; set; } public bool UseAdminSponsoredFamilies { get; set; }
public ProviderType ProviderType { get; set; } public ProviderType ProviderType { get; set; }
} }

View File

@ -20,7 +20,6 @@ public class VerifyOrganizationDomainCommand(
IDnsResolverService dnsResolverService, IDnsResolverService dnsResolverService,
IEventService eventService, IEventService eventService,
IGlobalSettings globalSettings, IGlobalSettings globalSettings,
IFeatureService featureService,
ICurrentContext currentContext, ICurrentContext currentContext,
ISavePolicyCommand savePolicyCommand, ISavePolicyCommand savePolicyCommand,
IMailService mailService, IMailService mailService,
@ -125,11 +124,8 @@ public class VerifyOrganizationDomainCommand(
private async Task DomainVerificationSideEffectsAsync(OrganizationDomain domain, IActingUser actingUser) private async Task DomainVerificationSideEffectsAsync(OrganizationDomain domain, IActingUser actingUser)
{ {
if (featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)) await EnableSingleOrganizationPolicyAsync(domain.OrganizationId, actingUser);
{ await SendVerifiedDomainUserEmailAsync(domain);
await EnableSingleOrganizationPolicyAsync(domain.OrganizationId, actingUser);
await SendVerifiedDomainUserEmailAsync(domain);
}
} }
private async Task EnableSingleOrganizationPolicyAsync(Guid organizationId, IActingUser actingUser) => private async Task EnableSingleOrganizationPolicyAsync(Guid organizationId, IActingUser actingUser) =>

View File

@ -24,9 +24,7 @@ public class GetOrganizationUsersClaimedStatusQuery : IGetOrganizationUsersClaim
// Users can only be claimed by an Organization that is enabled and can have organization domains // Users can only be claimed by an Organization that is enabled and can have organization domains
var organizationAbility = await _applicationCacheService.GetOrganizationAbilityAsync(organizationId); var organizationAbility = await _applicationCacheService.GetOrganizationAbilityAsync(organizationId);
// TODO: Replace "UseSso" with a new organization ability like "UseOrganizationDomains" (PM-11622). if (organizationAbility is { Enabled: true, UseOrganizationDomains: true })
// Verified domains were tied to SSO, so we currently check the "UseSso" organization ability.
if (organizationAbility is { Enabled: true, UseSso: true })
{ {
// Get all organization users with claimed domains by the organization // Get all organization users with claimed domains by the organization
var organizationUsersWithClaimedDomain = await _organizationUserRepository.GetManyByOrganizationWithClaimedDomainsAsync(organizationId); var organizationUsersWithClaimedDomain = await _organizationUserRepository.GetManyByOrganizationWithClaimedDomainsAsync(organizationId);

View File

@ -159,7 +159,7 @@ public class RemoveOrganizationUserCommand : IRemoveOrganizationUserCommand
throw new BadRequestException(RemoveAdminByCustomUserErrorMessage); throw new BadRequestException(RemoveAdminByCustomUserErrorMessage);
} }
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning) && deletingUserId.HasValue && eventSystemUser == null) if (deletingUserId.HasValue && eventSystemUser == null)
{ {
var claimedStatus = await _getOrganizationUsersClaimedStatusQuery.GetUsersOrganizationClaimedStatusAsync(orgUser.OrganizationId, new[] { orgUser.Id }); var claimedStatus = await _getOrganizationUsersClaimedStatusQuery.GetUsersOrganizationClaimedStatusAsync(orgUser.OrganizationId, new[] { orgUser.Id });
if (claimedStatus.TryGetValue(orgUser.Id, out var isClaimed) && isClaimed) if (claimedStatus.TryGetValue(orgUser.Id, out var isClaimed) && isClaimed)
@ -214,7 +214,7 @@ public class RemoveOrganizationUserCommand : IRemoveOrganizationUserCommand
deletingUserIsOwner = await _currentContext.OrganizationOwner(organizationId); deletingUserIsOwner = await _currentContext.OrganizationOwner(organizationId);
} }
var claimedStatus = _featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning) && deletingUserId.HasValue && eventSystemUser == null var claimedStatus = deletingUserId.HasValue && eventSystemUser == null
? await _getOrganizationUsersClaimedStatusQuery.GetUsersOrganizationClaimedStatusAsync(organizationId, filteredUsers.Select(u => u.Id)) ? await _getOrganizationUsersClaimedStatusQuery.GetUsersOrganizationClaimedStatusAsync(organizationId, filteredUsers.Select(u => u.Id))
: filteredUsers.ToDictionary(u => u.Id, u => false); : filteredUsers.ToDictionary(u => u.Id, u => false);
var result = new List<(OrganizationUser OrganizationUser, string ErrorMessage)>(); var result = new List<(OrganizationUser OrganizationUser, string ErrorMessage)>();

View File

@ -104,7 +104,8 @@ public class CloudOrganizationSignUpCommand(
RevisionDate = DateTime.UtcNow, RevisionDate = DateTime.UtcNow,
Status = OrganizationStatusType.Created, Status = OrganizationStatusType.Created,
UsePasswordManager = true, UsePasswordManager = true,
UseSecretsManager = signup.UseSecretsManager UseSecretsManager = signup.UseSecretsManager,
UseOrganizationDomains = plan.HasOrganizationDomains,
}; };
if (signup.UseSecretsManager) if (signup.UseSecretsManager)

View File

@ -0,0 +1,187 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Billing.Pricing;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Business;
using Bit.Core.Models.StaticStore;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Tools.Enums;
using Bit.Core.Tools.Models.Business;
using Bit.Core.Tools.Services;
using Bit.Core.Utilities;
namespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
public record ProviderClientOrganizationSignUpResponse(
Organization Organization,
Collection DefaultCollection);
public interface IProviderClientOrganizationSignUpCommand
{
/// <summary>
/// Sign up a new client organization for a provider.
/// </summary>
/// <param name="signup">The signup information.</param>
/// <returns>A tuple containing the new organization and its default collection.</returns>
Task<ProviderClientOrganizationSignUpResponse> SignUpClientOrganizationAsync(OrganizationSignup signup);
}
public class ProviderClientOrganizationSignUpCommand : IProviderClientOrganizationSignUpCommand
{
public const string PlanNullErrorMessage = "Password Manager Plan was null.";
public const string PlanDisabledErrorMessage = "Password Manager Plan is disabled.";
public const string AdditionalSeatsNegativeErrorMessage = "You can't subtract Password Manager seats!";
private readonly ICurrentContext _currentContext;
private readonly IPricingClient _pricingClient;
private readonly IReferenceEventService _referenceEventService;
private readonly IOrganizationRepository _organizationRepository;
private readonly IOrganizationApiKeyRepository _organizationApiKeyRepository;
private readonly IApplicationCacheService _applicationCacheService;
private readonly ICollectionRepository _collectionRepository;
public ProviderClientOrganizationSignUpCommand(
ICurrentContext currentContext,
IPricingClient pricingClient,
IReferenceEventService referenceEventService,
IOrganizationRepository organizationRepository,
IOrganizationApiKeyRepository organizationApiKeyRepository,
IApplicationCacheService applicationCacheService,
ICollectionRepository collectionRepository)
{
_currentContext = currentContext;
_pricingClient = pricingClient;
_referenceEventService = referenceEventService;
_organizationRepository = organizationRepository;
_organizationApiKeyRepository = organizationApiKeyRepository;
_applicationCacheService = applicationCacheService;
_collectionRepository = collectionRepository;
}
public async Task<ProviderClientOrganizationSignUpResponse> SignUpClientOrganizationAsync(OrganizationSignup signup)
{
var plan = await _pricingClient.GetPlanOrThrow(signup.Plan);
ValidatePlan(plan, signup.AdditionalSeats);
var organization = new Organization
{
// Pre-generate the org id so that we can save it with the Stripe subscription.
Id = CoreHelpers.GenerateComb(),
Name = signup.Name,
BillingEmail = signup.BillingEmail,
PlanType = plan!.Type,
Seats = signup.AdditionalSeats,
MaxCollections = plan.PasswordManager.MaxCollections,
MaxStorageGb = 1,
UsePolicies = plan.HasPolicies,
UseSso = plan.HasSso,
UseOrganizationDomains = plan.HasOrganizationDomains,
UseGroups = plan.HasGroups,
UseEvents = plan.HasEvents,
UseDirectory = plan.HasDirectory,
UseTotp = plan.HasTotp,
Use2fa = plan.Has2fa,
UseApi = plan.HasApi,
UseResetPassword = plan.HasResetPassword,
SelfHost = plan.HasSelfHost,
UsersGetPremium = plan.UsersGetPremium,
UseCustomPermissions = plan.HasCustomPermissions,
UseScim = plan.HasScim,
Plan = plan.Name,
Gateway = GatewayType.Stripe,
ReferenceData = signup.Owner.ReferenceData,
Enabled = true,
LicenseKey = CoreHelpers.SecureRandomString(20),
PublicKey = signup.PublicKey,
PrivateKey = signup.PrivateKey,
CreationDate = DateTime.UtcNow,
RevisionDate = DateTime.UtcNow,
Status = OrganizationStatusType.Created,
UsePasswordManager = true,
// Secrets Manager not available for purchase with Consolidated Billing.
UseSecretsManager = false,
};
var returnValue = await SignUpAsync(organization, signup.CollectionName);
await _referenceEventService.RaiseEventAsync(
new ReferenceEvent(ReferenceEventType.Signup, organization, _currentContext)
{
PlanName = plan.Name,
PlanType = plan.Type,
Seats = returnValue.Organization.Seats,
SignupInitiationPath = signup.InitiationPath,
Storage = returnValue.Organization.MaxStorageGb,
});
return returnValue;
}
private static void ValidatePlan(Plan plan, int additionalSeats)
{
if (plan is null)
{
throw new BadRequestException(PlanNullErrorMessage);
}
if (plan.Disabled)
{
throw new BadRequestException(PlanDisabledErrorMessage);
}
if (additionalSeats < 0)
{
throw new BadRequestException(AdditionalSeatsNegativeErrorMessage);
}
}
/// <summary>
/// Private helper method to create a new organization.
/// </summary>
private async Task<ProviderClientOrganizationSignUpResponse> SignUpAsync(
Organization organization, string collectionName)
{
try
{
await _organizationRepository.CreateAsync(organization);
await _organizationApiKeyRepository.CreateAsync(new OrganizationApiKey
{
OrganizationId = organization.Id,
ApiKey = CoreHelpers.SecureRandomString(30),
Type = OrganizationApiKeyType.Default,
RevisionDate = DateTime.UtcNow,
});
await _applicationCacheService.UpsertOrganizationAbilityAsync(organization);
Collection defaultCollection = null;
if (!string.IsNullOrWhiteSpace(collectionName))
{
defaultCollection = new Collection
{
Name = collectionName,
OrganizationId = organization.Id,
CreationDate = organization.CreationDate,
RevisionDate = organization.CreationDate
};
await _collectionRepository.CreateAsync(defaultCollection, null, null);
}
return new ProviderClientOrganizationSignUpResponse(organization, defaultCollection);
}
catch
{
if (organization.Id != default)
{
await _organizationRepository.DeleteAsync(organization);
await _applicationCacheService.DeleteOrganizationAbilityAsync(organization.Id);
}
throw;
}
}
}

View File

@ -61,16 +61,9 @@ public class SingleOrgPolicyValidator : IPolicyValidator
{ {
if (currentPolicy is not { Enabled: true } && policyUpdate is { Enabled: true }) if (currentPolicy is not { Enabled: true } && policyUpdate is { Enabled: true })
{ {
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)) var currentUser = _currentContext.UserId ?? Guid.Empty;
{ var isOwnerOrProvider = await _currentContext.OrganizationOwner(policyUpdate.OrganizationId);
var currentUser = _currentContext.UserId ?? Guid.Empty; await RevokeNonCompliantUsersAsync(policyUpdate.OrganizationId, policyUpdate.PerformedBy ?? new StandardUser(currentUser, isOwnerOrProvider));
var isOwnerOrProvider = await _currentContext.OrganizationOwner(policyUpdate.OrganizationId);
await RevokeNonCompliantUsersAsync(policyUpdate.OrganizationId, policyUpdate.PerformedBy ?? new StandardUser(currentUser, isOwnerOrProvider));
}
else
{
await RemoveNonCompliantUsersAsync(policyUpdate.OrganizationId);
}
} }
} }
@ -116,42 +109,6 @@ public class SingleOrgPolicyValidator : IPolicyValidator
_mailService.SendOrganizationUserRevokedForPolicySingleOrgEmailAsync(organization.DisplayName(), x.Email))); _mailService.SendOrganizationUserRevokedForPolicySingleOrgEmailAsync(organization.DisplayName(), x.Email)));
} }
private async Task RemoveNonCompliantUsersAsync(Guid organizationId)
{
// Remove non-compliant users
var savingUserId = _currentContext.UserId;
// Note: must get OrganizationUserUserDetails so that Email is always populated from the User object
var orgUsers = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(organizationId);
var org = await _organizationRepository.GetByIdAsync(organizationId);
if (org == null)
{
throw new NotFoundException(OrganizationNotFoundErrorMessage);
}
var removableOrgUsers = orgUsers.Where(ou =>
ou.Status != OrganizationUserStatusType.Invited &&
ou.Status != OrganizationUserStatusType.Revoked &&
ou.Type != OrganizationUserType.Owner &&
ou.Type != OrganizationUserType.Admin &&
ou.UserId != savingUserId
).ToList();
var userOrgs = await _organizationUserRepository.GetManyByManyUsersAsync(
removableOrgUsers.Select(ou => ou.UserId!.Value));
foreach (var orgUser in removableOrgUsers)
{
if (userOrgs.Any(ou => ou.UserId == orgUser.UserId
&& ou.OrganizationId != org.Id
&& ou.Status != OrganizationUserStatusType.Invited))
{
await _removeOrganizationUserCommand.RemoveUserAsync(organizationId, orgUser.Id, savingUserId);
await _mailService.SendOrganizationUserRemovedForPolicySingleOrgEmailAsync(
org.DisplayName(), orgUser.Email);
}
}
}
public async Task<string> ValidateAsync(PolicyUpdate policyUpdate, Policy? currentPolicy) public async Task<string> ValidateAsync(PolicyUpdate policyUpdate, Policy? currentPolicy)
{ {
if (policyUpdate is not { Enabled: true }) if (policyUpdate is not { Enabled: true })
@ -165,8 +122,7 @@ public class SingleOrgPolicyValidator : IPolicyValidator
return validateDecryptionErrorMessage; return validateDecryptionErrorMessage;
} }
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning) if (await _organizationHasVerifiedDomainsQuery.HasVerifiedDomainsAsync(policyUpdate.OrganizationId))
&& await _organizationHasVerifiedDomainsQuery.HasVerifiedDomainsAsync(policyUpdate.OrganizationId))
{ {
return ClaimedDomainSingleOrganizationRequiredErrorMessage; return ClaimedDomainSingleOrganizationRequiredErrorMessage;
} }

View File

@ -23,8 +23,6 @@ public class TwoFactorAuthenticationPolicyValidator : IPolicyValidator
private readonly IOrganizationRepository _organizationRepository; private readonly IOrganizationRepository _organizationRepository;
private readonly ICurrentContext _currentContext; private readonly ICurrentContext _currentContext;
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery; private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
private readonly IFeatureService _featureService;
private readonly IRevokeNonCompliantOrganizationUserCommand _revokeNonCompliantOrganizationUserCommand; private readonly IRevokeNonCompliantOrganizationUserCommand _revokeNonCompliantOrganizationUserCommand;
public const string NonCompliantMembersWillLoseAccessMessage = "Policy could not be enabled. Non-compliant members will lose access to their accounts. Identify members without two-step login from the policies column in the members page."; public const string NonCompliantMembersWillLoseAccessMessage = "Policy could not be enabled. Non-compliant members will lose access to their accounts. Identify members without two-step login from the policies column in the members page.";
@ -38,8 +36,6 @@ public class TwoFactorAuthenticationPolicyValidator : IPolicyValidator
IOrganizationRepository organizationRepository, IOrganizationRepository organizationRepository,
ICurrentContext currentContext, ICurrentContext currentContext,
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery, ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
IRemoveOrganizationUserCommand removeOrganizationUserCommand,
IFeatureService featureService,
IRevokeNonCompliantOrganizationUserCommand revokeNonCompliantOrganizationUserCommand) IRevokeNonCompliantOrganizationUserCommand revokeNonCompliantOrganizationUserCommand)
{ {
_organizationUserRepository = organizationUserRepository; _organizationUserRepository = organizationUserRepository;
@ -47,8 +43,6 @@ public class TwoFactorAuthenticationPolicyValidator : IPolicyValidator
_organizationRepository = organizationRepository; _organizationRepository = organizationRepository;
_currentContext = currentContext; _currentContext = currentContext;
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery; _twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
_removeOrganizationUserCommand = removeOrganizationUserCommand;
_featureService = featureService;
_revokeNonCompliantOrganizationUserCommand = revokeNonCompliantOrganizationUserCommand; _revokeNonCompliantOrganizationUserCommand = revokeNonCompliantOrganizationUserCommand;
} }
@ -56,16 +50,9 @@ public class TwoFactorAuthenticationPolicyValidator : IPolicyValidator
{ {
if (currentPolicy is not { Enabled: true } && policyUpdate is { Enabled: true }) if (currentPolicy is not { Enabled: true } && policyUpdate is { Enabled: true })
{ {
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)) var currentUser = _currentContext.UserId ?? Guid.Empty;
{ var isOwnerOrProvider = await _currentContext.OrganizationOwner(policyUpdate.OrganizationId);
var currentUser = _currentContext.UserId ?? Guid.Empty; await RevokeNonCompliantUsersAsync(policyUpdate.OrganizationId, policyUpdate.PerformedBy ?? new StandardUser(currentUser, isOwnerOrProvider));
var isOwnerOrProvider = await _currentContext.OrganizationOwner(policyUpdate.OrganizationId);
await RevokeNonCompliantUsersAsync(policyUpdate.OrganizationId, policyUpdate.PerformedBy ?? new StandardUser(currentUser, isOwnerOrProvider));
}
else
{
await RemoveNonCompliantUsersAsync(policyUpdate.OrganizationId);
}
} }
} }
@ -121,40 +108,6 @@ public class TwoFactorAuthenticationPolicyValidator : IPolicyValidator
_mailService.SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(organization.DisplayName(), x.Email))); _mailService.SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(organization.DisplayName(), x.Email)));
} }
private async Task RemoveNonCompliantUsersAsync(Guid organizationId)
{
var org = await _organizationRepository.GetByIdAsync(organizationId);
var savingUserId = _currentContext.UserId;
var orgUsers = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(organizationId);
var organizationUsersTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(orgUsers);
var removableOrgUsers = orgUsers.Where(ou =>
ou.Status != OrganizationUserStatusType.Invited && ou.Status != OrganizationUserStatusType.Revoked &&
ou.Type != OrganizationUserType.Owner && ou.Type != OrganizationUserType.Admin &&
ou.UserId != savingUserId);
// Reorder by HasMasterPassword to prioritize checking users without a master if they have 2FA enabled
foreach (var orgUser in removableOrgUsers.OrderBy(ou => ou.HasMasterPassword))
{
var userTwoFactorEnabled = organizationUsersTwoFactorEnabled.FirstOrDefault(u => u.user.Id == orgUser.Id)
.twoFactorIsEnabled;
if (!userTwoFactorEnabled)
{
if (!orgUser.HasMasterPassword)
{
throw new BadRequestException(
"Policy could not be enabled. Non-compliant members will lose access to their accounts. Identify members without two-step login from the policies column in the members page.");
}
await _removeOrganizationUserCommand.RemoveUserAsync(organizationId, orgUser.Id,
savingUserId);
await _mailService.SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(
org!.DisplayName(), orgUser.Email);
}
}
}
private static bool MembersWithNoMasterPasswordWillLoseAccess( private static bool MembersWithNoMasterPasswordWillLoseAccess(
IEnumerable<OrganizationUserUserDetails> orgUserDetails, IEnumerable<OrganizationUserUserDetails> orgUserDetails,
IEnumerable<(OrganizationUserUserDetails user, bool isTwoFactorEnabled)> organizationUsersTwoFactorEnabled) => IEnumerable<(OrganizationUserUserDetails user, bool isTwoFactorEnabled)> organizationUsersTwoFactorEnabled) =>

View File

@ -11,8 +11,6 @@ namespace Bit.Core.Services;
public interface IOrganizationService public interface IOrganizationService
{ {
Task ReplacePaymentMethodAsync(Guid organizationId, string paymentToken, PaymentMethodType paymentMethodType,
TaxInfo taxInfo);
Task CancelSubscriptionAsync(Guid organizationId, bool? endOfPeriod = null); Task CancelSubscriptionAsync(Guid organizationId, bool? endOfPeriod = null);
Task ReinstateSubscriptionAsync(Guid organizationId); Task ReinstateSubscriptionAsync(Guid organizationId);
Task<string> AdjustStorageAsync(Guid organizationId, short storageAdjustmentGb); Task<string> AdjustStorageAsync(Guid organizationId, short storageAdjustmentGb);
@ -20,9 +18,6 @@ public interface IOrganizationService
Task AutoAddSeatsAsync(Organization organization, int seatsToAdd); Task AutoAddSeatsAsync(Organization organization, int seatsToAdd);
Task<string> AdjustSeatsAsync(Guid organizationId, int seatAdjustment); Task<string> AdjustSeatsAsync(Guid organizationId, int seatAdjustment);
Task VerifyBankAsync(Guid organizationId, int amount1, int amount2); Task VerifyBankAsync(Guid organizationId, int amount1, int amount2);
#nullable enable
Task<(Organization organization, OrganizationUser organizationUser, Collection defaultCollection)> SignupClientAsync(OrganizationSignup signup);
#nullable disable
/// <summary> /// <summary>
/// Create a new organization on a self-hosted instance /// Create a new organization on a self-hosted instance
/// </summary> /// </summary>

View File

@ -93,16 +93,8 @@ public class OrganizationDomainService : IOrganizationDomainService
//Send email to administrators //Send email to administrators
if (adminEmails.Count > 0) if (adminEmails.Count > 0)
{ {
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)) await _mailService.SendUnclaimedOrganizationDomainEmailAsync(adminEmails,
{ domain.OrganizationId.ToString(), domain.DomainName);
await _mailService.SendUnclaimedOrganizationDomainEmailAsync(adminEmails,
domain.OrganizationId.ToString(), domain.DomainName);
}
else
{
await _mailService.SendUnverifiedOrganizationDomainEmailAsync(adminEmails,
domain.OrganizationId.ToString(), domain.DomainName);
}
} }
_logger.LogInformation(Constants.BypassFiltersEventId, "Expired domain: {domainName}", domain.DomainName); _logger.LogInformation(Constants.BypassFiltersEventId, "Expired domain: {domainName}", domain.DomainName);

View File

@ -144,27 +144,6 @@ public class OrganizationService : IOrganizationService
_sendOrganizationInvitesCommand = sendOrganizationInvitesCommand; _sendOrganizationInvitesCommand = sendOrganizationInvitesCommand;
} }
public async Task ReplacePaymentMethodAsync(Guid organizationId, string paymentToken,
PaymentMethodType paymentMethodType, TaxInfo taxInfo)
{
var organization = await GetOrgById(organizationId);
if (organization == null)
{
throw new NotFoundException();
}
await _paymentService.SaveTaxInfoAsync(organization, taxInfo);
var updated = await _paymentService.UpdatePaymentMethodAsync(
organization,
paymentMethodType,
paymentToken,
taxInfo);
if (updated)
{
await ReplaceAndUpdateCacheAsync(organization);
}
}
public async Task CancelSubscriptionAsync(Guid organizationId, bool? endOfPeriod = null) public async Task CancelSubscriptionAsync(Guid organizationId, bool? endOfPeriod = null)
{ {
var organization = await GetOrgById(organizationId); var organization = await GetOrgById(organizationId);
@ -431,65 +410,6 @@ public class OrganizationService : IOrganizationService
} }
} }
public async Task<(Organization organization, OrganizationUser organizationUser, Collection defaultCollection)> SignupClientAsync(OrganizationSignup signup)
{
var plan = await _pricingClient.GetPlanOrThrow(signup.Plan);
ValidatePlan(plan, signup.AdditionalSeats, "Password Manager");
var organization = new Organization
{
// Pre-generate the org id so that we can save it with the Stripe subscription.
Id = CoreHelpers.GenerateComb(),
Name = signup.Name,
BillingEmail = signup.BillingEmail,
PlanType = plan!.Type,
Seats = signup.AdditionalSeats,
MaxCollections = plan.PasswordManager.MaxCollections,
MaxStorageGb = 1,
UsePolicies = plan.HasPolicies,
UseSso = plan.HasSso,
UseGroups = plan.HasGroups,
UseEvents = plan.HasEvents,
UseDirectory = plan.HasDirectory,
UseTotp = plan.HasTotp,
Use2fa = plan.Has2fa,
UseApi = plan.HasApi,
UseResetPassword = plan.HasResetPassword,
SelfHost = plan.HasSelfHost,
UsersGetPremium = plan.UsersGetPremium,
UseCustomPermissions = plan.HasCustomPermissions,
UseScim = plan.HasScim,
Plan = plan.Name,
Gateway = GatewayType.Stripe,
ReferenceData = signup.Owner.ReferenceData,
Enabled = true,
LicenseKey = CoreHelpers.SecureRandomString(20),
PublicKey = signup.PublicKey,
PrivateKey = signup.PrivateKey,
CreationDate = DateTime.UtcNow,
RevisionDate = DateTime.UtcNow,
Status = OrganizationStatusType.Created,
UsePasswordManager = true,
// Secrets Manager not available for purchase with Consolidated Billing.
UseSecretsManager = false,
};
var returnValue = await SignUpAsync(organization, default, signup.OwnerKey, signup.CollectionName, false);
await _referenceEventService.RaiseEventAsync(
new ReferenceEvent(ReferenceEventType.Signup, organization, _currentContext)
{
PlanName = plan.Name,
PlanType = plan.Type,
Seats = returnValue.Item1.Seats,
SignupInitiationPath = signup.InitiationPath,
Storage = returnValue.Item1.MaxStorageGb,
});
return returnValue;
}
private async Task ValidateSignUpPoliciesAsync(Guid ownerId) private async Task ValidateSignUpPoliciesAsync(Guid ownerId)
{ {
var anySingleOrgPolicies = await _policyService.AnyPoliciesApplicableToUserAsync(ownerId, PolicyType.SingleOrg); var anySingleOrgPolicies = await _policyService.AnyPoliciesApplicableToUserAsync(ownerId, PolicyType.SingleOrg);
@ -570,6 +490,8 @@ public class OrganizationService : IOrganizationService
SmSeats = license.SmSeats, SmSeats = license.SmSeats,
SmServiceAccounts = license.SmServiceAccounts, SmServiceAccounts = license.SmServiceAccounts,
UseRiskInsights = license.UseRiskInsights, UseRiskInsights = license.UseRiskInsights,
UseOrganizationDomains = license.UseOrganizationDomains,
UseAdminSponsoredFamilies = license.UseAdminSponsoredFamilies,
}; };
var result = await SignUpAsync(organization, owner.Id, ownerKey, collectionName, false); var result = await SignUpAsync(organization, owner.Id, ownerKey, collectionName, false);

View File

@ -2,9 +2,24 @@
public enum EmergencyAccessStatusType : byte public enum EmergencyAccessStatusType : byte
{ {
/// <summary>
/// The user has been invited to be an emergency contact.
/// </summary>
Invited = 0, Invited = 0,
/// <summary>
/// The invited user, "grantee", has accepted the request to be an emergency contact.
/// </summary>
Accepted = 1, Accepted = 1,
/// <summary>
/// The inviting user, "grantor", has approved the grantee's acceptance.
/// </summary>
Confirmed = 2, Confirmed = 2,
/// <summary>
/// The grantee has initiated the recovery process.
/// </summary>
RecoveryInitiated = 3, RecoveryInitiated = 3,
/// <summary>
/// The grantee has excercised their emergency access.
/// </summary>
RecoveryApproved = 4, RecoveryApproved = 4,
} }

View File

@ -3,6 +3,7 @@ using Bit.Core.Auth.Entities;
using Bit.Core.Auth.Enums; using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Models.Data; using Bit.Core.Auth.Models.Data;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Vault.Models.Data; using Bit.Core.Vault.Models.Data;
@ -20,6 +21,15 @@ public interface IEmergencyAccessService
Task InitiateAsync(Guid id, User initiatingUser); Task InitiateAsync(Guid id, User initiatingUser);
Task ApproveAsync(Guid id, User approvingUser); Task ApproveAsync(Guid id, User approvingUser);
Task RejectAsync(Guid id, User rejectingUser); Task RejectAsync(Guid id, User rejectingUser);
/// <summary>
/// This request is made by the Grantee user to fetch the policies <see cref="Policy"/> for the Grantor User.
/// The Grantor User has to be the owner of the organization. <see cref="OrganizationUserType"/>
/// If the Grantor user has OrganizationUserType.Owner then the policies for the _Grantor_ user
/// are returned.
/// </summary>
/// <param name="id">EmergencyAccess.Id being acted on</param>
/// <param name="requestingUser">User making the request, this is the Grantee</param>
/// <returns>null if the GrantorUser is not an organization owner; A list of policies otherwise.</returns>
Task<ICollection<Policy>> GetPoliciesAsync(Guid id, User requestingUser); Task<ICollection<Policy>> GetPoliciesAsync(Guid id, User requestingUser);
Task<(EmergencyAccess, User)> TakeoverAsync(Guid id, User initiatingUser); Task<(EmergencyAccess, User)> TakeoverAsync(Guid id, User initiatingUser);
Task PasswordAsync(Guid id, User user, string newMasterPasswordHash, string key); Task PasswordAsync(Guid id, User user, string newMasterPasswordHash, string key);

View File

@ -3,7 +3,6 @@ using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Auth.Entities; using Bit.Core.Auth.Entities;
using Bit.Core.Auth.Enums; using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Models;
using Bit.Core.Auth.Models.Business.Tokenables; using Bit.Core.Auth.Models.Business.Tokenables;
using Bit.Core.Auth.Models.Data; using Bit.Core.Auth.Models.Data;
using Bit.Core.Entities; using Bit.Core.Entities;
@ -16,7 +15,6 @@ using Bit.Core.Tokens;
using Bit.Core.Vault.Models.Data; using Bit.Core.Vault.Models.Data;
using Bit.Core.Vault.Repositories; using Bit.Core.Vault.Repositories;
using Bit.Core.Vault.Services; using Bit.Core.Vault.Services;
using Microsoft.AspNetCore.Identity;
namespace Bit.Core.Auth.Services; namespace Bit.Core.Auth.Services;
@ -31,8 +29,6 @@ public class EmergencyAccessService : IEmergencyAccessService
private readonly IMailService _mailService; private readonly IMailService _mailService;
private readonly IUserService _userService; private readonly IUserService _userService;
private readonly GlobalSettings _globalSettings; private readonly GlobalSettings _globalSettings;
private readonly IPasswordHasher<User> _passwordHasher;
private readonly IOrganizationService _organizationService;
private readonly IDataProtectorTokenFactory<EmergencyAccessInviteTokenable> _dataProtectorTokenizer; private readonly IDataProtectorTokenFactory<EmergencyAccessInviteTokenable> _dataProtectorTokenizer;
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand; private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
@ -45,9 +41,7 @@ public class EmergencyAccessService : IEmergencyAccessService
ICipherService cipherService, ICipherService cipherService,
IMailService mailService, IMailService mailService,
IUserService userService, IUserService userService,
IPasswordHasher<User> passwordHasher,
GlobalSettings globalSettings, GlobalSettings globalSettings,
IOrganizationService organizationService,
IDataProtectorTokenFactory<EmergencyAccessInviteTokenable> dataProtectorTokenizer, IDataProtectorTokenFactory<EmergencyAccessInviteTokenable> dataProtectorTokenizer,
IRemoveOrganizationUserCommand removeOrganizationUserCommand) IRemoveOrganizationUserCommand removeOrganizationUserCommand)
{ {
@ -59,9 +53,7 @@ public class EmergencyAccessService : IEmergencyAccessService
_cipherService = cipherService; _cipherService = cipherService;
_mailService = mailService; _mailService = mailService;
_userService = userService; _userService = userService;
_passwordHasher = passwordHasher;
_globalSettings = globalSettings; _globalSettings = globalSettings;
_organizationService = organizationService;
_dataProtectorTokenizer = dataProtectorTokenizer; _dataProtectorTokenizer = dataProtectorTokenizer;
_removeOrganizationUserCommand = removeOrganizationUserCommand; _removeOrganizationUserCommand = removeOrganizationUserCommand;
} }
@ -126,7 +118,12 @@ public class EmergencyAccessService : IEmergencyAccessService
throw new BadRequestException("Emergency Access not valid."); throw new BadRequestException("Emergency Access not valid.");
} }
if (!_dataProtectorTokenizer.TryUnprotect(token, out var data) && data.IsValid(emergencyAccessId, user.Email)) if (!_dataProtectorTokenizer.TryUnprotect(token, out var data))
{
throw new BadRequestException("Invalid token.");
}
if (!data.IsValid(emergencyAccessId, user.Email))
{ {
throw new BadRequestException("Invalid token."); throw new BadRequestException("Invalid token.");
} }
@ -140,6 +137,8 @@ public class EmergencyAccessService : IEmergencyAccessService
throw new BadRequestException("Invitation already accepted."); throw new BadRequestException("Invitation already accepted.");
} }
// TODO PM-21687
// Might not be reachable since the Tokenable.IsValid() does an email comparison
if (string.IsNullOrWhiteSpace(emergencyAccess.Email) || if (string.IsNullOrWhiteSpace(emergencyAccess.Email) ||
!emergencyAccess.Email.Equals(user.Email, StringComparison.InvariantCultureIgnoreCase)) !emergencyAccess.Email.Equals(user.Email, StringComparison.InvariantCultureIgnoreCase))
{ {
@ -163,6 +162,8 @@ public class EmergencyAccessService : IEmergencyAccessService
public async Task DeleteAsync(Guid emergencyAccessId, Guid grantorId) public async Task DeleteAsync(Guid emergencyAccessId, Guid grantorId)
{ {
var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(emergencyAccessId); var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(emergencyAccessId);
// TODO PM-19438/PM-21687
// Not sure why the GrantorId and the GranteeId are supposed to be the same?
if (emergencyAccess == null || (emergencyAccess.GrantorId != grantorId && emergencyAccess.GranteeId != grantorId)) if (emergencyAccess == null || (emergencyAccess.GrantorId != grantorId && emergencyAccess.GranteeId != grantorId))
{ {
throw new BadRequestException("Emergency Access not valid."); throw new BadRequestException("Emergency Access not valid.");
@ -171,9 +172,9 @@ public class EmergencyAccessService : IEmergencyAccessService
await _emergencyAccessRepository.DeleteAsync(emergencyAccess); await _emergencyAccessRepository.DeleteAsync(emergencyAccess);
} }
public async Task<EmergencyAccess> ConfirmUserAsync(Guid emergencyAcccessId, string key, Guid confirmingUserId) public async Task<EmergencyAccess> ConfirmUserAsync(Guid emergencyAccessId, string key, Guid confirmingUserId)
{ {
var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(emergencyAcccessId); var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(emergencyAccessId);
if (emergencyAccess == null || emergencyAccess.Status != EmergencyAccessStatusType.Accepted || if (emergencyAccess == null || emergencyAccess.Status != EmergencyAccessStatusType.Accepted ||
emergencyAccess.GrantorId != confirmingUserId) emergencyAccess.GrantorId != confirmingUserId)
{ {
@ -224,7 +225,6 @@ public class EmergencyAccessService : IEmergencyAccessService
public async Task InitiateAsync(Guid id, User initiatingUser) public async Task InitiateAsync(Guid id, User initiatingUser)
{ {
var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(id); var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(id);
if (emergencyAccess == null || emergencyAccess.GranteeId != initiatingUser.Id || if (emergencyAccess == null || emergencyAccess.GranteeId != initiatingUser.Id ||
emergencyAccess.Status != EmergencyAccessStatusType.Confirmed) emergencyAccess.Status != EmergencyAccessStatusType.Confirmed)
{ {
@ -285,6 +285,9 @@ public class EmergencyAccessService : IEmergencyAccessService
public async Task<ICollection<Policy>> GetPoliciesAsync(Guid id, User requestingUser) public async Task<ICollection<Policy>> GetPoliciesAsync(Guid id, User requestingUser)
{ {
// TODO PM-21687
// Should we look up policies here or just verify the EmergencyAccess is correct
// and handle policy logic else where? Should this be a query/Command?
var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(id); var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(id);
if (!IsValidRequest(emergencyAccess, requestingUser, EmergencyAccessType.Takeover)) if (!IsValidRequest(emergencyAccess, requestingUser, EmergencyAccessType.Takeover))
@ -295,7 +298,9 @@ public class EmergencyAccessService : IEmergencyAccessService
var grantor = await _userRepository.GetByIdAsync(emergencyAccess.GrantorId); var grantor = await _userRepository.GetByIdAsync(emergencyAccess.GrantorId);
var grantorOrganizations = await _organizationUserRepository.GetManyByUserAsync(grantor.Id); var grantorOrganizations = await _organizationUserRepository.GetManyByUserAsync(grantor.Id);
var isOrganizationOwner = grantorOrganizations.Any<OrganizationUser>(organization => organization.Type == OrganizationUserType.Owner); var isOrganizationOwner = grantorOrganizations
.Any(organization => organization.Type == OrganizationUserType.Owner);
var policies = isOrganizationOwner ? await _policyRepository.GetManyByUserIdAsync(grantor.Id) : null; var policies = isOrganizationOwner ? await _policyRepository.GetManyByUserIdAsync(grantor.Id) : null;
return policies; return policies;
@ -311,7 +316,8 @@ public class EmergencyAccessService : IEmergencyAccessService
} }
var grantor = await _userRepository.GetByIdAsync(emergencyAccess.GrantorId); var grantor = await _userRepository.GetByIdAsync(emergencyAccess.GrantorId);
// TODO PM-21687
// Redundant check of the EmergencyAccessType -> checked in IsValidRequest() ln 308
if (emergencyAccess.Type == EmergencyAccessType.Takeover && grantor.UsesKeyConnector) if (emergencyAccess.Type == EmergencyAccessType.Takeover && grantor.UsesKeyConnector)
{ {
throw new BadRequestException("You cannot takeover an account that is using Key Connector."); throw new BadRequestException("You cannot takeover an account that is using Key Connector.");
@ -336,7 +342,9 @@ public class EmergencyAccessService : IEmergencyAccessService
grantor.LastPasswordChangeDate = grantor.RevisionDate; grantor.LastPasswordChangeDate = grantor.RevisionDate;
grantor.Key = key; grantor.Key = key;
// Disable TwoFactor providers since they will otherwise block logins // Disable TwoFactor providers since they will otherwise block logins
grantor.SetTwoFactorProviders(new Dictionary<TwoFactorProviderType, TwoFactorProvider>()); grantor.SetTwoFactorProviders([]);
// Disable New Device Verification since it will otherwise block logins
grantor.VerifyDevices = false;
await _userRepository.ReplaceAsync(grantor); await _userRepository.ReplaceAsync(grantor);
// Remove grantor from all organizations unless Owner // Remove grantor from all organizations unless Owner
@ -421,12 +429,22 @@ public class EmergencyAccessService : IEmergencyAccessService
await _mailService.SendEmergencyAccessInviteEmailAsync(emergencyAccess, invitingUsersName, token); await _mailService.SendEmergencyAccessInviteEmailAsync(emergencyAccess, invitingUsersName, token);
} }
private string NameOrEmail(User user) private static string NameOrEmail(User user)
{ {
return string.IsNullOrWhiteSpace(user.Name) ? user.Email : user.Name; return string.IsNullOrWhiteSpace(user.Name) ? user.Email : user.Name;
} }
private bool IsValidRequest(EmergencyAccess availableAccess, User requestingUser, EmergencyAccessType requestedAccessType)
/*
* Checks if EmergencyAccess Object is null
* Checks the requesting user is the same as the granteeUser (So we are checking for proper grantee action)
* Status _must_ equal RecoveryApproved (This means the grantor has invited, the grantee has accepted, and the grantor has approved so the shared key exists but hasn't been exercised yet)
* request type must equal the type of access requested (View or Takeover)
*/
private static bool IsValidRequest(
EmergencyAccess availableAccess,
User requestingUser,
EmergencyAccessType requestedAccessType)
{ {
return availableAccess != null && return availableAccess != null &&
availableAccess.GranteeId == requestingUser.Id && availableAccess.GranteeId == requestingUser.Id &&

View File

@ -108,6 +108,7 @@ public class RegisterUserCommand : IRegisterUserCommand
var result = await _userService.CreateUserAsync(user, masterPasswordHash); var result = await _userService.CreateUserAsync(user, masterPasswordHash);
if (result == IdentityResult.Success) if (result == IdentityResult.Success)
{ {
var sentWelcomeEmail = false;
if (!string.IsNullOrEmpty(user.ReferenceData)) if (!string.IsNullOrEmpty(user.ReferenceData))
{ {
var referenceData = JsonConvert.DeserializeObject<Dictionary<string, object>>(user.ReferenceData); var referenceData = JsonConvert.DeserializeObject<Dictionary<string, object>>(user.ReferenceData);
@ -115,6 +116,7 @@ public class RegisterUserCommand : IRegisterUserCommand
{ {
var initiationPath = value.ToString(); var initiationPath = value.ToString();
await SendAppropriateWelcomeEmailAsync(user, initiationPath); await SendAppropriateWelcomeEmailAsync(user, initiationPath);
sentWelcomeEmail = true;
if (!string.IsNullOrEmpty(initiationPath)) if (!string.IsNullOrEmpty(initiationPath))
{ {
await _referenceEventService.RaiseEventAsync( await _referenceEventService.RaiseEventAsync(
@ -128,6 +130,11 @@ public class RegisterUserCommand : IRegisterUserCommand
} }
} }
if (!sentWelcomeEmail)
{
await _mailService.SendWelcomeEmailAsync(user);
}
await _referenceEventService.RaiseEventAsync(new ReferenceEvent(ReferenceEventType.Signup, user, _currentContext)); await _referenceEventService.RaiseEventAsync(new ReferenceEvent(ReferenceEventType.Signup, user, _currentContext));
} }

Some files were not shown because too many files have changed in this diff Show More