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.yml @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)
**/packages.lock.json

View File

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

View File

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

View File

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

View File

@ -3,7 +3,7 @@
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Version>2025.5.0</Version>
<Version>2025.5.1</Version>
<RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace>
<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.Repositories;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Billing.Entities;
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.Exceptions;
using Bit.Core.Repositories;

View File

@ -7,13 +7,12 @@ using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Extensions;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Providers.Services;
using Bit.Core.Billing.Services;
using Bit.Core.Billing.Services.Implementations.AutomaticTax;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Microsoft.Extensions.DependencyInjection;
using Stripe;
namespace Bit.Commercial.Core.AdminConsole.Providers;
@ -23,7 +22,6 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
private readonly IEventService _eventService;
private readonly IMailService _mailService;
private readonly IOrganizationRepository _organizationRepository;
private readonly IOrganizationService _organizationService;
private readonly IProviderOrganizationRepository _providerOrganizationRepository;
private readonly IStripeAdapter _stripeAdapter;
private readonly IFeatureService _featureService;
@ -31,26 +29,22 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
private readonly ISubscriberService _subscriberService;
private readonly IHasConfirmedOwnersExceptQuery _hasConfirmedOwnersExceptQuery;
private readonly IPricingClient _pricingClient;
private readonly IAutomaticTaxStrategy _automaticTaxStrategy;
public RemoveOrganizationFromProviderCommand(
IEventService eventService,
IMailService mailService,
IOrganizationRepository organizationRepository,
IOrganizationService organizationService,
IProviderOrganizationRepository providerOrganizationRepository,
IStripeAdapter stripeAdapter,
IFeatureService featureService,
IProviderBillingService providerBillingService,
ISubscriberService subscriberService,
IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery,
IPricingClient pricingClient,
[FromKeyedServices(AutomaticTaxFactory.BusinessUse)] IAutomaticTaxStrategy automaticTaxStrategy)
IPricingClient pricingClient)
{
_eventService = eventService;
_mailService = mailService;
_organizationRepository = organizationRepository;
_organizationService = organizationService;
_providerOrganizationRepository = providerOrganizationRepository;
_stripeAdapter = stripeAdapter;
_featureService = featureService;
@ -58,7 +52,6 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
_subscriberService = subscriberService;
_hasConfirmedOwnersExceptQuery = hasConfirmedOwnersExceptQuery;
_pricingClient = pricingClient;
_automaticTaxStrategy = automaticTaxStrategy;
}
public async Task RemoveOrganizationFromProvider(
@ -76,7 +69,7 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
if (!await _hasConfirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync(
providerOrganization.OrganizationId,
Array.Empty<Guid>(),
[],
includeProvider: false))
{
throw new BadRequestException("Organization must have at least one confirmed owner.");
@ -101,7 +94,7 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
/// <summary>
/// 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
/// 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.
/// </summary>
private async Task ResetOrganizationBillingAsync(
@ -141,15 +134,18 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
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(
organization.Id,
organization.Name,
provider.Name,
provider.Name!,
organizationOwnerEmails);
}
}

View File

@ -5,12 +5,13 @@ using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.Models.Business.Provider;
using Bit.Core.AdminConsole.Models.Business.Tokenables;
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Models;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Services;
using Bit.Core.Billing.Providers.Services;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Enums;
@ -53,6 +54,7 @@ public class ProviderService : IProviderService
private readonly IApplicationCacheService _applicationCacheService;
private readonly IProviderBillingService _providerBillingService;
private readonly IPricingClient _pricingClient;
private readonly IProviderClientOrganizationSignUpCommand _providerClientOrganizationSignUpCommand;
public ProviderService(IProviderRepository providerRepository, IProviderUserRepository providerUserRepository,
IProviderOrganizationRepository providerOrganizationRepository, IUserRepository userRepository,
@ -61,7 +63,8 @@ public class ProviderService : IProviderService
IOrganizationRepository organizationRepository, GlobalSettings globalSettings,
ICurrentContext currentContext, IStripeAdapter stripeAdapter, IFeatureService featureService,
IDataProtectorTokenFactory<ProviderDeleteTokenable> providerDeleteTokenDataFactory,
IApplicationCacheService applicationCacheService, IProviderBillingService providerBillingService, IPricingClient pricingClient)
IApplicationCacheService applicationCacheService, IProviderBillingService providerBillingService, IPricingClient pricingClient,
IProviderClientOrganizationSignUpCommand providerClientOrganizationSignUpCommand)
{
_providerRepository = providerRepository;
_providerUserRepository = providerUserRepository;
@ -81,6 +84,7 @@ public class ProviderService : IProviderService
_applicationCacheService = applicationCacheService;
_providerBillingService = providerBillingService;
_pricingClient = pricingClient;
_providerClientOrganizationSignUpCommand = providerClientOrganizationSignUpCommand;
}
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);
var (organization, _, defaultCollection) = await _organizationService.SignupClientAsync(organizationSignup);
var signUpResponse = await _providerClientOrganizationSignUpCommand.SignUpClientOrganizationAsync(organizationSignup);
var providerOrganization = new ProviderOrganization
{
ProviderId = providerId,
OrganizationId = organization.Id,
OrganizationId = signUpResponse.Organization.Id,
Key = organizationSignup.OwnerKey,
};
@ -574,12 +578,12 @@ public class ProviderService : IProviderService
// 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
var defaultOwnerAccess = defaultCollection != null
var defaultOwnerAccess = signUpResponse.DefaultCollection != null
?
[
new CollectionAccessSelection
{
Id = defaultCollection.Id,
Id = signUpResponse.DefaultCollection.Id,
HidePasswords = false,
ReadOnly = false,
Manage = true
@ -587,7 +591,7 @@ public class ProviderService : IProviderService
]
: 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)[]
{
(

View File

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

View File

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

View File

@ -1,5 +1,5 @@
using System.Globalization;
using Bit.Commercial.Core.Billing.Models;
using Bit.Commercial.Core.Billing.Providers.Models;
using Bit.Core;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Entities.Provider;
@ -8,15 +8,17 @@ using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing;
using Bit.Core.Billing.Caches;
using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Entities;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Extensions;
using Bit.Core.Billing.Models;
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.Contracts;
using Bit.Core.Billing.Services.Implementations.AutomaticTax;
using Bit.Core.Billing.Tax.Models;
using Bit.Core.Billing.Tax.Services;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Business;
@ -25,15 +27,13 @@ using Bit.Core.Services;
using Bit.Core.Settings;
using Braintree;
using CsvHelper;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Stripe;
using static Bit.Core.Billing.Utilities;
using Customer = Stripe.Customer;
using Subscription = Stripe.Subscription;
namespace Bit.Commercial.Core.Billing;
namespace Bit.Commercial.Core.Billing.Providers.Services;
public class ProviderBillingService(
IBraintreeGateway braintreeGateway,
@ -50,8 +50,7 @@ public class ProviderBillingService(
ISetupIntentCache setupIntentCache,
IStripeAdapter stripeAdapter,
ISubscriberService subscriberService,
ITaxService taxService,
[FromKeyedServices(AutomaticTaxFactory.BusinessUse)] IAutomaticTaxStrategy automaticTaxStrategy)
ITaxService taxService)
: IProviderBillingService
{
public async Task AddExistingOrganization(
@ -97,6 +96,7 @@ public class ProviderBillingService(
organization.MaxStorageGb = plan.PasswordManager.BaseStorageGb;
organization.UsePolicies = plan.HasPolicies;
organization.UseSso = plan.HasSso;
organization.UseOrganizationDomains = plan.HasOrganizationDomains;
organization.UseGroups = plan.HasGroups;
organization.UseEvents = plan.HasEvents;
organization.UseDirectory = plan.HasDirectory;
@ -125,7 +125,7 @@ public class ProviderBillingService(
/*
* 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);
@ -233,7 +233,7 @@ public class ProviderBillingService(
var providerCustomer = await subscriberService.GetCustomerOrThrow(provider, new CustomerGetOptions
{
Expand = ["tax_ids"]
Expand = ["tax", "tax_ids"]
});
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);
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))
{
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}'.",
taxInfo.BillingAddressCountry,
taxInfo.TaxIdNumber);
throw new BadRequestException("billingTaxIdTypeInferenceError");
}
@ -692,6 +707,13 @@ public class ProviderBillingService(
customer.Metadata.ContainsKey(BraintreeCustomerIdKey) ||
setupIntent.IsUnverifiedBankAccount());
int? trialPeriodDays = provider.Type switch
{
ProviderType.Msp when usePaymentMethod => 14,
ProviderType.BusinessUnit when usePaymentMethod => 4,
_ => null
};
var subscriptionCreateOptions = new SubscriptionCreateOptions
{
CollectionMethod = usePaymentMethod ?
@ -705,17 +727,24 @@ public class ProviderBillingService(
},
OffSession = true,
ProrationBehavior = StripeConstants.ProrationBehavior.CreateProrations,
TrialPeriodDays = usePaymentMethod ? 14 : null
TrialPeriodDays = trialPeriodDays
};
if (featureService.IsEnabled(FeatureFlagKeys.PM19147_AutomaticTaxImprovements))
{
automaticTaxStrategy.SetCreateOptions(subscriptionCreateOptions, customer);
}
else
var setNonUSBusinessUseToReverseCharge =
featureService.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge);
if (setNonUSBusinessUseToReverseCharge)
{
subscriptionCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true };
}
else if (customer.HasRecognizedTaxLocation())
{
subscriptionCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions
{
Enabled = customer.Address.Country == "US" ||
customer.TaxIds.Any()
};
}
try
{

View File

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

View File

@ -1,13 +1,9 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Licenses;
using Bit.Core.Billing.Licenses.Extensions;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Pricing;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Core.SecretsManager.Queries.Projects.Interfaces;
using Bit.Core.SecretsManager.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings;
namespace Bit.Commercial.Core.SecretsManager.Queries.Projects;
@ -17,72 +13,42 @@ public class MaxProjectsQuery : IMaxProjectsQuery
private readonly IOrganizationRepository _organizationRepository;
private readonly IProjectRepository _projectRepository;
private readonly IGlobalSettings _globalSettings;
private readonly ILicensingService _licensingService;
private readonly IPricingClient _pricingClient;
public MaxProjectsQuery(
IOrganizationRepository organizationRepository,
IProjectRepository projectRepository,
IGlobalSettings globalSettings,
ILicensingService licensingService,
IPricingClient pricingClient)
{
_organizationRepository = organizationRepository;
_projectRepository = projectRepository;
_globalSettings = globalSettings;
_licensingService = licensingService;
_pricingClient = pricingClient;
}
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);
if (org == null)
{
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);
}
var projects = await _projectRepository.GetProjectCountByOrganizationIdAsync(organizationId);
return ((short? max, bool? overMax))(projects + projectsToAdd > maxProjects ? (maxProjects, true) : (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.");
return ((short? max, bool? overMax))(projects + projectsToAdd > plan.SecretsManager.MaxProjects ? (plan.SecretsManager.MaxProjects, true) : (plan.SecretsManager.MaxProjects, false));
}
}

View File

@ -1,9 +1,9 @@
using Bit.Commercial.Core.AdminConsole.Providers;
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.Services;
using Bit.Core.Billing.Services;
using Bit.Core.Billing.Providers.Services;
using Microsoft.Extensions.DependencyInjection;
namespace Bit.Commercial.Core.Utilities;

View File

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

View File

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

View File

@ -1,4 +1,5 @@
using Bit.Commercial.Core.AdminConsole.Providers;
using Bit.Core;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Entities.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.Enums;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Providers.Services;
using Bit.Core.Billing.Services;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
@ -223,31 +225,115 @@ public class RemoveOrganizationFromProviderCommandTests
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
{
Id = "subscription_id"
});
sutProvider.GetDependency<IAutomaticTaxStrategy>()
.When(x => x.SetCreateOptions(
Arg.Is<SubscriptionCreateOptions>(options =>
options.Customer == organization.GatewayCustomerId &&
options.CollectionMethod == StripeConstants.CollectionMethod.SendInvoice &&
options.DaysUntilDue == 30 &&
options.Metadata["organizationId"] == organization.Id.ToString() &&
options.OffSession == true &&
options.ProrationBehavior == StripeConstants.ProrationBehavior.CreateProrations &&
options.Items.First().Price == teamsMonthlyPlan.PasswordManager.StripeSeatPlanId &&
options.Items.First().Quantity == organization.Seats)
, Arg.Any<Customer>()))
.Do(x =>
await sutProvider.Sut.RemoveOrganizationFromProvider(provider, providerOrganization, organization);
await stripeAdapter.Received(1).SubscriptionCreateAsync(Arg.Is<SubscriptionCreateOptions>(options =>
options.Customer == organization.GatewayCustomerId &&
options.CollectionMethod == StripeConstants.CollectionMethod.SendInvoice &&
options.DaysUntilDue == 30 &&
options.AutomaticTax.Enabled == true &&
options.Metadata["organizationId"] == organization.Id.ToString() &&
options.OffSession == true &&
options.ProrationBehavior == StripeConstants.ProrationBehavior.CreateProrations &&
options.Items.First().Price == teamsMonthlyPlan.PasswordManager.StripeSeatPlanId &&
options.Items.First().Quantity == organization.Seats));
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 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.Models.Business.Provider;
using Bit.Core.AdminConsole.Models.Business.Tokenables;
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Models;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Services;
using Bit.Core.Billing.Providers.Services;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Enums;
@ -717,8 +718,8 @@ public class ProviderServiceTests
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
sutProvider.GetDependency<IOrganizationService>().SignupClientAsync(organizationSignup)
.Returns((organization, null as OrganizationUser, new Collection()));
sutProvider.GetDependency<IProviderClientOrganizationSignUpCommand>().SignUpClientOrganizationAsync(organizationSignup)
.Returns(new ProviderClientOrganizationSignUpResponse(organization, new Collection()));
var providerOrganization =
await sutProvider.Sut.CreateOrganizationAsync(provider.Id, organizationSignup, clientOwnerEmail, user);
@ -755,8 +756,8 @@ public class ProviderServiceTests
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
sutProvider.GetDependency<IOrganizationService>().SignupClientAsync(organizationSignup)
.Returns((organization, null as OrganizationUser, new Collection()));
sutProvider.GetDependency<IProviderClientOrganizationSignUpCommand>().SignUpClientOrganizationAsync(organizationSignup)
.Returns(new ProviderClientOrganizationSignUpResponse(organization, new Collection()));
await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.CreateOrganizationAsync(provider.Id, organizationSignup, clientOwnerEmail, user));
@ -782,8 +783,8 @@ public class ProviderServiceTests
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
sutProvider.GetDependency<IOrganizationService>().SignupClientAsync(organizationSignup)
.Returns((organization, null as OrganizationUser, new Collection()));
sutProvider.GetDependency<IProviderClientOrganizationSignUpCommand>().SignUpClientOrganizationAsync(organizationSignup)
.Returns(new ProviderClientOrganizationSignUpResponse(organization, new Collection()));
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);
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
sutProvider.GetDependency<IOrganizationService>().SignupClientAsync(organizationSignup)
.Returns((organization, null as OrganizationUser, defaultCollection));
sutProvider.GetDependency<IProviderClientOrganizationSignUpCommand>().SignUpClientOrganizationAsync(organizationSignup)
.Returns(new ProviderClientOrganizationSignUpResponse(organization, defaultCollection));
var providerOrganization =
await sutProvider.Sut.CreateOrganizationAsync(provider.Id, organizationSignup, clientOwnerEmail, user);

View File

@ -1,16 +1,16 @@
#nullable enable
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.Provider;
using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing;
using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Entities;
using Bit.Core.Billing.Enums;
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.Entities;
using Bit.Core.Enums;
@ -25,7 +25,7 @@ using NSubstitute;
using Stripe;
using Xunit;
namespace Bit.Commercial.Core.Test.Billing;
namespace Bit.Commercial.Core.Test.Billing.Providers;
public class BusinessUnitConverterTests
{

View File

@ -1,7 +1,7 @@
using System.Globalization;
using System.Net;
using Bit.Commercial.Core.Billing;
using Bit.Commercial.Core.Billing.Models;
using Bit.Commercial.Core.Billing.Providers.Models;
using Bit.Commercial.Core.Billing.Providers.Services;
using Bit.Core;
using Bit.Core.AdminConsole.Entities;
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.Billing.Caches;
using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Entities;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Models;
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.Contracts;
using Bit.Core.Billing.Tax.Services;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
@ -39,7 +40,7 @@ using Customer = Stripe.Customer;
using PaymentMethod = Stripe.PaymentMethod;
using Subscription = Stripe.Subscription;
namespace Bit.Commercial.Core.Test.Billing;
namespace Bit.Commercial.Core.Test.Billing.Providers;
[SutProviderCustomize]
public class ProviderBillingServiceTests
@ -261,7 +262,7 @@ public class ProviderBillingServiceTests
};
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);
sutProvider.GetDependency<IGlobalSettings>().BaseServiceUri
@ -311,6 +312,91 @@ public class ProviderBillingServiceTests
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
#region GenerateClientInvoiceReport
@ -1181,6 +1267,62 @@ public class ProviderBillingServiceTests
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]
public async Task SetupCustomer_Throws_BadRequestException_WhenTaxIdIsInvalid(
SutProvider<ProviderBillingService> sutProvider,
@ -1306,7 +1448,7 @@ public class ProviderBillingServiceTests
.Returns(new Customer
{
Id = "customer_id",
Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported }
Address = new Address { Country = "US" }
});
var providerPlans = new List<ProviderPlan>
@ -1358,7 +1500,7 @@ public class ProviderBillingServiceTests
var customer = new Customer
{
Id = "customer_id",
Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported }
Address = new Address { Country = "US" }
};
sutProvider.GetDependency<ISubscriberService>()
.GetCustomerOrThrow(
@ -1398,19 +1540,6 @@ public class ProviderBillingServiceTests
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>(
sub =>
sub.AutomaticTax.Enabled == true &&
@ -1442,11 +1571,11 @@ public class ProviderBillingServiceTests
var customer = new Customer
{
Id = "customer_id",
Address = new Address { Country = "US" },
InvoiceSettings = new CustomerInvoiceSettings
{
DefaultPaymentMethodId = "pm_123"
},
Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported }
}
};
sutProvider.GetDependency<ISubscriberService>()
@ -1487,19 +1616,6 @@ public class ProviderBillingServiceTests
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>()
.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true);
@ -1535,9 +1651,9 @@ public class ProviderBillingServiceTests
var customer = new Customer
{
Id = "customer_id",
Address = new Address { Country = "US" },
InvoiceSettings = new CustomerInvoiceSettings(),
Metadata = new Dictionary<string, string>(),
Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported }
Metadata = new Dictionary<string, string>()
};
sutProvider.GetDependency<ISubscriberService>()
@ -1578,19 +1694,6 @@ public class ProviderBillingServiceTests
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>()
.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true);
@ -1645,12 +1748,15 @@ public class ProviderBillingServiceTests
var customer = new Customer
{
Id = "customer_id",
Address = new Address
{
Country = "US"
},
InvoiceSettings = new CustomerInvoiceSettings(),
Metadata = new Dictionary<string, string>
{
["btCustomerId"] = "braintree_customer_id"
},
Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported }
}
};
sutProvider.GetDependency<ISubscriberService>()
@ -1691,22 +1797,92 @@ public class ProviderBillingServiceTests
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 =>
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true);
sutProvider.GetDependency<IStripeAdapter>().SubscriptionCreateAsync(Arg.Is<SubscriptionCreateOptions>(
sub =>
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
{
Enabled = true
};
});
DefaultPaymentMethodId = "pm_123"
}
};
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>()
.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true);
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge).Returns(true);
sutProvider.GetDependency<IStripeAdapter>().SubscriptionCreateAsync(Arg.Is<SubscriptionCreateOptions>(
sub =>
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.Enums.Provider;
using Bit.Core.Billing.Enums;
using Stripe;
using Xunit;
namespace Bit.Commercial.Core.Test.Billing;
namespace Bit.Commercial.Core.Test.Billing.Providers;
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.Attributes;
using Xunit;
namespace Bit.Commercial.Core.Test.Billing;
namespace Bit.Commercial.Core.Test.Billing.Tax;
[SutProviderCustomize]
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.Billing.Enums;
using Bit.Core.Billing.Licenses;
using Bit.Core.Billing.Pricing;
using Bit.Core.Exceptions;
using Bit.Core.Models.Business;
using Bit.Core.Repositories;
using Bit.Core.SecretsManager.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings;
using Bit.Core.Utilities;
using Bit.Test.Common.AutoFixture;
@ -22,11 +18,26 @@ namespace Bit.Commercial.Core.Test.SecretsManager.Queries.Projects;
[SutProviderCustomize]
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]
[BitAutoData]
public async Task GetByOrgIdAsync_OrganizationIsNull_ThrowsNotFound(SutProvider<MaxProjectsQuery> sutProvider,
Guid organizationId)
{
sutProvider.GetDependency<IGlobalSettings>().SelfHosted.Returns(false);
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(default).ReturnsNull();
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.GetByOrgIdAsync(organizationId, 1));
@ -35,54 +46,6 @@ public class MaxProjectsQueryTests
.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]
[BitAutoData(PlanType.TeamsMonthly2019)]
[BitAutoData(PlanType.TeamsMonthly2020)]
@ -97,57 +60,16 @@ public class MaxProjectsQueryTests
[BitAutoData(PlanType.EnterpriseAnnually2019)]
[BitAutoData(PlanType.EnterpriseAnnually2020)]
[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)
{
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);
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;
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
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);
sutProvider.GetDependency<IPricingClient>().GetPlan(organization.PlanType)
.Returns(StaticStore.GetPlan(organization.PlanType));
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, 4, 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)
{
organization.PlanType = planType;
@ -191,66 +113,8 @@ public class MaxProjectsQueryTests
sutProvider.GetDependency<IProjectRepository>().GetProjectCountByOrganizationIdAsync(organization.Id)
.Returns(projects);
sutProvider.GetDependency<IGlobalSettings>().SelfHosted.Returns(false);
var plan = StaticStore.GetPlan(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);
sutProvider.GetDependency<IPricingClient>().GetPlan(organization.PlanType)
.Returns(StaticStore.GetPlan(organization.PlanType));
var (max, overMax) = await sutProvider.Sut.GetByOrgIdAsync(organization.Id, projectsToAdd);

View File

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

View File

@ -70,6 +70,20 @@ services:
profiles:
- 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:
image: kenchan0130/simplesamlphp:1.19.8
container_name: idp

View File

@ -5,6 +5,7 @@ param(
[switch]$all,
[switch]$postgres,
[switch]$mysql,
[switch]$mariadb,
[switch]$mssql,
[switch]$sqlite,
[switch]$selfhost,
@ -15,11 +16,15 @@ param(
$ErrorActionPreference = "Stop"
$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;
}
if ($all -or $postgres -or $mysql -or $sqlite) {
if ($all -or $(Get-IsEFDatabase)) {
dotnet ef *> $null
if ($LASTEXITCODE -ne 0) {
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 @(
@($mysql, "MySQL", "MySqlMigrations", "mySql", 2),
@($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) {
continue

View File

@ -4,6 +4,7 @@
"rollForward": "latestFeature"
},
"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["username"] = username;
payload["password"] = password;
params.headers["Auth-Email"] = encoding.b64encode(username);
} else {
payload["scope"] = "api.organization";
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.Extensions;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Services;
using Bit.Core.Billing.Providers.Services;
using Bit.Core.Context;
using Bit.Core.Enums;
using Bit.Core.Models.OrganizationConnectionConfigs;
@ -462,6 +462,7 @@ public class OrganizationsController : Controller
organization.UsersGetPremium = model.UsersGetPremium;
organization.UseSecretsManager = model.UseSecretsManager;
organization.UseRiskInsights = model.UseRiskInsights;
organization.UseOrganizationDomains = model.UseOrganizationDomains;
organization.UseAdminSponsoredFamilies = model.UseAdminSponsoredFamilies;
//secrets

View File

@ -10,13 +10,13 @@ using Bit.Core.AdminConsole.Providers.Interfaces;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Entities;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Extensions;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Repositories;
using Bit.Core.Billing.Services;
using Bit.Core.Billing.Services.Contracts;
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.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;

View File

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

View File

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

View File

@ -2,8 +2,8 @@
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.Models.Data.Provider;
using Bit.Core.Billing.Entities;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Providers.Entities;
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")'>
<label class="form-check-label" asp-for="UseSso"></label>
</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">
<input type="checkbox" class="form-check-input" asp-for="UseKeyConnector" disabled='@(canEditPlan ? null : "disabled")'>
<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.UsePolicies))').checked = plan.hasPolicies;
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.UseDirectory))').checked = plan.hasDirectory;
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.Enums.Provider;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Services;
using Bit.Core.Billing.Providers.Services;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Core.Utilities;

View File

@ -1,8 +1,8 @@
using Bit.Admin.Billing.Models;
using Bit.Admin.Enums;
using Bit.Admin.Utilities;
using Bit.Core.Billing.Migration.Models;
using Bit.Core.Billing.Migration.Services;
using Bit.Core.Billing.Providers.Migration.Models;
using Bit.Core.Billing.Providers.Migration.Services;
using Bit.Core.Utilities;
using Microsoft.AspNetCore.Authorization;
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;

View File

@ -1,5 +1,5 @@
@using System.Text.Json
@model Bit.Core.Billing.Migration.Models.ProviderMigrationResult
@model Bit.Core.Billing.Providers.Migration.Models.ProviderMigrationResult
@{
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";
}

View File

@ -4,7 +4,6 @@ using Bit.Admin.Enums;
using Bit.Admin.Models;
using Bit.Admin.Services;
using Bit.Admin.Utilities;
using Bit.Core;
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
using Bit.Core.Repositories;
using Bit.Core.Services;
@ -89,7 +88,7 @@ public class UsersController : Controller
var ciphers = await _cipherRepository.GetManyByUserIdAsync(id);
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));
}
@ -106,7 +105,7 @@ public class UsersController : Controller
var billingInfo = await _paymentService.GetBillingAsync(user);
var billingHistoryInfo = await _paymentService.GetBillingHistoryAsync(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);
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);
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.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))
{
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 Bit.Admin.Services;
using Bit.Core.Billing.Extensions;
using Bit.Core.Billing.Migration;
using Bit.Core.Billing.Providers.Migration;
#if !OSS
using Bit.Commercial.Core.Utilities;

View File

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

View File

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

View File

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

View File

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

View File

@ -616,7 +616,6 @@ public class OrganizationUsersController : Controller
new OrganizationUserBulkResponseModel(r.OrganizationUserId, r.ErrorMessage)));
}
[RequireFeature(FeatureFlagKeys.AccountDeprovisioning)]
[HttpDelete("{id}/delete-account")]
[HttpPost("{id}/delete-account")]
public async Task DeleteAccount(Guid orgId, Guid id)
@ -635,7 +634,6 @@ public class OrganizationUsersController : Controller
await _deleteClaimedOrganizationUserAccountCommand.DeleteUserAsync(orgId, id, currentUser.Id);
}
[RequireFeature(FeatureFlagKeys.AccountDeprovisioning)]
[HttpDelete("delete-account")]
[HttpPost("delete-account")]
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)
{
if (!_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning))
{
return userIds.ToDictionary(kvp => kvp, kvp => false);
}
var usersOrganizationClaimedStatus = await _getOrganizationUsersClaimedStatusQuery.GetUsersOrganizationClaimedStatusAsync(orgId, userIds);
return usersOrganizationClaimedStatus;
}

View File

@ -25,7 +25,7 @@ using Bit.Core.Auth.Services;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Extensions;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Services;
using Bit.Core.Billing.Providers.Services;
using Bit.Core.Context;
using Bit.Core.Enums;
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.");
}
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
&& (await _userService.GetOrganizationsClaimingUserAsync(user.Id)).Any(x => x.Id == id))
if ((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.");
}

View File

@ -2,7 +2,6 @@
using Bit.Api.AdminConsole.Models.Response.Helpers;
using Bit.Api.AdminConsole.Models.Response.Organizations;
using Bit.Api.Models.Response;
using Bit.Core;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;
@ -79,7 +78,7 @@ public class PoliciesController : Controller
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);
}

View File

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

View File

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

View File

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

View File

@ -58,7 +58,8 @@ public class ProfileOrganizationResponseModel : ResponseModel
ProviderName = organization.ProviderName;
ProviderType = organization.ProviderType;
FamilySponsorshipFriendlyName = organization.FamilySponsorshipFriendlyName;
FamilySponsorshipAvailable = FamilySponsorshipFriendlyName == null &&
IsAdminInitiated = organization.IsAdminInitiated ?? false;
FamilySponsorshipAvailable = (FamilySponsorshipFriendlyName == null || IsAdminInitiated) &&
StaticStore.GetSponsoredPlan(PlanSponsorshipType.FamiliesForEnterprise)
.UsersCanSponsor(organization);
ProductTierType = organization.PlanType.GetProductTier();
@ -72,6 +73,7 @@ public class ProfileOrganizationResponseModel : ResponseModel
AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems;
UserIsClaimedByOrganization = organizationIdsClaimingUser.Contains(organization.OrganizationId);
UseRiskInsights = organization.UseRiskInsights;
UseOrganizationDomains = organization.UseOrganizationDomains;
UseAdminSponsoredFamilies = organization.UseAdminSponsoredFamilies;
if (organization.SsoConfig != null)
@ -135,7 +137,6 @@ public class ProfileOrganizationResponseModel : ResponseModel
public bool AllowAdminAccessToAllCollectionItems { get; set; }
/// <summary>
/// Obsolete.
///
/// See <see cref="UserIsClaimedByOrganization"/>
/// </summary>
[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;
}
/// <summary>
/// Indicates if the organization claims the user.
/// Indicates if the user is claimed by the organization.
/// </summary>
/// <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.
/// </remarks>
/// <returns>
/// False if the Account Deprovisioning feature flag is disabled.
/// </returns>
public bool UserIsClaimedByOrganization { get; set; }
public bool UseRiskInsights { get; set; }
public bool UseOrganizationDomains { 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;
AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems;
UseRiskInsights = organization.UseRiskInsights;
UseOrganizationDomains = organization.UseOrganizationDomains;
UseAdminSponsoredFamilies = organization.UseAdminSponsoredFamilies;
}
}

View File

@ -518,9 +518,8 @@ public class AccountsController : Controller
}
else
{
// If Account Deprovisioning is enabled, we need to check if the user is claimed by any organization.
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
&& await _userService.IsClaimedByAnyOrganizationAsync(user.Id))
// Check if the user is claimed by any organization.
if (await _userService.IsClaimedByAnyOrganizationAsync(user.Id))
{
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
using Bit.Api.Billing.Models.Responses;
using Bit.Core.Billing.Models.Api.Requests.Accounts;
using Bit.Core.Billing.Services;
using Bit.Core.Billing.Tax.Requests;
using Bit.Core.Services;
using Bit.Core.Utilities;
using Microsoft.AspNetCore.Authorization;

View File

@ -1,5 +1,5 @@
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.Repositories;
using Bit.Core.Services;

View File

@ -8,7 +8,9 @@ using Bit.Core;
using Bit.Core.Billing.Models;
using Bit.Core.Billing.Models.Sales;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Providers.Services;
using Bit.Core.Billing.Services;
using Bit.Core.Billing.Tax.Models;
using Bit.Core.Context;
using Bit.Core.Repositories;
using Bit.Core.Services;
@ -291,14 +293,20 @@ public class OrganizationBillingController(
sale.Organization.PlanType = plan.Type;
sale.Organization.Plan = plan.Name;
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);
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);
var taxInformation = TaxInformation.From(organizationSignup.TaxInfo);
await organizationBillingService.UpdatePaymentMethod(org, paymentSource, taxInformation);
await organizationBillingService.UpdatePaymentMethod(updatedOrg, paymentSource, taxInformation);
}
return TypedResults.Ok();

View File

@ -222,6 +222,20 @@ public class OrganizationSponsorshipsController : Controller
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")]
[HttpDelete("sponsored/{sponsoredOrgId}")]
[HttpPost("sponsored/{sponsoredOrgId}/remove")]
@ -271,8 +285,11 @@ public class OrganizationSponsorshipsController : Controller
}
var sponsorships = await _organizationSponsorshipRepository.GetManyBySponsoringOrganizationAsync(sponsoringOrgId);
return new ListResponseModel<OrganizationSponsorshipInvitesResponseModel>(sponsorships.Select(s =>
new OrganizationSponsorshipInvitesResponseModel(new OrganizationSponsorshipData(s))));
return new ListResponseModel<OrganizationSponsorshipInvitesResponseModel>(
sponsorships
.Where(s => s.IsAdminInitiated)
.Select(s => new OrganizationSponsorshipInvitesResponseModel(new OrganizationSponsorshipData(s)))
);
}

View File

@ -109,28 +109,6 @@ public class OrganizationsController(
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")]
[SelfHosted(NotSelfHostedOnly = true)]
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.Responses;
using Bit.Commercial.Core.Billing.Providers.Services;
using Bit.Core;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Models;
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.Tax.Models;
using Bit.Core.Context;
using Bit.Core.Models.BitStripe;
using Bit.Core.Services;
@ -147,13 +150,33 @@ public class ProviderBillingController(
var providerPlans = await providerPlanRepository.GetByProviderId(provider.Id);
var getProviderPriceFromStripe = featureService.IsEnabled(FeatureFlagKeys.PM21383_GetProviderPriceFromStripe);
var configuredProviderPlans = await Task.WhenAll(providerPlans.Select(async providerPlan =>
{
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(
providerPlan.Id,
providerPlan.ProviderId,
plan,
unitAmount,
providerPlan.SeatMinimum ?? 0,
providerPlan.PurchasedSeats ?? 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 Microsoft.AspNetCore.Authorization;
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 Bit.Core.Billing.Models;
using Bit.Core.Billing.Tax.Models;
namespace Bit.Api.Billing.Models.Requests;

View File

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

View File

@ -2,6 +2,8 @@
using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Models;
using Bit.Core.Billing.Providers.Models;
using Bit.Core.Billing.Tax.Models;
using Stripe;
namespace Bit.Api.Billing.Models.Responses;
@ -34,7 +36,7 @@ public record ProviderSubscriptionResponse(
.Select(providerPlan =>
{
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;
return new ProviderPlanResponse(
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;

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.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.Repositories;
using Bit.Core.Services;
@ -22,6 +26,7 @@ public class SelfHostedOrganizationSponsorshipsController : Controller
private readonly IRevokeSponsorshipCommand _revokeSponsorshipCommand;
private readonly ICurrentContext _currentContext;
private readonly IFeatureService _featureService;
private readonly IAuthorizationService _authorizationService;
public SelfHostedOrganizationSponsorshipsController(
ICreateSponsorshipCommand offerSponsorshipCommand,
@ -30,7 +35,8 @@ public class SelfHostedOrganizationSponsorshipsController : Controller
IOrganizationSponsorshipRepository organizationSponsorshipRepository,
IOrganizationUserRepository organizationUserRepository,
ICurrentContext currentContext,
IFeatureService featureService
IFeatureService featureService,
IAuthorizationService authorizationService
)
{
_offerSponsorshipCommand = offerSponsorshipCommand;
@ -40,6 +46,7 @@ public class SelfHostedOrganizationSponsorshipsController : Controller
_organizationUserRepository = organizationUserRepository;
_currentContext = currentContext;
_featureService = featureService;
_authorizationService = authorizationService;
}
[HttpPost("{sponsoringOrgId}/families-for-enterprise")]
@ -84,4 +91,41 @@ public class SelfHostedOrganizationSponsorshipsController : Controller
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 Guid? UserGuid { get; set; }
public string UserName { get; set; }
public string Email { get; set; }
public bool UsesKeyConnector { get; set; }
/// <summary>
/// A distinct list of the cipher ids associated with
/// A distinct list of the cipher ids associated with
/// the organization member
/// </summary>
public IEnumerable<string> CipherIds { get; set; }
public MemberCipherDetailsResponseModel(MemberAccessCipherDetails memberAccessCipherDetails)
{
this.UserGuid = memberAccessCipherDetails.UserGuid;
this.UserName = memberAccessCipherDetails.UserName;
this.Email = memberAccessCipherDetails.Email;
this.UsesKeyConnector = memberAccessCipherDetails.UsesKeyConnector;

View File

@ -12,17 +12,17 @@ namespace Bit.Api.KeyManagement.Validators;
/// </summary>
public class SendRotationValidator : IRotationValidator<IEnumerable<SendWithIdRequestModel>, IReadOnlyList<Send>>
{
private readonly ISendService _sendService;
private readonly ISendAuthorizationService _sendAuthorizationService;
private readonly ISendRepository _sendRepository;
/// <summary>
/// Instantiates a new <see cref="SendRotationValidator"/>
/// </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>
public SendRotationValidator(ISendService sendService, ISendRepository sendRepository)
public SendRotationValidator(ISendAuthorizationService sendAuthorizationService, ISendRepository sendRepository)
{
_sendService = sendService;
_sendAuthorizationService = sendAuthorizationService;
_sendRepository = sendRepository;
}
@ -44,7 +44,7 @@ public class SendRotationValidator : IRotationValidator<IEnumerable<SendWithIdRe
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;

View File

@ -32,6 +32,7 @@ public class PlanResponseModel : ResponseModel
HasTotp = plan.HasTotp;
Has2fa = plan.Has2fa;
HasSso = plan.HasSso;
HasOrganizationDomains = plan.HasOrganizationDomains;
HasResetPassword = plan.HasResetPassword;
UsersGetPremium = plan.UsersGetPremium;
UpgradeSortOrder = plan.UpgradeSortOrder;
@ -71,6 +72,7 @@ public class PlanResponseModel : ResponseModel
public bool Has2fa { get; set; }
public bool HasApi { get; set; }
public bool HasSso { get; set; }
public bool HasOrganizationDomains { get; set; }
public bool HasResetPassword { 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.ReportFeatures;
using Bit.Core.Auth.Models.Api.Request;
using Bit.Core.Tools.SendFeatures;
#if !OSS
using Bit.Commercial.Core.SecretsManager;
@ -186,6 +187,7 @@ public class Startup
services.AddPhishingDomainServices(globalSettings);
services.AddBillingQueries();
services.AddSendServices();
// Authorization Handlers
services.AddAuthorizationHandlers();

View File

@ -12,6 +12,8 @@ using Bit.Core.Settings;
using Bit.Core.Tools.Enums;
using Bit.Core.Tools.Models.Data;
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.Utilities;
using Microsoft.AspNetCore.Authorization;
@ -25,8 +27,10 @@ public class SendsController : Controller
{
private readonly ISendRepository _sendRepository;
private readonly IUserService _userService;
private readonly ISendService _sendService;
private readonly ISendAuthorizationService _sendAuthorizationService;
private readonly ISendFileStorageService _sendFileStorageService;
private readonly IAnonymousSendCommand _anonymousSendCommand;
private readonly INonAnonymousSendCommand _nonAnonymousSendCommand;
private readonly ILogger<SendsController> _logger;
private readonly GlobalSettings _globalSettings;
private readonly ICurrentContext _currentContext;
@ -34,7 +38,9 @@ public class SendsController : Controller
public SendsController(
ISendRepository sendRepository,
IUserService userService,
ISendService sendService,
ISendAuthorizationService sendAuthorizationService,
IAnonymousSendCommand anonymousSendCommand,
INonAnonymousSendCommand nonAnonymousSendCommand,
ISendFileStorageService sendFileStorageService,
ILogger<SendsController> logger,
GlobalSettings globalSettings,
@ -42,13 +48,16 @@ public class SendsController : Controller
{
_sendRepository = sendRepository;
_userService = userService;
_sendService = sendService;
_sendAuthorizationService = sendAuthorizationService;
_anonymousSendCommand = anonymousSendCommand;
_nonAnonymousSendCommand = nonAnonymousSendCommand;
_sendFileStorageService = sendFileStorageService;
_logger = logger;
_globalSettings = globalSettings;
_currentContext = currentContext;
}
#region Anonymous endpoints
[AllowAnonymous]
[HttpPost("access/{id}")]
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 (send, passwordRequired, passwordInvalid) =
await _sendService.AccessAsync(guid, model.Password);
if (passwordRequired)
var send = await _sendRepository.GetByIdAsync(guid);
SendAccessResult sendAuthResult =
await _sendAuthorizationService.AccessAsync(send, model.Password);
if (sendAuthResult.Equals(SendAccessResult.PasswordRequired))
{
return new UnauthorizedResult();
}
if (passwordInvalid)
if (sendAuthResult.Equals(SendAccessResult.PasswordInvalid))
{
await Task.Delay(2000);
throw new BadRequestException("Invalid password.");
}
if (send == null)
if (sendAuthResult.Equals(SendAccessResult.Denied))
{
throw new NotFoundException();
}
@ -106,19 +116,19 @@ public class SendsController : Controller
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);
if (passwordRequired)
if (result.Equals(SendAccessResult.PasswordRequired))
{
return new UnauthorizedResult();
}
if (passwordInvalid)
if (result.Equals(SendAccessResult.PasswordInvalid))
{
await Task.Delay(2000);
throw new BadRequestException("Invalid password.");
}
if (send == null)
if (result.Equals(SendAccessResult.Denied))
{
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}")]
public async Task<SendResponseModel> Get(string id)
{
@ -157,8 +206,8 @@ public class SendsController : Controller
{
model.ValidateCreation();
var userId = _userService.GetProperUserId(User).Value;
var send = model.ToSend(userId, _sendService);
await _sendService.SaveSendAsync(send);
var send = model.ToSend(userId, _sendAuthorizationService);
await _nonAnonymousSendCommand.SaveSendAsync(send);
return new SendResponseModel(send, _globalSettings);
}
@ -175,15 +224,15 @@ public class SendsController : Controller
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();
var userId = _userService.GetProperUserId(User).Value;
var (send, data) = model.ToSend(userId, model.File.FileName, _sendService);
var uploadUrl = await _sendService.SaveFileSendAsync(send, data, model.FileLength.Value);
var (send, data) = model.ToSend(userId, model.File.FileName, _sendAuthorizationService);
var uploadUrl = await _nonAnonymousSendCommand.SaveFileSendAsync(send, data, model.FileLength.Value);
return new SendFileUploadDataResponseModel
{
Url = uploadUrl,
@ -230,41 +279,7 @@ public class SendsController : Controller
var send = await _sendRepository.GetByIdAsync(new Guid(id));
await Request.GetFileAsync(async (stream) =>
{
await _sendService.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;
}
}
}
await _nonAnonymousSendCommand.UploadFileToExistingSendAsync(stream, send);
});
}
@ -279,7 +294,7 @@ public class SendsController : Controller
throw new NotFoundException();
}
await _sendService.SaveSendAsync(model.ToSend(send, _sendService));
await _nonAnonymousSendCommand.SaveSendAsync(model.ToSend(send, _sendAuthorizationService));
return new SendResponseModel(send, _globalSettings);
}
@ -294,7 +309,7 @@ public class SendsController : Controller
}
send.Password = null;
await _sendService.SaveSendAsync(send);
await _nonAnonymousSendCommand.SaveSendAsync(send);
return new SendResponseModel(send, _globalSettings);
}
@ -308,6 +323,8 @@ public class SendsController : Controller
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? HideEmail { get; set; }
public Send ToSend(Guid userId, ISendService sendService)
public Send ToSend(Guid userId, ISendAuthorizationService sendAuthorizationService)
{
var send = new Send
{
Type = Type,
UserId = (Guid?)userId
};
ToSend(send, sendService);
ToSend(send, sendAuthorizationService);
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
{
Type = Type,
UserId = (Guid?)userId
}, sendService);
}, sendAuthorizationService);
var data = new SendFileData(Name, Notes, fileName);
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)
{
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.ExpirationDate = ExpirationDate;
@ -133,7 +133,7 @@ public class SendRequestModel
existingSend.MaxAccessCount = MaxAccessCount;
if (!string.IsNullOrWhiteSpace(Password))
{
existingSend.Password = sendService.HashPassword(Password);
existingSend.Password = authorizationService.HashPassword(Password);
}
existingSend.Disabled = Disabled.GetValueOrDefault();
existingSend.HideEmail = HideEmail.GetValueOrDefault();

View File

@ -151,6 +151,16 @@ public class CiphersController : Controller
public async Task<CipherResponseModel> Post([FromBody] CipherRequestModel model)
{
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);
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)
{
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);
if (cipher.OrganizationId.HasValue && !await _currentContext.OrganizationUser(cipher.OrganizationId.Value))
{
@ -192,6 +212,16 @@ public class CiphersController : Controller
}
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);
var response = new CipherMiniResponseModel(cipher, _globalSettings, false);
@ -209,6 +239,15 @@ public class CiphersController : Controller
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);
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 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);
if (cipher == null || !cipher.OrganizationId.HasValue ||
@ -315,26 +363,10 @@ public class CiphersController : Controller
{
var org = _currentContext.GetOrganization(organizationId);
// If we're not an "admin", we don't need to check the ciphers
if (org is not ({ Type: OrganizationUserType.Owner or OrganizationUserType.Admin } or { Permissions.EditAnyCollection: true }))
// 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 }) || await _currentContext.ProviderUserForOrgAsync(organizationId))
{
// Are we a provider user? If so, we need to be sure we're not restricted
// 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;
}
return false;
}
// 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);
// If we're not an "admin", we don't need to check the ciphers
if (org is not ({ Type: OrganizationUserType.Owner or OrganizationUserType.Admin } or { Permissions.EditAnyCollection: true }))
// 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 }) || await _currentContext.ProviderUserForOrgAsync(organizationId))
{
// Are we a provider user? If so, we need to be sure we're not restricted
// 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;
}
return false;
}
// 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;
}
// Provider users can edit all ciphers if RestrictProviderAccess is disabled
// Provider users cannot edit ciphers
if (await _currentContext.ProviderUserForOrgAsync(organizationId))
{
return !_featureService.IsEnabled(FeatureFlagKeys.RestrictProviderAccess);
return false;
}
return false;
@ -485,10 +501,10 @@ public class CiphersController : Controller
return true;
}
// Provider users can only access organization ciphers if RestrictProviderAccess is disabled
// Provider users cannot access organization ciphers
if (await _currentContext.ProviderUserForOrgAsync(organizationId))
{
return !_featureService.IsEnabled(FeatureFlagKeys.RestrictProviderAccess);
return false;
}
return false;
@ -508,10 +524,10 @@ public class CiphersController : Controller
return true;
}
// Provider users can only access all ciphers if RestrictProviderAccess is disabled
// Provider users cannot access ciphers
if (await _currentContext.ProviderUserForOrgAsync(organizationId))
{
return !_featureService.IsEnabled(FeatureFlagKeys.RestrictProviderAccess);
return false;
}
return false;
@ -690,6 +706,15 @@ public class CiphersController : Controller
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);
var original = cipher.Clone();
@ -1051,6 +1076,18 @@ public class CiphersController : Controller
var ciphers = await _cipherRepository.GetManyByUserIdAsync(userId, withOrganizations: false);
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?)>();
foreach (var cipher in model.Ciphers)
{
@ -1086,9 +1123,8 @@ public class CiphersController : Controller
throw new BadRequestException(ModelState);
}
// If Account Deprovisioning is enabled, we need to check if the user is claimed by any organization.
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
&& await _userService.IsClaimedByAnyOrganizationAsync(user.Id))
// Check if the user is claimed by any organization.
if (await _userService.IsClaimedByAnyOrganizationAsync(user.Id))
{
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
{
/// <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; }
[StringLength(36)]

View File

@ -63,6 +63,12 @@ public class FreshdeskController : Controller
note += $"<li>Region: {_billingSettings.FreshDesk.Region}</li>";
var customFields = new Dictionary<string, object>();
var user = await _userRepository.GetByEmailAsync(ticketContactEmail);
if (user == null)
{
note += $"<li>No user found: {ticketContactEmail}</li>";
await CreateNote(ticketId, note);
}
if (user != null)
{
var userLink = $"{_globalSettings.BaseServiceUri.Admin}/users/edit/{user.Id}";
@ -121,18 +127,7 @@ public class FreshdeskController : Controller
Content = JsonContent.Create(updateBody),
};
await CallFreshdeskApiAsync(updateRequest);
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);
await CreateNote(ticketId, note);
}
return new OkResult();
@ -208,6 +203,21 @@ public class FreshdeskController : Controller
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)
{
// 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.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Entities;
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.Repositories;
using Stripe;

View File

@ -1,11 +1,11 @@
using Bit.Core;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Extensions;
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.Repositories;
using Bit.Core.Services;
@ -25,8 +25,7 @@ public class UpcomingInvoiceHandler(
IStripeEventService stripeEventService,
IStripeEventUtilityService stripeEventUtilityService,
IUserRepository userRepository,
IValidateSponsorshipCommand validateSponsorshipCommand,
IAutomaticTaxFactory automaticTaxFactory)
IValidateSponsorshipCommand validateSponsorshipCommand)
: IUpcomingInvoiceHandler
{
public async Task HandleAsync(Event parsedEvent)
@ -46,6 +45,8 @@ public class UpcomingInvoiceHandler(
var (organizationId, userId, providerId) = stripeEventUtilityService.GetIdsFromMetadata(subscription.Metadata);
var setNonUSBusinessUseToReverseCharge = featureService.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge);
if (organizationId.HasValue)
{
var organization = await organizationRepository.GetByIdAsync(organizationId.Value);
@ -55,7 +56,7 @@ public class UpcomingInvoiceHandler(
return;
}
await TryEnableAutomaticTaxAsync(subscription);
await AlignOrganizationTaxConcernsAsync(organization, subscription, parsedEvent.Id, setNonUSBusinessUseToReverseCharge);
var plan = await pricingClient.GetPlanOrThrow(organization.PlanType);
@ -100,7 +101,25 @@ public class UpcomingInvoiceHandler(
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)
{
@ -116,7 +135,7 @@ public class UpcomingInvoiceHandler(
return;
}
await TryEnableAutomaticTaxAsync(subscription);
await AlignProviderTaxConcernsAsync(provider, subscription, parsedEvent.Id, setNonUSBusinessUseToReverseCharge);
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 automaticTaxParameters = new AutomaticTaxFactoryParameters(subscription.Items.Select(x => x.Price.Id));
var automaticTaxStrategy = await automaticTaxFactory.CreateAsync(automaticTaxParameters);
var updateOptions = automaticTaxStrategy.GetUpdateOptions(subscription);
var nonUSBusinessUse =
organization.PlanType.GetProductTier() != ProductTierType.Families &&
subscription.Customer.Address.Country != "US";
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);
return;
setAutomaticTaxToEnabled = true;
}
if (subscription.AutomaticTax.Enabled ||
!subscription.Customer.HasBillingLocation() ||
await IsNonTaxableNonUSBusinessUseSubscription(subscription))
else
{
return;
setAutomaticTaxToEnabled =
subscription.Customer.HasRecognizedTaxLocation() &&
(subscription.Customer.Address.Country == "US" ||
(nonUSBusinessUse && subscription.Customer.TaxIds.Any()));
}
await stripeFacade.UpdateSubscription(subscription.Id,
new SubscriptionUpdateOptions
if (!subscription.AutomaticTax.Enabled && setAutomaticTaxToEnabled)
{
try
{
DefaultTaxRates = [],
AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }
});
await stripeFacade.UpdateSubscription(subscription.Id,
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(
pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually2019),
pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually)))
.Select(plan => plan.PasswordManager.StripePlanId);
if (subscription.Customer.Address.Country != "US" && subscription.Customer.TaxExempt != StripeConstants.TaxExempt.Reverse)
{
try
{
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" &&
localSubscription.Metadata.ContainsKey(StripeConstants.MetadataKeys.OrganizationId) &&
!localSubscription.Items.Select(item => item.Price.Id).Intersect(familyPriceIds).Any() &&
!localSubscription.Customer.TaxIds.Any();
setAutomaticTaxToEnabled = true;
}
else
{
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>
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>
/// If set to true, admins can initiate organization-issued sponsorships.
/// </summary>
@ -319,5 +324,7 @@ public class Organization : ITableObject<Guid>, IStorableSubscriber, IRevisable,
SmSeats = license.SmSeats;
SmServiceAccounts = license.SmServiceAccounts;
UseRiskInsights = license.UseRiskInsights;
UseOrganizationDomains = license.UseOrganizationDomains;
UseAdminSponsoredFamilies = license.UseAdminSponsoredFamilies;
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -20,7 +20,6 @@ public class VerifyOrganizationDomainCommand(
IDnsResolverService dnsResolverService,
IEventService eventService,
IGlobalSettings globalSettings,
IFeatureService featureService,
ICurrentContext currentContext,
ISavePolicyCommand savePolicyCommand,
IMailService mailService,
@ -125,11 +124,8 @@ public class VerifyOrganizationDomainCommand(
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) =>

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
var organizationAbility = await _applicationCacheService.GetOrganizationAbilityAsync(organizationId);
// TODO: Replace "UseSso" with a new organization ability like "UseOrganizationDomains" (PM-11622).
// Verified domains were tied to SSO, so we currently check the "UseSso" organization ability.
if (organizationAbility is { Enabled: true, UseSso: true })
if (organizationAbility is { Enabled: true, UseOrganizationDomains: true })
{
// Get all organization users with claimed domains by the organization
var organizationUsersWithClaimedDomain = await _organizationUserRepository.GetManyByOrganizationWithClaimedDomainsAsync(organizationId);

View File

@ -159,7 +159,7 @@ public class RemoveOrganizationUserCommand : IRemoveOrganizationUserCommand
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 });
if (claimedStatus.TryGetValue(orgUser.Id, out var isClaimed) && isClaimed)
@ -214,7 +214,7 @@ public class RemoveOrganizationUserCommand : IRemoveOrganizationUserCommand
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))
: filteredUsers.ToDictionary(u => u.Id, u => false);
var result = new List<(OrganizationUser OrganizationUser, string ErrorMessage)>();

View File

@ -104,7 +104,8 @@ public class CloudOrganizationSignUpCommand(
RevisionDate = DateTime.UtcNow,
Status = OrganizationStatusType.Created,
UsePasswordManager = true,
UseSecretsManager = signup.UseSecretsManager
UseSecretsManager = signup.UseSecretsManager,
UseOrganizationDomains = plan.HasOrganizationDomains,
};
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 (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning))
{
var currentUser = _currentContext.UserId ?? Guid.Empty;
var isOwnerOrProvider = await _currentContext.OrganizationOwner(policyUpdate.OrganizationId);
await RevokeNonCompliantUsersAsync(policyUpdate.OrganizationId, policyUpdate.PerformedBy ?? new StandardUser(currentUser, isOwnerOrProvider));
}
else
{
await RemoveNonCompliantUsersAsync(policyUpdate.OrganizationId);
}
var currentUser = _currentContext.UserId ?? Guid.Empty;
var isOwnerOrProvider = await _currentContext.OrganizationOwner(policyUpdate.OrganizationId);
await RevokeNonCompliantUsersAsync(policyUpdate.OrganizationId, policyUpdate.PerformedBy ?? new StandardUser(currentUser, isOwnerOrProvider));
}
}
@ -116,42 +109,6 @@ public class SingleOrgPolicyValidator : IPolicyValidator
_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)
{
if (policyUpdate is not { Enabled: true })
@ -165,8 +122,7 @@ public class SingleOrgPolicyValidator : IPolicyValidator
return validateDecryptionErrorMessage;
}
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
&& await _organizationHasVerifiedDomainsQuery.HasVerifiedDomainsAsync(policyUpdate.OrganizationId))
if (await _organizationHasVerifiedDomainsQuery.HasVerifiedDomainsAsync(policyUpdate.OrganizationId))
{
return ClaimedDomainSingleOrganizationRequiredErrorMessage;
}

View File

@ -23,8 +23,6 @@ public class TwoFactorAuthenticationPolicyValidator : IPolicyValidator
private readonly IOrganizationRepository _organizationRepository;
private readonly ICurrentContext _currentContext;
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
private readonly IFeatureService _featureService;
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.";
@ -38,8 +36,6 @@ public class TwoFactorAuthenticationPolicyValidator : IPolicyValidator
IOrganizationRepository organizationRepository,
ICurrentContext currentContext,
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
IRemoveOrganizationUserCommand removeOrganizationUserCommand,
IFeatureService featureService,
IRevokeNonCompliantOrganizationUserCommand revokeNonCompliantOrganizationUserCommand)
{
_organizationUserRepository = organizationUserRepository;
@ -47,8 +43,6 @@ public class TwoFactorAuthenticationPolicyValidator : IPolicyValidator
_organizationRepository = organizationRepository;
_currentContext = currentContext;
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
_removeOrganizationUserCommand = removeOrganizationUserCommand;
_featureService = featureService;
_revokeNonCompliantOrganizationUserCommand = revokeNonCompliantOrganizationUserCommand;
}
@ -56,16 +50,9 @@ public class TwoFactorAuthenticationPolicyValidator : IPolicyValidator
{
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);
await RevokeNonCompliantUsersAsync(policyUpdate.OrganizationId, policyUpdate.PerformedBy ?? new StandardUser(currentUser, isOwnerOrProvider));
}
else
{
await RemoveNonCompliantUsersAsync(policyUpdate.OrganizationId);
}
var currentUser = _currentContext.UserId ?? Guid.Empty;
var isOwnerOrProvider = await _currentContext.OrganizationOwner(policyUpdate.OrganizationId);
await RevokeNonCompliantUsersAsync(policyUpdate.OrganizationId, policyUpdate.PerformedBy ?? new StandardUser(currentUser, isOwnerOrProvider));
}
}
@ -121,40 +108,6 @@ public class TwoFactorAuthenticationPolicyValidator : IPolicyValidator
_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(
IEnumerable<OrganizationUserUserDetails> orgUserDetails,
IEnumerable<(OrganizationUserUserDetails user, bool isTwoFactorEnabled)> organizationUsersTwoFactorEnabled) =>

View File

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

View File

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

View File

@ -144,27 +144,6 @@ public class OrganizationService : IOrganizationService
_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)
{
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)
{
var anySingleOrgPolicies = await _policyService.AnyPoliciesApplicableToUserAsync(ownerId, PolicyType.SingleOrg);
@ -570,6 +490,8 @@ public class OrganizationService : IOrganizationService
SmSeats = license.SmSeats,
SmServiceAccounts = license.SmServiceAccounts,
UseRiskInsights = license.UseRiskInsights,
UseOrganizationDomains = license.UseOrganizationDomains,
UseAdminSponsoredFamilies = license.UseAdminSponsoredFamilies,
};
var result = await SignUpAsync(organization, owner.Id, ownerKey, collectionName, false);

View File

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

View File

@ -3,6 +3,7 @@ using Bit.Core.Auth.Entities;
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Models.Data;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Services;
using Bit.Core.Vault.Models.Data;
@ -20,6 +21,15 @@ public interface IEmergencyAccessService
Task InitiateAsync(Guid id, User initiatingUser);
Task ApproveAsync(Guid id, User approvingUser);
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<(EmergencyAccess, User)> TakeoverAsync(Guid id, User initiatingUser);
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.Auth.Entities;
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Models;
using Bit.Core.Auth.Models.Business.Tokenables;
using Bit.Core.Auth.Models.Data;
using Bit.Core.Entities;
@ -16,7 +15,6 @@ using Bit.Core.Tokens;
using Bit.Core.Vault.Models.Data;
using Bit.Core.Vault.Repositories;
using Bit.Core.Vault.Services;
using Microsoft.AspNetCore.Identity;
namespace Bit.Core.Auth.Services;
@ -31,8 +29,6 @@ public class EmergencyAccessService : IEmergencyAccessService
private readonly IMailService _mailService;
private readonly IUserService _userService;
private readonly GlobalSettings _globalSettings;
private readonly IPasswordHasher<User> _passwordHasher;
private readonly IOrganizationService _organizationService;
private readonly IDataProtectorTokenFactory<EmergencyAccessInviteTokenable> _dataProtectorTokenizer;
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
@ -45,9 +41,7 @@ public class EmergencyAccessService : IEmergencyAccessService
ICipherService cipherService,
IMailService mailService,
IUserService userService,
IPasswordHasher<User> passwordHasher,
GlobalSettings globalSettings,
IOrganizationService organizationService,
IDataProtectorTokenFactory<EmergencyAccessInviteTokenable> dataProtectorTokenizer,
IRemoveOrganizationUserCommand removeOrganizationUserCommand)
{
@ -59,9 +53,7 @@ public class EmergencyAccessService : IEmergencyAccessService
_cipherService = cipherService;
_mailService = mailService;
_userService = userService;
_passwordHasher = passwordHasher;
_globalSettings = globalSettings;
_organizationService = organizationService;
_dataProtectorTokenizer = dataProtectorTokenizer;
_removeOrganizationUserCommand = removeOrganizationUserCommand;
}
@ -126,7 +118,12 @@ public class EmergencyAccessService : IEmergencyAccessService
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.");
}
@ -140,6 +137,8 @@ public class EmergencyAccessService : IEmergencyAccessService
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) ||
!emergencyAccess.Email.Equals(user.Email, StringComparison.InvariantCultureIgnoreCase))
{
@ -163,6 +162,8 @@ public class EmergencyAccessService : IEmergencyAccessService
public async Task DeleteAsync(Guid emergencyAccessId, Guid grantorId)
{
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))
{
throw new BadRequestException("Emergency Access not valid.");
@ -171,9 +172,9 @@ public class EmergencyAccessService : IEmergencyAccessService
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 ||
emergencyAccess.GrantorId != confirmingUserId)
{
@ -224,7 +225,6 @@ public class EmergencyAccessService : IEmergencyAccessService
public async Task InitiateAsync(Guid id, User initiatingUser)
{
var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(id);
if (emergencyAccess == null || emergencyAccess.GranteeId != initiatingUser.Id ||
emergencyAccess.Status != EmergencyAccessStatusType.Confirmed)
{
@ -285,6 +285,9 @@ public class EmergencyAccessService : IEmergencyAccessService
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);
if (!IsValidRequest(emergencyAccess, requestingUser, EmergencyAccessType.Takeover))
@ -295,7 +298,9 @@ public class EmergencyAccessService : IEmergencyAccessService
var grantor = await _userRepository.GetByIdAsync(emergencyAccess.GrantorId);
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;
return policies;
@ -311,7 +316,8 @@ public class EmergencyAccessService : IEmergencyAccessService
}
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)
{
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.Key = key;
// 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);
// Remove grantor from all organizations unless Owner
@ -421,12 +429,22 @@ public class EmergencyAccessService : IEmergencyAccessService
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;
}
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 &&
availableAccess.GranteeId == requestingUser.Id &&

View File

@ -108,6 +108,7 @@ public class RegisterUserCommand : IRegisterUserCommand
var result = await _userService.CreateUserAsync(user, masterPasswordHash);
if (result == IdentityResult.Success)
{
var sentWelcomeEmail = false;
if (!string.IsNullOrEmpty(user.ReferenceData))
{
var referenceData = JsonConvert.DeserializeObject<Dictionary<string, object>>(user.ReferenceData);
@ -115,6 +116,7 @@ public class RegisterUserCommand : IRegisterUserCommand
{
var initiationPath = value.ToString();
await SendAppropriateWelcomeEmailAsync(user, initiationPath);
sentWelcomeEmail = true;
if (!string.IsNullOrEmpty(initiationPath))
{
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));
}

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