1
0
mirror of https://github.com/bitwarden/server.git synced 2025-04-06 21:48:12 -05:00

Merge branch 'main' into ac/pm-15621/refactor-delete-command

This commit is contained in:
Jimmy Vo 2025-03-06 10:52:41 -05:00
commit 90490ae7bd
No known key found for this signature in database
GPG Key ID: 7CB834D6F4FFCA11
357 changed files with 20850 additions and 2647 deletions

199
.github/renovate.json vendored
View File

@ -1,199 +0,0 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["github>bitwarden/renovate-config"],
"enabledManagers": [
"dockerfile",
"docker-compose",
"github-actions",
"npm",
"nuget"
],
"packageRules": [
{
"groupName": "dockerfile minor",
"matchManagers": ["dockerfile"],
"matchUpdateTypes": ["minor"]
},
{
"groupName": "docker-compose minor",
"matchManagers": ["docker-compose"],
"matchUpdateTypes": ["minor"]
},
{
"groupName": "github-action minor",
"matchManagers": ["github-actions"],
"matchUpdateTypes": ["minor"]
},
{
"matchManagers": ["dockerfile", "docker-compose"],
"commitMessagePrefix": "[deps] BRE:"
},
{
"matchPackageNames": ["DnsClient"],
"description": "Admin Console owned dependencies",
"commitMessagePrefix": "[deps] AC:",
"reviewers": ["team:team-admin-console-dev"]
},
{
"matchFileNames": ["src/Admin/package.json", "src/Sso/package.json"],
"description": "Admin & SSO npm packages",
"commitMessagePrefix": "[deps] Auth:",
"reviewers": ["team:team-auth-dev"]
},
{
"matchPackageNames": [
"Azure.Extensions.AspNetCore.DataProtection.Blobs",
"DuoUniversal",
"Fido2.AspNet",
"Duende.IdentityServer",
"Microsoft.Extensions.Identity.Stores",
"Otp.NET",
"Sustainsys.Saml2.AspNetCore2",
"YubicoDotNetClient"
],
"description": "Auth owned dependencies",
"commitMessagePrefix": "[deps] Auth:",
"reviewers": ["team:team-auth-dev"]
},
{
"matchPackageNames": [
"AutoFixture.AutoNSubstitute",
"AutoFixture.Xunit2",
"BenchmarkDotNet",
"BitPay.Light",
"Braintree",
"coverlet.collector",
"CsvHelper",
"Kralizek.AutoFixture.Extensions.MockHttp",
"Microsoft.AspNetCore.Mvc.Testing",
"Microsoft.Extensions.Logging",
"Microsoft.Extensions.Logging.Console",
"Newtonsoft.Json",
"NSubstitute",
"Sentry.Serilog",
"Serilog.AspNetCore",
"Serilog.Extensions.Logging",
"Serilog.Extensions.Logging.File",
"Serilog.Sinks.AzureCosmosDB",
"Serilog.Sinks.SyslogMessages",
"Stripe.net",
"Swashbuckle.AspNetCore",
"Swashbuckle.AspNetCore.SwaggerGen",
"xunit",
"xunit.runner.visualstudio"
],
"description": "Billing owned dependencies",
"commitMessagePrefix": "[deps] Billing:",
"reviewers": ["team:team-billing-dev"]
},
{
"matchPackagePatterns": ["^Microsoft.Extensions.Logging"],
"groupName": "Microsoft.Extensions.Logging",
"description": "Group Microsoft.Extensions.Logging to exclude them from the dotnet monorepo preset"
},
{
"matchPackageNames": [
"Dapper",
"dbup-sqlserver",
"dotnet-ef",
"linq2db.EntityFrameworkCore",
"Microsoft.Azure.Cosmos",
"Microsoft.Data.SqlClient",
"Microsoft.EntityFrameworkCore.Design",
"Microsoft.EntityFrameworkCore.InMemory",
"Microsoft.EntityFrameworkCore.Relational",
"Microsoft.EntityFrameworkCore.Sqlite",
"Microsoft.EntityFrameworkCore.SqlServer",
"Microsoft.Extensions.Caching.Cosmos",
"Microsoft.Extensions.Caching.SqlServer",
"Microsoft.Extensions.Caching.StackExchangeRedis",
"Npgsql.EntityFrameworkCore.PostgreSQL",
"Pomelo.EntityFrameworkCore.MySql"
],
"description": "DbOps owned dependencies",
"commitMessagePrefix": "[deps] DbOps:",
"reviewers": ["team:dept-dbops"]
},
{
"matchPackageNames": ["CommandDotNet", "YamlDotNet"],
"description": "DevOps owned dependencies",
"commitMessagePrefix": "[deps] BRE:",
"reviewers": ["team:dept-bre"]
},
{
"matchPackageNames": [
"AspNetCoreRateLimit",
"AspNetCoreRateLimit.Redis",
"Azure.Data.Tables",
"Azure.Messaging.EventGrid",
"Azure.Messaging.ServiceBus",
"Azure.Storage.Blobs",
"Azure.Storage.Queues",
"Microsoft.AspNetCore.Authentication.JwtBearer",
"Microsoft.AspNetCore.Http",
"Quartz"
],
"description": "Platform owned dependencies",
"commitMessagePrefix": "[deps] Platform:",
"reviewers": ["team:team-platform-dev"]
},
{
"matchPackagePatterns": ["EntityFrameworkCore", "^dotnet-ef"],
"groupName": "EntityFrameworkCore",
"description": "Group EntityFrameworkCore to exclude them from the dotnet monorepo preset"
},
{
"matchPackageNames": [
"AutoMapper.Extensions.Microsoft.DependencyInjection",
"AWSSDK.SimpleEmail",
"AWSSDK.SQS",
"Handlebars.Net",
"LaunchDarkly.ServerSdk",
"MailKit",
"Microsoft.AspNetCore.SignalR.Protocols.MessagePack",
"Microsoft.AspNetCore.SignalR.StackExchangeRedis",
"Microsoft.Azure.NotificationHubs",
"Microsoft.Extensions.Configuration.EnvironmentVariables",
"Microsoft.Extensions.Configuration.UserSecrets",
"Microsoft.Extensions.Configuration",
"Microsoft.Extensions.DependencyInjection.Abstractions",
"Microsoft.Extensions.DependencyInjection",
"SendGrid"
],
"description": "Tools owned dependencies",
"commitMessagePrefix": "[deps] Tools:",
"reviewers": ["team:team-tools-dev"]
},
{
"matchPackagePatterns": ["^Microsoft.AspNetCore.SignalR"],
"groupName": "SignalR",
"description": "Group SignalR to exclude them from the dotnet monorepo preset"
},
{
"matchPackagePatterns": ["^Microsoft.Extensions.Configuration"],
"groupName": "Microsoft.Extensions.Configuration",
"description": "Group Microsoft.Extensions.Configuration to exclude them from the dotnet monorepo preset"
},
{
"matchPackagePatterns": ["^Microsoft.Extensions.DependencyInjection"],
"groupName": "Microsoft.Extensions.DependencyInjection",
"description": "Group Microsoft.Extensions.DependencyInjection to exclude them from the dotnet monorepo preset"
},
{
"matchPackageNames": [
"AngleSharp",
"AspNetCore.HealthChecks.AzureServiceBus",
"AspNetCore.HealthChecks.AzureStorage",
"AspNetCore.HealthChecks.Network",
"AspNetCore.HealthChecks.Redis",
"AspNetCore.HealthChecks.SendGrid",
"AspNetCore.HealthChecks.SqlServer",
"AspNetCore.HealthChecks.Uris"
],
"description": "Vault owned dependencies",
"commitMessagePrefix": "[deps] Vault:",
"reviewers": ["team:team-vault-dev"]
}
],
"ignoreDeps": ["dotnet-sdk"]
}

199
.github/renovate.json5 vendored Normal file
View File

@ -0,0 +1,199 @@
{
$schema: "https://docs.renovatebot.com/renovate-schema.json",
extends: ["github>bitwarden/renovate-config"], // Extends our default configuration for pinned dependencies
enabledManagers: [
"dockerfile",
"docker-compose",
"github-actions",
"npm",
"nuget",
],
packageRules: [
{
groupName: "dockerfile minor",
matchManagers: ["dockerfile"],
matchUpdateTypes: ["minor"],
},
{
groupName: "docker-compose minor",
matchManagers: ["docker-compose"],
matchUpdateTypes: ["minor"],
},
{
groupName: "github-action minor",
matchManagers: ["github-actions"],
matchUpdateTypes: ["minor"],
},
{
matchManagers: ["dockerfile", "docker-compose"],
commitMessagePrefix: "[deps] BRE:",
},
{
matchPackageNames: ["DnsClient"],
description: "Admin Console owned dependencies",
commitMessagePrefix: "[deps] AC:",
reviewers: ["team:team-admin-console-dev"],
},
{
matchFileNames: ["src/Admin/package.json", "src/Sso/package.json"],
description: "Admin & SSO npm packages",
commitMessagePrefix: "[deps] Auth:",
reviewers: ["team:team-auth-dev"],
},
{
matchPackageNames: [
"Azure.Extensions.AspNetCore.DataProtection.Blobs",
"DuoUniversal",
"Fido2.AspNet",
"Duende.IdentityServer",
"Microsoft.Extensions.Identity.Stores",
"Otp.NET",
"Sustainsys.Saml2.AspNetCore2",
"YubicoDotNetClient",
],
description: "Auth owned dependencies",
commitMessagePrefix: "[deps] Auth:",
reviewers: ["team:team-auth-dev"],
},
{
matchPackageNames: [
"AutoFixture.AutoNSubstitute",
"AutoFixture.Xunit2",
"BenchmarkDotNet",
"BitPay.Light",
"Braintree",
"coverlet.collector",
"CsvHelper",
"Kralizek.AutoFixture.Extensions.MockHttp",
"Microsoft.AspNetCore.Mvc.Testing",
"Microsoft.Extensions.Logging",
"Microsoft.Extensions.Logging.Console",
"Newtonsoft.Json",
"NSubstitute",
"Sentry.Serilog",
"Serilog.AspNetCore",
"Serilog.Extensions.Logging",
"Serilog.Extensions.Logging.File",
"Serilog.Sinks.AzureCosmosDB",
"Serilog.Sinks.SyslogMessages",
"Stripe.net",
"Swashbuckle.AspNetCore",
"Swashbuckle.AspNetCore.SwaggerGen",
"xunit",
"xunit.runner.visualstudio",
],
description: "Billing owned dependencies",
commitMessagePrefix: "[deps] Billing:",
reviewers: ["team:team-billing-dev"],
},
{
matchPackagePatterns: ["^Microsoft.Extensions.Logging"],
groupName: "Microsoft.Extensions.Logging",
description: "Group Microsoft.Extensions.Logging to exclude them from the dotnet monorepo preset",
},
{
matchPackageNames: [
"Dapper",
"dbup-sqlserver",
"dotnet-ef",
"linq2db.EntityFrameworkCore",
"Microsoft.Azure.Cosmos",
"Microsoft.Data.SqlClient",
"Microsoft.EntityFrameworkCore.Design",
"Microsoft.EntityFrameworkCore.InMemory",
"Microsoft.EntityFrameworkCore.Relational",
"Microsoft.EntityFrameworkCore.Sqlite",
"Microsoft.EntityFrameworkCore.SqlServer",
"Microsoft.Extensions.Caching.Cosmos",
"Microsoft.Extensions.Caching.SqlServer",
"Microsoft.Extensions.Caching.StackExchangeRedis",
"Npgsql.EntityFrameworkCore.PostgreSQL",
"Pomelo.EntityFrameworkCore.MySql",
],
description: "DbOps owned dependencies",
commitMessagePrefix: "[deps] DbOps:",
reviewers: ["team:dept-dbops"],
},
{
matchPackageNames: ["CommandDotNet", "YamlDotNet"],
description: "DevOps owned dependencies",
commitMessagePrefix: "[deps] BRE:",
reviewers: ["team:dept-bre"],
},
{
matchPackageNames: [
"AspNetCoreRateLimit",
"AspNetCoreRateLimit.Redis",
"Azure.Data.Tables",
"Azure.Messaging.EventGrid",
"Azure.Messaging.ServiceBus",
"Azure.Storage.Blobs",
"Azure.Storage.Queues",
"Microsoft.AspNetCore.Authentication.JwtBearer",
"Microsoft.AspNetCore.Http",
"Quartz",
],
description: "Platform owned dependencies",
commitMessagePrefix: "[deps] Platform:",
reviewers: ["team:team-platform-dev"],
},
{
matchPackagePatterns: ["EntityFrameworkCore", "^dotnet-ef"],
groupName: "EntityFrameworkCore",
description: "Group EntityFrameworkCore to exclude them from the dotnet monorepo preset",
},
{
matchPackageNames: [
"AutoMapper.Extensions.Microsoft.DependencyInjection",
"AWSSDK.SimpleEmail",
"AWSSDK.SQS",
"Handlebars.Net",
"LaunchDarkly.ServerSdk",
"MailKit",
"Microsoft.AspNetCore.SignalR.Protocols.MessagePack",
"Microsoft.AspNetCore.SignalR.StackExchangeRedis",
"Microsoft.Azure.NotificationHubs",
"Microsoft.Extensions.Configuration.EnvironmentVariables",
"Microsoft.Extensions.Configuration.UserSecrets",
"Microsoft.Extensions.Configuration",
"Microsoft.Extensions.DependencyInjection.Abstractions",
"Microsoft.Extensions.DependencyInjection",
"SendGrid",
],
description: "Tools owned dependencies",
commitMessagePrefix: "[deps] Tools:",
reviewers: ["team:team-tools-dev"],
},
{
matchPackagePatterns: ["^Microsoft.AspNetCore.SignalR"],
groupName: "SignalR",
description: "Group SignalR to exclude them from the dotnet monorepo preset",
},
{
matchPackagePatterns: ["^Microsoft.Extensions.Configuration"],
groupName: "Microsoft.Extensions.Configuration",
description: "Group Microsoft.Extensions.Configuration to exclude them from the dotnet monorepo preset",
},
{
matchPackagePatterns: ["^Microsoft.Extensions.DependencyInjection"],
groupName: "Microsoft.Extensions.DependencyInjection",
description: "Group Microsoft.Extensions.DependencyInjection to exclude them from the dotnet monorepo preset",
},
{
matchPackageNames: [
"AngleSharp",
"AspNetCore.HealthChecks.AzureServiceBus",
"AspNetCore.HealthChecks.AzureStorage",
"AspNetCore.HealthChecks.Network",
"AspNetCore.HealthChecks.Redis",
"AspNetCore.HealthChecks.SendGrid",
"AspNetCore.HealthChecks.SqlServer",
"AspNetCore.HealthChecks.Uris",
],
description: "Vault owned dependencies",
commitMessagePrefix: "[deps] Vault:",
reviewers: ["team:team-vault-dev"],
},
],
ignoreDeps: ["dotnet-sdk"],
}

View File

@ -17,6 +17,7 @@ on:
- "src/Infrastructure.Dapper/**" # Changes to SQL Server Dapper Repository Layer - "src/Infrastructure.Dapper/**" # Changes to SQL Server Dapper Repository Layer
- "src/Infrastructure.EntityFramework/**" # Changes to Entity Framework Repository Layer - "src/Infrastructure.EntityFramework/**" # Changes to Entity Framework Repository Layer
- "test/Infrastructure.IntegrationTest/**" # Any changes to the tests - "test/Infrastructure.IntegrationTest/**" # Any changes to the tests
- "src/**/Entities/**/*.cs" # Database entity definitions
pull_request: pull_request:
paths: paths:
- ".github/workflows/test-database.yml" # This file - ".github/workflows/test-database.yml" # This file
@ -28,6 +29,7 @@ on:
- "src/Infrastructure.Dapper/**" # Changes to SQL Server Dapper Repository Layer - "src/Infrastructure.Dapper/**" # Changes to SQL Server Dapper Repository Layer
- "src/Infrastructure.EntityFramework/**" # Changes to Entity Framework Repository Layer - "src/Infrastructure.EntityFramework/**" # Changes to Entity Framework Repository Layer
- "test/Infrastructure.IntegrationTest/**" # Any changes to the tests - "test/Infrastructure.IntegrationTest/**" # Any changes to the tests
- "src/**/Entities/**/*.cs" # Database entity definitions
jobs: jobs:
check-test-secrets: check-test-secrets:
@ -144,7 +146,7 @@ jobs:
# Unified MariaDB # Unified MariaDB
BW_TEST_DATABASES__4__TYPE: "MySql" BW_TEST_DATABASES__4__TYPE: "MySql"
BW_TEST_DATABASES__4__CONNECTIONSTRING: "server=localhost;port=4306;uid=root;pwd=mariadb-password;database=vault_dev;Allow User Variables=true" BW_TEST_DATABASES__4__CONNECTIONSTRING: "server=localhost;port=4306;uid=root;pwd=mariadb-password;database=vault_dev;Allow User Variables=true"
run: dotnet test --logger "trx;LogFileName=infrastructure-test-results.trx" run: dotnet test --logger "trx;LogFileName=infrastructure-test-results.trx" /p:CoverletOutputFormatter="cobertura" --collect:"XPlat Code Coverage"
shell: pwsh shell: pwsh
- name: Print MySQL Logs - name: Print MySQL Logs
@ -172,6 +174,9 @@ jobs:
reporter: dotnet-trx reporter: dotnet-trx
fail-on-error: true fail-on-error: true
- name: Upload to codecov.io
uses: codecov/codecov-action@1e68e06f1dbfde0e4cefc87efeba9e4643565303 # v5.1.2
- name: Docker Compose down - name: Docker Compose down
if: always() if: always()
working-directory: "dev" working-directory: "dev"

View File

@ -3,7 +3,7 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<Version>2025.2.0</Version> <Version>2025.2.2</Version>
<RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace> <RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
@ -13,6 +13,7 @@
<Nullable Condition="'$(Nullable)' == '' and '$(IsTestProject)' == 'true'">annotations</Nullable> <Nullable Condition="'$(Nullable)' == '' and '$(IsTestProject)' == 'true'">annotations</Nullable>
<!-- Uncomment the below line when we are ready to enable nullable repo wide --> <!-- Uncomment the below line when we are ready to enable nullable repo wide -->
<!-- <Nullable Condition="'$(Nullable)' == '' and '$(IsTestProject)' != 'true'">enable</Nullable> --> <!-- <Nullable Condition="'$(Nullable)' == '' and '$(IsTestProject)' != 'true'">enable</Nullable> -->
<TreatWarningsAsErrors Condition="'$(TreatWarningsAsErrors)' == ''">true</TreatWarningsAsErrors>
</PropertyGroup> </PropertyGroup>
<!-- <!--

View File

@ -5,12 +5,12 @@ using Bit.Core.AdminConsole.Providers.Interfaces;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Constants; using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Extensions;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Services; using Bit.Core.Billing.Services;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Utilities;
using Stripe; using Stripe;
namespace Bit.Commercial.Core.AdminConsole.Providers; namespace Bit.Commercial.Core.AdminConsole.Providers;
@ -27,6 +27,7 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
private readonly IProviderBillingService _providerBillingService; private readonly IProviderBillingService _providerBillingService;
private readonly ISubscriberService _subscriberService; private readonly ISubscriberService _subscriberService;
private readonly IHasConfirmedOwnersExceptQuery _hasConfirmedOwnersExceptQuery; private readonly IHasConfirmedOwnersExceptQuery _hasConfirmedOwnersExceptQuery;
private readonly IPricingClient _pricingClient;
public RemoveOrganizationFromProviderCommand( public RemoveOrganizationFromProviderCommand(
IEventService eventService, IEventService eventService,
@ -38,7 +39,8 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
IFeatureService featureService, IFeatureService featureService,
IProviderBillingService providerBillingService, IProviderBillingService providerBillingService,
ISubscriberService subscriberService, ISubscriberService subscriberService,
IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery) IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery,
IPricingClient pricingClient)
{ {
_eventService = eventService; _eventService = eventService;
_mailService = mailService; _mailService = mailService;
@ -50,6 +52,7 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
_providerBillingService = providerBillingService; _providerBillingService = providerBillingService;
_subscriberService = subscriberService; _subscriberService = subscriberService;
_hasConfirmedOwnersExceptQuery = hasConfirmedOwnersExceptQuery; _hasConfirmedOwnersExceptQuery = hasConfirmedOwnersExceptQuery;
_pricingClient = pricingClient;
} }
public async Task RemoveOrganizationFromProvider( public async Task RemoveOrganizationFromProvider(
@ -110,7 +113,7 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
Email = organization.BillingEmail Email = organization.BillingEmail
}); });
var plan = StaticStore.GetPlan(organization.PlanType).PasswordManager; var plan = await _pricingClient.GetPlanOrThrow(organization.PlanType);
var subscriptionCreateOptions = new SubscriptionCreateOptions var subscriptionCreateOptions = new SubscriptionCreateOptions
{ {
@ -124,7 +127,7 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
}, },
OffSession = true, OffSession = true,
ProrationBehavior = StripeConstants.ProrationBehavior.CreateProrations, ProrationBehavior = StripeConstants.ProrationBehavior.CreateProrations,
Items = [new SubscriptionItemOptions { Price = plan.StripeSeatPlanId, Quantity = organization.Seats }] Items = [new SubscriptionItemOptions { Price = plan.PasswordManager.StripeSeatPlanId, Quantity = organization.Seats }]
}; };
var subscription = await _stripeAdapter.SubscriptionCreateAsync(subscriptionCreateOptions); var subscription = await _stripeAdapter.SubscriptionCreateAsync(subscriptionCreateOptions);

View File

@ -8,6 +8,7 @@ using Bit.Core.AdminConsole.Models.Business.Tokenables;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Services; using Bit.Core.AdminConsole.Services;
using Bit.Core.Billing.Enums; using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Services; using Bit.Core.Billing.Services;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Entities; using Bit.Core.Entities;
@ -50,6 +51,7 @@ public class ProviderService : IProviderService
private readonly IDataProtectorTokenFactory<ProviderDeleteTokenable> _providerDeleteTokenDataFactory; private readonly IDataProtectorTokenFactory<ProviderDeleteTokenable> _providerDeleteTokenDataFactory;
private readonly IApplicationCacheService _applicationCacheService; private readonly IApplicationCacheService _applicationCacheService;
private readonly IProviderBillingService _providerBillingService; private readonly IProviderBillingService _providerBillingService;
private readonly IPricingClient _pricingClient;
public ProviderService(IProviderRepository providerRepository, IProviderUserRepository providerUserRepository, public ProviderService(IProviderRepository providerRepository, IProviderUserRepository providerUserRepository,
IProviderOrganizationRepository providerOrganizationRepository, IUserRepository userRepository, IProviderOrganizationRepository providerOrganizationRepository, IUserRepository userRepository,
@ -58,7 +60,7 @@ public class ProviderService : IProviderService
IOrganizationRepository organizationRepository, GlobalSettings globalSettings, IOrganizationRepository organizationRepository, GlobalSettings globalSettings,
ICurrentContext currentContext, IStripeAdapter stripeAdapter, IFeatureService featureService, ICurrentContext currentContext, IStripeAdapter stripeAdapter, IFeatureService featureService,
IDataProtectorTokenFactory<ProviderDeleteTokenable> providerDeleteTokenDataFactory, IDataProtectorTokenFactory<ProviderDeleteTokenable> providerDeleteTokenDataFactory,
IApplicationCacheService applicationCacheService, IProviderBillingService providerBillingService) IApplicationCacheService applicationCacheService, IProviderBillingService providerBillingService, IPricingClient pricingClient)
{ {
_providerRepository = providerRepository; _providerRepository = providerRepository;
_providerUserRepository = providerUserRepository; _providerUserRepository = providerUserRepository;
@ -77,6 +79,7 @@ public class ProviderService : IProviderService
_providerDeleteTokenDataFactory = providerDeleteTokenDataFactory; _providerDeleteTokenDataFactory = providerDeleteTokenDataFactory;
_applicationCacheService = applicationCacheService; _applicationCacheService = applicationCacheService;
_providerBillingService = providerBillingService; _providerBillingService = providerBillingService;
_pricingClient = pricingClient;
} }
public async Task<Provider> CompleteSetupAsync(Provider provider, Guid ownerUserId, string token, string key, TaxInfo taxInfo = null) public async Task<Provider> CompleteSetupAsync(Provider provider, Guid ownerUserId, string token, string key, TaxInfo taxInfo = null)
@ -452,30 +455,31 @@ public class ProviderService : IProviderService
if (!string.IsNullOrWhiteSpace(organization.GatewaySubscriptionId)) if (!string.IsNullOrWhiteSpace(organization.GatewaySubscriptionId))
{ {
var subscriptionItem = await GetSubscriptionItemAsync(organization.GatewaySubscriptionId, var plan = await _pricingClient.GetPlanOrThrow(organization.PlanType);
GetStripeSeatPlanId(organization.PlanType));
var subscriptionItem = await GetSubscriptionItemAsync(
organization.GatewaySubscriptionId,
plan.PasswordManager.StripeSeatPlanId);
var extractedPlanType = PlanTypeMappings(organization); var extractedPlanType = PlanTypeMappings(organization);
var extractedPlan = await _pricingClient.GetPlanOrThrow(extractedPlanType);
if (subscriptionItem != null) if (subscriptionItem != null)
{ {
await UpdateSubscriptionAsync(subscriptionItem, GetStripeSeatPlanId(extractedPlanType), organization); await UpdateSubscriptionAsync(subscriptionItem, extractedPlan.PasswordManager.StripeSeatPlanId, organization);
} }
} }
await _organizationRepository.UpsertAsync(organization); await _organizationRepository.UpsertAsync(organization);
} }
private async Task<Stripe.SubscriptionItem> GetSubscriptionItemAsync(string subscriptionId, string oldPlanId) private async Task<SubscriptionItem> GetSubscriptionItemAsync(string subscriptionId, string oldPlanId)
{ {
var subscriptionDetails = await _stripeAdapter.SubscriptionGetAsync(subscriptionId); var subscriptionDetails = await _stripeAdapter.SubscriptionGetAsync(subscriptionId);
return subscriptionDetails.Items.Data.FirstOrDefault(item => item.Price.Id == oldPlanId); return subscriptionDetails.Items.Data.FirstOrDefault(item => item.Price.Id == oldPlanId);
} }
private static string GetStripeSeatPlanId(PlanType planType) private async Task UpdateSubscriptionAsync(SubscriptionItem subscriptionItem, string extractedPlanType, Organization organization)
{
return StaticStore.GetPlan(planType).PasswordManager.StripeSeatPlanId;
}
private async Task UpdateSubscriptionAsync(Stripe.SubscriptionItem subscriptionItem, string extractedPlanType, Organization organization)
{ {
try try
{ {

View File

@ -10,6 +10,7 @@ using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Entities; using Bit.Core.Billing.Entities;
using Bit.Core.Billing.Enums; using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Models; using Bit.Core.Billing.Models;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Repositories; using Bit.Core.Billing.Repositories;
using Bit.Core.Billing.Services; using Bit.Core.Billing.Services;
using Bit.Core.Billing.Services.Contracts; using Bit.Core.Billing.Services.Contracts;
@ -32,6 +33,7 @@ public class ProviderBillingService(
ILogger<ProviderBillingService> logger, ILogger<ProviderBillingService> logger,
IOrganizationRepository organizationRepository, IOrganizationRepository organizationRepository,
IPaymentService paymentService, IPaymentService paymentService,
IPricingClient pricingClient,
IProviderInvoiceItemRepository providerInvoiceItemRepository, IProviderInvoiceItemRepository providerInvoiceItemRepository,
IProviderOrganizationRepository providerOrganizationRepository, IProviderOrganizationRepository providerOrganizationRepository,
IProviderPlanRepository providerPlanRepository, IProviderPlanRepository providerPlanRepository,
@ -77,8 +79,7 @@ public class ProviderBillingService(
var managedPlanType = await GetManagedPlanTypeAsync(provider, organization); var managedPlanType = await GetManagedPlanTypeAsync(provider, organization);
// TODO: Replace with PricingClient var plan = await pricingClient.GetPlanOrThrow(managedPlanType);
var plan = StaticStore.GetPlan(managedPlanType);
organization.Plan = plan.Name; organization.Plan = plan.Name;
organization.PlanType = plan.Type; organization.PlanType = plan.Type;
organization.MaxCollections = plan.PasswordManager.MaxCollections; organization.MaxCollections = plan.PasswordManager.MaxCollections;
@ -111,12 +112,30 @@ public class ProviderBillingService(
Key = key Key = key
}; };
/*
* 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.
*/
await ScaleSeats(provider, organization.PlanType, organization.Seats!.Value);
await Task.WhenAll( await Task.WhenAll(
organizationRepository.ReplaceAsync(organization), organizationRepository.ReplaceAsync(organization),
providerOrganizationRepository.CreateAsync(providerOrganization), providerOrganizationRepository.CreateAsync(providerOrganization)
ScaleSeats(provider, organization.PlanType, organization.Seats!.Value)
); );
var clientCustomer = await subscriberService.GetCustomer(organization);
if (clientCustomer.Balance != 0)
{
await stripeAdapter.CustomerBalanceTransactionCreate(provider.GatewayCustomerId,
new CustomerBalanceTransactionCreateOptions
{
Amount = clientCustomer.Balance,
Currency = "USD",
Description = $"Unused, prorated time for client organization with ID {organization.Id}."
});
}
await eventService.LogProviderOrganizationEventAsync( await eventService.LogProviderOrganizationEventAsync(
providerOrganization, providerOrganization,
EventType.ProviderOrganization_Added); EventType.ProviderOrganization_Added);
@ -136,7 +155,8 @@ public class ProviderBillingService(
return; return;
} }
var oldPlanConfiguration = StaticStore.GetPlan(plan.PlanType); var oldPlanConfiguration = await pricingClient.GetPlanOrThrow(plan.PlanType);
var newPlanConfiguration = await pricingClient.GetPlanOrThrow(command.NewPlan);
plan.PlanType = command.NewPlan; plan.PlanType = command.NewPlan;
await providerPlanRepository.ReplaceAsync(plan); await providerPlanRepository.ReplaceAsync(plan);
@ -160,7 +180,7 @@ public class ProviderBillingService(
[ [
new SubscriptionItemOptions new SubscriptionItemOptions
{ {
Price = StaticStore.GetPlan(command.NewPlan).PasswordManager.StripeProviderPortalSeatPlanId, Price = newPlanConfiguration.PasswordManager.StripeProviderPortalSeatPlanId,
Quantity = oldSubscriptionItem!.Quantity Quantity = oldSubscriptionItem!.Quantity
}, },
new SubscriptionItemOptions new SubscriptionItemOptions
@ -186,7 +206,7 @@ public class ProviderBillingService(
throw new ConflictException($"Organization '{providerOrganization.Id}' not found."); throw new ConflictException($"Organization '{providerOrganization.Id}' not found.");
} }
organization.PlanType = command.NewPlan; organization.PlanType = command.NewPlan;
organization.Plan = StaticStore.GetPlan(command.NewPlan).Name; organization.Plan = newPlanConfiguration.Name;
await organizationRepository.ReplaceAsync(organization); await organizationRepository.ReplaceAsync(organization);
} }
} }
@ -329,7 +349,7 @@ public class ProviderBillingService(
{ {
var (organization, _) = pair; var (organization, _) = pair;
var planName = DerivePlanName(provider, organization); var planName = await DerivePlanName(provider, organization);
var addable = new AddableOrganization( var addable = new AddableOrganization(
organization.Id, organization.Id,
@ -350,7 +370,7 @@ public class ProviderBillingService(
return addable with { Disabled = requiresPurchase }; return addable with { Disabled = requiresPurchase };
})); }));
string DerivePlanName(Provider localProvider, Organization localOrganization) async Task<string> DerivePlanName(Provider localProvider, Organization localOrganization)
{ {
if (localProvider.Type == ProviderType.Msp) if (localProvider.Type == ProviderType.Msp)
{ {
@ -362,8 +382,7 @@ public class ProviderBillingService(
}; };
} }
// TODO: Replace with PricingClient var plan = await pricingClient.GetPlanOrThrow(localOrganization.PlanType);
var plan = StaticStore.GetPlan(localOrganization.PlanType);
return plan.Name; return plan.Name;
} }
} }
@ -550,7 +569,7 @@ public class ProviderBillingService(
foreach (var providerPlan in providerPlans) foreach (var providerPlan in providerPlans)
{ {
var plan = StaticStore.GetPlan(providerPlan.PlanType); var plan = await pricingClient.GetPlanOrThrow(providerPlan.PlanType);
if (!providerPlan.IsConfigured()) if (!providerPlan.IsConfigured())
{ {
@ -634,8 +653,10 @@ public class ProviderBillingService(
if (providerPlan.SeatMinimum != newPlanConfiguration.SeatsMinimum) if (providerPlan.SeatMinimum != newPlanConfiguration.SeatsMinimum)
{ {
var priceId = StaticStore.GetPlan(newPlanConfiguration.Plan).PasswordManager var newPlan = await pricingClient.GetPlanOrThrow(newPlanConfiguration.Plan);
.StripeProviderPortalSeatPlanId;
var priceId = newPlan.PasswordManager.StripeProviderPortalSeatPlanId;
var subscriptionItem = subscription.Items.First(item => item.Price.Id == priceId); var subscriptionItem = subscription.Items.First(item => item.Price.Id == priceId);
if (providerPlan.PurchasedSeats == 0) if (providerPlan.PurchasedSeats == 0)
@ -699,7 +720,7 @@ public class ProviderBillingService(
ProviderPlan providerPlan, ProviderPlan providerPlan,
int newlyAssignedSeats) => async (currentlySubscribedSeats, newlySubscribedSeats) => int newlyAssignedSeats) => async (currentlySubscribedSeats, newlySubscribedSeats) =>
{ {
var plan = StaticStore.GetPlan(providerPlan.PlanType); var plan = await pricingClient.GetPlanOrThrow(providerPlan.PlanType);
await paymentService.AdjustSeats( await paymentService.AdjustSeats(
provider, provider,
@ -723,7 +744,7 @@ public class ProviderBillingService(
var providerOrganizations = var providerOrganizations =
await providerOrganizationRepository.GetManyDetailsByProviderAsync(provider.Id); await providerOrganizationRepository.GetManyDetailsByProviderAsync(provider.Id);
var plan = StaticStore.GetPlan(planType); var plan = await pricingClient.GetPlanOrThrow(planType);
return providerOrganizations return providerOrganizations
.Where(providerOrganization => providerOrganization.Plan == plan.Name && providerOrganization.Status == OrganizationStatusType.Managed) .Where(providerOrganization => providerOrganization.Plan == plan.Name && providerOrganization.Status == OrganizationStatusType.Managed)

View File

@ -28,6 +28,7 @@ public class MaxProjectsQuery : IMaxProjectsQuery
throw new NotFoundException(); throw new NotFoundException();
} }
// TODO: PRICING -> https://bitwarden.atlassian.net/browse/PM-17122
var plan = StaticStore.GetPlan(org.PlanType); var plan = StaticStore.GetPlan(org.PlanType);
if (plan?.SecretsManager == null) if (plan?.SecretsManager == null)
{ {

View File

@ -1,8 +1,10 @@
using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Interfaces; using Bit.Core;
using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Interfaces;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Scim.Groups.Interfaces; using Bit.Scim.Groups.Interfaces;
using Bit.Scim.Models; using Bit.Scim.Models;
using Bit.Scim.Utilities; using Bit.Scim.Utilities;
@ -22,9 +24,10 @@ public class GroupsController : Controller
private readonly IGetGroupsListQuery _getGroupsListQuery; private readonly IGetGroupsListQuery _getGroupsListQuery;
private readonly IDeleteGroupCommand _deleteGroupCommand; private readonly IDeleteGroupCommand _deleteGroupCommand;
private readonly IPatchGroupCommand _patchGroupCommand; private readonly IPatchGroupCommand _patchGroupCommand;
private readonly IPatchGroupCommandvNext _patchGroupCommandvNext;
private readonly IPostGroupCommand _postGroupCommand; private readonly IPostGroupCommand _postGroupCommand;
private readonly IPutGroupCommand _putGroupCommand; private readonly IPutGroupCommand _putGroupCommand;
private readonly ILogger<GroupsController> _logger; private readonly IFeatureService _featureService;
public GroupsController( public GroupsController(
IGroupRepository groupRepository, IGroupRepository groupRepository,
@ -32,18 +35,21 @@ public class GroupsController : Controller
IGetGroupsListQuery getGroupsListQuery, IGetGroupsListQuery getGroupsListQuery,
IDeleteGroupCommand deleteGroupCommand, IDeleteGroupCommand deleteGroupCommand,
IPatchGroupCommand patchGroupCommand, IPatchGroupCommand patchGroupCommand,
IPatchGroupCommandvNext patchGroupCommandvNext,
IPostGroupCommand postGroupCommand, IPostGroupCommand postGroupCommand,
IPutGroupCommand putGroupCommand, IPutGroupCommand putGroupCommand,
ILogger<GroupsController> logger) IFeatureService featureService
)
{ {
_groupRepository = groupRepository; _groupRepository = groupRepository;
_organizationRepository = organizationRepository; _organizationRepository = organizationRepository;
_getGroupsListQuery = getGroupsListQuery; _getGroupsListQuery = getGroupsListQuery;
_deleteGroupCommand = deleteGroupCommand; _deleteGroupCommand = deleteGroupCommand;
_patchGroupCommand = patchGroupCommand; _patchGroupCommand = patchGroupCommand;
_patchGroupCommandvNext = patchGroupCommandvNext;
_postGroupCommand = postGroupCommand; _postGroupCommand = postGroupCommand;
_putGroupCommand = putGroupCommand; _putGroupCommand = putGroupCommand;
_logger = logger; _featureService = featureService;
} }
[HttpGet("{id}")] [HttpGet("{id}")]
@ -97,8 +103,21 @@ public class GroupsController : Controller
[HttpPatch("{id}")] [HttpPatch("{id}")]
public async Task<IActionResult> Patch(Guid organizationId, Guid id, [FromBody] ScimPatchModel model) public async Task<IActionResult> Patch(Guid organizationId, Guid id, [FromBody] ScimPatchModel model)
{ {
if (_featureService.IsEnabled(FeatureFlagKeys.ShortcutDuplicatePatchRequests))
{
var group = await _groupRepository.GetByIdAsync(id);
if (group == null || group.OrganizationId != organizationId)
{
throw new NotFoundException("Group not found.");
}
await _patchGroupCommandvNext.PatchGroupAsync(group, model);
return new NoContentResult();
}
var organization = await _organizationRepository.GetByIdAsync(organizationId); var organization = await _organizationRepository.GetByIdAsync(organizationId);
await _patchGroupCommand.PatchGroupAsync(organization, id, model); await _patchGroupCommand.PatchGroupAsync(organization, id, model);
return new NoContentResult(); return new NoContentResult();
} }

View File

@ -0,0 +1,9 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Scim.Models;
namespace Bit.Scim.Groups.Interfaces;
public interface IPatchGroupCommandvNext
{
Task PatchGroupAsync(Group group, ScimPatchModel model);
}

View File

@ -0,0 +1,170 @@
using System.Text.Json;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Interfaces;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Scim.Groups.Interfaces;
using Bit.Scim.Models;
using Bit.Scim.Utilities;
namespace Bit.Scim.Groups;
public class PatchGroupCommandvNext : IPatchGroupCommandvNext
{
private readonly IGroupRepository _groupRepository;
private readonly IGroupService _groupService;
private readonly IUpdateGroupCommand _updateGroupCommand;
private readonly ILogger<PatchGroupCommandvNext> _logger;
private readonly IOrganizationRepository _organizationRepository;
public PatchGroupCommandvNext(
IGroupRepository groupRepository,
IGroupService groupService,
IUpdateGroupCommand updateGroupCommand,
ILogger<PatchGroupCommandvNext> logger,
IOrganizationRepository organizationRepository)
{
_groupRepository = groupRepository;
_groupService = groupService;
_updateGroupCommand = updateGroupCommand;
_logger = logger;
_organizationRepository = organizationRepository;
}
public async Task PatchGroupAsync(Group group, ScimPatchModel model)
{
foreach (var operation in model.Operations)
{
await HandleOperationAsync(group, operation);
}
}
private async Task HandleOperationAsync(Group group, ScimPatchModel.OperationModel operation)
{
switch (operation.Op?.ToLowerInvariant())
{
// Replace a list of members
case PatchOps.Replace when operation.Path?.ToLowerInvariant() == PatchPaths.Members:
{
var ids = GetOperationValueIds(operation.Value);
await _groupRepository.UpdateUsersAsync(group.Id, ids);
break;
}
// Replace group name from path
case PatchOps.Replace when operation.Path?.ToLowerInvariant() == PatchPaths.DisplayName:
{
group.Name = operation.Value.GetString();
var organization = await _organizationRepository.GetByIdAsync(group.OrganizationId);
if (organization == null)
{
throw new NotFoundException();
}
await _updateGroupCommand.UpdateGroupAsync(group, organization, EventSystemUser.SCIM);
break;
}
// Replace group name from value object
case PatchOps.Replace when
string.IsNullOrWhiteSpace(operation.Path) &&
operation.Value.TryGetProperty("displayName", out var displayNameProperty):
{
group.Name = displayNameProperty.GetString();
var organization = await _organizationRepository.GetByIdAsync(group.OrganizationId);
if (organization == null)
{
throw new NotFoundException();
}
await _updateGroupCommand.UpdateGroupAsync(group, organization, EventSystemUser.SCIM);
break;
}
// Add a single member
case PatchOps.Add when
!string.IsNullOrWhiteSpace(operation.Path) &&
operation.Path.StartsWith("members[value eq ", StringComparison.OrdinalIgnoreCase) &&
TryGetOperationPathId(operation.Path, out var addId):
{
await AddMembersAsync(group, [addId]);
break;
}
// Add a list of members
case PatchOps.Add when
operation.Path?.ToLowerInvariant() == PatchPaths.Members:
{
await AddMembersAsync(group, GetOperationValueIds(operation.Value));
break;
}
// Remove a single member
case PatchOps.Remove when
!string.IsNullOrWhiteSpace(operation.Path) &&
operation.Path.StartsWith("members[value eq ", StringComparison.OrdinalIgnoreCase) &&
TryGetOperationPathId(operation.Path, out var removeId):
{
await _groupService.DeleteUserAsync(group, removeId, EventSystemUser.SCIM);
break;
}
// Remove a list of members
case PatchOps.Remove when
operation.Path?.ToLowerInvariant() == PatchPaths.Members:
{
var orgUserIds = (await _groupRepository.GetManyUserIdsByIdAsync(group.Id)).ToHashSet();
foreach (var v in GetOperationValueIds(operation.Value))
{
orgUserIds.Remove(v);
}
await _groupRepository.UpdateUsersAsync(group.Id, orgUserIds);
break;
}
default:
{
_logger.LogWarning("Group patch operation not handled: {OperationOp}:{OperationPath}", operation.Op, operation.Path);
break;
}
}
}
private async Task AddMembersAsync(Group group, HashSet<Guid> usersToAdd)
{
// Azure Entra ID is known to send redundant "add" requests for each existing member every time any member
// is removed. To avoid excessive load on the database, we check against the high availability replica and
// return early if they already exist.
var groupMembers = await _groupRepository.GetManyUserIdsByIdAsync(group.Id, useReadOnlyReplica: true);
if (usersToAdd.IsSubsetOf(groupMembers))
{
_logger.LogDebug("Ignoring duplicate SCIM request to add members {Members} to group {Group}", usersToAdd, group.Id);
return;
}
await _groupRepository.AddGroupUsersByIdAsync(group.Id, usersToAdd);
}
private static HashSet<Guid> GetOperationValueIds(JsonElement objArray)
{
var ids = new HashSet<Guid>();
foreach (var obj in objArray.EnumerateArray())
{
if (obj.TryGetProperty("value", out var valueProperty))
{
if (valueProperty.TryGetGuid(out var guid))
{
ids.Add(guid);
}
}
}
return ids;
}
private static bool TryGetOperationPathId(string path, out Guid pathId)
{
// Parse Guid from string like: members[value eq "{GUID}"}]
return Guid.TryParse(path.Substring(18).Replace("\"]", string.Empty), out pathId);
}
}

View File

@ -1,11 +1,8 @@
using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Interfaces;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Scim.Context;
using Bit.Scim.Groups.Interfaces; using Bit.Scim.Groups.Interfaces;
using Bit.Scim.Models; using Bit.Scim.Models;
@ -14,17 +11,13 @@ namespace Bit.Scim.Groups;
public class PostGroupCommand : IPostGroupCommand public class PostGroupCommand : IPostGroupCommand
{ {
private readonly IGroupRepository _groupRepository; private readonly IGroupRepository _groupRepository;
private readonly IScimContext _scimContext;
private readonly ICreateGroupCommand _createGroupCommand; private readonly ICreateGroupCommand _createGroupCommand;
public PostGroupCommand( public PostGroupCommand(
IGroupRepository groupRepository, IGroupRepository groupRepository,
IOrganizationRepository organizationRepository,
IScimContext scimContext,
ICreateGroupCommand createGroupCommand) ICreateGroupCommand createGroupCommand)
{ {
_groupRepository = groupRepository; _groupRepository = groupRepository;
_scimContext = scimContext;
_createGroupCommand = createGroupCommand; _createGroupCommand = createGroupCommand;
} }
@ -50,11 +43,6 @@ public class PostGroupCommand : IPostGroupCommand
private async Task UpdateGroupMembersAsync(Group group, ScimGroupRequestModel model) private async Task UpdateGroupMembersAsync(Group group, ScimGroupRequestModel model)
{ {
if (_scimContext.RequestScimProvider != ScimProviderType.Okta)
{
return;
}
if (model.Members == null) if (model.Members == null)
{ {
return; return;

View File

@ -1,10 +1,8 @@
using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Interfaces;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Scim.Context;
using Bit.Scim.Groups.Interfaces; using Bit.Scim.Groups.Interfaces;
using Bit.Scim.Models; using Bit.Scim.Models;
@ -13,16 +11,13 @@ namespace Bit.Scim.Groups;
public class PutGroupCommand : IPutGroupCommand public class PutGroupCommand : IPutGroupCommand
{ {
private readonly IGroupRepository _groupRepository; private readonly IGroupRepository _groupRepository;
private readonly IScimContext _scimContext;
private readonly IUpdateGroupCommand _updateGroupCommand; private readonly IUpdateGroupCommand _updateGroupCommand;
public PutGroupCommand( public PutGroupCommand(
IGroupRepository groupRepository, IGroupRepository groupRepository,
IScimContext scimContext,
IUpdateGroupCommand updateGroupCommand) IUpdateGroupCommand updateGroupCommand)
{ {
_groupRepository = groupRepository; _groupRepository = groupRepository;
_scimContext = scimContext;
_updateGroupCommand = updateGroupCommand; _updateGroupCommand = updateGroupCommand;
} }
@ -43,12 +38,6 @@ public class PutGroupCommand : IPutGroupCommand
private async Task UpdateGroupMembersAsync(Group group, ScimGroupRequestModel model) private async Task UpdateGroupMembersAsync(Group group, ScimGroupRequestModel model)
{ {
if (_scimContext.RequestScimProvider != ScimProviderType.Okta &&
_scimContext.RequestScimProvider != ScimProviderType.Ping)
{
return;
}
if (model.Members == null) if (model.Members == null)
{ {
return; return;

View File

@ -8,7 +8,7 @@ using Bit.Core.Utilities;
using Bit.Scim.Context; using Bit.Scim.Context;
using Bit.Scim.Utilities; using Bit.Scim.Utilities;
using Bit.SharedWeb.Utilities; using Bit.SharedWeb.Utilities;
using IdentityModel; using Duende.IdentityModel;
using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.DependencyInjection.Extensions;
using Stripe; using Stripe;

View File

@ -3,7 +3,7 @@ using System.Text.Encodings.Web;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Scim.Context; using Bit.Scim.Context;
using IdentityModel; using Duende.IdentityModel;
using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;

View File

@ -7,3 +7,16 @@ public static class ScimConstants
public const string Scim2SchemaUser = "urn:ietf:params:scim:schemas:core:2.0:User"; public const string Scim2SchemaUser = "urn:ietf:params:scim:schemas:core:2.0:User";
public const string Scim2SchemaGroup = "urn:ietf:params:scim:schemas:core:2.0:Group"; public const string Scim2SchemaGroup = "urn:ietf:params:scim:schemas:core:2.0:Group";
} }
public static class PatchOps
{
public const string Replace = "replace";
public const string Add = "add";
public const string Remove = "remove";
}
public static class PatchPaths
{
public const string Members = "members";
public const string DisplayName = "displayname";
}

View File

@ -10,6 +10,7 @@ public static class ScimServiceCollectionExtensions
public static void AddScimGroupCommands(this IServiceCollection services) public static void AddScimGroupCommands(this IServiceCollection services)
{ {
services.AddScoped<IPatchGroupCommand, PatchGroupCommand>(); services.AddScoped<IPatchGroupCommand, PatchGroupCommand>();
services.AddScoped<IPatchGroupCommandvNext, PatchGroupCommandvNext>();
services.AddScoped<IPostGroupCommand, PostGroupCommand>(); services.AddScoped<IPostGroupCommand, PostGroupCommand>();
services.AddScoped<IPutGroupCommand, PutGroupCommand>(); services.AddScoped<IPutGroupCommand, PutGroupCommand>();
} }

View File

@ -19,10 +19,10 @@ using Bit.Core.Tokens;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using Bit.Sso.Models; using Bit.Sso.Models;
using Bit.Sso.Utilities; using Bit.Sso.Utilities;
using Duende.IdentityModel;
using Duende.IdentityServer; using Duende.IdentityServer;
using Duende.IdentityServer.Services; using Duende.IdentityServer.Services;
using Duende.IdentityServer.Stores; using Duende.IdentityServer.Stores;
using IdentityModel;
using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;

View File

@ -7,9 +7,9 @@ using Bit.Core.Settings;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using Bit.Sso.Models; using Bit.Sso.Models;
using Bit.Sso.Utilities; using Bit.Sso.Utilities;
using Duende.IdentityModel;
using Duende.IdentityServer; using Duende.IdentityServer;
using Duende.IdentityServer.Infrastructure; using Duende.IdentityServer.Infrastructure;
using IdentityModel;
using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.OpenIdConnect; using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;

View File

@ -17,7 +17,7 @@
"css-loader": "7.1.2", "css-loader": "7.1.2",
"expose-loader": "5.0.0", "expose-loader": "5.0.0",
"mini-css-extract-plugin": "2.9.2", "mini-css-extract-plugin": "2.9.2",
"sass": "1.79.5", "sass": "1.85.0",
"sass-loader": "16.0.4", "sass-loader": "16.0.4",
"webpack": "5.97.1", "webpack": "5.97.1",
"webpack-cli": "5.1.4" "webpack-cli": "5.1.4"
@ -98,12 +98,13 @@
} }
}, },
"node_modules/@parcel/watcher": { "node_modules/@parcel/watcher": {
"version": "2.5.0", "version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.0.tgz", "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz",
"integrity": "sha512-i0GV1yJnm2n3Yq1qw6QrUrd/LI9bE8WEBOTtOkpCXHHdyN3TAGgqAK/DAT05z4fq2x04cARXt2pDmjWjL92iTQ==", "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==",
"dev": true, "dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
"optional": true,
"dependencies": { "dependencies": {
"detect-libc": "^1.0.3", "detect-libc": "^1.0.3",
"is-glob": "^4.0.3", "is-glob": "^4.0.3",
@ -118,25 +119,25 @@
"url": "https://opencollective.com/parcel" "url": "https://opencollective.com/parcel"
}, },
"optionalDependencies": { "optionalDependencies": {
"@parcel/watcher-android-arm64": "2.5.0", "@parcel/watcher-android-arm64": "2.5.1",
"@parcel/watcher-darwin-arm64": "2.5.0", "@parcel/watcher-darwin-arm64": "2.5.1",
"@parcel/watcher-darwin-x64": "2.5.0", "@parcel/watcher-darwin-x64": "2.5.1",
"@parcel/watcher-freebsd-x64": "2.5.0", "@parcel/watcher-freebsd-x64": "2.5.1",
"@parcel/watcher-linux-arm-glibc": "2.5.0", "@parcel/watcher-linux-arm-glibc": "2.5.1",
"@parcel/watcher-linux-arm-musl": "2.5.0", "@parcel/watcher-linux-arm-musl": "2.5.1",
"@parcel/watcher-linux-arm64-glibc": "2.5.0", "@parcel/watcher-linux-arm64-glibc": "2.5.1",
"@parcel/watcher-linux-arm64-musl": "2.5.0", "@parcel/watcher-linux-arm64-musl": "2.5.1",
"@parcel/watcher-linux-x64-glibc": "2.5.0", "@parcel/watcher-linux-x64-glibc": "2.5.1",
"@parcel/watcher-linux-x64-musl": "2.5.0", "@parcel/watcher-linux-x64-musl": "2.5.1",
"@parcel/watcher-win32-arm64": "2.5.0", "@parcel/watcher-win32-arm64": "2.5.1",
"@parcel/watcher-win32-ia32": "2.5.0", "@parcel/watcher-win32-ia32": "2.5.1",
"@parcel/watcher-win32-x64": "2.5.0" "@parcel/watcher-win32-x64": "2.5.1"
} }
}, },
"node_modules/@parcel/watcher-android-arm64": { "node_modules/@parcel/watcher-android-arm64": {
"version": "2.5.0", "version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.0.tgz", "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz",
"integrity": "sha512-qlX4eS28bUcQCdribHkg/herLe+0A9RyYC+mm2PXpncit8z5b3nSqGVzMNR3CmtAOgRutiZ02eIJJgP/b1iEFQ==", "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -155,9 +156,9 @@
} }
}, },
"node_modules/@parcel/watcher-darwin-arm64": { "node_modules/@parcel/watcher-darwin-arm64": {
"version": "2.5.0", "version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.0.tgz", "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz",
"integrity": "sha512-hyZ3TANnzGfLpRA2s/4U1kbw2ZI4qGxaRJbBH2DCSREFfubMswheh8TeiC1sGZ3z2jUf3s37P0BBlrD3sjVTUw==", "integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -176,9 +177,9 @@
} }
}, },
"node_modules/@parcel/watcher-darwin-x64": { "node_modules/@parcel/watcher-darwin-x64": {
"version": "2.5.0", "version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.0.tgz", "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz",
"integrity": "sha512-9rhlwd78saKf18fT869/poydQK8YqlU26TMiNg7AIu7eBp9adqbJZqmdFOsbZ5cnLp5XvRo9wcFmNHgHdWaGYA==", "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -197,9 +198,9 @@
} }
}, },
"node_modules/@parcel/watcher-freebsd-x64": { "node_modules/@parcel/watcher-freebsd-x64": {
"version": "2.5.0", "version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.0.tgz", "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz",
"integrity": "sha512-syvfhZzyM8kErg3VF0xpV8dixJ+RzbUaaGaeb7uDuz0D3FK97/mZ5AJQ3XNnDsXX7KkFNtyQyFrXZzQIcN49Tw==", "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -218,9 +219,9 @@
} }
}, },
"node_modules/@parcel/watcher-linux-arm-glibc": { "node_modules/@parcel/watcher-linux-arm-glibc": {
"version": "2.5.0", "version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.0.tgz", "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz",
"integrity": "sha512-0VQY1K35DQET3dVYWpOaPFecqOT9dbuCfzjxoQyif1Wc574t3kOSkKevULddcR9znz1TcklCE7Ht6NIxjvTqLA==", "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@ -239,9 +240,9 @@
} }
}, },
"node_modules/@parcel/watcher-linux-arm-musl": { "node_modules/@parcel/watcher-linux-arm-musl": {
"version": "2.5.0", "version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.0.tgz", "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz",
"integrity": "sha512-6uHywSIzz8+vi2lAzFeltnYbdHsDm3iIB57d4g5oaB9vKwjb6N6dRIgZMujw4nm5r6v9/BQH0noq6DzHrqr2pA==", "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@ -260,9 +261,9 @@
} }
}, },
"node_modules/@parcel/watcher-linux-arm64-glibc": { "node_modules/@parcel/watcher-linux-arm64-glibc": {
"version": "2.5.0", "version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.0.tgz", "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz",
"integrity": "sha512-BfNjXwZKxBy4WibDb/LDCriWSKLz+jJRL3cM/DllnHH5QUyoiUNEp3GmL80ZqxeumoADfCCP19+qiYiC8gUBjA==", "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -281,9 +282,9 @@
} }
}, },
"node_modules/@parcel/watcher-linux-arm64-musl": { "node_modules/@parcel/watcher-linux-arm64-musl": {
"version": "2.5.0", "version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.0.tgz", "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz",
"integrity": "sha512-S1qARKOphxfiBEkwLUbHjCY9BWPdWnW9j7f7Hb2jPplu8UZ3nes7zpPOW9bkLbHRvWM0WDTsjdOTUgW0xLBN1Q==", "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -302,9 +303,9 @@
} }
}, },
"node_modules/@parcel/watcher-linux-x64-glibc": { "node_modules/@parcel/watcher-linux-x64-glibc": {
"version": "2.5.0", "version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.0.tgz", "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz",
"integrity": "sha512-d9AOkusyXARkFD66S6zlGXyzx5RvY+chTP9Jp0ypSTC9d4lzyRs9ovGf/80VCxjKddcUvnsGwCHWuF2EoPgWjw==", "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -323,9 +324,9 @@
} }
}, },
"node_modules/@parcel/watcher-linux-x64-musl": { "node_modules/@parcel/watcher-linux-x64-musl": {
"version": "2.5.0", "version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.0.tgz", "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz",
"integrity": "sha512-iqOC+GoTDoFyk/VYSFHwjHhYrk8bljW6zOhPuhi5t9ulqiYq1togGJB5e3PwYVFFfeVgc6pbz3JdQyDoBszVaA==", "integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -344,9 +345,9 @@
} }
}, },
"node_modules/@parcel/watcher-win32-arm64": { "node_modules/@parcel/watcher-win32-arm64": {
"version": "2.5.0", "version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.0.tgz", "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz",
"integrity": "sha512-twtft1d+JRNkM5YbmexfcH/N4znDtjgysFaV9zvZmmJezQsKpkfLYJ+JFV3uygugK6AtIM2oADPkB2AdhBrNig==", "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -365,9 +366,9 @@
} }
}, },
"node_modules/@parcel/watcher-win32-ia32": { "node_modules/@parcel/watcher-win32-ia32": {
"version": "2.5.0", "version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.0.tgz", "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz",
"integrity": "sha512-+rgpsNRKwo8A53elqbbHXdOMtY/tAtTzManTWShB5Kk54N8Q9mzNWV7tV+IbGueCbcj826MfWGU3mprWtuf1TA==", "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@ -386,9 +387,9 @@
} }
}, },
"node_modules/@parcel/watcher-win32-x64": { "node_modules/@parcel/watcher-win32-x64": {
"version": "2.5.0", "version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.0.tgz", "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz",
"integrity": "sha512-lPrxve92zEHdgeff3aiu4gDOIt4u7sJYha6wbdEZDCDUhtjTsOMiaJzG5lMY4GkWH8p0fMmO2Ppq5G5XXG+DQw==", "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -454,9 +455,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "22.10.2", "version": "22.13.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.5.tgz",
"integrity": "sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==", "integrity": "sha512-+lTU0PxZXn0Dr1NBtC7Y8cR21AJr87dLLU953CWA6pMxxv/UDc7jYAY90upcrie1nRcD6XNG5HOYEDtgW5TxAg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -771,6 +772,7 @@
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true,
"dependencies": { "dependencies": {
"fill-range": "^7.1.1" "fill-range": "^7.1.1"
}, },
@ -779,9 +781,9 @@
} }
}, },
"node_modules/browserslist": { "node_modules/browserslist": {
"version": "4.24.3", "version": "4.24.4",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.3.tgz", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz",
"integrity": "sha512-1CPmv8iobE2fyRMV97dAcMVegvvWKxmq94hkLiAkUGwKVTyDLw33K+ZxiFrREKmmps4rIw6grcCFCnTMSZ/YiA==", "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@ -819,9 +821,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/caniuse-lite": { "node_modules/caniuse-lite": {
"version": "1.0.30001690", "version": "1.0.30001700",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001690.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001700.tgz",
"integrity": "sha512-5ExiE3qQN6oF8Clf8ifIDcMRCRE/dMGcETG/XGMD8/XiXm6HXQgQTh1yZYLXXpSOsEUlJm1Xr7kGULZTuGtP/w==", "integrity": "sha512-2S6XIXwaE7K7erT8dY+kLQcpa5ms63XlRkMkReXjle+kf6c5g38vyMl+Z5y8dSxOFDhcFe+nxnn261PLxBSQsQ==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@ -964,6 +966,7 @@
"integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"optional": true,
"bin": { "bin": {
"detect-libc": "bin/detect-libc.js" "detect-libc": "bin/detect-libc.js"
}, },
@ -972,16 +975,16 @@
} }
}, },
"node_modules/electron-to-chromium": { "node_modules/electron-to-chromium": {
"version": "1.5.75", "version": "1.5.103",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.75.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.103.tgz",
"integrity": "sha512-Lf3++DumRE/QmweGjU+ZcKqQ+3bKkU/qjaKYhIJKEOhgIO9Xs6IiAQFkfFoj+RhgDk4LUeNsLo6plExHqSyu6Q==", "integrity": "sha512-P6+XzIkfndgsrjROJWfSvVEgNHtPgbhVyTkwLjUM2HU/h7pZRORgaTlHqfAikqxKmdJMLW8fftrdGWbd/Ds0FA==",
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/enhanced-resolve": { "node_modules/enhanced-resolve": {
"version": "5.18.0", "version": "5.18.1",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.0.tgz", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz",
"integrity": "sha512-0/r0MySGYG8YqlayBZ6MuCfECmHFdJ5qyPh8s8wa5Hnm6SaFLSK1VYCbj+NKp090Nm1caZhD+QTnmxO7esYGyQ==", "integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -1006,9 +1009,9 @@
} }
}, },
"node_modules/es-module-lexer": { "node_modules/es-module-lexer": {
"version": "1.5.4", "version": "1.6.0",
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.6.0.tgz",
"integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==", "integrity": "sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
@ -1111,10 +1114,20 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/fast-uri": { "node_modules/fast-uri": {
"version": "3.0.3", "version": "3.0.6",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.3.tgz", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz",
"integrity": "sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==", "integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==",
"dev": true, "dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"license": "BSD-3-Clause" "license": "BSD-3-Clause"
}, },
"node_modules/fastest-levenshtein": { "node_modules/fastest-levenshtein": {
@ -1133,6 +1146,7 @@
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true,
"dependencies": { "dependencies": {
"to-regex-range": "^5.0.1" "to-regex-range": "^5.0.1"
}, },
@ -1234,9 +1248,9 @@
} }
}, },
"node_modules/immutable": { "node_modules/immutable": {
"version": "4.3.7", "version": "5.0.3",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.7.tgz", "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.0.3.tgz",
"integrity": "sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==", "integrity": "sha512-P8IdPQHq3lA1xVeBRi5VPqUm5HDgKnx0Ru51wZz5mjxHr5n3RWhjIpOFU7ybkUxfB+5IToy+OLaHYDBIWsv+uw==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
@ -1292,6 +1306,7 @@
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true,
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@ -1302,6 +1317,7 @@
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true,
"dependencies": { "dependencies": {
"is-extglob": "^2.1.1" "is-extglob": "^2.1.1"
}, },
@ -1315,6 +1331,7 @@
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true,
"engines": { "engines": {
"node": ">=0.12.0" "node": ">=0.12.0"
} }
@ -1430,6 +1447,7 @@
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true,
"dependencies": { "dependencies": {
"braces": "^3.0.3", "braces": "^3.0.3",
"picomatch": "^2.3.1" "picomatch": "^2.3.1"
@ -1513,7 +1531,8 @@
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT",
"optional": true
}, },
"node_modules/node-releases": { "node_modules/node-releases": {
"version": "2.0.19", "version": "2.0.19",
@ -1601,6 +1620,7 @@
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true,
"engines": { "engines": {
"node": ">=8.6" "node": ">=8.6"
}, },
@ -1622,9 +1642,9 @@
} }
}, },
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.4.49", "version": "8.5.3",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz",
"integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@ -1642,7 +1662,7 @@
], ],
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"nanoid": "^3.3.7", "nanoid": "^3.3.8",
"picocolors": "^1.1.1", "picocolors": "^1.1.1",
"source-map-js": "^1.2.1" "source-map-js": "^1.2.1"
}, },
@ -1714,9 +1734,9 @@
} }
}, },
"node_modules/postcss-selector-parser": { "node_modules/postcss-selector-parser": {
"version": "7.0.0", "version": "7.1.0",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.0.0.tgz", "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz",
"integrity": "sha512-9RbEr1Y7FFfptd/1eEdntyjMwLeghW1bHX9GWjXo19vx4ytPQhANltvVxDggzJl7mnWM+dX28kb6cyS/4iQjlQ==", "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -1755,13 +1775,13 @@
} }
}, },
"node_modules/readdirp": { "node_modules/readdirp": {
"version": "4.0.2", "version": "4.1.2",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
"integrity": "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==", "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 14.16.0" "node": ">= 14.18.0"
}, },
"funding": { "funding": {
"type": "individual", "type": "individual",
@ -1857,15 +1877,14 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/sass": { "node_modules/sass": {
"version": "1.79.5", "version": "1.85.0",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.79.5.tgz", "resolved": "https://registry.npmjs.org/sass/-/sass-1.85.0.tgz",
"integrity": "sha512-W1h5kp6bdhqFh2tk3DsI771MoEJjvrSY/2ihJRJS4pjIyfJCw0nTsxqhnrUzaLMOJjFchj8rOvraI/YUVjtx5g==", "integrity": "sha512-3ToiC1xZ1Y8aU7+CkgCI/tqyuPXEmYGJXO7H4uqp0xkLXUqp88rQQ4j1HmP37xSJLbCJPaIiv+cT1y+grssrww==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@parcel/watcher": "^2.4.1",
"chokidar": "^4.0.0", "chokidar": "^4.0.0",
"immutable": "^4.0.0", "immutable": "^5.0.2",
"source-map-js": ">=0.6.2 <2.0.0" "source-map-js": ">=0.6.2 <2.0.0"
}, },
"bin": { "bin": {
@ -1873,6 +1892,9 @@
}, },
"engines": { "engines": {
"node": ">=14.0.0" "node": ">=14.0.0"
},
"optionalDependencies": {
"@parcel/watcher": "^2.4.1"
} }
}, },
"node_modules/sass-loader": { "node_modules/sass-loader": {
@ -1937,9 +1959,9 @@
} }
}, },
"node_modules/semver": { "node_modules/semver": {
"version": "7.6.3", "version": "7.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
"integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
"dev": true, "dev": true,
"license": "ISC", "license": "ISC",
"bin": { "bin": {
@ -2066,9 +2088,9 @@
} }
}, },
"node_modules/terser": { "node_modules/terser": {
"version": "5.37.0", "version": "5.39.0",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.37.0.tgz", "resolved": "https://registry.npmjs.org/terser/-/terser-5.39.0.tgz",
"integrity": "sha512-B8wRRkmre4ERucLM/uXx4MOV5cbnOlVAqUst+1+iLKPI0dOgFO28f84ptoQt9HEI537PMzfYa/d+GEPKTRXmYA==", "integrity": "sha512-LBAhFyLho16harJoWMg/nZsQYgTrg5jXOn2nCYjRUcZZEdE3qa2zb8QEDRUGVZBW4rlazf2fxkg8tztybTaqWw==",
"dev": true, "dev": true,
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"dependencies": { "dependencies": {
@ -2125,6 +2147,7 @@
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true,
"dependencies": { "dependencies": {
"is-number": "^7.0.0" "is-number": "^7.0.0"
}, },
@ -2140,9 +2163,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/update-browserslist-db": { "node_modules/update-browserslist-db": {
"version": "1.1.1", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.2.tgz",
"integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", "integrity": "sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@ -2161,7 +2184,7 @@
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"escalade": "^3.2.0", "escalade": "^3.2.0",
"picocolors": "^1.1.0" "picocolors": "^1.1.1"
}, },
"bin": { "bin": {
"update-browserslist-db": "cli.js" "update-browserslist-db": "cli.js"

View File

@ -16,7 +16,7 @@
"css-loader": "7.1.2", "css-loader": "7.1.2",
"expose-loader": "5.0.0", "expose-loader": "5.0.0",
"mini-css-extract-plugin": "2.9.2", "mini-css-extract-plugin": "2.9.2",
"sass": "1.79.5", "sass": "1.85.0",
"sass-loader": "16.0.4", "sass-loader": "16.0.4",
"webpack": "5.97.1", "webpack": "5.97.1",
"webpack-cli": "5.1.4" "webpack-cli": "5.1.4"

View File

@ -6,6 +6,7 @@ using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Constants; using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Enums; using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Services; using Bit.Core.Billing.Services;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
@ -205,6 +206,8 @@ public class RemoveOrganizationFromProviderCommandTests
var teamsMonthlyPlan = StaticStore.GetPlan(PlanType.TeamsMonthly); var teamsMonthlyPlan = StaticStore.GetPlan(PlanType.TeamsMonthly);
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(PlanType.TeamsMonthly).Returns(teamsMonthlyPlan);
sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>().HasConfirmedOwnersExceptAsync( sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>().HasConfirmedOwnersExceptAsync(
providerOrganization.OrganizationId, providerOrganization.OrganizationId,
[], [],

View File

@ -7,6 +7,7 @@ using Bit.Core.AdminConsole.Models.Business.Provider;
using Bit.Core.AdminConsole.Models.Business.Tokenables; using Bit.Core.AdminConsole.Models.Business.Tokenables;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Enums; using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Services; using Bit.Core.Billing.Services;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Entities; using Bit.Core.Entities;
@ -550,8 +551,14 @@ public class ProviderServiceTests
organization.PlanType = PlanType.EnterpriseMonthly; organization.PlanType = PlanType.EnterpriseMonthly;
organization.Plan = "Enterprise (Monthly)"; organization.Plan = "Enterprise (Monthly)";
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType)
.Returns(StaticStore.GetPlan(organization.PlanType));
var expectedPlanType = PlanType.EnterpriseMonthly2020; var expectedPlanType = PlanType.EnterpriseMonthly2020;
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(expectedPlanType)
.Returns(StaticStore.GetPlan(expectedPlanType));
var expectedPlanId = "2020-enterprise-org-seat-monthly"; var expectedPlanId = "2020-enterprise-org-seat-monthly";
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider); sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);

View File

@ -9,6 +9,7 @@ using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Constants; using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Entities; using Bit.Core.Billing.Entities;
using Bit.Core.Billing.Enums; using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Repositories; using Bit.Core.Billing.Repositories;
using Bit.Core.Billing.Services; using Bit.Core.Billing.Services;
using Bit.Core.Billing.Services.Contracts; using Bit.Core.Billing.Services.Contracts;
@ -128,6 +129,9 @@ public class ProviderBillingServiceTests
.GetByIdAsync(Arg.Is<Guid>(p => p == providerPlanId)) .GetByIdAsync(Arg.Is<Guid>(p => p == providerPlanId))
.Returns(existingPlan); .Returns(existingPlan);
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(existingPlan.PlanType)
.Returns(StaticStore.GetPlan(existingPlan.PlanType));
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>(); var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
stripeAdapter.ProviderSubscriptionGetAsync( stripeAdapter.ProviderSubscriptionGetAsync(
Arg.Is(provider.GatewaySubscriptionId), Arg.Is(provider.GatewaySubscriptionId),
@ -156,6 +160,9 @@ public class ProviderBillingServiceTests
var command = var command =
new ChangeProviderPlanCommand(providerPlanId, PlanType.EnterpriseMonthly, provider.GatewaySubscriptionId); new ChangeProviderPlanCommand(providerPlanId, PlanType.EnterpriseMonthly, provider.GatewaySubscriptionId);
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(command.NewPlan)
.Returns(StaticStore.GetPlan(command.NewPlan));
// Act // Act
await sutProvider.Sut.ChangePlan(command); await sutProvider.Sut.ChangePlan(command);
@ -390,6 +397,12 @@ public class ProviderBillingServiceTests
} }
}; };
foreach (var plan in providerPlans)
{
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(plan.PlanType)
.Returns(StaticStore.GetPlan(plan.PlanType));
}
sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id).Returns(providerPlans); sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id).Returns(providerPlans);
// 50 seats currently assigned with a seat minimum of 100 // 50 seats currently assigned with a seat minimum of 100
@ -451,6 +464,12 @@ public class ProviderBillingServiceTests
} }
}; };
foreach (var plan in providerPlans)
{
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(plan.PlanType)
.Returns(StaticStore.GetPlan(plan.PlanType));
}
var providerPlan = providerPlans.First(); var providerPlan = providerPlans.First();
sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id).Returns(providerPlans); sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id).Returns(providerPlans);
@ -515,6 +534,12 @@ public class ProviderBillingServiceTests
} }
}; };
foreach (var plan in providerPlans)
{
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(plan.PlanType)
.Returns(StaticStore.GetPlan(plan.PlanType));
}
var providerPlan = providerPlans.First(); var providerPlan = providerPlans.First();
sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id).Returns(providerPlans); sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id).Returns(providerPlans);
@ -579,6 +604,12 @@ public class ProviderBillingServiceTests
} }
}; };
foreach (var plan in providerPlans)
{
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(plan.PlanType)
.Returns(StaticStore.GetPlan(plan.PlanType));
}
var providerPlan = providerPlans.First(); var providerPlan = providerPlans.First();
sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id).Returns(providerPlans); sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id).Returns(providerPlans);
@ -636,6 +667,8 @@ public class ProviderBillingServiceTests
} }
]); ]);
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(planType).Returns(StaticStore.GetPlan(planType));
sutProvider.GetDependency<IProviderOrganizationRepository>().GetManyDetailsByProviderAsync(provider.Id).Returns( sutProvider.GetDependency<IProviderOrganizationRepository>().GetManyDetailsByProviderAsync(provider.Id).Returns(
[ [
new ProviderOrganizationOrganizationDetails new ProviderOrganizationOrganizationDetails
@ -672,6 +705,8 @@ public class ProviderBillingServiceTests
} }
]); ]);
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(planType).Returns(StaticStore.GetPlan(planType));
sutProvider.GetDependency<IProviderOrganizationRepository>().GetManyDetailsByProviderAsync(provider.Id).Returns( sutProvider.GetDependency<IProviderOrganizationRepository>().GetManyDetailsByProviderAsync(provider.Id).Returns(
[ [
new ProviderOrganizationOrganizationDetails new ProviderOrganizationOrganizationDetails
@ -856,6 +891,9 @@ public class ProviderBillingServiceTests
sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id) sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id)
.Returns(providerPlans); .Returns(providerPlans);
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(PlanType.EnterpriseMonthly)
.Returns(StaticStore.GetPlan(PlanType.EnterpriseMonthly));
await ThrowsBillingExceptionAsync(() => sutProvider.Sut.SetupSubscription(provider)); await ThrowsBillingExceptionAsync(() => sutProvider.Sut.SetupSubscription(provider));
await sutProvider.GetDependency<IStripeAdapter>() await sutProvider.GetDependency<IStripeAdapter>()
@ -881,6 +919,9 @@ public class ProviderBillingServiceTests
sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id) sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id)
.Returns(providerPlans); .Returns(providerPlans);
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(PlanType.TeamsMonthly)
.Returns(StaticStore.GetPlan(PlanType.TeamsMonthly));
await ThrowsBillingExceptionAsync(() => sutProvider.Sut.SetupSubscription(provider)); await ThrowsBillingExceptionAsync(() => sutProvider.Sut.SetupSubscription(provider));
await sutProvider.GetDependency<IStripeAdapter>() await sutProvider.GetDependency<IStripeAdapter>()
@ -923,6 +964,12 @@ public class ProviderBillingServiceTests
} }
}; };
foreach (var plan in providerPlans)
{
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(plan.PlanType)
.Returns(StaticStore.GetPlan(plan.PlanType));
}
sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id) sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id)
.Returns(providerPlans); .Returns(providerPlans);
@ -968,6 +1015,12 @@ public class ProviderBillingServiceTests
} }
}; };
foreach (var plan in providerPlans)
{
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(plan.PlanType)
.Returns(StaticStore.GetPlan(plan.PlanType));
}
sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id) sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id)
.Returns(providerPlans); .Returns(providerPlans);
@ -1066,6 +1119,12 @@ public class ProviderBillingServiceTests
new() { PlanType = PlanType.TeamsMonthly, SeatMinimum = 30, PurchasedSeats = 0, AllocatedSeats = 25 } new() { PlanType = PlanType.TeamsMonthly, SeatMinimum = 30, PurchasedSeats = 0, AllocatedSeats = 25 }
}; };
foreach (var plan in providerPlans)
{
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(plan.PlanType)
.Returns(StaticStore.GetPlan(plan.PlanType));
}
providerPlanRepository.GetByProviderId(provider.Id).Returns(providerPlans); providerPlanRepository.GetByProviderId(provider.Id).Returns(providerPlans);
var command = new UpdateProviderSeatMinimumsCommand( var command = new UpdateProviderSeatMinimumsCommand(
@ -1139,6 +1198,12 @@ public class ProviderBillingServiceTests
new() { PlanType = PlanType.TeamsMonthly, SeatMinimum = 30, PurchasedSeats = 0, AllocatedSeats = 15 } new() { PlanType = PlanType.TeamsMonthly, SeatMinimum = 30, PurchasedSeats = 0, AllocatedSeats = 15 }
}; };
foreach (var plan in providerPlans)
{
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(plan.PlanType)
.Returns(StaticStore.GetPlan(plan.PlanType));
}
providerPlanRepository.GetByProviderId(provider.Id).Returns(providerPlans); providerPlanRepository.GetByProviderId(provider.Id).Returns(providerPlans);
var command = new UpdateProviderSeatMinimumsCommand( var command = new UpdateProviderSeatMinimumsCommand(
@ -1212,6 +1277,12 @@ public class ProviderBillingServiceTests
new() { PlanType = PlanType.TeamsMonthly, SeatMinimum = 50, PurchasedSeats = 20 } new() { PlanType = PlanType.TeamsMonthly, SeatMinimum = 50, PurchasedSeats = 20 }
}; };
foreach (var plan in providerPlans)
{
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(plan.PlanType)
.Returns(StaticStore.GetPlan(plan.PlanType));
}
providerPlanRepository.GetByProviderId(provider.Id).Returns(providerPlans); providerPlanRepository.GetByProviderId(provider.Id).Returns(providerPlans);
var command = new UpdateProviderSeatMinimumsCommand( var command = new UpdateProviderSeatMinimumsCommand(
@ -1279,6 +1350,12 @@ public class ProviderBillingServiceTests
new() { PlanType = PlanType.TeamsMonthly, SeatMinimum = 50, PurchasedSeats = 20 } new() { PlanType = PlanType.TeamsMonthly, SeatMinimum = 50, PurchasedSeats = 20 }
}; };
foreach (var plan in providerPlans)
{
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(plan.PlanType)
.Returns(StaticStore.GetPlan(plan.PlanType));
}
providerPlanRepository.GetByProviderId(provider.Id).Returns(providerPlans); providerPlanRepository.GetByProviderId(provider.Id).Returns(providerPlans);
var command = new UpdateProviderSeatMinimumsCommand( var command = new UpdateProviderSeatMinimumsCommand(
@ -1352,6 +1429,12 @@ public class ProviderBillingServiceTests
new() { PlanType = PlanType.TeamsMonthly, SeatMinimum = 30, PurchasedSeats = 0 } new() { PlanType = PlanType.TeamsMonthly, SeatMinimum = 30, PurchasedSeats = 0 }
}; };
foreach (var plan in providerPlans)
{
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(plan.PlanType)
.Returns(StaticStore.GetPlan(plan.PlanType));
}
providerPlanRepository.GetByProviderId(provider.Id).Returns(providerPlans); providerPlanRepository.GetByProviderId(provider.Id).Returns(providerPlans);
var command = new UpdateProviderSeatMinimumsCommand( var command = new UpdateProviderSeatMinimumsCommand(

View File

@ -0,0 +1,237 @@
using System.Text.Json;
using Bit.Scim.IntegrationTest.Factories;
using Bit.Scim.Models;
using Bit.Scim.Utilities;
using Bit.Test.Common.Helpers;
using Xunit;
namespace Bit.Scim.IntegrationTest.Controllers.v2;
public class GroupsControllerPatchTests : IClassFixture<ScimApplicationFactory>, IAsyncLifetime
{
private readonly ScimApplicationFactory _factory;
public GroupsControllerPatchTests(ScimApplicationFactory factory)
{
_factory = factory;
}
public Task InitializeAsync()
{
var databaseContext = _factory.GetDatabaseContext();
_factory.ReinitializeDbForTests(databaseContext);
return Task.CompletedTask;
}
Task IAsyncLifetime.DisposeAsync() => Task.CompletedTask;
[Fact]
public async Task Patch_ReplaceDisplayName_Success()
{
var organizationId = ScimApplicationFactory.TestOrganizationId1;
var groupId = ScimApplicationFactory.TestGroupId1;
var newDisplayName = "Patch Display Name";
var inputModel = new ScimPatchModel
{
Operations = new List<ScimPatchModel.OperationModel>()
{
new ScimPatchModel.OperationModel
{
Op = "replace",
Value = JsonDocument.Parse($"{{\"displayName\":\"{newDisplayName}\"}}").RootElement
}
},
Schemas = new List<string>() { ScimConstants.Scim2SchemaGroup }
};
var context = await _factory.GroupsPatchAsync(organizationId, groupId, inputModel);
Assert.Equal(StatusCodes.Status204NoContent, context.Response.StatusCode);
var databaseContext = _factory.GetDatabaseContext();
var group = databaseContext.Groups.FirstOrDefault(g => g.Id == groupId);
Assert.Equal(newDisplayName, group.Name);
Assert.Equal(ScimApplicationFactory.InitialGroupUsersCount, databaseContext.GroupUsers.Count());
Assert.True(databaseContext.GroupUsers.Any(gu => gu.OrganizationUserId == ScimApplicationFactory.TestOrganizationUserId1));
Assert.True(databaseContext.GroupUsers.Any(gu => gu.OrganizationUserId == ScimApplicationFactory.TestOrganizationUserId4));
}
[Fact]
public async Task Patch_ReplaceMembers_Success()
{
var organizationId = ScimApplicationFactory.TestOrganizationId1;
var groupId = ScimApplicationFactory.TestGroupId1;
var inputModel = new ScimPatchModel
{
Operations = new List<ScimPatchModel.OperationModel>()
{
new ScimPatchModel.OperationModel
{
Op = "replace",
Path = "members",
Value = JsonDocument.Parse($"[{{\"value\":\"{ScimApplicationFactory.TestOrganizationUserId2}\"}}]").RootElement
}
},
Schemas = new List<string>() { ScimConstants.Scim2SchemaGroup }
};
var context = await _factory.GroupsPatchAsync(organizationId, groupId, inputModel);
Assert.Equal(StatusCodes.Status204NoContent, context.Response.StatusCode);
var databaseContext = _factory.GetDatabaseContext();
Assert.Single(databaseContext.GroupUsers);
Assert.Equal(ScimApplicationFactory.InitialGroupUsersCount - 1, databaseContext.GroupUsers.Count());
var groupUser = databaseContext.GroupUsers.FirstOrDefault();
Assert.Equal(ScimApplicationFactory.TestOrganizationUserId2, groupUser.OrganizationUserId);
}
[Fact]
public async Task Patch_AddSingleMember_Success()
{
var organizationId = ScimApplicationFactory.TestOrganizationId1;
var groupId = ScimApplicationFactory.TestGroupId1;
var inputModel = new ScimPatchModel
{
Operations = new List<ScimPatchModel.OperationModel>()
{
new ScimPatchModel.OperationModel
{
Op = "add",
Path = $"members[value eq \"{ScimApplicationFactory.TestOrganizationUserId2}\"]",
Value = JsonDocument.Parse("{}").RootElement
}
},
Schemas = new List<string>() { ScimConstants.Scim2SchemaGroup }
};
var context = await _factory.GroupsPatchAsync(organizationId, groupId, inputModel);
Assert.Equal(StatusCodes.Status204NoContent, context.Response.StatusCode);
var databaseContext = _factory.GetDatabaseContext();
Assert.Equal(ScimApplicationFactory.InitialGroupUsersCount + 1, databaseContext.GroupUsers.Count());
Assert.True(databaseContext.GroupUsers.Any(gu => gu.GroupId == groupId && gu.OrganizationUserId == ScimApplicationFactory.TestOrganizationUserId1));
Assert.True(databaseContext.GroupUsers.Any(gu => gu.GroupId == groupId && gu.OrganizationUserId == ScimApplicationFactory.TestOrganizationUserId2));
Assert.True(databaseContext.GroupUsers.Any(gu => gu.GroupId == groupId && gu.OrganizationUserId == ScimApplicationFactory.TestOrganizationUserId4));
}
[Fact]
public async Task Patch_AddListMembers_Success()
{
var organizationId = ScimApplicationFactory.TestOrganizationId1;
var groupId = ScimApplicationFactory.TestGroupId2;
var inputModel = new ScimPatchModel
{
Operations = new List<ScimPatchModel.OperationModel>()
{
new ScimPatchModel.OperationModel
{
Op = "add",
Path = "members",
Value = JsonDocument.Parse($"[{{\"value\":\"{ScimApplicationFactory.TestOrganizationUserId2}\"}},{{\"value\":\"{ScimApplicationFactory.TestOrganizationUserId3}\"}}]").RootElement
}
},
Schemas = new List<string>() { ScimConstants.Scim2SchemaGroup }
};
var context = await _factory.GroupsPatchAsync(organizationId, groupId, inputModel);
Assert.Equal(StatusCodes.Status204NoContent, context.Response.StatusCode);
var databaseContext = _factory.GetDatabaseContext();
Assert.True(databaseContext.GroupUsers.Any(gu => gu.GroupId == groupId && gu.OrganizationUserId == ScimApplicationFactory.TestOrganizationUserId2));
Assert.True(databaseContext.GroupUsers.Any(gu => gu.GroupId == groupId && gu.OrganizationUserId == ScimApplicationFactory.TestOrganizationUserId3));
}
[Fact]
public async Task Patch_RemoveSingleMember_ReplaceDisplayName_Success()
{
var organizationId = ScimApplicationFactory.TestOrganizationId1;
var groupId = ScimApplicationFactory.TestGroupId1;
var newDisplayName = "Patch Display Name";
var inputModel = new ScimPatchModel
{
Operations = new List<ScimPatchModel.OperationModel>()
{
new ScimPatchModel.OperationModel
{
Op = "remove",
Path = $"members[value eq \"{ScimApplicationFactory.TestOrganizationUserId1}\"]",
Value = JsonDocument.Parse("{}").RootElement
},
new ScimPatchModel.OperationModel
{
Op = "replace",
Value = JsonDocument.Parse($"{{\"displayName\":\"{newDisplayName}\"}}").RootElement
}
},
Schemas = new List<string>() { ScimConstants.Scim2SchemaGroup }
};
var context = await _factory.GroupsPatchAsync(organizationId, groupId, inputModel);
Assert.Equal(StatusCodes.Status204NoContent, context.Response.StatusCode);
var databaseContext = _factory.GetDatabaseContext();
Assert.Equal(ScimApplicationFactory.InitialGroupUsersCount - 1, databaseContext.GroupUsers.Count());
Assert.Equal(ScimApplicationFactory.InitialGroupCount, databaseContext.Groups.Count());
var group = databaseContext.Groups.FirstOrDefault(g => g.Id == groupId);
Assert.Equal(newDisplayName, group.Name);
}
[Fact]
public async Task Patch_RemoveListMembers_Success()
{
var organizationId = ScimApplicationFactory.TestOrganizationId1;
var groupId = ScimApplicationFactory.TestGroupId1;
var inputModel = new ScimPatchModel
{
Operations = new List<ScimPatchModel.OperationModel>()
{
new ScimPatchModel.OperationModel
{
Op = "remove",
Path = "members",
Value = JsonDocument.Parse($"[{{\"value\":\"{ScimApplicationFactory.TestOrganizationUserId1}\"}}, {{\"value\":\"{ScimApplicationFactory.TestOrganizationUserId4}\"}}]").RootElement
}
},
Schemas = new List<string>() { ScimConstants.Scim2SchemaGroup }
};
var context = await _factory.GroupsPatchAsync(organizationId, groupId, inputModel);
Assert.Equal(StatusCodes.Status204NoContent, context.Response.StatusCode);
var databaseContext = _factory.GetDatabaseContext();
Assert.Empty(databaseContext.GroupUsers);
}
[Fact]
public async Task Patch_NotFound()
{
var organizationId = ScimApplicationFactory.TestOrganizationId1;
var groupId = Guid.NewGuid();
var inputModel = new Models.ScimPatchModel
{
Operations = new List<ScimPatchModel.OperationModel>(),
Schemas = new List<string>() { ScimConstants.Scim2SchemaGroup }
};
var expectedResponse = new ScimErrorResponseModel
{
Status = StatusCodes.Status404NotFound,
Detail = "Group not found.",
Schemas = new List<string> { ScimConstants.Scim2SchemaError }
};
var context = await _factory.GroupsPatchAsync(organizationId, groupId, inputModel);
Assert.Equal(StatusCodes.Status404NotFound, context.Response.StatusCode);
var responseModel = JsonSerializer.Deserialize<ScimErrorResponseModel>(context.Response.Body, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
AssertHelper.AssertPropertyEqual(expectedResponse, responseModel);
}
}

View File

@ -0,0 +1,251 @@
using System.Text.Json;
using Bit.Core;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Services;
using Bit.Scim.Groups.Interfaces;
using Bit.Scim.IntegrationTest.Factories;
using Bit.Scim.Models;
using Bit.Scim.Utilities;
using Bit.Test.Common.Helpers;
using NSubstitute;
using NSubstitute.ExceptionExtensions;
using Xunit;
namespace Bit.Scim.IntegrationTest.Controllers.v2;
public class GroupsControllerPatchTestsvNext : IClassFixture<ScimApplicationFactory>, IAsyncLifetime
{
private readonly ScimApplicationFactory _factory;
public GroupsControllerPatchTestsvNext(ScimApplicationFactory factory)
{
_factory = factory;
// Enable the feature flag for new PatchGroupsCommand and stub out the old command to be safe
_factory.SubstituteService((IFeatureService featureService)
=> featureService.IsEnabled(FeatureFlagKeys.ShortcutDuplicatePatchRequests).Returns(true));
_factory.SubstituteService((IPatchGroupCommand patchGroupCommand)
=> patchGroupCommand.PatchGroupAsync(Arg.Any<Organization>(), Arg.Any<Guid>(), Arg.Any<ScimPatchModel>())
.ThrowsAsync(new Exception("This test suite should be testing the vNext command, but the existing command was called.")));
}
public Task InitializeAsync()
{
var databaseContext = _factory.GetDatabaseContext();
_factory.ReinitializeDbForTests(databaseContext);
return Task.CompletedTask;
}
Task IAsyncLifetime.DisposeAsync() => Task.CompletedTask;
[Fact]
public async Task Patch_ReplaceDisplayName_Success()
{
var organizationId = ScimApplicationFactory.TestOrganizationId1;
var groupId = ScimApplicationFactory.TestGroupId1;
var newDisplayName = "Patch Display Name";
var inputModel = new ScimPatchModel
{
Operations = new List<ScimPatchModel.OperationModel>()
{
new ScimPatchModel.OperationModel
{
Op = "replace",
Value = JsonDocument.Parse($"{{\"displayName\":\"{newDisplayName}\"}}").RootElement
}
},
Schemas = new List<string>() { ScimConstants.Scim2SchemaGroup }
};
var context = await _factory.GroupsPatchAsync(organizationId, groupId, inputModel);
Assert.Equal(StatusCodes.Status204NoContent, context.Response.StatusCode);
var databaseContext = _factory.GetDatabaseContext();
var group = databaseContext.Groups.FirstOrDefault(g => g.Id == groupId);
Assert.Equal(newDisplayName, group.Name);
Assert.Equal(ScimApplicationFactory.InitialGroupUsersCount, databaseContext.GroupUsers.Count());
Assert.True(databaseContext.GroupUsers.Any(gu => gu.OrganizationUserId == ScimApplicationFactory.TestOrganizationUserId1));
Assert.True(databaseContext.GroupUsers.Any(gu => gu.OrganizationUserId == ScimApplicationFactory.TestOrganizationUserId4));
}
[Fact]
public async Task Patch_ReplaceMembers_Success()
{
var organizationId = ScimApplicationFactory.TestOrganizationId1;
var groupId = ScimApplicationFactory.TestGroupId1;
var inputModel = new ScimPatchModel
{
Operations = new List<ScimPatchModel.OperationModel>()
{
new ScimPatchModel.OperationModel
{
Op = "replace",
Path = "members",
Value = JsonDocument.Parse($"[{{\"value\":\"{ScimApplicationFactory.TestOrganizationUserId2}\"}}]").RootElement
}
},
Schemas = new List<string>() { ScimConstants.Scim2SchemaGroup }
};
var context = await _factory.GroupsPatchAsync(organizationId, groupId, inputModel);
Assert.Equal(StatusCodes.Status204NoContent, context.Response.StatusCode);
var databaseContext = _factory.GetDatabaseContext();
Assert.Single(databaseContext.GroupUsers);
Assert.Equal(ScimApplicationFactory.InitialGroupUsersCount - 1, databaseContext.GroupUsers.Count());
var groupUser = databaseContext.GroupUsers.FirstOrDefault();
Assert.Equal(ScimApplicationFactory.TestOrganizationUserId2, groupUser.OrganizationUserId);
}
[Fact]
public async Task Patch_AddSingleMember_Success()
{
var organizationId = ScimApplicationFactory.TestOrganizationId1;
var groupId = ScimApplicationFactory.TestGroupId1;
var inputModel = new ScimPatchModel
{
Operations = new List<ScimPatchModel.OperationModel>()
{
new ScimPatchModel.OperationModel
{
Op = "add",
Path = $"members[value eq \"{ScimApplicationFactory.TestOrganizationUserId2}\"]",
Value = JsonDocument.Parse("{}").RootElement
}
},
Schemas = new List<string>() { ScimConstants.Scim2SchemaGroup }
};
var context = await _factory.GroupsPatchAsync(organizationId, groupId, inputModel);
Assert.Equal(StatusCodes.Status204NoContent, context.Response.StatusCode);
var databaseContext = _factory.GetDatabaseContext();
Assert.Equal(ScimApplicationFactory.InitialGroupUsersCount + 1, databaseContext.GroupUsers.Count());
Assert.True(databaseContext.GroupUsers.Any(gu => gu.GroupId == groupId && gu.OrganizationUserId == ScimApplicationFactory.TestOrganizationUserId1));
Assert.True(databaseContext.GroupUsers.Any(gu => gu.GroupId == groupId && gu.OrganizationUserId == ScimApplicationFactory.TestOrganizationUserId2));
Assert.True(databaseContext.GroupUsers.Any(gu => gu.GroupId == groupId && gu.OrganizationUserId == ScimApplicationFactory.TestOrganizationUserId4));
}
[Fact]
public async Task Patch_AddListMembers_Success()
{
var organizationId = ScimApplicationFactory.TestOrganizationId1;
var groupId = ScimApplicationFactory.TestGroupId2;
var inputModel = new ScimPatchModel
{
Operations = new List<ScimPatchModel.OperationModel>()
{
new ScimPatchModel.OperationModel
{
Op = "add",
Path = "members",
Value = JsonDocument.Parse($"[{{\"value\":\"{ScimApplicationFactory.TestOrganizationUserId2}\"}},{{\"value\":\"{ScimApplicationFactory.TestOrganizationUserId3}\"}}]").RootElement
}
},
Schemas = new List<string>() { ScimConstants.Scim2SchemaGroup }
};
var context = await _factory.GroupsPatchAsync(organizationId, groupId, inputModel);
Assert.Equal(StatusCodes.Status204NoContent, context.Response.StatusCode);
var databaseContext = _factory.GetDatabaseContext();
Assert.True(databaseContext.GroupUsers.Any(gu => gu.GroupId == groupId && gu.OrganizationUserId == ScimApplicationFactory.TestOrganizationUserId2));
Assert.True(databaseContext.GroupUsers.Any(gu => gu.GroupId == groupId && gu.OrganizationUserId == ScimApplicationFactory.TestOrganizationUserId3));
}
[Fact]
public async Task Patch_RemoveSingleMember_ReplaceDisplayName_Success()
{
var organizationId = ScimApplicationFactory.TestOrganizationId1;
var groupId = ScimApplicationFactory.TestGroupId1;
var newDisplayName = "Patch Display Name";
var inputModel = new ScimPatchModel
{
Operations = new List<ScimPatchModel.OperationModel>()
{
new ScimPatchModel.OperationModel
{
Op = "remove",
Path = $"members[value eq \"{ScimApplicationFactory.TestOrganizationUserId1}\"]",
Value = JsonDocument.Parse("{}").RootElement
},
new ScimPatchModel.OperationModel
{
Op = "replace",
Value = JsonDocument.Parse($"{{\"displayName\":\"{newDisplayName}\"}}").RootElement
}
},
Schemas = new List<string>() { ScimConstants.Scim2SchemaGroup }
};
var context = await _factory.GroupsPatchAsync(organizationId, groupId, inputModel);
Assert.Equal(StatusCodes.Status204NoContent, context.Response.StatusCode);
var databaseContext = _factory.GetDatabaseContext();
Assert.Equal(ScimApplicationFactory.InitialGroupUsersCount - 1, databaseContext.GroupUsers.Count());
Assert.Equal(ScimApplicationFactory.InitialGroupCount, databaseContext.Groups.Count());
var group = databaseContext.Groups.FirstOrDefault(g => g.Id == groupId);
Assert.Equal(newDisplayName, group.Name);
}
[Fact]
public async Task Patch_RemoveListMembers_Success()
{
var organizationId = ScimApplicationFactory.TestOrganizationId1;
var groupId = ScimApplicationFactory.TestGroupId1;
var inputModel = new ScimPatchModel
{
Operations = new List<ScimPatchModel.OperationModel>()
{
new ScimPatchModel.OperationModel
{
Op = "remove",
Path = "members",
Value = JsonDocument.Parse($"[{{\"value\":\"{ScimApplicationFactory.TestOrganizationUserId1}\"}}, {{\"value\":\"{ScimApplicationFactory.TestOrganizationUserId4}\"}}]").RootElement
}
},
Schemas = new List<string>() { ScimConstants.Scim2SchemaGroup }
};
var context = await _factory.GroupsPatchAsync(organizationId, groupId, inputModel);
Assert.Equal(StatusCodes.Status204NoContent, context.Response.StatusCode);
var databaseContext = _factory.GetDatabaseContext();
Assert.Empty(databaseContext.GroupUsers);
}
[Fact]
public async Task Patch_NotFound()
{
var organizationId = ScimApplicationFactory.TestOrganizationId1;
var groupId = Guid.NewGuid();
var inputModel = new Models.ScimPatchModel
{
Operations = new List<ScimPatchModel.OperationModel>(),
Schemas = new List<string>() { ScimConstants.Scim2SchemaGroup }
};
var expectedResponse = new ScimErrorResponseModel
{
Status = StatusCodes.Status404NotFound,
Detail = "Group not found.",
Schemas = new List<string> { ScimConstants.Scim2SchemaError }
};
var context = await _factory.GroupsPatchAsync(organizationId, groupId, inputModel);
Assert.Equal(StatusCodes.Status404NotFound, context.Response.StatusCode);
var responseModel = JsonSerializer.Deserialize<ScimErrorResponseModel>(context.Response.Body, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
AssertHelper.AssertPropertyEqual(expectedResponse, responseModel);
}
}

View File

@ -9,9 +9,6 @@ namespace Bit.Scim.IntegrationTest.Controllers.v2;
public class GroupsControllerTests : IClassFixture<ScimApplicationFactory>, IAsyncLifetime public class GroupsControllerTests : IClassFixture<ScimApplicationFactory>, IAsyncLifetime
{ {
private const int _initialGroupCount = 3;
private const int _initialGroupUsersCount = 2;
private readonly ScimApplicationFactory _factory; private readonly ScimApplicationFactory _factory;
public GroupsControllerTests(ScimApplicationFactory factory) public GroupsControllerTests(ScimApplicationFactory factory)
@ -237,10 +234,10 @@ public class GroupsControllerTests : IClassFixture<ScimApplicationFactory>, IAsy
AssertHelper.AssertPropertyEqual(expectedResponse, responseModel, "Id"); AssertHelper.AssertPropertyEqual(expectedResponse, responseModel, "Id");
var databaseContext = _factory.GetDatabaseContext(); var databaseContext = _factory.GetDatabaseContext();
Assert.Equal(_initialGroupCount + 1, databaseContext.Groups.Count()); Assert.Equal(ScimApplicationFactory.InitialGroupCount + 1, databaseContext.Groups.Count());
Assert.True(databaseContext.Groups.Any(g => g.Name == displayName && g.ExternalId == externalId)); Assert.True(databaseContext.Groups.Any(g => g.Name == displayName && g.ExternalId == externalId));
Assert.Equal(_initialGroupUsersCount + 1, databaseContext.GroupUsers.Count()); Assert.Equal(ScimApplicationFactory.InitialGroupUsersCount + 1, databaseContext.GroupUsers.Count());
Assert.True(databaseContext.GroupUsers.Any(gu => gu.GroupId == responseModel.Id && gu.OrganizationUserId == ScimApplicationFactory.TestOrganizationUserId1)); Assert.True(databaseContext.GroupUsers.Any(gu => gu.GroupId == responseModel.Id && gu.OrganizationUserId == ScimApplicationFactory.TestOrganizationUserId1));
} }
@ -281,7 +278,7 @@ public class GroupsControllerTests : IClassFixture<ScimApplicationFactory>, IAsy
Assert.Equal(StatusCodes.Status409Conflict, context.Response.StatusCode); Assert.Equal(StatusCodes.Status409Conflict, context.Response.StatusCode);
var databaseContext = _factory.GetDatabaseContext(); var databaseContext = _factory.GetDatabaseContext();
Assert.Equal(_initialGroupCount, databaseContext.Groups.Count()); Assert.Equal(ScimApplicationFactory.InitialGroupCount, databaseContext.Groups.Count());
Assert.False(databaseContext.Groups.Any(g => g.Name == "New Group")); Assert.False(databaseContext.Groups.Any(g => g.Name == "New Group"));
} }
@ -354,216 +351,6 @@ public class GroupsControllerTests : IClassFixture<ScimApplicationFactory>, IAsy
AssertHelper.AssertPropertyEqual(expectedResponse, responseModel); AssertHelper.AssertPropertyEqual(expectedResponse, responseModel);
} }
[Fact]
public async Task Patch_ReplaceDisplayName_Success()
{
var organizationId = ScimApplicationFactory.TestOrganizationId1;
var groupId = ScimApplicationFactory.TestGroupId1;
var newDisplayName = "Patch Display Name";
var inputModel = new ScimPatchModel
{
Operations = new List<ScimPatchModel.OperationModel>()
{
new ScimPatchModel.OperationModel
{
Op = "replace",
Value = JsonDocument.Parse($"{{\"displayName\":\"{newDisplayName}\"}}").RootElement
}
},
Schemas = new List<string>() { ScimConstants.Scim2SchemaGroup }
};
var context = await _factory.GroupsPatchAsync(organizationId, groupId, inputModel);
Assert.Equal(StatusCodes.Status204NoContent, context.Response.StatusCode);
var databaseContext = _factory.GetDatabaseContext();
var group = databaseContext.Groups.FirstOrDefault(g => g.Id == groupId);
Assert.Equal(newDisplayName, group.Name);
Assert.Equal(_initialGroupUsersCount, databaseContext.GroupUsers.Count());
Assert.True(databaseContext.GroupUsers.Any(gu => gu.OrganizationUserId == ScimApplicationFactory.TestOrganizationUserId1));
Assert.True(databaseContext.GroupUsers.Any(gu => gu.OrganizationUserId == ScimApplicationFactory.TestOrganizationUserId4));
}
[Fact]
public async Task Patch_ReplaceMembers_Success()
{
var organizationId = ScimApplicationFactory.TestOrganizationId1;
var groupId = ScimApplicationFactory.TestGroupId1;
var inputModel = new ScimPatchModel
{
Operations = new List<ScimPatchModel.OperationModel>()
{
new ScimPatchModel.OperationModel
{
Op = "replace",
Path = "members",
Value = JsonDocument.Parse($"[{{\"value\":\"{ScimApplicationFactory.TestOrganizationUserId2}\"}}]").RootElement
}
},
Schemas = new List<string>() { ScimConstants.Scim2SchemaGroup }
};
var context = await _factory.GroupsPatchAsync(organizationId, groupId, inputModel);
Assert.Equal(StatusCodes.Status204NoContent, context.Response.StatusCode);
var databaseContext = _factory.GetDatabaseContext();
Assert.Single(databaseContext.GroupUsers);
Assert.Equal(_initialGroupUsersCount - 1, databaseContext.GroupUsers.Count());
var groupUser = databaseContext.GroupUsers.FirstOrDefault();
Assert.Equal(ScimApplicationFactory.TestOrganizationUserId2, groupUser.OrganizationUserId);
}
[Fact]
public async Task Patch_AddSingleMember_Success()
{
var organizationId = ScimApplicationFactory.TestOrganizationId1;
var groupId = ScimApplicationFactory.TestGroupId1;
var inputModel = new ScimPatchModel
{
Operations = new List<ScimPatchModel.OperationModel>()
{
new ScimPatchModel.OperationModel
{
Op = "add",
Path = $"members[value eq \"{ScimApplicationFactory.TestOrganizationUserId2}\"]",
Value = JsonDocument.Parse("{}").RootElement
}
},
Schemas = new List<string>() { ScimConstants.Scim2SchemaGroup }
};
var context = await _factory.GroupsPatchAsync(organizationId, groupId, inputModel);
Assert.Equal(StatusCodes.Status204NoContent, context.Response.StatusCode);
var databaseContext = _factory.GetDatabaseContext();
Assert.Equal(_initialGroupUsersCount + 1, databaseContext.GroupUsers.Count());
Assert.True(databaseContext.GroupUsers.Any(gu => gu.GroupId == groupId && gu.OrganizationUserId == ScimApplicationFactory.TestOrganizationUserId1));
Assert.True(databaseContext.GroupUsers.Any(gu => gu.GroupId == groupId && gu.OrganizationUserId == ScimApplicationFactory.TestOrganizationUserId2));
Assert.True(databaseContext.GroupUsers.Any(gu => gu.GroupId == groupId && gu.OrganizationUserId == ScimApplicationFactory.TestOrganizationUserId4));
}
[Fact]
public async Task Patch_AddListMembers_Success()
{
var organizationId = ScimApplicationFactory.TestOrganizationId1;
var groupId = ScimApplicationFactory.TestGroupId2;
var inputModel = new ScimPatchModel
{
Operations = new List<ScimPatchModel.OperationModel>()
{
new ScimPatchModel.OperationModel
{
Op = "add",
Path = "members",
Value = JsonDocument.Parse($"[{{\"value\":\"{ScimApplicationFactory.TestOrganizationUserId2}\"}},{{\"value\":\"{ScimApplicationFactory.TestOrganizationUserId3}\"}}]").RootElement
}
},
Schemas = new List<string>() { ScimConstants.Scim2SchemaGroup }
};
var context = await _factory.GroupsPatchAsync(organizationId, groupId, inputModel);
Assert.Equal(StatusCodes.Status204NoContent, context.Response.StatusCode);
var databaseContext = _factory.GetDatabaseContext();
Assert.True(databaseContext.GroupUsers.Any(gu => gu.GroupId == groupId && gu.OrganizationUserId == ScimApplicationFactory.TestOrganizationUserId2));
Assert.True(databaseContext.GroupUsers.Any(gu => gu.GroupId == groupId && gu.OrganizationUserId == ScimApplicationFactory.TestOrganizationUserId3));
}
[Fact]
public async Task Patch_RemoveSingleMember_ReplaceDisplayName_Success()
{
var organizationId = ScimApplicationFactory.TestOrganizationId1;
var groupId = ScimApplicationFactory.TestGroupId1;
var newDisplayName = "Patch Display Name";
var inputModel = new ScimPatchModel
{
Operations = new List<ScimPatchModel.OperationModel>()
{
new ScimPatchModel.OperationModel
{
Op = "remove",
Path = $"members[value eq \"{ScimApplicationFactory.TestOrganizationUserId1}\"]",
Value = JsonDocument.Parse("{}").RootElement
},
new ScimPatchModel.OperationModel
{
Op = "replace",
Value = JsonDocument.Parse($"{{\"displayName\":\"{newDisplayName}\"}}").RootElement
}
},
Schemas = new List<string>() { ScimConstants.Scim2SchemaGroup }
};
var context = await _factory.GroupsPatchAsync(organizationId, groupId, inputModel);
Assert.Equal(StatusCodes.Status204NoContent, context.Response.StatusCode);
var databaseContext = _factory.GetDatabaseContext();
Assert.Equal(_initialGroupUsersCount - 1, databaseContext.GroupUsers.Count());
Assert.Equal(_initialGroupCount, databaseContext.Groups.Count());
var group = databaseContext.Groups.FirstOrDefault(g => g.Id == groupId);
Assert.Equal(newDisplayName, group.Name);
}
[Fact]
public async Task Patch_RemoveListMembers_Success()
{
var organizationId = ScimApplicationFactory.TestOrganizationId1;
var groupId = ScimApplicationFactory.TestGroupId1;
var inputModel = new ScimPatchModel
{
Operations = new List<ScimPatchModel.OperationModel>()
{
new ScimPatchModel.OperationModel
{
Op = "remove",
Path = "members",
Value = JsonDocument.Parse($"[{{\"value\":\"{ScimApplicationFactory.TestOrganizationUserId1}\"}}, {{\"value\":\"{ScimApplicationFactory.TestOrganizationUserId4}\"}}]").RootElement
}
},
Schemas = new List<string>() { ScimConstants.Scim2SchemaGroup }
};
var context = await _factory.GroupsPatchAsync(organizationId, groupId, inputModel);
Assert.Equal(StatusCodes.Status204NoContent, context.Response.StatusCode);
var databaseContext = _factory.GetDatabaseContext();
Assert.Empty(databaseContext.GroupUsers);
}
[Fact]
public async Task Patch_NotFound()
{
var organizationId = ScimApplicationFactory.TestOrganizationId1;
var groupId = Guid.NewGuid();
var inputModel = new Models.ScimPatchModel
{
Operations = new List<ScimPatchModel.OperationModel>(),
Schemas = new List<string>() { ScimConstants.Scim2SchemaGroup }
};
var expectedResponse = new ScimErrorResponseModel
{
Status = StatusCodes.Status404NotFound,
Detail = "Group not found.",
Schemas = new List<string> { ScimConstants.Scim2SchemaError }
};
var context = await _factory.GroupsPatchAsync(organizationId, groupId, inputModel);
Assert.Equal(StatusCodes.Status404NotFound, context.Response.StatusCode);
var responseModel = JsonSerializer.Deserialize<ScimErrorResponseModel>(context.Response.Body, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
AssertHelper.AssertPropertyEqual(expectedResponse, responseModel);
}
[Fact] [Fact]
public async Task Delete_Success() public async Task Delete_Success()
{ {
@ -575,7 +362,7 @@ public class GroupsControllerTests : IClassFixture<ScimApplicationFactory>, IAsy
Assert.Equal(StatusCodes.Status204NoContent, context.Response.StatusCode); Assert.Equal(StatusCodes.Status204NoContent, context.Response.StatusCode);
var databaseContext = _factory.GetDatabaseContext(); var databaseContext = _factory.GetDatabaseContext();
Assert.Equal(_initialGroupCount - 1, databaseContext.Groups.Count()); Assert.Equal(ScimApplicationFactory.InitialGroupCount - 1, databaseContext.Groups.Count());
Assert.True(databaseContext.Groups.FirstOrDefault(g => g.Id == groupId) == null); Assert.True(databaseContext.Groups.FirstOrDefault(g => g.Id == groupId) == null);
} }

View File

@ -9,8 +9,6 @@ using Bit.Infrastructure.EntityFramework.Repositories;
using Bit.IntegrationTestCommon.Factories; using Bit.IntegrationTestCommon.Factories;
using Bit.Scim.Models; using Bit.Scim.Models;
using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Microsoft.Net.Http.Headers; using Microsoft.Net.Http.Headers;
@ -18,7 +16,8 @@ namespace Bit.Scim.IntegrationTest.Factories;
public class ScimApplicationFactory : WebApplicationFactoryBase<Startup> public class ScimApplicationFactory : WebApplicationFactoryBase<Startup>
{ {
public readonly new TestServer Server; public const int InitialGroupCount = 3;
public const int InitialGroupUsersCount = 2;
public static readonly Guid TestUserId1 = Guid.Parse("2e8173db-8e8d-4de1-ac38-91b15c6d8dcb"); public static readonly Guid TestUserId1 = Guid.Parse("2e8173db-8e8d-4de1-ac38-91b15c6d8dcb");
public static readonly Guid TestUserId2 = Guid.Parse("b57846fc-0e94-4c93-9de5-9d0389eeadfb"); public static readonly Guid TestUserId2 = Guid.Parse("b57846fc-0e94-4c93-9de5-9d0389eeadfb");
@ -33,10 +32,10 @@ public class ScimApplicationFactory : WebApplicationFactoryBase<Startup>
public static readonly Guid TestOrganizationUserId3 = Guid.Parse("be2f9045-e2b6-4173-ad44-4c69c3ea8140"); public static readonly Guid TestOrganizationUserId3 = Guid.Parse("be2f9045-e2b6-4173-ad44-4c69c3ea8140");
public static readonly Guid TestOrganizationUserId4 = Guid.Parse("1f5689b7-e96e-4840-b0b1-eb3d5b5fd514"); public static readonly Guid TestOrganizationUserId4 = Guid.Parse("1f5689b7-e96e-4840-b0b1-eb3d5b5fd514");
public ScimApplicationFactory() protected override void ConfigureWebHost(IWebHostBuilder builder)
{
WebApplicationFactory<Startup> webApplicationFactory = WithWebHostBuilder(builder =>
{ {
base.ConfigureWebHost(builder);
builder.ConfigureServices(services => builder.ConfigureServices(services =>
{ {
services services
@ -56,9 +55,6 @@ public class ScimApplicationFactory : WebApplicationFactoryBase<Startup>
services.Remove(mailService); services.Remove(mailService);
services.AddSingleton<IMailService, NoopMailService>(); services.AddSingleton<IMailService, NoopMailService>();
}); });
});
Server = webApplicationFactory.Server;
} }
public async Task<HttpContext> GroupsGetAsync(Guid organizationId, Guid id) public async Task<HttpContext> GroupsGetAsync(Guid organizationId, Guid id)

View File

@ -0,0 +1,381 @@
using System.Text.Json;
using AutoFixture;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Interfaces;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Enums;
using Bit.Core.Repositories;
using Bit.Scim.Groups;
using Bit.Scim.Models;
using Bit.Scim.Utilities;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
namespace Bit.Scim.Test.Groups;
[SutProviderCustomize]
public class PatchGroupCommandvNextTests
{
[Theory]
[BitAutoData]
public async Task PatchGroup_ReplaceListMembers_Success(SutProvider<PatchGroupCommandvNext> sutProvider,
Organization organization, Group group, IEnumerable<Guid> userIds)
{
group.OrganizationId = organization.Id;
var scimPatchModel = new ScimPatchModel
{
Operations = new List<ScimPatchModel.OperationModel>
{
new()
{
Op = "replace",
Path = "members",
Value = JsonDocument.Parse(JsonSerializer.Serialize(userIds.Select(uid => new { value = uid }).ToArray())).RootElement
}
},
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
};
await sutProvider.Sut.PatchGroupAsync(group, scimPatchModel);
await sutProvider.GetDependency<IGroupRepository>().Received(1).UpdateUsersAsync(
group.Id,
Arg.Is<IEnumerable<Guid>>(arg =>
arg.Count() == userIds.Count() &&
arg.ToHashSet().SetEquals(userIds)));
}
[Theory]
[BitAutoData]
public async Task PatchGroup_ReplaceDisplayNameFromPath_Success(
SutProvider<PatchGroupCommandvNext> sutProvider, Organization organization, Group group, string displayName)
{
group.OrganizationId = organization.Id;
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(organization.Id)
.Returns(organization);
var scimPatchModel = new ScimPatchModel
{
Operations = new List<ScimPatchModel.OperationModel>
{
new()
{
Op = "replace",
Path = "displayname",
Value = JsonDocument.Parse($"\"{displayName}\"").RootElement
}
},
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
};
await sutProvider.Sut.PatchGroupAsync(group, scimPatchModel);
await sutProvider.GetDependency<IUpdateGroupCommand>().Received(1).UpdateGroupAsync(group, organization, EventSystemUser.SCIM);
Assert.Equal(displayName, group.Name);
}
[Theory]
[BitAutoData]
public async Task PatchGroup_ReplaceDisplayNameFromValueObject_Success(SutProvider<PatchGroupCommandvNext> sutProvider, Organization organization, Group group, string displayName)
{
group.OrganizationId = organization.Id;
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(organization.Id)
.Returns(organization);
var scimPatchModel = new ScimPatchModel
{
Operations = new List<ScimPatchModel.OperationModel>
{
new()
{
Op = "replace",
Value = JsonDocument.Parse($"{{\"displayName\":\"{displayName}\"}}").RootElement
}
},
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
};
await sutProvider.Sut.PatchGroupAsync(group, scimPatchModel);
await sutProvider.GetDependency<IUpdateGroupCommand>().Received(1).UpdateGroupAsync(group, organization, EventSystemUser.SCIM);
Assert.Equal(displayName, group.Name);
}
[Theory]
[BitAutoData]
public async Task PatchGroup_AddSingleMember_Success(SutProvider<PatchGroupCommandvNext> sutProvider, Organization organization, Group group, ICollection<Guid> existingMembers, Guid userId)
{
group.OrganizationId = organization.Id;
sutProvider.GetDependency<IGroupRepository>()
.GetManyUserIdsByIdAsync(group.Id, true)
.Returns(existingMembers);
var scimPatchModel = new ScimPatchModel
{
Operations = new List<ScimPatchModel.OperationModel>
{
new()
{
Op = "add",
Path = $"members[value eq \"{userId}\"]",
}
},
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
};
await sutProvider.Sut.PatchGroupAsync(group, scimPatchModel);
await sutProvider.GetDependency<IGroupRepository>().Received(1).AddGroupUsersByIdAsync(
group.Id,
Arg.Is<IEnumerable<Guid>>(arg => arg.Single() == userId));
}
[Theory]
[BitAutoData]
public async Task PatchGroup_AddSingleMember_ReturnsEarlyIfAlreadyInGroup(
SutProvider<PatchGroupCommandvNext> sutProvider,
Organization organization,
Group group,
ICollection<Guid> existingMembers)
{
// User being added is already in group
var userId = existingMembers.First();
group.OrganizationId = organization.Id;
sutProvider.GetDependency<IGroupRepository>()
.GetManyUserIdsByIdAsync(group.Id, true)
.Returns(existingMembers);
var scimPatchModel = new ScimPatchModel
{
Operations = new List<ScimPatchModel.OperationModel>
{
new()
{
Op = "add",
Path = $"members[value eq \"{userId}\"]",
}
},
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
};
await sutProvider.Sut.PatchGroupAsync(group, scimPatchModel);
await sutProvider.GetDependency<IGroupRepository>()
.DidNotReceiveWithAnyArgs()
.AddGroupUsersByIdAsync(default, default);
}
[Theory]
[BitAutoData]
public async Task PatchGroup_AddListMembers_Success(SutProvider<PatchGroupCommandvNext> sutProvider, Organization organization, Group group, ICollection<Guid> existingMembers, ICollection<Guid> userIds)
{
group.OrganizationId = organization.Id;
sutProvider.GetDependency<IGroupRepository>()
.GetManyUserIdsByIdAsync(group.Id, true)
.Returns(existingMembers);
var scimPatchModel = new ScimPatchModel
{
Operations = new List<ScimPatchModel.OperationModel>
{
new()
{
Op = "add",
Path = $"members",
Value = JsonDocument.Parse(JsonSerializer.Serialize(userIds.Select(uid => new { value = uid }).ToArray())).RootElement
}
},
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
};
await sutProvider.Sut.PatchGroupAsync(group, scimPatchModel);
await sutProvider.GetDependency<IGroupRepository>().Received(1).AddGroupUsersByIdAsync(
group.Id,
Arg.Is<IEnumerable<Guid>>(arg =>
arg.Count() == userIds.Count &&
arg.ToHashSet().SetEquals(userIds)));
}
[Theory]
[BitAutoData]
public async Task PatchGroup_AddListMembers_IgnoresDuplicatesInRequest(
SutProvider<PatchGroupCommandvNext> sutProvider, Organization organization, Group group,
ICollection<Guid> existingMembers)
{
// Create 3 userIds
var fixture = new Fixture { RepeatCount = 3 };
var userIds = fixture.CreateMany<Guid>().ToList();
// Copy the list and add a duplicate
var userIdsWithDuplicate = userIds.Append(userIds.First()).ToList();
Assert.Equal(4, userIdsWithDuplicate.Count);
group.OrganizationId = organization.Id;
sutProvider.GetDependency<IGroupRepository>()
.GetManyUserIdsByIdAsync(group.Id, true)
.Returns(existingMembers);
var scimPatchModel = new ScimPatchModel
{
Operations = new List<ScimPatchModel.OperationModel>
{
new()
{
Op = "add",
Path = $"members",
Value = JsonDocument.Parse(JsonSerializer
.Serialize(userIdsWithDuplicate
.Select(uid => new { value = uid })
.ToArray())).RootElement
}
},
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
};
await sutProvider.Sut.PatchGroupAsync(group, scimPatchModel);
await sutProvider.GetDependency<IGroupRepository>().Received(1).AddGroupUsersByIdAsync(
group.Id,
Arg.Is<IEnumerable<Guid>>(arg =>
arg.Count() == 3 &&
arg.ToHashSet().SetEquals(userIds)));
}
[Theory]
[BitAutoData]
public async Task PatchGroup_AddListMembers_SuccessIfOnlySomeUsersAreInGroup(
SutProvider<PatchGroupCommandvNext> sutProvider,
Organization organization, Group group,
ICollection<Guid> existingMembers,
ICollection<Guid> userIds)
{
// A user is already in the group, but some still need to be added
userIds.Add(existingMembers.First());
group.OrganizationId = organization.Id;
sutProvider.GetDependency<IGroupRepository>()
.GetManyUserIdsByIdAsync(group.Id, true)
.Returns(existingMembers);
var scimPatchModel = new ScimPatchModel
{
Operations = new List<ScimPatchModel.OperationModel>
{
new()
{
Op = "add",
Path = $"members",
Value = JsonDocument.Parse(JsonSerializer.Serialize(userIds.Select(uid => new { value = uid }).ToArray())).RootElement
}
},
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
};
await sutProvider.Sut.PatchGroupAsync(group, scimPatchModel);
await sutProvider.GetDependency<IGroupRepository>()
.Received(1)
.AddGroupUsersByIdAsync(
group.Id,
Arg.Is<IEnumerable<Guid>>(arg =>
arg.Count() == userIds.Count &&
arg.ToHashSet().SetEquals(userIds)));
}
[Theory]
[BitAutoData]
public async Task PatchGroup_RemoveSingleMember_Success(SutProvider<PatchGroupCommandvNext> sutProvider, Organization organization, Group group, Guid userId)
{
group.OrganizationId = organization.Id;
var scimPatchModel = new Models.ScimPatchModel
{
Operations = new List<ScimPatchModel.OperationModel>
{
new ScimPatchModel.OperationModel
{
Op = "remove",
Path = $"members[value eq \"{userId}\"]",
}
},
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
};
await sutProvider.Sut.PatchGroupAsync(group, scimPatchModel);
await sutProvider.GetDependency<IGroupService>().Received(1).DeleteUserAsync(group, userId, EventSystemUser.SCIM);
}
[Theory]
[BitAutoData]
public async Task PatchGroup_RemoveListMembers_Success(SutProvider<PatchGroupCommandvNext> sutProvider,
Organization organization, Group group, ICollection<Guid> existingMembers)
{
List<Guid> usersToRemove = [existingMembers.First(), existingMembers.Skip(1).First()];
group.OrganizationId = organization.Id;
sutProvider.GetDependency<IGroupRepository>()
.GetManyUserIdsByIdAsync(group.Id)
.Returns(existingMembers);
var scimPatchModel = new Models.ScimPatchModel
{
Operations = new List<ScimPatchModel.OperationModel>
{
new()
{
Op = "remove",
Path = $"members",
Value = JsonDocument.Parse(JsonSerializer.Serialize(usersToRemove.Select(uid => new { value = uid }).ToArray())).RootElement
}
},
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
};
await sutProvider.Sut.PatchGroupAsync(group, scimPatchModel);
var expectedRemainingUsers = existingMembers.Skip(2).ToList();
await sutProvider.GetDependency<IGroupRepository>()
.Received(1)
.UpdateUsersAsync(
group.Id,
Arg.Is<IEnumerable<Guid>>(arg =>
arg.Count() == expectedRemainingUsers.Count &&
arg.ToHashSet().SetEquals(expectedRemainingUsers)));
}
[Theory]
[BitAutoData]
public async Task PatchGroup_NoAction_Success(
SutProvider<PatchGroupCommandvNext> sutProvider, Organization organization, Group group)
{
group.OrganizationId = organization.Id;
var scimPatchModel = new Models.ScimPatchModel
{
Operations = new List<ScimPatchModel.OperationModel>(),
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
};
await sutProvider.Sut.PatchGroupAsync(group, scimPatchModel);
await sutProvider.GetDependency<IGroupRepository>().DidNotReceiveWithAnyArgs().UpdateUsersAsync(default, default);
await sutProvider.GetDependency<IGroupRepository>().DidNotReceiveWithAnyArgs().GetManyUserIdsByIdAsync(default);
await sutProvider.GetDependency<IUpdateGroupCommand>().DidNotReceiveWithAnyArgs().UpdateGroupAsync(default, default);
await sutProvider.GetDependency<IGroupService>().DidNotReceiveWithAnyArgs().DeleteUserAsync(default, default);
}
}

View File

@ -1,10 +1,8 @@
using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Interfaces;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Scim.Context;
using Bit.Scim.Groups; using Bit.Scim.Groups;
using Bit.Scim.Models; using Bit.Scim.Models;
using Bit.Scim.Utilities; using Bit.Scim.Utilities;
@ -73,10 +71,6 @@ public class PostGroupCommandTests
.GetManyByOrganizationIdAsync(organization.Id) .GetManyByOrganizationIdAsync(organization.Id)
.Returns(groups); .Returns(groups);
sutProvider.GetDependency<IScimContext>()
.RequestScimProvider
.Returns(ScimProviderType.Okta);
var group = await sutProvider.Sut.PostGroupAsync(organization, scimGroupRequestModel); var group = await sutProvider.Sut.PostGroupAsync(organization, scimGroupRequestModel);
await sutProvider.GetDependency<ICreateGroupCommand>().Received(1).CreateGroupAsync(group, organization, EventSystemUser.SCIM, null); await sutProvider.GetDependency<ICreateGroupCommand>().Received(1).CreateGroupAsync(group, organization, EventSystemUser.SCIM, null);

View File

@ -1,10 +1,8 @@
using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Interfaces;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Scim.Context;
using Bit.Scim.Groups; using Bit.Scim.Groups;
using Bit.Scim.Models; using Bit.Scim.Models;
using Bit.Scim.Utilities; using Bit.Scim.Utilities;
@ -62,10 +60,6 @@ public class PutGroupCommandTests
.GetByIdAsync(group.Id) .GetByIdAsync(group.Id)
.Returns(group); .Returns(group);
sutProvider.GetDependency<IScimContext>()
.RequestScimProvider
.Returns(ScimProviderType.Okta);
var inputModel = new ScimGroupRequestModel var inputModel = new ScimGroupRequestModel
{ {
DisplayName = displayName, DisplayName = displayName,

View File

@ -109,6 +109,21 @@ services:
profiles: profiles:
- proxy - proxy
service-bus:
container_name: service-bus
image: mcr.microsoft.com/azure-messaging/servicebus-emulator:latest
pull_policy: always
volumes:
- "./servicebusemulator_config.json:/ServiceBus_Emulator/ConfigFiles/Config.json"
ports:
- "5672:5672"
environment:
SQL_SERVER: mssql
MSSQL_SA_PASSWORD: "${MSSQL_PASSWORD}"
ACCEPT_EULA: "Y"
profiles:
- servicebus
volumes: volumes:
mssql_dev_data: mssql_dev_data:
postgres_dev_data: postgres_dev_data:

View File

@ -0,0 +1,38 @@
{
"UserConfig": {
"Namespaces": [
{
"Name": "sbemulatorns",
"Queues": [
{
"Name": "queue.1",
"Properties": {
"DeadLetteringOnMessageExpiration": false,
"DefaultMessageTimeToLive": "PT1H",
"DuplicateDetectionHistoryTimeWindow": "PT20S",
"ForwardDeadLetteredMessagesTo": "",
"ForwardTo": "",
"LockDuration": "PT1M",
"MaxDeliveryCount": 3,
"RequiresDuplicateDetection": false,
"RequiresSession": false
}
}
],
"Topics": [
{
"Name": "event-logging",
"Subscriptions": [
{
"Name": "events-write-subscription"
}
]
}
]
}
],
"Logging": {
"Type": "File"
}
}
}

View File

@ -16,7 +16,6 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Folder Include="Billing\Controllers\" /> <Folder Include="Billing\Controllers\" />
<Folder Include="Billing\Models\" />
</ItemGroup> </ItemGroup>
<Choose> <Choose>

View File

@ -10,6 +10,7 @@ using Bit.Core.AdminConsole.Providers.Interfaces;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Enums; using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Extensions;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Services; using Bit.Core.Billing.Services;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Enums; using Bit.Core.Enums;
@ -56,8 +57,8 @@ public class OrganizationsController : Controller
private readonly IProviderOrganizationRepository _providerOrganizationRepository; private readonly IProviderOrganizationRepository _providerOrganizationRepository;
private readonly IRemoveOrganizationFromProviderCommand _removeOrganizationFromProviderCommand; private readonly IRemoveOrganizationFromProviderCommand _removeOrganizationFromProviderCommand;
private readonly IProviderBillingService _providerBillingService; private readonly IProviderBillingService _providerBillingService;
private readonly IFeatureService _featureService;
private readonly IOrganizationInitiateDeleteCommand _organizationInitiateDeleteCommand; private readonly IOrganizationInitiateDeleteCommand _organizationInitiateDeleteCommand;
private readonly IPricingClient _pricingClient;
public OrganizationsController( public OrganizationsController(
IOrganizationService organizationService, IOrganizationService organizationService,
@ -84,8 +85,8 @@ public class OrganizationsController : Controller
IProviderOrganizationRepository providerOrganizationRepository, IProviderOrganizationRepository providerOrganizationRepository,
IRemoveOrganizationFromProviderCommand removeOrganizationFromProviderCommand, IRemoveOrganizationFromProviderCommand removeOrganizationFromProviderCommand,
IProviderBillingService providerBillingService, IProviderBillingService providerBillingService,
IFeatureService featureService, IOrganizationInitiateDeleteCommand organizationInitiateDeleteCommand,
IOrganizationInitiateDeleteCommand organizationInitiateDeleteCommand) IPricingClient pricingClient)
{ {
_organizationService = organizationService; _organizationService = organizationService;
_organizationRepository = organizationRepository; _organizationRepository = organizationRepository;
@ -111,8 +112,8 @@ public class OrganizationsController : Controller
_providerOrganizationRepository = providerOrganizationRepository; _providerOrganizationRepository = providerOrganizationRepository;
_removeOrganizationFromProviderCommand = removeOrganizationFromProviderCommand; _removeOrganizationFromProviderCommand = removeOrganizationFromProviderCommand;
_providerBillingService = providerBillingService; _providerBillingService = providerBillingService;
_featureService = featureService;
_organizationInitiateDeleteCommand = organizationInitiateDeleteCommand; _organizationInitiateDeleteCommand = organizationInitiateDeleteCommand;
_pricingClient = pricingClient;
} }
[RequirePermission(Permission.Org_List_View)] [RequirePermission(Permission.Org_List_View)]
@ -212,6 +213,8 @@ public class OrganizationsController : Controller
? await _organizationUserRepository.GetOccupiedSmSeatCountByOrganizationIdAsync(organization.Id) ? await _organizationUserRepository.GetOccupiedSmSeatCountByOrganizationIdAsync(organization.Id)
: -1; : -1;
var plans = await _pricingClient.ListPlans();
return View(new OrganizationEditModel( return View(new OrganizationEditModel(
organization, organization,
provider, provider,
@ -224,6 +227,7 @@ public class OrganizationsController : Controller
billingHistoryInfo, billingHistoryInfo,
billingSyncConnection, billingSyncConnection,
_globalSettings, _globalSettings,
plans,
secrets, secrets,
projects, projects,
serviceAccounts, serviceAccounts,
@ -253,8 +257,9 @@ public class OrganizationsController : Controller
UpdateOrganization(organization, model); UpdateOrganization(organization, model);
if (organization.UseSecretsManager && var plan = await _pricingClient.GetPlanOrThrow(organization.PlanType);
!StaticStore.GetPlan(organization.PlanType).SupportsSecretsManager)
if (organization.UseSecretsManager && !plan.SupportsSecretsManager)
{ {
TempData["Error"] = "Plan does not support Secrets Manager"; TempData["Error"] = "Plan does not support Secrets Manager";
return RedirectToAction("Edit", new { id }); return RedirectToAction("Edit", new { id });

View File

@ -3,7 +3,6 @@ using System.Net;
using Bit.Admin.AdminConsole.Models; using Bit.Admin.AdminConsole.Models;
using Bit.Admin.Enums; using Bit.Admin.Enums;
using Bit.Admin.Utilities; using Bit.Admin.Utilities;
using Bit.Core;
using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.Providers.Interfaces; using Bit.Core.AdminConsole.Providers.Interfaces;
@ -133,11 +132,6 @@ public class ProvidersController : Controller
[HttpGet("providers/create/multi-organization-enterprise")] [HttpGet("providers/create/multi-organization-enterprise")]
public IActionResult CreateMultiOrganizationEnterprise(int enterpriseMinimumSeats, string ownerEmail = null) public IActionResult CreateMultiOrganizationEnterprise(int enterpriseMinimumSeats, string ownerEmail = null)
{ {
if (!_featureService.IsEnabled(FeatureFlagKeys.PM12275_MultiOrganizationEnterprises))
{
return RedirectToAction("Create");
}
return View(new CreateMultiOrganizationEnterpriseProviderModel return View(new CreateMultiOrganizationEnterpriseProviderModel
{ {
OwnerEmail = ownerEmail, OwnerEmail = ownerEmail,
@ -211,10 +205,6 @@ public class ProvidersController : Controller
} }
var provider = model.ToProvider(); var provider = model.ToProvider();
if (!_featureService.IsEnabled(FeatureFlagKeys.PM12275_MultiOrganizationEnterprises))
{
return RedirectToAction("Create");
}
await _createProviderCommand.CreateMultiOrganizationEnterpriseAsync( await _createProviderCommand.CreateMultiOrganizationEnterpriseAsync(
provider, provider,
model.OwnerEmail, model.OwnerEmail,
@ -235,7 +225,8 @@ public class ProvidersController : Controller
var users = await _providerUserRepository.GetManyDetailsByProviderAsync(id); var users = await _providerUserRepository.GetManyDetailsByProviderAsync(id);
var providerOrganizations = await _providerOrganizationRepository.GetManyDetailsByProviderAsync(id); var providerOrganizations = await _providerOrganizationRepository.GetManyDetailsByProviderAsync(id);
return View(new ProviderViewModel(provider, users, providerOrganizations)); var providerPlans = await _providerPlanRepository.GetByProviderId(id);
return View(new ProviderViewModel(provider, users, providerOrganizations, providerPlans.ToList()));
} }
[SelfHosted(NotSelfHostedOnly = true)] [SelfHosted(NotSelfHostedOnly = true)]
@ -250,6 +241,18 @@ public class ProvidersController : Controller
return View(provider); return View(provider);
} }
[SelfHosted(NotSelfHostedOnly = true)]
public async Task<IActionResult> Cancel(Guid id)
{
var provider = await GetEditModel(id);
if (provider == null)
{
return RedirectToAction("Index");
}
return RedirectToAction("Edit", new { id });
}
[HttpPost] [HttpPost]
[ValidateAntiForgeryToken] [ValidateAntiForgeryToken]
[SelfHosted(NotSelfHostedOnly = true)] [SelfHosted(NotSelfHostedOnly = true)]

View File

@ -8,6 +8,7 @@ using Bit.Core.Billing.Models;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.Models.StaticStore;
using Bit.Core.Settings; using Bit.Core.Settings;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using Bit.Core.Vault.Entities; using Bit.Core.Vault.Entities;
@ -17,6 +18,8 @@ namespace Bit.Admin.AdminConsole.Models;
public class OrganizationEditModel : OrganizationViewModel public class OrganizationEditModel : OrganizationViewModel
{ {
private readonly List<Plan> _plans;
public OrganizationEditModel() { } public OrganizationEditModel() { }
public OrganizationEditModel(Provider provider) public OrganizationEditModel(Provider provider)
@ -40,6 +43,7 @@ public class OrganizationEditModel : OrganizationViewModel
BillingHistoryInfo billingHistoryInfo, BillingHistoryInfo billingHistoryInfo,
IEnumerable<OrganizationConnection> connections, IEnumerable<OrganizationConnection> connections,
GlobalSettings globalSettings, GlobalSettings globalSettings,
List<Plan> plans,
int secrets, int secrets,
int projects, int projects,
int serviceAccounts, int serviceAccounts,
@ -96,6 +100,8 @@ public class OrganizationEditModel : OrganizationViewModel
MaxAutoscaleSmSeats = org.MaxAutoscaleSmSeats; MaxAutoscaleSmSeats = org.MaxAutoscaleSmSeats;
SmServiceAccounts = org.SmServiceAccounts; SmServiceAccounts = org.SmServiceAccounts;
MaxAutoscaleSmServiceAccounts = org.MaxAutoscaleSmServiceAccounts; MaxAutoscaleSmServiceAccounts = org.MaxAutoscaleSmServiceAccounts;
_plans = plans;
} }
public BillingInfo BillingInfo { get; set; } public BillingInfo BillingInfo { get; set; }
@ -183,7 +189,7 @@ public class OrganizationEditModel : OrganizationViewModel
* Add mappings for individual properties as you need them * Add mappings for individual properties as you need them
*/ */
public object GetPlansHelper() => public object GetPlansHelper() =>
StaticStore.Plans _plans
.Select(p => .Select(p =>
{ {
var plan = new var plan = new

View File

@ -19,7 +19,7 @@ public class ProviderEditModel : ProviderViewModel, IValidatableObject
IEnumerable<ProviderOrganizationOrganizationDetails> organizations, IEnumerable<ProviderOrganizationOrganizationDetails> organizations,
IReadOnlyCollection<ProviderPlan> providerPlans, IReadOnlyCollection<ProviderPlan> providerPlans,
string gatewayCustomerUrl = null, string gatewayCustomerUrl = null,
string gatewaySubscriptionUrl = null) : base(provider, providerUsers, organizations) string gatewaySubscriptionUrl = null) : base(provider, providerUsers, organizations, providerPlans)
{ {
Name = provider.DisplayName(); Name = provider.DisplayName();
BusinessName = provider.DisplayBusinessName(); BusinessName = provider.DisplayBusinessName();

View File

@ -1,6 +1,9 @@
using Bit.Core.AdminConsole.Entities.Provider; using Bit.Admin.Billing.Models;
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.Models.Data.Provider; using Bit.Core.AdminConsole.Models.Data.Provider;
using Bit.Core.Billing.Entities;
using Bit.Core.Billing.Enums;
namespace Bit.Admin.AdminConsole.Models; namespace Bit.Admin.AdminConsole.Models;
@ -8,17 +11,57 @@ public class ProviderViewModel
{ {
public ProviderViewModel() { } public ProviderViewModel() { }
public ProviderViewModel(Provider provider, IEnumerable<ProviderUserUserDetails> providerUsers, IEnumerable<ProviderOrganizationOrganizationDetails> organizations) public ProviderViewModel(
Provider provider,
IEnumerable<ProviderUserUserDetails> providerUsers,
IEnumerable<ProviderOrganizationOrganizationDetails> organizations,
IReadOnlyCollection<ProviderPlan> providerPlans)
{ {
Provider = provider; Provider = provider;
UserCount = providerUsers.Count(); UserCount = providerUsers.Count();
ProviderAdmins = providerUsers.Where(u => u.Type == ProviderUserType.ProviderAdmin); ProviderAdmins = providerUsers.Where(u => u.Type == ProviderUserType.ProviderAdmin);
ProviderOrganizations = organizations.Where(o => o.ProviderId == provider.Id); ProviderOrganizations = organizations.Where(o => o.ProviderId == provider.Id);
if (Provider.Type == ProviderType.Msp)
{
var usedTeamsSeats = ProviderOrganizations.Where(po => po.PlanType == PlanType.TeamsMonthly)
.Sum(po => po.OccupiedSeats) ?? 0;
var teamsProviderPlan = providerPlans.FirstOrDefault(plan => plan.PlanType == PlanType.TeamsMonthly);
if (teamsProviderPlan != null && teamsProviderPlan.IsConfigured())
{
ProviderPlanViewModels.Add(new ProviderPlanViewModel("Teams (Monthly) Subscription", teamsProviderPlan, usedTeamsSeats));
}
var usedEnterpriseSeats = ProviderOrganizations.Where(po => po.PlanType == PlanType.EnterpriseMonthly)
.Sum(po => po.OccupiedSeats) ?? 0;
var enterpriseProviderPlan = providerPlans.FirstOrDefault(plan => plan.PlanType == PlanType.EnterpriseMonthly);
if (enterpriseProviderPlan != null && enterpriseProviderPlan.IsConfigured())
{
ProviderPlanViewModels.Add(new ProviderPlanViewModel("Enterprise (Monthly) Subscription", enterpriseProviderPlan, usedEnterpriseSeats));
}
}
else if (Provider.Type == ProviderType.MultiOrganizationEnterprise)
{
var usedEnterpriseSeats = ProviderOrganizations.Where(po => po.PlanType == PlanType.EnterpriseMonthly)
.Sum(po => po.OccupiedSeats).GetValueOrDefault(0);
var enterpriseProviderPlan = providerPlans.FirstOrDefault();
if (enterpriseProviderPlan != null && enterpriseProviderPlan.IsConfigured())
{
var planLabel = enterpriseProviderPlan.PlanType switch
{
PlanType.EnterpriseMonthly => "Enterprise (Monthly) Subscription",
PlanType.EnterpriseAnnually => "Enterprise (Annually) Subscription",
_ => string.Empty
};
ProviderPlanViewModels.Add(new ProviderPlanViewModel(planLabel, enterpriseProviderPlan, usedEnterpriseSeats));
}
}
} }
public int UserCount { get; set; } public int UserCount { get; set; }
public Provider Provider { get; set; } public Provider Provider { get; set; }
public IEnumerable<ProviderUserUserDetails> ProviderAdmins { get; set; } public IEnumerable<ProviderUserUserDetails> ProviderAdmins { get; set; }
public IEnumerable<ProviderOrganizationOrganizationDetails> ProviderOrganizations { get; set; } public IEnumerable<ProviderOrganizationOrganizationDetails> ProviderOrganizations { get; set; }
public List<ProviderPlanViewModel> ProviderPlanViewModels { get; set; } = [];
} }

View File

@ -12,11 +12,6 @@
var providerTypes = Enum.GetValues<ProviderType>() var providerTypes = Enum.GetValues<ProviderType>()
.OrderBy(x => x.GetDisplayAttribute().Order) .OrderBy(x => x.GetDisplayAttribute().Order)
.ToList(); .ToList();
if (!FeatureService.IsEnabled(FeatureFlagKeys.PM12275_MultiOrganizationEnterprises))
{
providerTypes.Remove(ProviderType.MultiOrganizationEnterprise);
}
} }
<h1>Create Provider</h1> <h1>Create Provider</h1>

View File

@ -19,7 +19,7 @@
<div class="d-flex mt-4"> <div class="d-flex mt-4">
<button type="submit" class="btn btn-primary" form="edit-form">Save</button> <button type="submit" class="btn btn-primary" form="edit-form">Save</button>
<div class="ms-auto d-flex"> <div class="ms-auto d-flex">
<form asp-controller="Providers" asp-action="Edit" asp-route-id="@Model.Provider.Id" <form asp-controller="Providers" asp-action="Cancel" asp-route-id="@Model.Provider.Id"
onsubmit="return confirm('Are you sure you want to cancel?')"> onsubmit="return confirm('Are you sure you want to cancel?')">
<button class="btn btn-outline-secondary" type="submit">Cancel</button> <button class="btn btn-outline-secondary" type="submit">Cancel</button>
</form> </form>

View File

@ -17,6 +17,10 @@
<h2>Provider Information</h2> <h2>Provider Information</h2>
@await Html.PartialAsync("_ViewInformation", Model) @await Html.PartialAsync("_ViewInformation", Model)
@if (Model.ProviderPlanViewModels.Any())
{
@await Html.PartialAsync("~/Billing/Views/Providers/ProviderPlans.cshtml", Model.ProviderPlanViewModels)
}
@await Html.PartialAsync("Admins", Model) @await Html.PartialAsync("Admins", Model)
<form method="post" id="edit-form"> <form method="post" id="edit-form">
<div asp-validation-summary="All" class="alert alert-danger"></div> <div asp-validation-summary="All" class="alert alert-danger"></div>
@ -71,8 +75,6 @@
break; break;
} }
case ProviderType.MultiOrganizationEnterprise: case ProviderType.MultiOrganizationEnterprise:
{
@if (FeatureService.IsEnabled(FeatureFlagKeys.PM12275_MultiOrganizationEnterprises) && Model.Provider.Type == ProviderType.MultiOrganizationEnterprise)
{ {
<div class="row"> <div class="row">
<div class="col-sm"> <div class="col-sm">
@ -97,7 +99,6 @@
</div> </div>
</div> </div>
</div> </div>
}
break; break;
} }
} }

View File

@ -7,5 +7,9 @@
<h2>Information</h2> <h2>Information</h2>
@await Html.PartialAsync("_ViewInformation", Model) @await Html.PartialAsync("_ViewInformation", Model)
@if (Model.ProviderPlanViewModels.Any())
{
@await Html.PartialAsync("ProviderPlans", Model.ProviderPlanViewModels)
}
@await Html.PartialAsync("Admins", Model) @await Html.PartialAsync("Admins", Model)
@await Html.PartialAsync("Organizations", Model) @await Html.PartialAsync("Organizations", Model)

View File

@ -0,0 +1,26 @@
using Bit.Core.Billing.Entities;
namespace Bit.Admin.Billing.Models;
public class ProviderPlanViewModel
{
public string Name { get; set; }
public int PurchasedSeats { get; set; }
public int AssignedSeats { get; set; }
public int UsedSeats { get; set; }
public int RemainingSeats { get; set; }
public ProviderPlanViewModel(
string name,
ProviderPlan providerPlan,
int usedSeats)
{
var purchasedSeats = (providerPlan.SeatMinimum ?? 0) + (providerPlan.PurchasedSeats ?? 0);
Name = name;
PurchasedSeats = purchasedSeats;
AssignedSeats = providerPlan.AllocatedSeats ?? 0;
UsedSeats = usedSeats;
RemainingSeats = purchasedSeats - AssignedSeats;
}
}

View File

@ -0,0 +1,18 @@
@model List<Bit.Admin.Billing.Models.ProviderPlanViewModel>
@foreach (var plan in Model)
{
<h2>@plan.Name</h2>
<dl class="row">
<dt class="col-sm-4 col-lg-3">Purchased Seats</dt>
<dd class="col-sm-8 col-lg-9">@plan.PurchasedSeats</dd>
<dt class="col-sm-4 col-lg-3">Assigned Seats</dt>
<dd class="col-sm-8 col-lg-9">@plan.AssignedSeats</dd>
<dt class="col-sm-4 col-lg-3">Used Seats</dt>
<dd class="col-sm-8 col-lg-9">@plan.UsedSeats</dd>
<dt class="col-sm-4 col-lg-3">Remaining Seats</dt>
<dd class="col-sm-8 col-lg-9">@plan.RemainingSeats</dd>
</dl>
}

View File

@ -102,12 +102,13 @@ public class UsersController : Controller
return RedirectToAction("Index"); return RedirectToAction("Index");
} }
var ciphers = await _cipherRepository.GetManyByUserIdAsync(id); var ciphers = await _cipherRepository.GetManyByUserIdAsync(id, withOrganizations: false);
var billingInfo = await _paymentService.GetBillingAsync(user); var billingInfo = await _paymentService.GetBillingAsync(user);
var billingHistoryInfo = await _paymentService.GetBillingHistoryAsync(user); var billingHistoryInfo = await _paymentService.GetBillingHistoryAsync(user);
var isTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user); var isTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user);
var verifiedDomain = await AccountDeprovisioningEnabled(user.Id); var verifiedDomain = await AccountDeprovisioningEnabled(user.Id);
var deviceVerificationRequired = await _userService.ActiveNewDeviceVerificationException(user.Id); var deviceVerificationRequired = await _userService.ActiveNewDeviceVerificationException(user.Id);
return View(new UserEditModel(user, isTwoFactorEnabled, ciphers, billingInfo, billingHistoryInfo, _globalSettings, verifiedDomain, deviceVerificationRequired)); return View(new UserEditModel(user, isTwoFactorEnabled, ciphers, billingInfo, billingHistoryInfo, _globalSettings, verifiedDomain, deviceVerificationRequired));
} }

View File

@ -9,8 +9,7 @@
var canViewUserInformation = AccessControlService.UserHasPermission(Permission.User_UserInformation_View); var canViewUserInformation = AccessControlService.UserHasPermission(Permission.User_UserInformation_View);
var canViewNewDeviceException = AccessControlService.UserHasPermission(Permission.User_NewDeviceException_Edit) && var canViewNewDeviceException = AccessControlService.UserHasPermission(Permission.User_NewDeviceException_Edit) &&
GlobalSettings.EnableNewDeviceVerification && GlobalSettings.EnableNewDeviceVerification;
FeatureService.IsEnabled(Bit.Core.FeatureFlagKeys.NewDeviceVerification);
var canViewBillingInformation = AccessControlService.UserHasPermission(Permission.User_BillingInformation_View); var canViewBillingInformation = AccessControlService.UserHasPermission(Permission.User_BillingInformation_View);
var canViewGeneral = AccessControlService.UserHasPermission(Permission.User_GeneralDetails_View); var canViewGeneral = AccessControlService.UserHasPermission(Permission.User_GeneralDetails_View);
var canViewPremium = AccessControlService.UserHasPermission(Permission.User_Premium_View); var canViewPremium = AccessControlService.UserHasPermission(Permission.User_Premium_View);

View File

@ -18,7 +18,7 @@
"css-loader": "7.1.2", "css-loader": "7.1.2",
"expose-loader": "5.0.0", "expose-loader": "5.0.0",
"mini-css-extract-plugin": "2.9.2", "mini-css-extract-plugin": "2.9.2",
"sass": "1.79.5", "sass": "1.85.0",
"sass-loader": "16.0.4", "sass-loader": "16.0.4",
"webpack": "5.97.1", "webpack": "5.97.1",
"webpack-cli": "5.1.4" "webpack-cli": "5.1.4"
@ -99,12 +99,13 @@
} }
}, },
"node_modules/@parcel/watcher": { "node_modules/@parcel/watcher": {
"version": "2.5.0", "version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.0.tgz", "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz",
"integrity": "sha512-i0GV1yJnm2n3Yq1qw6QrUrd/LI9bE8WEBOTtOkpCXHHdyN3TAGgqAK/DAT05z4fq2x04cARXt2pDmjWjL92iTQ==", "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==",
"dev": true, "dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
"optional": true,
"dependencies": { "dependencies": {
"detect-libc": "^1.0.3", "detect-libc": "^1.0.3",
"is-glob": "^4.0.3", "is-glob": "^4.0.3",
@ -119,25 +120,25 @@
"url": "https://opencollective.com/parcel" "url": "https://opencollective.com/parcel"
}, },
"optionalDependencies": { "optionalDependencies": {
"@parcel/watcher-android-arm64": "2.5.0", "@parcel/watcher-android-arm64": "2.5.1",
"@parcel/watcher-darwin-arm64": "2.5.0", "@parcel/watcher-darwin-arm64": "2.5.1",
"@parcel/watcher-darwin-x64": "2.5.0", "@parcel/watcher-darwin-x64": "2.5.1",
"@parcel/watcher-freebsd-x64": "2.5.0", "@parcel/watcher-freebsd-x64": "2.5.1",
"@parcel/watcher-linux-arm-glibc": "2.5.0", "@parcel/watcher-linux-arm-glibc": "2.5.1",
"@parcel/watcher-linux-arm-musl": "2.5.0", "@parcel/watcher-linux-arm-musl": "2.5.1",
"@parcel/watcher-linux-arm64-glibc": "2.5.0", "@parcel/watcher-linux-arm64-glibc": "2.5.1",
"@parcel/watcher-linux-arm64-musl": "2.5.0", "@parcel/watcher-linux-arm64-musl": "2.5.1",
"@parcel/watcher-linux-x64-glibc": "2.5.0", "@parcel/watcher-linux-x64-glibc": "2.5.1",
"@parcel/watcher-linux-x64-musl": "2.5.0", "@parcel/watcher-linux-x64-musl": "2.5.1",
"@parcel/watcher-win32-arm64": "2.5.0", "@parcel/watcher-win32-arm64": "2.5.1",
"@parcel/watcher-win32-ia32": "2.5.0", "@parcel/watcher-win32-ia32": "2.5.1",
"@parcel/watcher-win32-x64": "2.5.0" "@parcel/watcher-win32-x64": "2.5.1"
} }
}, },
"node_modules/@parcel/watcher-android-arm64": { "node_modules/@parcel/watcher-android-arm64": {
"version": "2.5.0", "version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.0.tgz", "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz",
"integrity": "sha512-qlX4eS28bUcQCdribHkg/herLe+0A9RyYC+mm2PXpncit8z5b3nSqGVzMNR3CmtAOgRutiZ02eIJJgP/b1iEFQ==", "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -156,9 +157,9 @@
} }
}, },
"node_modules/@parcel/watcher-darwin-arm64": { "node_modules/@parcel/watcher-darwin-arm64": {
"version": "2.5.0", "version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.0.tgz", "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz",
"integrity": "sha512-hyZ3TANnzGfLpRA2s/4U1kbw2ZI4qGxaRJbBH2DCSREFfubMswheh8TeiC1sGZ3z2jUf3s37P0BBlrD3sjVTUw==", "integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -177,9 +178,9 @@
} }
}, },
"node_modules/@parcel/watcher-darwin-x64": { "node_modules/@parcel/watcher-darwin-x64": {
"version": "2.5.0", "version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.0.tgz", "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz",
"integrity": "sha512-9rhlwd78saKf18fT869/poydQK8YqlU26TMiNg7AIu7eBp9adqbJZqmdFOsbZ5cnLp5XvRo9wcFmNHgHdWaGYA==", "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -198,9 +199,9 @@
} }
}, },
"node_modules/@parcel/watcher-freebsd-x64": { "node_modules/@parcel/watcher-freebsd-x64": {
"version": "2.5.0", "version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.0.tgz", "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz",
"integrity": "sha512-syvfhZzyM8kErg3VF0xpV8dixJ+RzbUaaGaeb7uDuz0D3FK97/mZ5AJQ3XNnDsXX7KkFNtyQyFrXZzQIcN49Tw==", "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -219,9 +220,9 @@
} }
}, },
"node_modules/@parcel/watcher-linux-arm-glibc": { "node_modules/@parcel/watcher-linux-arm-glibc": {
"version": "2.5.0", "version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.0.tgz", "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz",
"integrity": "sha512-0VQY1K35DQET3dVYWpOaPFecqOT9dbuCfzjxoQyif1Wc574t3kOSkKevULddcR9znz1TcklCE7Ht6NIxjvTqLA==", "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@ -240,9 +241,9 @@
} }
}, },
"node_modules/@parcel/watcher-linux-arm-musl": { "node_modules/@parcel/watcher-linux-arm-musl": {
"version": "2.5.0", "version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.0.tgz", "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz",
"integrity": "sha512-6uHywSIzz8+vi2lAzFeltnYbdHsDm3iIB57d4g5oaB9vKwjb6N6dRIgZMujw4nm5r6v9/BQH0noq6DzHrqr2pA==", "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@ -261,9 +262,9 @@
} }
}, },
"node_modules/@parcel/watcher-linux-arm64-glibc": { "node_modules/@parcel/watcher-linux-arm64-glibc": {
"version": "2.5.0", "version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.0.tgz", "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz",
"integrity": "sha512-BfNjXwZKxBy4WibDb/LDCriWSKLz+jJRL3cM/DllnHH5QUyoiUNEp3GmL80ZqxeumoADfCCP19+qiYiC8gUBjA==", "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -282,9 +283,9 @@
} }
}, },
"node_modules/@parcel/watcher-linux-arm64-musl": { "node_modules/@parcel/watcher-linux-arm64-musl": {
"version": "2.5.0", "version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.0.tgz", "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz",
"integrity": "sha512-S1qARKOphxfiBEkwLUbHjCY9BWPdWnW9j7f7Hb2jPplu8UZ3nes7zpPOW9bkLbHRvWM0WDTsjdOTUgW0xLBN1Q==", "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -303,9 +304,9 @@
} }
}, },
"node_modules/@parcel/watcher-linux-x64-glibc": { "node_modules/@parcel/watcher-linux-x64-glibc": {
"version": "2.5.0", "version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.0.tgz", "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz",
"integrity": "sha512-d9AOkusyXARkFD66S6zlGXyzx5RvY+chTP9Jp0ypSTC9d4lzyRs9ovGf/80VCxjKddcUvnsGwCHWuF2EoPgWjw==", "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -324,9 +325,9 @@
} }
}, },
"node_modules/@parcel/watcher-linux-x64-musl": { "node_modules/@parcel/watcher-linux-x64-musl": {
"version": "2.5.0", "version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.0.tgz", "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz",
"integrity": "sha512-iqOC+GoTDoFyk/VYSFHwjHhYrk8bljW6zOhPuhi5t9ulqiYq1togGJB5e3PwYVFFfeVgc6pbz3JdQyDoBszVaA==", "integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -345,9 +346,9 @@
} }
}, },
"node_modules/@parcel/watcher-win32-arm64": { "node_modules/@parcel/watcher-win32-arm64": {
"version": "2.5.0", "version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.0.tgz", "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz",
"integrity": "sha512-twtft1d+JRNkM5YbmexfcH/N4znDtjgysFaV9zvZmmJezQsKpkfLYJ+JFV3uygugK6AtIM2oADPkB2AdhBrNig==", "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -366,9 +367,9 @@
} }
}, },
"node_modules/@parcel/watcher-win32-ia32": { "node_modules/@parcel/watcher-win32-ia32": {
"version": "2.5.0", "version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.0.tgz", "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz",
"integrity": "sha512-+rgpsNRKwo8A53elqbbHXdOMtY/tAtTzManTWShB5Kk54N8Q9mzNWV7tV+IbGueCbcj826MfWGU3mprWtuf1TA==", "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@ -387,9 +388,9 @@
} }
}, },
"node_modules/@parcel/watcher-win32-x64": { "node_modules/@parcel/watcher-win32-x64": {
"version": "2.5.0", "version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.0.tgz", "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz",
"integrity": "sha512-lPrxve92zEHdgeff3aiu4gDOIt4u7sJYha6wbdEZDCDUhtjTsOMiaJzG5lMY4GkWH8p0fMmO2Ppq5G5XXG+DQw==", "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -455,9 +456,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "22.10.2", "version": "22.13.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.5.tgz",
"integrity": "sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==", "integrity": "sha512-+lTU0PxZXn0Dr1NBtC7Y8cR21AJr87dLLU953CWA6pMxxv/UDc7jYAY90upcrie1nRcD6XNG5HOYEDtgW5TxAg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -772,6 +773,7 @@
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true,
"dependencies": { "dependencies": {
"fill-range": "^7.1.1" "fill-range": "^7.1.1"
}, },
@ -780,9 +782,9 @@
} }
}, },
"node_modules/browserslist": { "node_modules/browserslist": {
"version": "4.24.3", "version": "4.24.4",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.3.tgz", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz",
"integrity": "sha512-1CPmv8iobE2fyRMV97dAcMVegvvWKxmq94hkLiAkUGwKVTyDLw33K+ZxiFrREKmmps4rIw6grcCFCnTMSZ/YiA==", "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@ -820,9 +822,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/caniuse-lite": { "node_modules/caniuse-lite": {
"version": "1.0.30001690", "version": "1.0.30001700",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001690.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001700.tgz",
"integrity": "sha512-5ExiE3qQN6oF8Clf8ifIDcMRCRE/dMGcETG/XGMD8/XiXm6HXQgQTh1yZYLXXpSOsEUlJm1Xr7kGULZTuGtP/w==", "integrity": "sha512-2S6XIXwaE7K7erT8dY+kLQcpa5ms63XlRkMkReXjle+kf6c5g38vyMl+Z5y8dSxOFDhcFe+nxnn261PLxBSQsQ==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@ -965,6 +967,7 @@
"integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"optional": true,
"bin": { "bin": {
"detect-libc": "bin/detect-libc.js" "detect-libc": "bin/detect-libc.js"
}, },
@ -973,16 +976,16 @@
} }
}, },
"node_modules/electron-to-chromium": { "node_modules/electron-to-chromium": {
"version": "1.5.75", "version": "1.5.103",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.75.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.103.tgz",
"integrity": "sha512-Lf3++DumRE/QmweGjU+ZcKqQ+3bKkU/qjaKYhIJKEOhgIO9Xs6IiAQFkfFoj+RhgDk4LUeNsLo6plExHqSyu6Q==", "integrity": "sha512-P6+XzIkfndgsrjROJWfSvVEgNHtPgbhVyTkwLjUM2HU/h7pZRORgaTlHqfAikqxKmdJMLW8fftrdGWbd/Ds0FA==",
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/enhanced-resolve": { "node_modules/enhanced-resolve": {
"version": "5.18.0", "version": "5.18.1",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.0.tgz", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz",
"integrity": "sha512-0/r0MySGYG8YqlayBZ6MuCfECmHFdJ5qyPh8s8wa5Hnm6SaFLSK1VYCbj+NKp090Nm1caZhD+QTnmxO7esYGyQ==", "integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -1007,9 +1010,9 @@
} }
}, },
"node_modules/es-module-lexer": { "node_modules/es-module-lexer": {
"version": "1.5.4", "version": "1.6.0",
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.6.0.tgz",
"integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==", "integrity": "sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
@ -1112,10 +1115,20 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/fast-uri": { "node_modules/fast-uri": {
"version": "3.0.3", "version": "3.0.6",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.3.tgz", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz",
"integrity": "sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==", "integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==",
"dev": true, "dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"license": "BSD-3-Clause" "license": "BSD-3-Clause"
}, },
"node_modules/fastest-levenshtein": { "node_modules/fastest-levenshtein": {
@ -1134,6 +1147,7 @@
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true,
"dependencies": { "dependencies": {
"to-regex-range": "^5.0.1" "to-regex-range": "^5.0.1"
}, },
@ -1235,9 +1249,9 @@
} }
}, },
"node_modules/immutable": { "node_modules/immutable": {
"version": "4.3.7", "version": "5.0.3",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.7.tgz", "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.0.3.tgz",
"integrity": "sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==", "integrity": "sha512-P8IdPQHq3lA1xVeBRi5VPqUm5HDgKnx0Ru51wZz5mjxHr5n3RWhjIpOFU7ybkUxfB+5IToy+OLaHYDBIWsv+uw==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
@ -1293,6 +1307,7 @@
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true,
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@ -1303,6 +1318,7 @@
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true,
"dependencies": { "dependencies": {
"is-extglob": "^2.1.1" "is-extglob": "^2.1.1"
}, },
@ -1316,6 +1332,7 @@
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true,
"engines": { "engines": {
"node": ">=0.12.0" "node": ">=0.12.0"
} }
@ -1431,6 +1448,7 @@
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true,
"dependencies": { "dependencies": {
"braces": "^3.0.3", "braces": "^3.0.3",
"picomatch": "^2.3.1" "picomatch": "^2.3.1"
@ -1514,7 +1532,8 @@
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT",
"optional": true
}, },
"node_modules/node-releases": { "node_modules/node-releases": {
"version": "2.0.19", "version": "2.0.19",
@ -1602,6 +1621,7 @@
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true,
"engines": { "engines": {
"node": ">=8.6" "node": ">=8.6"
}, },
@ -1623,9 +1643,9 @@
} }
}, },
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.4.49", "version": "8.5.3",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz",
"integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@ -1643,7 +1663,7 @@
], ],
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"nanoid": "^3.3.7", "nanoid": "^3.3.8",
"picocolors": "^1.1.1", "picocolors": "^1.1.1",
"source-map-js": "^1.2.1" "source-map-js": "^1.2.1"
}, },
@ -1715,9 +1735,9 @@
} }
}, },
"node_modules/postcss-selector-parser": { "node_modules/postcss-selector-parser": {
"version": "7.0.0", "version": "7.1.0",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.0.0.tgz", "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz",
"integrity": "sha512-9RbEr1Y7FFfptd/1eEdntyjMwLeghW1bHX9GWjXo19vx4ytPQhANltvVxDggzJl7mnWM+dX28kb6cyS/4iQjlQ==", "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -1756,13 +1776,13 @@
} }
}, },
"node_modules/readdirp": { "node_modules/readdirp": {
"version": "4.0.2", "version": "4.1.2",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
"integrity": "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==", "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 14.16.0" "node": ">= 14.18.0"
}, },
"funding": { "funding": {
"type": "individual", "type": "individual",
@ -1858,15 +1878,14 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/sass": { "node_modules/sass": {
"version": "1.79.5", "version": "1.85.0",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.79.5.tgz", "resolved": "https://registry.npmjs.org/sass/-/sass-1.85.0.tgz",
"integrity": "sha512-W1h5kp6bdhqFh2tk3DsI771MoEJjvrSY/2ihJRJS4pjIyfJCw0nTsxqhnrUzaLMOJjFchj8rOvraI/YUVjtx5g==", "integrity": "sha512-3ToiC1xZ1Y8aU7+CkgCI/tqyuPXEmYGJXO7H4uqp0xkLXUqp88rQQ4j1HmP37xSJLbCJPaIiv+cT1y+grssrww==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@parcel/watcher": "^2.4.1",
"chokidar": "^4.0.0", "chokidar": "^4.0.0",
"immutable": "^4.0.0", "immutable": "^5.0.2",
"source-map-js": ">=0.6.2 <2.0.0" "source-map-js": ">=0.6.2 <2.0.0"
}, },
"bin": { "bin": {
@ -1874,6 +1893,9 @@
}, },
"engines": { "engines": {
"node": ">=14.0.0" "node": ">=14.0.0"
},
"optionalDependencies": {
"@parcel/watcher": "^2.4.1"
} }
}, },
"node_modules/sass-loader": { "node_modules/sass-loader": {
@ -1938,9 +1960,9 @@
} }
}, },
"node_modules/semver": { "node_modules/semver": {
"version": "7.6.3", "version": "7.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
"integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
"dev": true, "dev": true,
"license": "ISC", "license": "ISC",
"bin": { "bin": {
@ -2067,9 +2089,9 @@
} }
}, },
"node_modules/terser": { "node_modules/terser": {
"version": "5.37.0", "version": "5.39.0",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.37.0.tgz", "resolved": "https://registry.npmjs.org/terser/-/terser-5.39.0.tgz",
"integrity": "sha512-B8wRRkmre4ERucLM/uXx4MOV5cbnOlVAqUst+1+iLKPI0dOgFO28f84ptoQt9HEI537PMzfYa/d+GEPKTRXmYA==", "integrity": "sha512-LBAhFyLho16harJoWMg/nZsQYgTrg5jXOn2nCYjRUcZZEdE3qa2zb8QEDRUGVZBW4rlazf2fxkg8tztybTaqWw==",
"dev": true, "dev": true,
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"dependencies": { "dependencies": {
@ -2126,6 +2148,7 @@
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true,
"dependencies": { "dependencies": {
"is-number": "^7.0.0" "is-number": "^7.0.0"
}, },
@ -2149,9 +2172,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/update-browserslist-db": { "node_modules/update-browserslist-db": {
"version": "1.1.1", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.2.tgz",
"integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", "integrity": "sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@ -2170,7 +2193,7 @@
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"escalade": "^3.2.0", "escalade": "^3.2.0",
"picocolors": "^1.1.0" "picocolors": "^1.1.1"
}, },
"bin": { "bin": {
"update-browserslist-db": "cli.js" "update-browserslist-db": "cli.js"

View File

@ -17,7 +17,7 @@
"css-loader": "7.1.2", "css-loader": "7.1.2",
"expose-loader": "5.0.0", "expose-loader": "5.0.0",
"mini-css-extract-plugin": "2.9.2", "mini-css-extract-plugin": "2.9.2",
"sass": "1.79.5", "sass": "1.85.0",
"sass-loader": "16.0.4", "sass-loader": "16.0.4",
"webpack": "5.97.1", "webpack": "5.97.1",
"webpack-cli": "5.1.4" "webpack-cli": "5.1.4"

View File

@ -13,6 +13,7 @@ using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Auth.Enums; using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Repositories; using Bit.Core.Auth.Repositories;
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
using Bit.Core.Billing.Pricing;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
@ -55,6 +56,7 @@ public class OrganizationUsersController : Controller
private readonly IDeleteManagedOrganizationUserAccountCommand _deleteManagedOrganizationUserAccountCommand; private readonly IDeleteManagedOrganizationUserAccountCommand _deleteManagedOrganizationUserAccountCommand;
private readonly IGetOrganizationUsersManagementStatusQuery _getOrganizationUsersManagementStatusQuery; private readonly IGetOrganizationUsersManagementStatusQuery _getOrganizationUsersManagementStatusQuery;
private readonly IFeatureService _featureService; private readonly IFeatureService _featureService;
private readonly IPricingClient _pricingClient;
public OrganizationUsersController( public OrganizationUsersController(
IOrganizationRepository organizationRepository, IOrganizationRepository organizationRepository,
@ -77,7 +79,8 @@ public class OrganizationUsersController : Controller
IRemoveOrganizationUserCommand removeOrganizationUserCommand, IRemoveOrganizationUserCommand removeOrganizationUserCommand,
IDeleteManagedOrganizationUserAccountCommand deleteManagedOrganizationUserAccountCommand, IDeleteManagedOrganizationUserAccountCommand deleteManagedOrganizationUserAccountCommand,
IGetOrganizationUsersManagementStatusQuery getOrganizationUsersManagementStatusQuery, IGetOrganizationUsersManagementStatusQuery getOrganizationUsersManagementStatusQuery,
IFeatureService featureService) IFeatureService featureService,
IPricingClient pricingClient)
{ {
_organizationRepository = organizationRepository; _organizationRepository = organizationRepository;
_organizationUserRepository = organizationUserRepository; _organizationUserRepository = organizationUserRepository;
@ -100,6 +103,7 @@ public class OrganizationUsersController : Controller
_deleteManagedOrganizationUserAccountCommand = deleteManagedOrganizationUserAccountCommand; _deleteManagedOrganizationUserAccountCommand = deleteManagedOrganizationUserAccountCommand;
_getOrganizationUsersManagementStatusQuery = getOrganizationUsersManagementStatusQuery; _getOrganizationUsersManagementStatusQuery = getOrganizationUsersManagementStatusQuery;
_featureService = featureService; _featureService = featureService;
_pricingClient = pricingClient;
} }
[HttpGet("{id}")] [HttpGet("{id}")]
@ -648,7 +652,9 @@ public class OrganizationUsersController : Controller
if (additionalSmSeatsRequired > 0) if (additionalSmSeatsRequired > 0)
{ {
var organization = await _organizationRepository.GetByIdAsync(orgId); var organization = await _organizationRepository.GetByIdAsync(orgId);
var update = new SecretsManagerSubscriptionUpdate(organization, true) // TODO: https://bitwarden.atlassian.net/browse/PM-17000
var plan = await _pricingClient.GetPlanOrThrow(organization!.PlanType);
var update = new SecretsManagerSubscriptionUpdate(organization, plan, true)
.AdjustSeats(additionalSmSeatsRequired); .AdjustSeats(additionalSmSeatsRequired);
await _updateSecretsManagerSubscriptionCommand.UpdateSubscriptionAsync(update); await _updateSecretsManagerSubscriptionCommand.UpdateSubscriptionAsync(update);
} }

View File

@ -22,6 +22,7 @@ using Bit.Core.Auth.Repositories;
using Bit.Core.Auth.Services; using Bit.Core.Auth.Services;
using Bit.Core.Billing.Enums; using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Extensions;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Services; using Bit.Core.Billing.Services;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Enums; using Bit.Core.Enums;
@ -60,6 +61,7 @@ public class OrganizationsController : Controller
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand; private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
private readonly ICloudOrganizationSignUpCommand _cloudOrganizationSignUpCommand; private readonly ICloudOrganizationSignUpCommand _cloudOrganizationSignUpCommand;
private readonly IOrganizationDeleteCommand _organizationDeleteCommand; private readonly IOrganizationDeleteCommand _organizationDeleteCommand;
private readonly IPricingClient _pricingClient;
public OrganizationsController( public OrganizationsController(
IOrganizationRepository organizationRepository, IOrganizationRepository organizationRepository,
@ -81,7 +83,8 @@ public class OrganizationsController : Controller
IDataProtectorTokenFactory<OrgDeleteTokenable> orgDeleteTokenDataFactory, IDataProtectorTokenFactory<OrgDeleteTokenable> orgDeleteTokenDataFactory,
IRemoveOrganizationUserCommand removeOrganizationUserCommand, IRemoveOrganizationUserCommand removeOrganizationUserCommand,
ICloudOrganizationSignUpCommand cloudOrganizationSignUpCommand, ICloudOrganizationSignUpCommand cloudOrganizationSignUpCommand,
IOrganizationDeleteCommand organizationDeleteCommand) IOrganizationDeleteCommand organizationDeleteCommand,
IPricingClient pricingClient)
{ {
_organizationRepository = organizationRepository; _organizationRepository = organizationRepository;
_organizationUserRepository = organizationUserRepository; _organizationUserRepository = organizationUserRepository;
@ -103,6 +106,7 @@ public class OrganizationsController : Controller
_removeOrganizationUserCommand = removeOrganizationUserCommand; _removeOrganizationUserCommand = removeOrganizationUserCommand;
_cloudOrganizationSignUpCommand = cloudOrganizationSignUpCommand; _cloudOrganizationSignUpCommand = cloudOrganizationSignUpCommand;
_organizationDeleteCommand = organizationDeleteCommand; _organizationDeleteCommand = organizationDeleteCommand;
_pricingClient = pricingClient;
} }
[HttpGet("{id}")] [HttpGet("{id}")]
@ -120,7 +124,8 @@ public class OrganizationsController : Controller
throw new NotFoundException(); throw new NotFoundException();
} }
return new OrganizationResponseModel(organization); var plan = await _pricingClient.GetPlan(organization.PlanType);
return new OrganizationResponseModel(organization, plan);
} }
[HttpGet("")] [HttpGet("")]
@ -181,7 +186,8 @@ public class OrganizationsController : Controller
var organizationSignup = model.ToOrganizationSignup(user); var organizationSignup = model.ToOrganizationSignup(user);
var result = await _cloudOrganizationSignUpCommand.SignUpOrganizationAsync(organizationSignup); var result = await _cloudOrganizationSignUpCommand.SignUpOrganizationAsync(organizationSignup);
return new OrganizationResponseModel(result.Organization); var plan = await _pricingClient.GetPlanOrThrow(result.Organization.PlanType);
return new OrganizationResponseModel(result.Organization, plan);
} }
[HttpPost("create-without-payment")] [HttpPost("create-without-payment")]
@ -196,7 +202,8 @@ public class OrganizationsController : Controller
var organizationSignup = model.ToOrganizationSignup(user); var organizationSignup = model.ToOrganizationSignup(user);
var result = await _cloudOrganizationSignUpCommand.SignUpOrganizationAsync(organizationSignup); var result = await _cloudOrganizationSignUpCommand.SignUpOrganizationAsync(organizationSignup);
return new OrganizationResponseModel(result.Organization); var plan = await _pricingClient.GetPlanOrThrow(result.Organization.PlanType);
return new OrganizationResponseModel(result.Organization, plan);
} }
[HttpPut("{id}")] [HttpPut("{id}")]
@ -224,7 +231,8 @@ public class OrganizationsController : Controller
} }
await _organizationService.UpdateAsync(model.ToOrganization(organization, _globalSettings), updateBilling); await _organizationService.UpdateAsync(model.ToOrganization(organization, _globalSettings), updateBilling);
return new OrganizationResponseModel(organization); var plan = await _pricingClient.GetPlan(organization.PlanType);
return new OrganizationResponseModel(organization, plan);
} }
[HttpPost("{id}/storage")] [HttpPost("{id}/storage")]
@ -358,8 +366,8 @@ public class OrganizationsController : Controller
if (model.Type == OrganizationApiKeyType.BillingSync || model.Type == OrganizationApiKeyType.Scim) if (model.Type == OrganizationApiKeyType.BillingSync || model.Type == OrganizationApiKeyType.Scim)
{ {
// Non-enterprise orgs should not be able to create or view an apikey of billing sync/scim key types // Non-enterprise orgs should not be able to create or view an apikey of billing sync/scim key types
var plan = StaticStore.GetPlan(organization.PlanType); var productTier = organization.PlanType.GetProductTier();
if (plan.ProductTier is not ProductTierType.Enterprise and not ProductTierType.Teams) if (productTier is not ProductTierType.Enterprise and not ProductTierType.Teams)
{ {
throw new NotFoundException(); throw new NotFoundException();
} }
@ -542,7 +550,8 @@ public class OrganizationsController : Controller
} }
await _organizationService.UpdateAsync(model.ToOrganization(organization, _featureService), eventType: EventType.Organization_CollectionManagement_Updated); await _organizationService.UpdateAsync(model.ToOrganization(organization, _featureService), eventType: EventType.Organization_CollectionManagement_Updated);
return new OrganizationResponseModel(organization); var plan = await _pricingClient.GetPlan(organization.PlanType);
return new OrganizationResponseModel(organization, plan);
} }
[HttpGet("{id}/plan-type")] [HttpGet("{id}/plan-type")]

View File

@ -4,6 +4,7 @@ using Bit.Core.AdminConsole.Entities;
using Bit.Core.Billing.Enums; using Bit.Core.Billing.Enums;
using Bit.Core.Models.Api; using Bit.Core.Models.Api;
using Bit.Core.Models.Business; using Bit.Core.Models.Business;
using Bit.Core.Models.StaticStore;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using Constants = Bit.Core.Constants; using Constants = Bit.Core.Constants;
@ -11,8 +12,10 @@ namespace Bit.Api.AdminConsole.Models.Response.Organizations;
public class OrganizationResponseModel : ResponseModel public class OrganizationResponseModel : ResponseModel
{ {
public OrganizationResponseModel(Organization organization, string obj = "organization") public OrganizationResponseModel(
: base(obj) Organization organization,
Plan plan,
string obj = "organization") : base(obj)
{ {
if (organization == null) if (organization == null)
{ {
@ -28,7 +31,8 @@ public class OrganizationResponseModel : ResponseModel
BusinessCountry = organization.BusinessCountry; BusinessCountry = organization.BusinessCountry;
BusinessTaxNumber = organization.BusinessTaxNumber; BusinessTaxNumber = organization.BusinessTaxNumber;
BillingEmail = organization.BillingEmail; BillingEmail = organization.BillingEmail;
Plan = new PlanResponseModel(StaticStore.GetPlan(organization.PlanType)); // Self-Host instances only require plan information that can be derived from the Organization record.
Plan = plan != null ? new PlanResponseModel(plan) : new PlanResponseModel(organization);
PlanType = organization.PlanType; PlanType = organization.PlanType;
Seats = organization.Seats; Seats = organization.Seats;
MaxAutoscaleSeats = organization.MaxAutoscaleSeats; MaxAutoscaleSeats = organization.MaxAutoscaleSeats;
@ -110,7 +114,9 @@ public class OrganizationResponseModel : ResponseModel
public class OrganizationSubscriptionResponseModel : OrganizationResponseModel public class OrganizationSubscriptionResponseModel : OrganizationResponseModel
{ {
public OrganizationSubscriptionResponseModel(Organization organization) : base(organization, "organizationSubscription") public OrganizationSubscriptionResponseModel(
Organization organization,
Plan plan) : base(organization, plan, "organizationSubscription")
{ {
Expiration = organization.ExpirationDate; Expiration = organization.ExpirationDate;
StorageName = organization.Storage.HasValue ? StorageName = organization.Storage.HasValue ?
@ -119,8 +125,11 @@ public class OrganizationSubscriptionResponseModel : OrganizationResponseModel
Math.Round(organization.Storage.Value / 1073741824D, 2) : 0; // 1 GB Math.Round(organization.Storage.Value / 1073741824D, 2) : 0; // 1 GB
} }
public OrganizationSubscriptionResponseModel(Organization organization, SubscriptionInfo subscription, bool hideSensitiveData) public OrganizationSubscriptionResponseModel(
: this(organization) Organization organization,
SubscriptionInfo subscription,
Plan plan,
bool hideSensitiveData) : this(organization, plan)
{ {
Subscription = subscription.Subscription != null ? new BillingSubscription(subscription.Subscription) : null; Subscription = subscription.Subscription != null ? new BillingSubscription(subscription.Subscription) : null;
UpcomingInvoice = subscription.UpcomingInvoice != null ? new BillingSubscriptionUpcomingInvoice(subscription.UpcomingInvoice) : null; UpcomingInvoice = subscription.UpcomingInvoice != null ? new BillingSubscriptionUpcomingInvoice(subscription.UpcomingInvoice) : null;
@ -142,7 +151,7 @@ public class OrganizationSubscriptionResponseModel : OrganizationResponseModel
} }
public OrganizationSubscriptionResponseModel(Organization organization, OrganizationLicense license) : public OrganizationSubscriptionResponseModel(Organization organization, OrganizationLicense license) :
this(organization) this(organization, (Plan)null)
{ {
if (license != null) if (license != null)
{ {

View File

@ -3,6 +3,7 @@ using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.Auth.Enums; using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Models.Data; using Bit.Core.Auth.Models.Data;
using Bit.Core.Billing.Enums; using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Extensions;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Models.Api; using Bit.Core.Models.Api;
using Bit.Core.Models.Data; using Bit.Core.Models.Data;
@ -37,7 +38,7 @@ public class ProfileOrganizationResponseModel : ResponseModel
UsePasswordManager = organization.UsePasswordManager; UsePasswordManager = organization.UsePasswordManager;
UsersGetPremium = organization.UsersGetPremium; UsersGetPremium = organization.UsersGetPremium;
UseCustomPermissions = organization.UseCustomPermissions; UseCustomPermissions = organization.UseCustomPermissions;
UseActivateAutofillPolicy = StaticStore.GetPlan(organization.PlanType).ProductTier == ProductTierType.Enterprise; UseActivateAutofillPolicy = organization.PlanType.GetProductTier() == ProductTierType.Enterprise;
SelfHost = organization.SelfHost; SelfHost = organization.SelfHost;
Seats = organization.Seats; Seats = organization.Seats;
MaxCollections = organization.MaxCollections; MaxCollections = organization.MaxCollections;
@ -60,7 +61,7 @@ public class ProfileOrganizationResponseModel : ResponseModel
FamilySponsorshipAvailable = FamilySponsorshipFriendlyName == null && FamilySponsorshipAvailable = FamilySponsorshipFriendlyName == null &&
StaticStore.GetSponsoredPlan(PlanSponsorshipType.FamiliesForEnterprise) StaticStore.GetSponsoredPlan(PlanSponsorshipType.FamiliesForEnterprise)
.UsersCanSponsor(organization); .UsersCanSponsor(organization);
ProductTierType = StaticStore.GetPlan(organization.PlanType).ProductTier; ProductTierType = organization.PlanType.GetProductTier();
FamilySponsorshipLastSyncDate = organization.FamilySponsorshipLastSyncDate; FamilySponsorshipLastSyncDate = organization.FamilySponsorshipLastSyncDate;
FamilySponsorshipToDelete = organization.FamilySponsorshipToDelete; FamilySponsorshipToDelete = organization.FamilySponsorshipToDelete;
FamilySponsorshipValidUntil = organization.FamilySponsorshipValidUntil; FamilySponsorshipValidUntil = organization.FamilySponsorshipValidUntil;

View File

@ -1,8 +1,8 @@
using Bit.Core.AdminConsole.Models.Data.Provider; using Bit.Core.AdminConsole.Models.Data.Provider;
using Bit.Core.Billing.Enums; using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Extensions;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Models.Data; using Bit.Core.Models.Data;
using Bit.Core.Utilities;
namespace Bit.Api.AdminConsole.Models.Response; namespace Bit.Api.AdminConsole.Models.Response;
@ -26,7 +26,7 @@ public class ProfileProviderOrganizationResponseModel : ProfileOrganizationRespo
UseResetPassword = organization.UseResetPassword; UseResetPassword = organization.UseResetPassword;
UsersGetPremium = organization.UsersGetPremium; UsersGetPremium = organization.UsersGetPremium;
UseCustomPermissions = organization.UseCustomPermissions; UseCustomPermissions = organization.UseCustomPermissions;
UseActivateAutofillPolicy = StaticStore.GetPlan(organization.PlanType).ProductTier == ProductTierType.Enterprise; UseActivateAutofillPolicy = organization.PlanType.GetProductTier() == ProductTierType.Enterprise;
SelfHost = organization.SelfHost; SelfHost = organization.SelfHost;
Seats = organization.Seats; Seats = organization.Seats;
MaxCollections = organization.MaxCollections; MaxCollections = organization.MaxCollections;
@ -44,7 +44,7 @@ public class ProfileProviderOrganizationResponseModel : ProfileOrganizationRespo
ProviderId = organization.ProviderId; ProviderId = organization.ProviderId;
ProviderName = organization.ProviderName; ProviderName = organization.ProviderName;
ProviderType = organization.ProviderType; ProviderType = organization.ProviderType;
ProductTierType = StaticStore.GetPlan(organization.PlanType).ProductTier; ProductTierType = organization.PlanType.GetProductTier();
LimitCollectionCreation = organization.LimitCollectionCreation; LimitCollectionCreation = organization.LimitCollectionCreation;
LimitCollectionDeletion = organization.LimitCollectionDeletion; LimitCollectionDeletion = organization.LimitCollectionDeletion;
LimitItemDeletion = organization.LimitItemDeletion; LimitItemDeletion = organization.LimitItemDeletion;

View File

@ -4,6 +4,8 @@
<MvcRazorCompileOnPublish>false</MvcRazorCompileOnPublish> <MvcRazorCompileOnPublish>false</MvcRazorCompileOnPublish>
<DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile> <DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile>
<ANCMPreConfiguredForIIS>true</ANCMPreConfiguredForIIS> <ANCMPreConfiguredForIIS>true</ANCMPreConfiguredForIIS>
<!-- Temp exclusions until warnings are fixed -->
<WarningsNotAsErrors>$(WarningsNotAsErrors);CS8604</WarningsNotAsErrors>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">

View File

@ -149,11 +149,11 @@ public class AccountsController : Controller
throw new BadRequestException("MasterPasswordHash", "Invalid password."); throw new BadRequestException("MasterPasswordHash", "Invalid password.");
} }
// If Account Deprovisioning is enabled, we need to check if the user is managed by any organization. var managedUserValidationResult = await _userService.ValidateManagedUserDomainAsync(user, model.NewEmail);
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
&& await _userService.IsManagedByAnyOrganizationAsync(user.Id)) if (!managedUserValidationResult.Succeeded)
{ {
throw new BadRequestException("Cannot change emails for accounts owned by an organization. Contact your organization administrator for additional details."); throw new BadRequestException(managedUserValidationResult.Errors);
} }
await _userService.InitiateEmailChangeAsync(user, model.NewEmail); await _userService.InitiateEmailChangeAsync(user, model.NewEmail);
@ -173,13 +173,6 @@ public class AccountsController : Controller
throw new BadRequestException("You cannot change your email when using Key Connector."); throw new BadRequestException("You cannot change your email when using Key Connector.");
} }
// If Account Deprovisioning is enabled, we need to check if the user is managed by any organization.
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
&& await _userService.IsManagedByAnyOrganizationAsync(user.Id))
{
throw new BadRequestException("Cannot change emails for accounts owned by an organization. Contact your organization administrator for additional details.");
}
var result = await _userService.ChangeEmailAsync(user, model.MasterPasswordHash, model.NewEmail, var result = await _userService.ChangeEmailAsync(user, model.MasterPasswordHash, model.NewEmail,
model.NewMasterPasswordHash, model.Token, model.Key); model.NewMasterPasswordHash, model.Token, model.Key);
if (result.Succeeded) if (result.Succeeded)

View File

@ -288,12 +288,17 @@ public class TwoFactorController : Controller
return response; return response;
} }
/// <summary>
/// This endpoint is only used to set-up email two factor authentication.
/// </summary>
/// <param name="model">secret verification model</param>
/// <returns>void</returns>
[HttpPost("send-email")] [HttpPost("send-email")]
public async Task SendEmail([FromBody] TwoFactorEmailRequestModel model) public async Task SendEmail([FromBody] TwoFactorEmailRequestModel model)
{ {
var user = await CheckAsync(model, false, true); var user = await CheckAsync(model, false, true);
model.ToUser(user); model.ToUser(user);
await _userService.SendTwoFactorEmailAsync(user); await _userService.SendTwoFactorEmailAsync(user, false);
} }
[AllowAnonymous] [AllowAnonymous]
@ -304,7 +309,7 @@ public class TwoFactorController : Controller
if (user != null) if (user != null)
{ {
// check if 2FA email is from passwordless // Check if 2FA email is from Passwordless.
if (!string.IsNullOrEmpty(requestModel.AuthRequestAccessCode)) if (!string.IsNullOrEmpty(requestModel.AuthRequestAccessCode))
{ {
if (await _verifyAuthRequestCommand if (await _verifyAuthRequestCommand
@ -317,17 +322,14 @@ public class TwoFactorController : Controller
} }
else if (!string.IsNullOrEmpty(requestModel.SsoEmail2FaSessionToken)) else if (!string.IsNullOrEmpty(requestModel.SsoEmail2FaSessionToken))
{ {
if (this.ValidateSsoEmail2FaToken(requestModel.SsoEmail2FaSessionToken, user)) if (ValidateSsoEmail2FaToken(requestModel.SsoEmail2FaSessionToken, user))
{ {
await _userService.SendTwoFactorEmailAsync(user); await _userService.SendTwoFactorEmailAsync(user);
return; return;
} }
else
{ await ThrowDelayedBadRequestExceptionAsync(
await this.ThrowDelayedBadRequestExceptionAsync( "Cannot send two-factor email: a valid, non-expired SSO Email 2FA Session token is required to send 2FA emails.");
"Cannot send two-factor email: a valid, non-expired SSO Email 2FA Session token is required to send 2FA emails.",
2000);
}
} }
else if (await _userService.VerifySecretAsync(user, requestModel.Secret)) else if (await _userService.VerifySecretAsync(user, requestModel.Secret))
{ {
@ -336,8 +338,7 @@ public class TwoFactorController : Controller
} }
} }
await this.ThrowDelayedBadRequestExceptionAsync( await ThrowDelayedBadRequestExceptionAsync("Cannot send two-factor email.");
"Cannot send two-factor email.", 2000);
} }
[HttpPut("email")] [HttpPut("email")]
@ -374,7 +375,7 @@ public class TwoFactorController : Controller
public async Task<TwoFactorProviderResponseModel> PutOrganizationDisable(string id, public async Task<TwoFactorProviderResponseModel> PutOrganizationDisable(string id,
[FromBody] TwoFactorProviderRequestModel model) [FromBody] TwoFactorProviderRequestModel model)
{ {
var user = await CheckAsync(model, false); await CheckAsync(model, false);
var orgIdGuid = new Guid(id); var orgIdGuid = new Guid(id);
if (!await _currentContext.ManagePolicies(orgIdGuid)) if (!await _currentContext.ManagePolicies(orgIdGuid))
@ -401,6 +402,10 @@ public class TwoFactorController : Controller
return response; return response;
} }
/// <summary>
/// To be removed when the feature flag pm-17128-recovery-code-login is removed PM-18175.
/// </summary>
[Obsolete("Two Factor recovery is handled in the TwoFactorAuthenticationValidator.")]
[HttpPost("recover")] [HttpPost("recover")]
[AllowAnonymous] [AllowAnonymous]
public async Task PostRecover([FromBody] TwoFactorRecoveryRequestModel model) public async Task PostRecover([FromBody] TwoFactorRecoveryRequestModel model)
@ -463,11 +468,9 @@ public class TwoFactorController : Controller
await Task.Delay(2000); await Task.Delay(2000);
throw new BadRequestException(name, $"{name} is invalid."); throw new BadRequestException(name, $"{name} is invalid.");
} }
else
{
await Task.Delay(500); await Task.Delay(500);
} }
}
private bool ValidateSsoEmail2FaToken(string ssoEmail2FaSessionToken, User user) private bool ValidateSsoEmail2FaToken(string ssoEmail2FaSessionToken, User user)
{ {

View File

@ -18,6 +18,7 @@ public class AuthRequestResponseModel : ResponseModel
Id = authRequest.Id; Id = authRequest.Id;
PublicKey = authRequest.PublicKey; PublicKey = authRequest.PublicKey;
RequestDeviceIdentifier = authRequest.RequestDeviceIdentifier;
RequestDeviceTypeValue = authRequest.RequestDeviceType; RequestDeviceTypeValue = authRequest.RequestDeviceType;
RequestDeviceType = authRequest.RequestDeviceType.GetType().GetMember(authRequest.RequestDeviceType.ToString()) RequestDeviceType = authRequest.RequestDeviceType.GetType().GetMember(authRequest.RequestDeviceType.ToString())
.FirstOrDefault()?.GetCustomAttribute<DisplayAttribute>()?.GetName(); .FirstOrDefault()?.GetCustomAttribute<DisplayAttribute>()?.GetName();
@ -32,6 +33,7 @@ public class AuthRequestResponseModel : ResponseModel
public Guid Id { get; set; } public Guid Id { get; set; }
public string PublicKey { get; set; } public string PublicKey { get; set; }
public string RequestDeviceIdentifier { get; set; }
public DeviceType RequestDeviceTypeValue { get; set; } public DeviceType RequestDeviceTypeValue { get; set; }
public string RequestDeviceType { get; set; } public string RequestDeviceType { get; set; }
public string RequestIpAddress { get; set; } public string RequestIpAddress { get; set; }

View File

@ -4,6 +4,7 @@ using Bit.Api.Billing.Models.Requests;
using Bit.Api.Billing.Models.Responses; using Bit.Api.Billing.Models.Responses;
using Bit.Core.Billing.Models; using Bit.Core.Billing.Models;
using Bit.Core.Billing.Models.Sales; using Bit.Core.Billing.Models.Sales;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Services; using Bit.Core.Billing.Services;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Repositories; using Bit.Core.Repositories;
@ -21,6 +22,7 @@ public class OrganizationBillingController(
IOrganizationBillingService organizationBillingService, IOrganizationBillingService organizationBillingService,
IOrganizationRepository organizationRepository, IOrganizationRepository organizationRepository,
IPaymentService paymentService, IPaymentService paymentService,
IPricingClient pricingClient,
ISubscriberService subscriberService, ISubscriberService subscriberService,
IPaymentHistoryService paymentHistoryService, IPaymentHistoryService paymentHistoryService,
IUserService userService) : BaseBillingController IUserService userService) : BaseBillingController
@ -279,7 +281,7 @@ public class OrganizationBillingController(
} }
var organizationSignup = model.ToOrganizationSignup(user); var organizationSignup = model.ToOrganizationSignup(user);
var sale = OrganizationSale.From(organization, organizationSignup); var sale = OrganizationSale.From(organization, organizationSignup);
var plan = StaticStore.GetPlan(model.PlanType); var plan = await pricingClient.GetPlanOrThrow(model.PlanType);
sale.Organization.PlanType = plan.Type; sale.Organization.PlanType = plan.Type;
sale.Organization.Plan = plan.Name; sale.Organization.Plan = plan.Name;
sale.SubscriptionSetup.SkipTrial = true; sale.SubscriptionSetup.SkipTrial = true;

View File

@ -8,6 +8,7 @@ using Bit.Core.AdminConsole.Entities;
using Bit.Core.Billing.Constants; using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Entities; using Bit.Core.Billing.Entities;
using Bit.Core.Billing.Models; using Bit.Core.Billing.Models;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Repositories; using Bit.Core.Billing.Repositories;
using Bit.Core.Billing.Services; using Bit.Core.Billing.Services;
using Bit.Core.Context; using Bit.Core.Context;
@ -45,7 +46,8 @@ public class OrganizationsController(
IAddSecretsManagerSubscriptionCommand addSecretsManagerSubscriptionCommand, IAddSecretsManagerSubscriptionCommand addSecretsManagerSubscriptionCommand,
IReferenceEventService referenceEventService, IReferenceEventService referenceEventService,
ISubscriberService subscriberService, ISubscriberService subscriberService,
IOrganizationInstallationRepository organizationInstallationRepository) IOrganizationInstallationRepository organizationInstallationRepository,
IPricingClient pricingClient)
: Controller : Controller
{ {
[HttpGet("{id:guid}/subscription")] [HttpGet("{id:guid}/subscription")]
@ -62,8 +64,19 @@ public class OrganizationsController(
throw new NotFoundException(); throw new NotFoundException();
} }
if (!globalSettings.SelfHosted && organization.Gateway != null) if (globalSettings.SelfHosted)
{ {
var orgLicense = await licensingService.ReadOrganizationLicenseAsync(organization);
return new OrganizationSubscriptionResponseModel(organization, orgLicense);
}
var plan = await pricingClient.GetPlanOrThrow(organization.PlanType);
if (string.IsNullOrEmpty(organization.GatewaySubscriptionId))
{
return new OrganizationSubscriptionResponseModel(organization, plan);
}
var subscriptionInfo = await paymentService.GetSubscriptionAsync(organization); var subscriptionInfo = await paymentService.GetSubscriptionAsync(organization);
if (subscriptionInfo == null) if (subscriptionInfo == null)
{ {
@ -72,16 +85,7 @@ public class OrganizationsController(
var hideSensitiveData = !await currentContext.EditSubscription(id); var hideSensitiveData = !await currentContext.EditSubscription(id);
return new OrganizationSubscriptionResponseModel(organization, subscriptionInfo, hideSensitiveData); return new OrganizationSubscriptionResponseModel(organization, subscriptionInfo, plan, hideSensitiveData);
}
if (globalSettings.SelfHosted)
{
var orgLicense = await licensingService.ReadOrganizationLicenseAsync(organization);
return new OrganizationSubscriptionResponseModel(organization, orgLicense);
}
return new OrganizationSubscriptionResponseModel(organization);
} }
[HttpGet("{id:guid}/license")] [HttpGet("{id:guid}/license")]
@ -165,7 +169,8 @@ public class OrganizationsController(
organization = await AdjustOrganizationSeatsForSmTrialAsync(id, organization, model); organization = await AdjustOrganizationSeatsForSmTrialAsync(id, organization, model);
var organizationUpdate = model.ToSecretsManagerSubscriptionUpdate(organization); var plan = await pricingClient.GetPlanOrThrow(organization.PlanType);
var organizationUpdate = model.ToSecretsManagerSubscriptionUpdate(organization, plan);
await updateSecretsManagerSubscriptionCommand.UpdateSubscriptionAsync(organizationUpdate); await updateSecretsManagerSubscriptionCommand.UpdateSubscriptionAsync(organizationUpdate);

View File

@ -2,6 +2,7 @@
using Bit.Api.Billing.Models.Responses; using Bit.Api.Billing.Models.Responses;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Models; using Bit.Core.Billing.Models;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Repositories; using Bit.Core.Billing.Repositories;
using Bit.Core.Billing.Services; using Bit.Core.Billing.Services;
using Bit.Core.Context; using Bit.Core.Context;
@ -20,6 +21,7 @@ namespace Bit.Api.Billing.Controllers;
public class ProviderBillingController( public class ProviderBillingController(
ICurrentContext currentContext, ICurrentContext currentContext,
ILogger<BaseProviderController> logger, ILogger<BaseProviderController> logger,
IPricingClient pricingClient,
IProviderBillingService providerBillingService, IProviderBillingService providerBillingService,
IProviderPlanRepository providerPlanRepository, IProviderPlanRepository providerPlanRepository,
IProviderRepository providerRepository, IProviderRepository providerRepository,
@ -84,13 +86,25 @@ public class ProviderBillingController(
var providerPlans = await providerPlanRepository.GetByProviderId(provider.Id); var providerPlans = await providerPlanRepository.GetByProviderId(provider.Id);
var configuredProviderPlans = await Task.WhenAll(providerPlans.Select(async providerPlan =>
{
var plan = await pricingClient.GetPlanOrThrow(providerPlan.PlanType);
return new ConfiguredProviderPlan(
providerPlan.Id,
providerPlan.ProviderId,
plan,
providerPlan.SeatMinimum ?? 0,
providerPlan.PurchasedSeats ?? 0,
providerPlan.AllocatedSeats ?? 0);
}));
var taxInformation = GetTaxInformation(subscription.Customer); var taxInformation = GetTaxInformation(subscription.Customer);
var subscriptionSuspension = await GetSubscriptionSuspensionAsync(stripeAdapter, subscription); var subscriptionSuspension = await GetSubscriptionSuspensionAsync(stripeAdapter, subscription);
var response = ProviderSubscriptionResponse.From( var response = ProviderSubscriptionResponse.From(
subscription, subscription,
providerPlans, configuredProviderPlans,
taxInformation, taxInformation,
subscriptionSuspension, subscriptionSuspension,
provider); provider);

View File

@ -1,9 +1,7 @@
using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.Billing.Entities;
using Bit.Core.Billing.Enums; using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Models; using Bit.Core.Billing.Models;
using Bit.Core.Utilities;
using Stripe; using Stripe;
namespace Bit.Api.Billing.Models.Responses; namespace Bit.Api.Billing.Models.Responses;
@ -25,26 +23,24 @@ public record ProviderSubscriptionResponse(
public static ProviderSubscriptionResponse From( public static ProviderSubscriptionResponse From(
Subscription subscription, Subscription subscription,
ICollection<ProviderPlan> providerPlans, ICollection<ConfiguredProviderPlan> providerPlans,
TaxInformation taxInformation, TaxInformation taxInformation,
SubscriptionSuspension subscriptionSuspension, SubscriptionSuspension subscriptionSuspension,
Provider provider) Provider provider)
{ {
var providerPlanResponses = providerPlans var providerPlanResponses = providerPlans
.Where(providerPlan => providerPlan.IsConfigured()) .Select(providerPlan =>
.Select(ConfiguredProviderPlan.From)
.Select(configuredProviderPlan =>
{ {
var plan = StaticStore.GetPlan(configuredProviderPlan.PlanType); var plan = providerPlan.Plan;
var cost = (configuredProviderPlan.SeatMinimum + configuredProviderPlan.PurchasedSeats) * plan.PasswordManager.ProviderPortalSeatPrice; var cost = (providerPlan.SeatMinimum + providerPlan.PurchasedSeats) * plan.PasswordManager.ProviderPortalSeatPrice;
var cadence = plan.IsAnnual ? _annualCadence : _monthlyCadence; var cadence = plan.IsAnnual ? _annualCadence : _monthlyCadence;
return new ProviderPlanResponse( return new ProviderPlanResponse(
plan.Name, plan.Name,
plan.Type, plan.Type,
plan.ProductTier, plan.ProductTier,
configuredProviderPlan.SeatMinimum, providerPlan.SeatMinimum,
configuredProviderPlan.PurchasedSeats, providerPlan.PurchasedSeats,
configuredProviderPlan.AssignedSeats, providerPlan.AssignedSeats,
cost, cost,
cadence); cadence);
}); });

View File

@ -1,6 +1,7 @@
using System.Net; using System.Net;
using Bit.Api.Billing.Public.Models; using Bit.Api.Billing.Public.Models;
using Bit.Api.Models.Public.Response; using Bit.Api.Models.Public.Response;
using Bit.Core.Billing.Pricing;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface; using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
using Bit.Core.Repositories; using Bit.Core.Repositories;
@ -21,19 +22,22 @@ public class OrganizationController : Controller
private readonly IOrganizationRepository _organizationRepository; private readonly IOrganizationRepository _organizationRepository;
private readonly IUpdateSecretsManagerSubscriptionCommand _updateSecretsManagerSubscriptionCommand; private readonly IUpdateSecretsManagerSubscriptionCommand _updateSecretsManagerSubscriptionCommand;
private readonly ILogger<OrganizationController> _logger; private readonly ILogger<OrganizationController> _logger;
private readonly IPricingClient _pricingClient;
public OrganizationController( public OrganizationController(
IOrganizationService organizationService, IOrganizationService organizationService,
ICurrentContext currentContext, ICurrentContext currentContext,
IOrganizationRepository organizationRepository, IOrganizationRepository organizationRepository,
IUpdateSecretsManagerSubscriptionCommand updateSecretsManagerSubscriptionCommand, IUpdateSecretsManagerSubscriptionCommand updateSecretsManagerSubscriptionCommand,
ILogger<OrganizationController> logger) ILogger<OrganizationController> logger,
IPricingClient pricingClient)
{ {
_organizationService = organizationService; _organizationService = organizationService;
_currentContext = currentContext; _currentContext = currentContext;
_organizationRepository = organizationRepository; _organizationRepository = organizationRepository;
_updateSecretsManagerSubscriptionCommand = updateSecretsManagerSubscriptionCommand; _updateSecretsManagerSubscriptionCommand = updateSecretsManagerSubscriptionCommand;
_logger = logger; _logger = logger;
_pricingClient = pricingClient;
} }
/// <summary> /// <summary>
@ -140,7 +144,8 @@ public class OrganizationController : Controller
return "Organization has no access to Secrets Manager."; return "Organization has no access to Secrets Manager.";
} }
var secretsManagerUpdate = model.SecretsManager.ToSecretsManagerSubscriptionUpdate(organization); var plan = await _pricingClient.GetPlanOrThrow(organization.PlanType);
var secretsManagerUpdate = model.SecretsManager.ToSecretsManagerSubscriptionUpdate(organization, plan);
await _updateSecretsManagerSubscriptionCommand.UpdateSubscriptionAsync(secretsManagerUpdate); await _updateSecretsManagerSubscriptionCommand.UpdateSubscriptionAsync(secretsManagerUpdate);
return string.Empty; return string.Empty;

View File

@ -1,6 +1,7 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities;
using Bit.Core.Models.Business; using Bit.Core.Models.Business;
using Bit.Core.Models.StaticStore;
namespace Bit.Api.Billing.Public.Models; namespace Bit.Api.Billing.Public.Models;
@ -93,17 +94,17 @@ public class SecretsManagerSubscriptionUpdateModel
set { _maxAutoScaleServiceAccounts = value < 0 ? null : value; } set { _maxAutoScaleServiceAccounts = value < 0 ? null : value; }
} }
public virtual SecretsManagerSubscriptionUpdate ToSecretsManagerSubscriptionUpdate(Organization organization) public virtual SecretsManagerSubscriptionUpdate ToSecretsManagerSubscriptionUpdate(Organization organization, Plan plan)
{ {
var update = UpdateUpdateMaxAutoScale(organization); var update = UpdateUpdateMaxAutoScale(organization, plan);
UpdateSeats(organization, update); UpdateSeats(organization, update);
UpdateServiceAccounts(organization, update); UpdateServiceAccounts(organization, update);
return update; return update;
} }
private SecretsManagerSubscriptionUpdate UpdateUpdateMaxAutoScale(Organization organization) private SecretsManagerSubscriptionUpdate UpdateUpdateMaxAutoScale(Organization organization, Plan plan)
{ {
var update = new SecretsManagerSubscriptionUpdate(organization, false) var update = new SecretsManagerSubscriptionUpdate(organization, plan, false)
{ {
MaxAutoscaleSmSeats = MaxAutoScaleSeats ?? organization.MaxAutoscaleSmSeats, MaxAutoscaleSmSeats = MaxAutoScaleSeats ?? organization.MaxAutoscaleSmSeats,
MaxAutoscaleSmServiceAccounts = MaxAutoScaleServiceAccounts ?? organization.MaxAutoscaleSmServiceAccounts MaxAutoscaleSmServiceAccounts = MaxAutoScaleServiceAccounts ?? organization.MaxAutoscaleSmServiceAccounts

View File

@ -23,6 +23,6 @@ public class ConfigController : Controller
[HttpGet("")] [HttpGet("")]
public ConfigResponseModel GetConfigs() public ConfigResponseModel GetConfigs()
{ {
return new ConfigResponseModel(_globalSettings, _featureService.GetAll()); return new ConfigResponseModel(_featureService, _globalSettings);
} }
} }

View File

@ -186,6 +186,19 @@ public class DevicesController : Controller
await _deviceService.SaveAsync(model.ToDevice(device)); await _deviceService.SaveAsync(model.ToDevice(device));
} }
[HttpPut("identifier/{identifier}/web-push-auth")]
[HttpPost("identifier/{identifier}/web-push-auth")]
public async Task PutWebPushAuth(string identifier, [FromBody] WebPushAuthRequestModel model)
{
var device = await _deviceRepository.GetByIdentifierAsync(identifier, _userService.GetProperUserId(User).Value);
if (device == null)
{
throw new NotFoundException();
}
await _deviceService.SaveAsync(model.ToData(), device);
}
[AllowAnonymous] [AllowAnonymous]
[HttpPut("identifier/{identifier}/clear-token")] [HttpPut("identifier/{identifier}/clear-token")]
[HttpPost("identifier/{identifier}/clear-token")] [HttpPost("identifier/{identifier}/clear-token")]

View File

@ -1,5 +1,5 @@
using Bit.Api.Models.Response; using Bit.Api.Models.Response;
using Bit.Core.Utilities; using Bit.Core.Billing.Pricing;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@ -7,13 +7,15 @@ namespace Bit.Api.Controllers;
[Route("plans")] [Route("plans")]
[Authorize("Web")] [Authorize("Web")]
public class PlansController : Controller public class PlansController(
IPricingClient pricingClient) : Controller
{ {
[HttpGet("")] [HttpGet("")]
[AllowAnonymous] [AllowAnonymous]
public ListResponseModel<PlanResponseModel> Get() public async Task<ListResponseModel<PlanResponseModel>> Get()
{ {
var responses = StaticStore.Plans.Select(plan => new PlanResponseModel(plan)); var plans = await pricingClient.ListPlans();
var responses = plans.Select(plan => new PlanResponseModel(plan));
return new ListResponseModel<PlanResponseModel>(responses); return new ListResponseModel<PlanResponseModel>(responses);
} }
} }

View File

@ -64,7 +64,8 @@ public class SelfHostedOrganizationLicensesController : Controller
var result = await _organizationService.SignUpAsync(license, user, model.Key, var result = await _organizationService.SignUpAsync(license, user, model.Key,
model.CollectionName, model.Keys?.PublicKey, model.Keys?.EncryptedPrivateKey); model.CollectionName, model.Keys?.PublicKey, model.Keys?.EncryptedPrivateKey);
return new OrganizationResponseModel(result.Item1);
return new OrganizationResponseModel(result.Item1, null);
} }
[HttpPost("{id}")] [HttpPost("{id}")]

View File

@ -1,6 +1,7 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.NotificationHub;
using Bit.Core.Utilities; using Bit.Core.Utilities;
namespace Bit.Api.Models.Request; namespace Bit.Api.Models.Request;
@ -37,6 +38,26 @@ public class DeviceRequestModel
} }
} }
public class WebPushAuthRequestModel
{
[Required]
public string Endpoint { get; set; }
[Required]
public string P256dh { get; set; }
[Required]
public string Auth { get; set; }
public WebPushRegistrationData ToData()
{
return new WebPushRegistrationData
{
Endpoint = Endpoint,
P256dh = P256dh,
Auth = Auth
};
}
}
public class DeviceTokenRequestModel public class DeviceTokenRequestModel
{ {
[StringLength(255)] [StringLength(255)]

View File

@ -1,6 +1,7 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities;
using Bit.Core.Models.Business; using Bit.Core.Models.Business;
using Bit.Core.Models.StaticStore;
namespace Bit.Api.Models.Request.Organizations; namespace Bit.Api.Models.Request.Organizations;
@ -12,9 +13,9 @@ public class SecretsManagerSubscriptionUpdateRequestModel
public int ServiceAccountAdjustment { get; set; } public int ServiceAccountAdjustment { get; set; }
public int? MaxAutoscaleServiceAccounts { get; set; } public int? MaxAutoscaleServiceAccounts { get; set; }
public virtual SecretsManagerSubscriptionUpdate ToSecretsManagerSubscriptionUpdate(Organization organization) public virtual SecretsManagerSubscriptionUpdate ToSecretsManagerSubscriptionUpdate(Organization organization, Plan plan)
{ {
return new SecretsManagerSubscriptionUpdate(organization, false) return new SecretsManagerSubscriptionUpdate(organization, plan, false)
{ {
MaxAutoscaleSmSeats = MaxAutoscaleSeats, MaxAutoscaleSmSeats = MaxAutoscaleSeats,
MaxAutoscaleSmServiceAccounts = MaxAutoscaleServiceAccounts MaxAutoscaleSmServiceAccounts = MaxAutoscaleServiceAccounts

View File

@ -1,4 +1,7 @@
using Bit.Core.Models.Api; using Bit.Core;
using Bit.Core.Enums;
using Bit.Core.Models.Api;
using Bit.Core.Services;
using Bit.Core.Settings; using Bit.Core.Settings;
using Bit.Core.Utilities; using Bit.Core.Utilities;
@ -11,6 +14,7 @@ public class ConfigResponseModel : ResponseModel
public ServerConfigResponseModel Server { get; set; } public ServerConfigResponseModel Server { get; set; }
public EnvironmentConfigResponseModel Environment { get; set; } public EnvironmentConfigResponseModel Environment { get; set; }
public IDictionary<string, object> FeatureStates { get; set; } public IDictionary<string, object> FeatureStates { get; set; }
public PushSettings Push { get; set; }
public ServerSettingsResponseModel Settings { get; set; } public ServerSettingsResponseModel Settings { get; set; }
public ConfigResponseModel() : base("config") public ConfigResponseModel() : base("config")
@ -23,8 +27,9 @@ public class ConfigResponseModel : ResponseModel
} }
public ConfigResponseModel( public ConfigResponseModel(
IGlobalSettings globalSettings, IFeatureService featureService,
IDictionary<string, object> featureStates) : base("config") IGlobalSettings globalSettings
) : base("config")
{ {
Version = AssemblyHelpers.GetVersion(); Version = AssemblyHelpers.GetVersion();
GitHash = AssemblyHelpers.GetGitHash(); GitHash = AssemblyHelpers.GetGitHash();
@ -37,7 +42,9 @@ public class ConfigResponseModel : ResponseModel
Notifications = globalSettings.BaseServiceUri.Notifications, Notifications = globalSettings.BaseServiceUri.Notifications,
Sso = globalSettings.BaseServiceUri.Sso Sso = globalSettings.BaseServiceUri.Sso
}; };
FeatureStates = featureStates; FeatureStates = featureService.GetAll();
var webPushEnabled = FeatureStates.TryGetValue(FeatureFlagKeys.WebPush, out var webPushEnabledValue) ? (bool)webPushEnabledValue : false;
Push = PushSettings.Build(webPushEnabled, globalSettings);
Settings = new ServerSettingsResponseModel Settings = new ServerSettingsResponseModel
{ {
DisableUserRegistration = globalSettings.DisableUserRegistration DisableUserRegistration = globalSettings.DisableUserRegistration
@ -61,6 +68,23 @@ public class EnvironmentConfigResponseModel
public string Sso { get; set; } public string Sso { get; set; }
} }
public class PushSettings
{
public PushTechnologyType PushTechnology { get; private init; }
public string VapidPublicKey { get; private init; }
public static PushSettings Build(bool webPushEnabled, IGlobalSettings globalSettings)
{
var vapidPublicKey = webPushEnabled ? globalSettings.WebPush.VapidPublicKey : null;
var pushTechnology = vapidPublicKey != null ? PushTechnologyType.WebPush : PushTechnologyType.SignalR;
return new()
{
VapidPublicKey = vapidPublicKey,
PushTechnology = pushTechnology
};
}
}
public class ServerSettingsResponseModel public class ServerSettingsResponseModel
{ {
public bool DisableUserRegistration { get; set; } public bool DisableUserRegistration { get; set; }

View File

@ -1,4 +1,6 @@
using Bit.Core.Billing.Enums; using Bit.Core.AdminConsole.Entities;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Extensions;
using Bit.Core.Models.Api; using Bit.Core.Models.Api;
using Bit.Core.Models.StaticStore; using Bit.Core.Models.StaticStore;
@ -44,6 +46,13 @@ public class PlanResponseModel : ResponseModel
PasswordManager = new PasswordManagerPlanFeaturesResponseModel(plan.PasswordManager); PasswordManager = new PasswordManagerPlanFeaturesResponseModel(plan.PasswordManager);
} }
public PlanResponseModel(Organization organization, string obj = "plan") : base(obj)
{
Type = organization.PlanType;
ProductTier = organization.PlanType.GetProductTier();
Name = organization.Plan;
}
public PlanType Type { get; set; } public PlanType Type { get; set; }
public ProductTierType ProductTier { get; set; } public ProductTierType ProductTier { get; set; }
public string Name { get; set; } public string Name { get; set; }

View File

@ -1,6 +1,7 @@
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.Models.Api; using Bit.Core.Models.Api;
using Bit.Core.NotificationHub;
using Bit.Core.Platform.Push; using Bit.Core.Platform.Push;
using Bit.Core.Settings; using Bit.Core.Settings;
using Bit.Core.Utilities; using Bit.Core.Utilities;
@ -22,14 +23,14 @@ public class PushController : Controller
private readonly IPushNotificationService _pushNotificationService; private readonly IPushNotificationService _pushNotificationService;
private readonly IWebHostEnvironment _environment; private readonly IWebHostEnvironment _environment;
private readonly ICurrentContext _currentContext; private readonly ICurrentContext _currentContext;
private readonly GlobalSettings _globalSettings; private readonly IGlobalSettings _globalSettings;
public PushController( public PushController(
IPushRegistrationService pushRegistrationService, IPushRegistrationService pushRegistrationService,
IPushNotificationService pushNotificationService, IPushNotificationService pushNotificationService,
IWebHostEnvironment environment, IWebHostEnvironment environment,
ICurrentContext currentContext, ICurrentContext currentContext,
GlobalSettings globalSettings) IGlobalSettings globalSettings)
{ {
_currentContext = currentContext; _currentContext = currentContext;
_environment = environment; _environment = environment;
@ -39,22 +40,22 @@ public class PushController : Controller
} }
[HttpPost("register")] [HttpPost("register")]
public async Task PostRegister([FromBody] PushRegistrationRequestModel model) public async Task RegisterAsync([FromBody] PushRegistrationRequestModel model)
{ {
CheckUsage(); CheckUsage();
await _pushRegistrationService.CreateOrUpdateRegistrationAsync(model.PushToken, Prefix(model.DeviceId), await _pushRegistrationService.CreateOrUpdateRegistrationAsync(new PushRegistrationData(model.PushToken), Prefix(model.DeviceId),
Prefix(model.UserId), Prefix(model.Identifier), model.Type); Prefix(model.UserId), Prefix(model.Identifier), model.Type, model.OrganizationIds.Select(Prefix), model.InstallationId);
} }
[HttpPost("delete")] [HttpPost("delete")]
public async Task PostDelete([FromBody] PushDeviceRequestModel model) public async Task DeleteAsync([FromBody] PushDeviceRequestModel model)
{ {
CheckUsage(); CheckUsage();
await _pushRegistrationService.DeleteRegistrationAsync(Prefix(model.Id)); await _pushRegistrationService.DeleteRegistrationAsync(Prefix(model.Id));
} }
[HttpPut("add-organization")] [HttpPut("add-organization")]
public async Task PutAddOrganization([FromBody] PushUpdateRequestModel model) public async Task AddOrganizationAsync([FromBody] PushUpdateRequestModel model)
{ {
CheckUsage(); CheckUsage();
await _pushRegistrationService.AddUserRegistrationOrganizationAsync( await _pushRegistrationService.AddUserRegistrationOrganizationAsync(
@ -63,7 +64,7 @@ public class PushController : Controller
} }
[HttpPut("delete-organization")] [HttpPut("delete-organization")]
public async Task PutDeleteOrganization([FromBody] PushUpdateRequestModel model) public async Task DeleteOrganizationAsync([FromBody] PushUpdateRequestModel model)
{ {
CheckUsage(); CheckUsage();
await _pushRegistrationService.DeleteUserRegistrationOrganizationAsync( await _pushRegistrationService.DeleteUserRegistrationOrganizationAsync(
@ -72,19 +73,30 @@ public class PushController : Controller
} }
[HttpPost("send")] [HttpPost("send")]
public async Task PostSend([FromBody] PushSendRequestModel model) public async Task SendAsync([FromBody] PushSendRequestModel model)
{ {
CheckUsage(); CheckUsage();
if (!string.IsNullOrWhiteSpace(model.UserId)) if (!string.IsNullOrWhiteSpace(model.InstallationId))
{
if (_currentContext.InstallationId!.Value.ToString() != model.InstallationId!)
{
throw new BadRequestException("InstallationId does not match current context.");
}
await _pushNotificationService.SendPayloadToInstallationAsync(
_currentContext.InstallationId.Value.ToString(), model.Type, model.Payload, Prefix(model.Identifier),
Prefix(model.DeviceId), model.ClientType);
}
else if (!string.IsNullOrWhiteSpace(model.UserId))
{ {
await _pushNotificationService.SendPayloadToUserAsync(Prefix(model.UserId), await _pushNotificationService.SendPayloadToUserAsync(Prefix(model.UserId),
model.Type.Value, model.Payload, Prefix(model.Identifier), Prefix(model.DeviceId)); model.Type, model.Payload, Prefix(model.Identifier), Prefix(model.DeviceId), model.ClientType);
} }
else if (!string.IsNullOrWhiteSpace(model.OrganizationId)) else if (!string.IsNullOrWhiteSpace(model.OrganizationId))
{ {
await _pushNotificationService.SendPayloadToOrganizationAsync(Prefix(model.OrganizationId), await _pushNotificationService.SendPayloadToOrganizationAsync(Prefix(model.OrganizationId),
model.Type.Value, model.Payload, Prefix(model.Identifier), Prefix(model.DeviceId)); model.Type, model.Payload, Prefix(model.Identifier), Prefix(model.DeviceId), model.ClientType);
} }
} }
@ -95,7 +107,7 @@ public class PushController : Controller
return null; return null;
} }
return $"{_currentContext.InstallationId.Value}_{value}"; return $"{_currentContext.InstallationId!.Value}_{value}";
} }
private void CheckUsage() private void CheckUsage()

View File

@ -0,0 +1,11 @@
using System.ComponentModel.DataAnnotations;
namespace Bit.Core.Enums;
public enum PushTechnologyType
{
[Display(Name = "SignalR")]
SignalR = 0,
[Display(Name = "WebPush")]
WebPush = 1,
}

View File

@ -1,6 +1,7 @@
using Bit.Api.Models.Response; using Bit.Api.Models.Response;
using Bit.Api.SecretsManager.Models.Request; using Bit.Api.SecretsManager.Models.Request;
using Bit.Api.SecretsManager.Models.Response; using Bit.Api.SecretsManager.Models.Response;
using Bit.Core.Billing.Pricing;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
@ -37,6 +38,7 @@ public class ServiceAccountsController : Controller
private readonly IUpdateServiceAccountCommand _updateServiceAccountCommand; private readonly IUpdateServiceAccountCommand _updateServiceAccountCommand;
private readonly IDeleteServiceAccountsCommand _deleteServiceAccountsCommand; private readonly IDeleteServiceAccountsCommand _deleteServiceAccountsCommand;
private readonly IRevokeAccessTokensCommand _revokeAccessTokensCommand; private readonly IRevokeAccessTokensCommand _revokeAccessTokensCommand;
private readonly IPricingClient _pricingClient;
public ServiceAccountsController( public ServiceAccountsController(
ICurrentContext currentContext, ICurrentContext currentContext,
@ -52,7 +54,8 @@ public class ServiceAccountsController : Controller
ICreateServiceAccountCommand createServiceAccountCommand, ICreateServiceAccountCommand createServiceAccountCommand,
IUpdateServiceAccountCommand updateServiceAccountCommand, IUpdateServiceAccountCommand updateServiceAccountCommand,
IDeleteServiceAccountsCommand deleteServiceAccountsCommand, IDeleteServiceAccountsCommand deleteServiceAccountsCommand,
IRevokeAccessTokensCommand revokeAccessTokensCommand) IRevokeAccessTokensCommand revokeAccessTokensCommand,
IPricingClient pricingClient)
{ {
_currentContext = currentContext; _currentContext = currentContext;
_userService = userService; _userService = userService;
@ -66,6 +69,7 @@ public class ServiceAccountsController : Controller
_updateServiceAccountCommand = updateServiceAccountCommand; _updateServiceAccountCommand = updateServiceAccountCommand;
_deleteServiceAccountsCommand = deleteServiceAccountsCommand; _deleteServiceAccountsCommand = deleteServiceAccountsCommand;
_revokeAccessTokensCommand = revokeAccessTokensCommand; _revokeAccessTokensCommand = revokeAccessTokensCommand;
_pricingClient = pricingClient;
_createAccessTokenCommand = createAccessTokenCommand; _createAccessTokenCommand = createAccessTokenCommand;
_updateSecretsManagerSubscriptionCommand = updateSecretsManagerSubscriptionCommand; _updateSecretsManagerSubscriptionCommand = updateSecretsManagerSubscriptionCommand;
} }
@ -124,7 +128,9 @@ public class ServiceAccountsController : Controller
if (newServiceAccountSlotsRequired > 0) if (newServiceAccountSlotsRequired > 0)
{ {
var org = await _organizationRepository.GetByIdAsync(organizationId); var org = await _organizationRepository.GetByIdAsync(organizationId);
var update = new SecretsManagerSubscriptionUpdate(org, true) // TODO: https://bitwarden.atlassian.net/browse/PM-17002
var plan = await _pricingClient.GetPlanOrThrow(org!.PlanType);
var update = new SecretsManagerSubscriptionUpdate(org, plan, true)
.AdjustServiceAccounts(newServiceAccountSlotsRequired); .AdjustServiceAccounts(newServiceAccountSlotsRequired);
await _updateSecretsManagerSubscriptionCommand.UpdateSubscriptionAsync(update); await _updateSecretsManagerSubscriptionCommand.UpdateSubscriptionAsync(update);
} }

View File

@ -5,7 +5,7 @@ using Bit.Core.Settings;
using AspNetCoreRateLimit; using AspNetCoreRateLimit;
using Stripe; using Stripe;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using IdentityModel; using Duende.IdentityModel;
using System.Globalization; using System.Globalization;
using Bit.Api.AdminConsole.Models.Request.Organizations; using Bit.Api.AdminConsole.Models.Request.Organizations;
using Bit.Api.Auth.Models.Request; using Bit.Api.Auth.Models.Request;

View File

@ -96,12 +96,6 @@ public class ImportCiphersController : Controller
return true; return true;
} }
//Users allowed to import if they CanCreate Collections
if (!(await _authorizationService.AuthorizeAsync(User, collections, BulkCollectionOperations.Create)).Succeeded)
{
return false;
}
//Calling Repository instead of Service as we want to get all the collections, regardless of permission //Calling Repository instead of Service as we want to get all the collections, regardless of permission
//Permissions check will be done later on AuthorizationService //Permissions check will be done later on AuthorizationService
var orgCollectionIds = var orgCollectionIds =
@ -118,6 +112,12 @@ public class ImportCiphersController : Controller
return false; return false;
}; };
//Users allowed to import if they CanCreate Collections
if (!(await _authorizationService.AuthorizeAsync(User, collections, BulkCollectionOperations.Create)).Succeeded)
{
return false;
}
return true; return true;
} }
} }

View File

@ -36,8 +36,6 @@ public static class ServiceCollectionExtensions
} }
}); });
config.CustomSchemaIds(type => type.FullName);
config.SwaggerDoc("internal", new OpenApiInfo { Title = "Bitwarden Internal API", Version = "latest" }); config.SwaggerDoc("internal", new OpenApiInfo { Title = "Bitwarden Internal API", Version = "latest" });
config.AddSecurityDefinition("oauth2-client-credentials", new OpenApiSecurityScheme config.AddSecurityDefinition("oauth2-client-credentials", new OpenApiSecurityScheme

View File

@ -22,19 +22,22 @@ public class SecurityTaskController : Controller
private readonly IMarkTaskAsCompleteCommand _markTaskAsCompleteCommand; private readonly IMarkTaskAsCompleteCommand _markTaskAsCompleteCommand;
private readonly IGetTasksForOrganizationQuery _getTasksForOrganizationQuery; private readonly IGetTasksForOrganizationQuery _getTasksForOrganizationQuery;
private readonly ICreateManyTasksCommand _createManyTasksCommand; private readonly ICreateManyTasksCommand _createManyTasksCommand;
private readonly ICreateManyTaskNotificationsCommand _createManyTaskNotificationsCommand;
public SecurityTaskController( public SecurityTaskController(
IUserService userService, IUserService userService,
IGetTaskDetailsForUserQuery getTaskDetailsForUserQuery, IGetTaskDetailsForUserQuery getTaskDetailsForUserQuery,
IMarkTaskAsCompleteCommand markTaskAsCompleteCommand, IMarkTaskAsCompleteCommand markTaskAsCompleteCommand,
IGetTasksForOrganizationQuery getTasksForOrganizationQuery, IGetTasksForOrganizationQuery getTasksForOrganizationQuery,
ICreateManyTasksCommand createManyTasksCommand) ICreateManyTasksCommand createManyTasksCommand,
ICreateManyTaskNotificationsCommand createManyTaskNotificationsCommand)
{ {
_userService = userService; _userService = userService;
_getTaskDetailsForUserQuery = getTaskDetailsForUserQuery; _getTaskDetailsForUserQuery = getTaskDetailsForUserQuery;
_markTaskAsCompleteCommand = markTaskAsCompleteCommand; _markTaskAsCompleteCommand = markTaskAsCompleteCommand;
_getTasksForOrganizationQuery = getTasksForOrganizationQuery; _getTasksForOrganizationQuery = getTasksForOrganizationQuery;
_createManyTasksCommand = createManyTasksCommand; _createManyTasksCommand = createManyTasksCommand;
_createManyTaskNotificationsCommand = createManyTaskNotificationsCommand;
} }
/// <summary> /// <summary>
@ -87,6 +90,9 @@ public class SecurityTaskController : Controller
[FromBody] BulkCreateSecurityTasksRequestModel model) [FromBody] BulkCreateSecurityTasksRequestModel model)
{ {
var securityTasks = await _createManyTasksCommand.CreateAsync(orgId, model.Tasks); var securityTasks = await _createManyTasksCommand.CreateAsync(orgId, model.Tasks);
await _createManyTaskNotificationsCommand.CreateAsync(orgId, securityTasks);
var response = securityTasks.Select(x => new SecurityTasksResponseModel(x)).ToList(); var response = securityTasks.Select(x => new SecurityTasksResponseModel(x)).ToList();
return new ListResponseModel<SecurityTasksResponseModel>(response); return new ListResponseModel<SecurityTasksResponseModel>(response);
} }

View File

@ -3,6 +3,8 @@
<PropertyGroup> <PropertyGroup>
<UserSecretsId>bitwarden-Billing</UserSecretsId> <UserSecretsId>bitwarden-Billing</UserSecretsId>
<MvcRazorCompileOnPublish>false</MvcRazorCompileOnPublish> <MvcRazorCompileOnPublish>false</MvcRazorCompileOnPublish>
<!-- Temp exclusions until warnings are fixed -->
<WarningsNotAsErrors>$(WarningsNotAsErrors);CS9113</WarningsNotAsErrors>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition=" '$(RunConfiguration)' == 'Billing' " /> <PropertyGroup Condition=" '$(RunConfiguration)' == 'Billing' " />

View File

@ -0,0 +1,7 @@
namespace Bit.Billing.Constants;
public static class BitPayInvoiceStatus
{
public const string Confirmed = "confirmed";
public const string Complete = "complete";
}

View File

@ -0,0 +1,6 @@
namespace Bit.Billing.Constants;
public static class BitPayNotificationCode
{
public const string InvoiceConfirmed = "invoice_confirmed";
}

View File

@ -1,6 +1,8 @@
using System.Globalization; using System.Globalization;
using Bit.Billing.Constants;
using Bit.Billing.Models; using Bit.Billing.Models;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Services;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Repositories; using Bit.Core.Repositories;
@ -25,6 +27,7 @@ public class BitPayController : Controller
private readonly IMailService _mailService; private readonly IMailService _mailService;
private readonly IPaymentService _paymentService; private readonly IPaymentService _paymentService;
private readonly ILogger<BitPayController> _logger; private readonly ILogger<BitPayController> _logger;
private readonly IPremiumUserBillingService _premiumUserBillingService;
public BitPayController( public BitPayController(
IOptions<BillingSettings> billingSettings, IOptions<BillingSettings> billingSettings,
@ -35,7 +38,8 @@ public class BitPayController : Controller
IProviderRepository providerRepository, IProviderRepository providerRepository,
IMailService mailService, IMailService mailService,
IPaymentService paymentService, IPaymentService paymentService,
ILogger<BitPayController> logger) ILogger<BitPayController> logger,
IPremiumUserBillingService premiumUserBillingService)
{ {
_billingSettings = billingSettings?.Value; _billingSettings = billingSettings?.Value;
_bitPayClient = bitPayClient; _bitPayClient = bitPayClient;
@ -46,6 +50,7 @@ public class BitPayController : Controller
_mailService = mailService; _mailService = mailService;
_paymentService = paymentService; _paymentService = paymentService;
_logger = logger; _logger = logger;
_premiumUserBillingService = premiumUserBillingService;
} }
[HttpPost("ipn")] [HttpPost("ipn")]
@ -61,7 +66,7 @@ public class BitPayController : Controller
return new BadRequestResult(); return new BadRequestResult();
} }
if (model.Event.Name != "invoice_confirmed") if (model.Event.Name != BitPayNotificationCode.InvoiceConfirmed)
{ {
// Only processing confirmed invoice events for now. // Only processing confirmed invoice events for now.
return new OkResult(); return new OkResult();
@ -71,20 +76,20 @@ public class BitPayController : Controller
if (invoice == null) if (invoice == null)
{ {
// Request forged...? // Request forged...?
_logger.LogWarning("Invoice not found. #" + model.Data.Id); _logger.LogWarning("Invoice not found. #{InvoiceId}", model.Data.Id);
return new BadRequestResult(); return new BadRequestResult();
} }
if (invoice.Status != "confirmed" && invoice.Status != "completed") if (invoice.Status != BitPayInvoiceStatus.Confirmed && invoice.Status != BitPayInvoiceStatus.Complete)
{ {
_logger.LogWarning("Invoice status of '" + invoice.Status + "' is not acceptable. #" + invoice.Id); _logger.LogWarning("Invoice status of '{InvoiceStatus}' is not acceptable. #{InvoiceId}", invoice.Status, invoice.Id);
return new BadRequestResult(); return new BadRequestResult();
} }
if (invoice.Currency != "USD") if (invoice.Currency != "USD")
{ {
// Only process USD payments // Only process USD payments
_logger.LogWarning("Non USD payment received. #" + invoice.Id); _logger.LogWarning("Non USD payment received. #{InvoiceId}", invoice.Id);
return new OkResult(); return new OkResult();
} }
@ -145,10 +150,7 @@ public class BitPayController : Controller
if (user != null) if (user != null)
{ {
billingEmail = user.BillingEmailAddress(); billingEmail = user.BillingEmailAddress();
if (await _paymentService.CreditAccountAsync(user, tx.Amount)) await _premiumUserBillingService.Credit(user, tx.Amount);
{
await _userRepository.ReplaceAsync(user);
}
} }
} }
else if (tx.ProviderId.HasValue) else if (tx.ProviderId.HasValue)

View File

@ -1,6 +1,7 @@
using System.Text; using System.Text;
using Bit.Billing.Models; using Bit.Billing.Models;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Services;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Repositories; using Bit.Core.Repositories;
@ -23,6 +24,7 @@ public class PayPalController : Controller
private readonly ITransactionRepository _transactionRepository; private readonly ITransactionRepository _transactionRepository;
private readonly IUserRepository _userRepository; private readonly IUserRepository _userRepository;
private readonly IProviderRepository _providerRepository; private readonly IProviderRepository _providerRepository;
private readonly IPremiumUserBillingService _premiumUserBillingService;
public PayPalController( public PayPalController(
IOptions<BillingSettings> billingSettings, IOptions<BillingSettings> billingSettings,
@ -32,7 +34,8 @@ public class PayPalController : Controller
IPaymentService paymentService, IPaymentService paymentService,
ITransactionRepository transactionRepository, ITransactionRepository transactionRepository,
IUserRepository userRepository, IUserRepository userRepository,
IProviderRepository providerRepository) IProviderRepository providerRepository,
IPremiumUserBillingService premiumUserBillingService)
{ {
_billingSettings = billingSettings?.Value; _billingSettings = billingSettings?.Value;
_logger = logger; _logger = logger;
@ -42,6 +45,7 @@ public class PayPalController : Controller
_transactionRepository = transactionRepository; _transactionRepository = transactionRepository;
_userRepository = userRepository; _userRepository = userRepository;
_providerRepository = providerRepository; _providerRepository = providerRepository;
_premiumUserBillingService = premiumUserBillingService;
} }
[HttpPost("ipn")] [HttpPost("ipn")]
@ -257,10 +261,9 @@ public class PayPalController : Controller
{ {
var user = await _userRepository.GetByIdAsync(transaction.UserId.Value); var user = await _userRepository.GetByIdAsync(transaction.UserId.Value);
if (await _paymentService.CreditAccountAsync(user, transaction.Amount)) if (user != null)
{ {
await _userRepository.ReplaceAsync(user); await _premiumUserBillingService.Credit(user, transaction.Amount);
billingEmail = user.BillingEmailAddress(); billingEmail = user.BillingEmailAddress();
} }
} }

View File

@ -23,9 +23,9 @@ public class SubscriptionCancellationJob(
} }
var subscription = await stripeFacade.GetSubscription(subscriptionId); var subscription = await stripeFacade.GetSubscription(subscriptionId);
if (subscription?.Status != "unpaid") if (subscription?.Status != "unpaid" ||
subscription.LatestInvoice?.BillingReason is not ("subscription_cycle" or "subscription_create"))
{ {
// Subscription is no longer unpaid, skip cancellation
return; return;
} }

View File

@ -1,6 +1,8 @@
using Bit.Billing.Constants; using Bit.Billing.Constants;
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Enums; using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Pricing;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Platform.Push; using Bit.Core.Platform.Push;
using Bit.Core.Repositories; using Bit.Core.Repositories;
@ -8,7 +10,6 @@ using Bit.Core.Services;
using Bit.Core.Tools.Enums; using Bit.Core.Tools.Enums;
using Bit.Core.Tools.Models.Business; using Bit.Core.Tools.Models.Business;
using Bit.Core.Tools.Services; using Bit.Core.Tools.Services;
using Bit.Core.Utilities;
using Event = Stripe.Event; using Event = Stripe.Event;
namespace Bit.Billing.Services.Implementations; namespace Bit.Billing.Services.Implementations;
@ -17,7 +18,6 @@ public class PaymentSucceededHandler : IPaymentSucceededHandler
{ {
private readonly ILogger<PaymentSucceededHandler> _logger; private readonly ILogger<PaymentSucceededHandler> _logger;
private readonly IStripeEventService _stripeEventService; private readonly IStripeEventService _stripeEventService;
private readonly IOrganizationService _organizationService;
private readonly IUserService _userService; private readonly IUserService _userService;
private readonly IStripeFacade _stripeFacade; private readonly IStripeFacade _stripeFacade;
private readonly IProviderRepository _providerRepository; private readonly IProviderRepository _providerRepository;
@ -27,6 +27,8 @@ public class PaymentSucceededHandler : IPaymentSucceededHandler
private readonly IUserRepository _userRepository; private readonly IUserRepository _userRepository;
private readonly IStripeEventUtilityService _stripeEventUtilityService; private readonly IStripeEventUtilityService _stripeEventUtilityService;
private readonly IPushNotificationService _pushNotificationService; private readonly IPushNotificationService _pushNotificationService;
private readonly IOrganizationEnableCommand _organizationEnableCommand;
private readonly IPricingClient _pricingClient;
public PaymentSucceededHandler( public PaymentSucceededHandler(
ILogger<PaymentSucceededHandler> logger, ILogger<PaymentSucceededHandler> logger,
@ -39,8 +41,9 @@ public class PaymentSucceededHandler : IPaymentSucceededHandler
IUserRepository userRepository, IUserRepository userRepository,
IStripeEventUtilityService stripeEventUtilityService, IStripeEventUtilityService stripeEventUtilityService,
IUserService userService, IUserService userService,
IOrganizationService organizationService, IPushNotificationService pushNotificationService,
IPushNotificationService pushNotificationService) IOrganizationEnableCommand organizationEnableCommand,
IPricingClient pricingClient)
{ {
_logger = logger; _logger = logger;
_stripeEventService = stripeEventService; _stripeEventService = stripeEventService;
@ -52,8 +55,9 @@ public class PaymentSucceededHandler : IPaymentSucceededHandler
_userRepository = userRepository; _userRepository = userRepository;
_stripeEventUtilityService = stripeEventUtilityService; _stripeEventUtilityService = stripeEventUtilityService;
_userService = userService; _userService = userService;
_organizationService = organizationService;
_pushNotificationService = pushNotificationService; _pushNotificationService = pushNotificationService;
_organizationEnableCommand = organizationEnableCommand;
_pricingClient = pricingClient;
} }
/// <summary> /// <summary>
@ -95,9 +99,9 @@ public class PaymentSucceededHandler : IPaymentSucceededHandler
return; return;
} }
var teamsMonthly = StaticStore.GetPlan(PlanType.TeamsMonthly); var teamsMonthly = await _pricingClient.GetPlanOrThrow(PlanType.TeamsMonthly);
var enterpriseMonthly = StaticStore.GetPlan(PlanType.EnterpriseMonthly); var enterpriseMonthly = await _pricingClient.GetPlanOrThrow(PlanType.EnterpriseMonthly);
var teamsMonthlyLineItem = var teamsMonthlyLineItem =
subscription.Items.Data.FirstOrDefault(item => subscription.Items.Data.FirstOrDefault(item =>
@ -136,14 +140,21 @@ public class PaymentSucceededHandler : IPaymentSucceededHandler
} }
else if (organizationId.HasValue) else if (organizationId.HasValue)
{ {
if (!subscription.Items.Any(i => var organization = await _organizationRepository.GetByIdAsync(organizationId.Value);
StaticStore.Plans.Any(p => p.PasswordManager.StripePlanId == i.Plan.Id)))
if (organization == null)
{ {
return; return;
} }
await _organizationService.EnableAsync(organizationId.Value, subscription.CurrentPeriodEnd); var plan = await _pricingClient.GetPlanOrThrow(organization.PlanType);
var organization = await _organizationRepository.GetByIdAsync(organizationId.Value);
if (subscription.Items.All(item => plan.PasswordManager.StripePlanId != item.Plan.Id))
{
return;
}
await _organizationEnableCommand.EnableAsync(organizationId.Value, subscription.CurrentPeriodEnd);
await _pushNotificationService.PushSyncOrganizationStatusAsync(organization); await _pushNotificationService.PushSyncOrganizationStatusAsync(organization);
await _referenceEventService.RaiseEventAsync( await _referenceEventService.RaiseEventAsync(

View File

@ -1,15 +1,17 @@
using Bit.Billing.Constants; using Bit.Billing.Constants;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Entities; using Bit.Core.Billing.Entities;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Repositories; using Bit.Core.Billing.Repositories;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Utilities; using Bit.Core.Repositories;
using Stripe; using Stripe;
namespace Bit.Billing.Services.Implementations; namespace Bit.Billing.Services.Implementations;
public class ProviderEventService( public class ProviderEventService(
ILogger<ProviderEventService> logger, IOrganizationRepository organizationRepository,
IPricingClient pricingClient,
IProviderInvoiceItemRepository providerInvoiceItemRepository, IProviderInvoiceItemRepository providerInvoiceItemRepository,
IProviderOrganizationRepository providerOrganizationRepository, IProviderOrganizationRepository providerOrganizationRepository,
IProviderPlanRepository providerPlanRepository, IProviderPlanRepository providerPlanRepository,
@ -54,7 +56,14 @@ public class ProviderEventService(
continue; continue;
} }
var plan = StaticStore.Plans.Single(x => x.Name == client.Plan && providerPlans.Any(y => y.PlanType == x.Type)); var organization = await organizationRepository.GetByIdAsync(client.OrganizationId);
if (organization == null)
{
return;
}
var plan = await pricingClient.GetPlanOrThrow(organization.PlanType);
var discountedPercentage = (100 - (invoice.Discount?.Coupon?.PercentOff ?? 0)) / 100; var discountedPercentage = (100 - (invoice.Discount?.Coupon?.PercentOff ?? 0)) / 100;
@ -76,7 +85,7 @@ public class ProviderEventService(
foreach (var providerPlan in providerPlans.Where(x => x.PurchasedSeats is null or 0)) foreach (var providerPlan in providerPlans.Where(x => x.PurchasedSeats is null or 0))
{ {
var plan = StaticStore.GetPlan(providerPlan.PlanType); var plan = await pricingClient.GetPlanOrThrow(providerPlan.PlanType);
var clientSeats = invoiceItems var clientSeats = invoiceItems
.Where(item => item.PlanName == plan.Name) .Where(item => item.PlanName == plan.Name)

View File

@ -1,4 +1,5 @@
using Bit.Billing.Constants; using Bit.Billing.Constants;
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;
using Bit.Core.Services; using Bit.Core.Services;
using Event = Stripe.Event; using Event = Stripe.Event;
namespace Bit.Billing.Services.Implementations; namespace Bit.Billing.Services.Implementations;
@ -6,20 +7,20 @@ namespace Bit.Billing.Services.Implementations;
public class SubscriptionDeletedHandler : ISubscriptionDeletedHandler public class SubscriptionDeletedHandler : ISubscriptionDeletedHandler
{ {
private readonly IStripeEventService _stripeEventService; private readonly IStripeEventService _stripeEventService;
private readonly IOrganizationService _organizationService;
private readonly IUserService _userService; private readonly IUserService _userService;
private readonly IStripeEventUtilityService _stripeEventUtilityService; private readonly IStripeEventUtilityService _stripeEventUtilityService;
private readonly IOrganizationDisableCommand _organizationDisableCommand;
public SubscriptionDeletedHandler( public SubscriptionDeletedHandler(
IStripeEventService stripeEventService, IStripeEventService stripeEventService,
IOrganizationService organizationService,
IUserService userService, IUserService userService,
IStripeEventUtilityService stripeEventUtilityService) IStripeEventUtilityService stripeEventUtilityService,
IOrganizationDisableCommand organizationDisableCommand)
{ {
_stripeEventService = stripeEventService; _stripeEventService = stripeEventService;
_organizationService = organizationService;
_userService = userService; _userService = userService;
_stripeEventUtilityService = stripeEventUtilityService; _stripeEventUtilityService = stripeEventUtilityService;
_organizationDisableCommand = organizationDisableCommand;
} }
/// <summary> /// <summary>
@ -33,15 +34,18 @@ public class SubscriptionDeletedHandler : ISubscriptionDeletedHandler
var subCanceled = subscription.Status == StripeSubscriptionStatus.Canceled; var subCanceled = subscription.Status == StripeSubscriptionStatus.Canceled;
const string providerMigrationCancellationComment = "Cancelled as part of provider migration to Consolidated Billing"; const string providerMigrationCancellationComment = "Cancelled as part of provider migration to Consolidated Billing";
const string addedToProviderCancellationComment = "Organization was added to Provider";
if (!subCanceled) if (!subCanceled)
{ {
return; return;
} }
if (organizationId.HasValue && subscription is not { CancellationDetails.Comment: providerMigrationCancellationComment }) if (organizationId.HasValue &&
subscription.CancellationDetails.Comment != providerMigrationCancellationComment &&
!subscription.CancellationDetails.Comment.Contains(addedToProviderCancellationComment))
{ {
await _organizationService.DisableAsync(organizationId.Value, subscription.CurrentPeriodEnd); await _organizationDisableCommand.DisableAsync(organizationId.Value, subscription.CurrentPeriodEnd);
} }
else if (userId.HasValue) else if (userId.HasValue)
{ {

View File

@ -1,11 +1,12 @@
using Bit.Billing.Constants; using Bit.Billing.Constants;
using Bit.Billing.Jobs; using Bit.Billing.Jobs;
using Bit.Core; using Bit.Core;
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;
using Bit.Core.Billing.Pricing;
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces; using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
using Bit.Core.Platform.Push; using Bit.Core.Platform.Push;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Utilities;
using Quartz; using Quartz;
using Stripe; using Stripe;
using Event = Stripe.Event; using Event = Stripe.Event;
@ -24,6 +25,9 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler
private readonly IOrganizationRepository _organizationRepository; private readonly IOrganizationRepository _organizationRepository;
private readonly ISchedulerFactory _schedulerFactory; private readonly ISchedulerFactory _schedulerFactory;
private readonly IFeatureService _featureService; private readonly IFeatureService _featureService;
private readonly IOrganizationEnableCommand _organizationEnableCommand;
private readonly IOrganizationDisableCommand _organizationDisableCommand;
private readonly IPricingClient _pricingClient;
public SubscriptionUpdatedHandler( public SubscriptionUpdatedHandler(
IStripeEventService stripeEventService, IStripeEventService stripeEventService,
@ -35,7 +39,10 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler
IPushNotificationService pushNotificationService, IPushNotificationService pushNotificationService,
IOrganizationRepository organizationRepository, IOrganizationRepository organizationRepository,
ISchedulerFactory schedulerFactory, ISchedulerFactory schedulerFactory,
IFeatureService featureService) IFeatureService featureService,
IOrganizationEnableCommand organizationEnableCommand,
IOrganizationDisableCommand organizationDisableCommand,
IPricingClient pricingClient)
{ {
_stripeEventService = stripeEventService; _stripeEventService = stripeEventService;
_stripeEventUtilityService = stripeEventUtilityService; _stripeEventUtilityService = stripeEventUtilityService;
@ -47,6 +54,9 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler
_organizationRepository = organizationRepository; _organizationRepository = organizationRepository;
_schedulerFactory = schedulerFactory; _schedulerFactory = schedulerFactory;
_featureService = featureService; _featureService = featureService;
_organizationEnableCommand = organizationEnableCommand;
_organizationDisableCommand = organizationDisableCommand;
_pricingClient = pricingClient;
} }
/// <summary> /// <summary>
@ -55,7 +65,7 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler
/// <param name="parsedEvent"></param> /// <param name="parsedEvent"></param>
public async Task HandleAsync(Event parsedEvent) public async Task HandleAsync(Event parsedEvent)
{ {
var subscription = await _stripeEventService.GetSubscription(parsedEvent, true, ["customer", "discounts"]); var subscription = await _stripeEventService.GetSubscription(parsedEvent, true, ["customer", "discounts", "latest_invoice"]);
var (organizationId, userId, providerId) = _stripeEventUtilityService.GetIdsFromMetadata(subscription.Metadata); var (organizationId, userId, providerId) = _stripeEventUtilityService.GetIdsFromMetadata(subscription.Metadata);
switch (subscription.Status) switch (subscription.Status)
@ -63,8 +73,9 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler
case StripeSubscriptionStatus.Unpaid or StripeSubscriptionStatus.IncompleteExpired case StripeSubscriptionStatus.Unpaid or StripeSubscriptionStatus.IncompleteExpired
when organizationId.HasValue: when organizationId.HasValue:
{ {
await _organizationService.DisableAsync(organizationId.Value, subscription.CurrentPeriodEnd); await _organizationDisableCommand.DisableAsync(organizationId.Value, subscription.CurrentPeriodEnd);
if (subscription.Status == StripeSubscriptionStatus.Unpaid) if (subscription.Status == StripeSubscriptionStatus.Unpaid &&
subscription.LatestInvoice is { BillingReason: "subscription_cycle" or "subscription_create" })
{ {
await ScheduleCancellationJobAsync(subscription.Id, organizationId.Value); await ScheduleCancellationJobAsync(subscription.Id, organizationId.Value);
} }
@ -90,9 +101,12 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler
} }
case StripeSubscriptionStatus.Active when organizationId.HasValue: case StripeSubscriptionStatus.Active when organizationId.HasValue:
{ {
await _organizationService.EnableAsync(organizationId.Value); await _organizationEnableCommand.EnableAsync(organizationId.Value);
var organization = await _organizationRepository.GetByIdAsync(organizationId.Value); var organization = await _organizationRepository.GetByIdAsync(organizationId.Value);
if (organization != null)
{
await _pushNotificationService.PushSyncOrganizationStatusAsync(organization); await _pushNotificationService.PushSyncOrganizationStatusAsync(organization);
}
break; break;
} }
case StripeSubscriptionStatus.Active: case StripeSubscriptionStatus.Active:
@ -145,7 +159,8 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler
/// </summary> /// </summary>
/// <param name="parsedEvent"></param> /// <param name="parsedEvent"></param>
/// <param name="subscription"></param> /// <param name="subscription"></param>
private async Task RemovePasswordManagerCouponIfRemovingSecretsManagerTrialAsync(Event parsedEvent, private async Task RemovePasswordManagerCouponIfRemovingSecretsManagerTrialAsync(
Event parsedEvent,
Subscription subscription) Subscription subscription)
{ {
if (parsedEvent.Data.PreviousAttributes?.items is null) if (parsedEvent.Data.PreviousAttributes?.items is null)
@ -153,6 +168,22 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler
return; return;
} }
var organization = subscription.Metadata.TryGetValue("organizationId", out var organizationId)
? await _organizationRepository.GetByIdAsync(Guid.Parse(organizationId))
: null;
if (organization == null)
{
return;
}
var plan = await _pricingClient.GetPlanOrThrow(organization.PlanType);
if (!plan.SupportsSecretsManager)
{
return;
}
var previousSubscription = parsedEvent.Data var previousSubscription = parsedEvent.Data
.PreviousAttributes .PreviousAttributes
.ToObject<Subscription>() as Subscription; .ToObject<Subscription>() as Subscription;
@ -160,17 +191,14 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler
// This being false doesn't necessarily mean that the organization doesn't subscribe to Secrets Manager. // This being false doesn't necessarily mean that the organization doesn't subscribe to Secrets Manager.
// If there are changes to any subscription item, Stripe sends every item in the subscription, both // If there are changes to any subscription item, Stripe sends every item in the subscription, both
// changed and unchanged. // changed and unchanged.
var previousSubscriptionHasSecretsManager = previousSubscription?.Items is not null && var previousSubscriptionHasSecretsManager =
previousSubscription.Items.Any(previousItem => previousSubscription?.Items is not null &&
StaticStore.Plans.Any(p => previousSubscription.Items.Any(
p.SecretsManager is not null && previousSubscriptionItem => previousSubscriptionItem.Plan.Id == plan.SecretsManager.StripeSeatPlanId);
p.SecretsManager.StripeSeatPlanId ==
previousItem.Plan.Id));
var currentSubscriptionHasSecretsManager = subscription.Items.Any(i => var currentSubscriptionHasSecretsManager =
StaticStore.Plans.Any(p => subscription.Items.Any(
p.SecretsManager is not null && currentSubscriptionItem => currentSubscriptionItem.Plan.Id == plan.SecretsManager.StripeSeatPlanId);
p.SecretsManager.StripeSeatPlanId == i.Plan.Id));
if (!previousSubscriptionHasSecretsManager || currentSubscriptionHasSecretsManager) if (!previousSubscriptionHasSecretsManager || currentSubscriptionHasSecretsManager)
{ {
@ -200,9 +228,11 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler
private async Task ScheduleCancellationJobAsync(string subscriptionId, Guid organizationId) private async Task ScheduleCancellationJobAsync(string subscriptionId, Guid organizationId)
{ {
var isResellerManagedOrgAlertEnabled = _featureService.IsEnabled(FeatureFlagKeys.ResellerManagedOrgAlert); var isResellerManagedOrgAlertEnabled = _featureService.IsEnabled(FeatureFlagKeys.ResellerManagedOrgAlert);
if (!isResellerManagedOrgAlertEnabled)
if (isResellerManagedOrgAlertEnabled)
{ {
return;
}
var scheduler = await _schedulerFactory.GetScheduler(); var scheduler = await _schedulerFactory.GetScheduler();
var job = JobBuilder.Create<SubscriptionCancellationJob>() var job = JobBuilder.Create<SubscriptionCancellationJob>()
@ -219,4 +249,3 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler
await scheduler.ScheduleJob(job, trigger); await scheduler.ScheduleJob(job, trigger);
} }
} }
}

View File

@ -1,103 +1,79 @@
using Bit.Billing.Constants; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Entities; using Bit.Core.Billing.Constants;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Extensions;
using Bit.Core.Billing.Pricing;
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces; using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Utilities;
using Stripe; using Stripe;
using Event = Stripe.Event; using Event = Stripe.Event;
namespace Bit.Billing.Services.Implementations; namespace Bit.Billing.Services.Implementations;
public class UpcomingInvoiceHandler : IUpcomingInvoiceHandler public class UpcomingInvoiceHandler(
{
private readonly ILogger<StripeEventProcessor> _logger;
private readonly IStripeEventService _stripeEventService;
private readonly IUserService _userService;
private readonly IStripeFacade _stripeFacade;
private readonly IMailService _mailService;
private readonly IProviderRepository _providerRepository;
private readonly IValidateSponsorshipCommand _validateSponsorshipCommand;
private readonly IOrganizationRepository _organizationRepository;
private readonly IStripeEventUtilityService _stripeEventUtilityService;
public UpcomingInvoiceHandler(
ILogger<StripeEventProcessor> logger, ILogger<StripeEventProcessor> logger,
IStripeEventService stripeEventService,
IUserService userService,
IStripeFacade stripeFacade,
IMailService mailService, IMailService mailService,
IProviderRepository providerRepository,
IValidateSponsorshipCommand validateSponsorshipCommand,
IOrganizationRepository organizationRepository, IOrganizationRepository organizationRepository,
IStripeEventUtilityService stripeEventUtilityService) IPricingClient pricingClient,
IProviderRepository providerRepository,
IStripeFacade stripeFacade,
IStripeEventService stripeEventService,
IStripeEventUtilityService stripeEventUtilityService,
IUserRepository userRepository,
IValidateSponsorshipCommand validateSponsorshipCommand)
: IUpcomingInvoiceHandler
{ {
_logger = logger;
_stripeEventService = stripeEventService;
_userService = userService;
_stripeFacade = stripeFacade;
_mailService = mailService;
_providerRepository = providerRepository;
_validateSponsorshipCommand = validateSponsorshipCommand;
_organizationRepository = organizationRepository;
_stripeEventUtilityService = stripeEventUtilityService;
}
/// <summary>
/// Handles the <see cref="HandledStripeWebhook.UpcomingInvoice"/> event type from Stripe.
/// </summary>
/// <param name="parsedEvent"></param>
/// <exception cref="Exception"></exception>
public async Task HandleAsync(Event parsedEvent) public async Task HandleAsync(Event parsedEvent)
{ {
var invoice = await _stripeEventService.GetInvoice(parsedEvent); var invoice = await stripeEventService.GetInvoice(parsedEvent);
if (string.IsNullOrEmpty(invoice.SubscriptionId)) if (string.IsNullOrEmpty(invoice.SubscriptionId))
{ {
_logger.LogWarning("Received 'invoice.upcoming' Event with ID '{eventId}' that did not include a Subscription ID", parsedEvent.Id); logger.LogInformation("Received 'invoice.upcoming' Event with ID '{eventId}' that did not include a Subscription ID", parsedEvent.Id);
return; return;
} }
var subscription = await _stripeFacade.GetSubscription(invoice.SubscriptionId); var subscription = await stripeFacade.GetSubscription(invoice.SubscriptionId, new SubscriptionGetOptions
if (subscription == null)
{ {
throw new Exception( Expand = ["customer.tax", "customer.tax_ids"]
$"Received null Subscription from Stripe for ID '{invoice.SubscriptionId}' while processing Event with ID '{parsedEvent.Id}'"); });
}
var updatedSubscription = await TryEnableAutomaticTaxAsync(subscription); var (organizationId, userId, providerId) = stripeEventUtilityService.GetIdsFromMetadata(subscription.Metadata);
var (organizationId, userId, providerId) = _stripeEventUtilityService.GetIdsFromMetadata(updatedSubscription.Metadata);
var invoiceLineItemDescriptions = invoice.Lines.Select(i => i.Description).ToList();
if (organizationId.HasValue) if (organizationId.HasValue)
{ {
if (_stripeEventUtilityService.IsSponsoredSubscription(updatedSubscription)) var organization = await organizationRepository.GetByIdAsync(organizationId.Value);
{
var sponsorshipIsValid =
await _validateSponsorshipCommand.ValidateSponsorshipAsync(organizationId.Value);
if (!sponsorshipIsValid)
{
// If the sponsorship is invalid, then the subscription was updated to use the regular families plan
// price. Given that this is the case, we need the new invoice amount
subscription = await _stripeFacade.GetSubscription(subscription.Id,
new SubscriptionGetOptions { Expand = ["latest_invoice"] });
invoice = subscription.LatestInvoice; if (organization == null)
invoiceLineItemDescriptions = invoice.Lines.Select(i => i.Description).ToList();
}
}
var organization = await _organizationRepository.GetByIdAsync(organizationId.Value);
if (organization == null || !OrgPlanForInvoiceNotifications(organization))
{ {
return; return;
} }
await SendEmails(new List<string> { organization.BillingEmail }); await TryEnableAutomaticTaxAsync(subscription);
var plan = await pricingClient.GetPlanOrThrow(organization.PlanType);
if (!plan.IsAnnual)
{
return;
}
if (stripeEventUtilityService.IsSponsoredSubscription(subscription))
{
var sponsorshipIsValid = await validateSponsorshipCommand.ValidateSponsorshipAsync(organizationId.Value);
if (!sponsorshipIsValid)
{
/*
* If the sponsorship is invalid, then the subscription was updated to use the regular families plan
* price. Given that this is the case, we need the new invoice amount
*/
invoice = await stripeFacade.GetInvoice(subscription.LatestInvoiceId);
}
}
await SendUpcomingInvoiceEmailsAsync(new List<string> { organization.BillingEmail }, invoice);
/* /*
* TODO: https://bitwarden.atlassian.net/browse/PM-4862 * TODO: https://bitwarden.atlassian.net/browse/PM-4862
@ -112,66 +88,81 @@ public class UpcomingInvoiceHandler : IUpcomingInvoiceHandler
} }
else if (userId.HasValue) else if (userId.HasValue)
{ {
var user = await _userService.GetUserByIdAsync(userId.Value); var user = await userRepository.GetByIdAsync(userId.Value);
if (user?.Premium == true) if (user == null)
{ {
await SendEmails(new List<string> { user.Email }); return;
}
await TryEnableAutomaticTaxAsync(subscription);
if (user.Premium)
{
await SendUpcomingInvoiceEmailsAsync(new List<string> { user.Email }, invoice);
} }
} }
else if (providerId.HasValue) else if (providerId.HasValue)
{ {
var provider = await _providerRepository.GetByIdAsync(providerId.Value); var provider = await providerRepository.GetByIdAsync(providerId.Value);
if (provider == null) if (provider == null)
{ {
_logger.LogError(
"Received invoice.Upcoming webhook ({EventID}) for Provider ({ProviderID}) that does not exist",
parsedEvent.Id,
providerId.Value);
return; return;
} }
await SendEmails(new List<string> { provider.BillingEmail }); await TryEnableAutomaticTaxAsync(subscription);
await SendUpcomingInvoiceEmailsAsync(new List<string> { provider.BillingEmail }, invoice);
}
} }
return; private async Task SendUpcomingInvoiceEmailsAsync(IEnumerable<string> emails, Invoice invoice)
/*
* Sends emails to the given email addresses.
*/
async Task SendEmails(IEnumerable<string> emails)
{ {
var validEmails = emails.Where(e => !string.IsNullOrEmpty(e)); var validEmails = emails.Where(e => !string.IsNullOrEmpty(e));
var items = invoice.Lines.Select(i => i.Description).ToList();
if (invoice.NextPaymentAttempt.HasValue && invoice.AmountDue > 0) if (invoice.NextPaymentAttempt.HasValue && invoice.AmountDue > 0)
{ {
await _mailService.SendInvoiceUpcoming( await mailService.SendInvoiceUpcoming(
validEmails, validEmails,
invoice.AmountDue / 100M, invoice.AmountDue / 100M,
invoice.NextPaymentAttempt.Value, invoice.NextPaymentAttempt.Value,
invoiceLineItemDescriptions, items,
true); true);
} }
} }
private async Task TryEnableAutomaticTaxAsync(Subscription subscription)
{
if (subscription.AutomaticTax.Enabled ||
!subscription.Customer.HasBillingLocation() ||
await IsNonTaxableNonUSBusinessUseSubscription(subscription))
{
return;
} }
private async Task<Subscription> TryEnableAutomaticTaxAsync(Subscription subscription) await stripeFacade.UpdateSubscription(subscription.Id,
{ new SubscriptionUpdateOptions
if (subscription.AutomaticTax.Enabled)
{
return subscription;
}
var subscriptionUpdateOptions = new SubscriptionUpdateOptions
{ {
DefaultTaxRates = [],
AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true } AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }
}; });
return await _stripeFacade.UpdateSubscription(subscription.Id, subscriptionUpdateOptions); return;
}
private static bool OrgPlanForInvoiceNotifications(Organization org) => StaticStore.GetPlan(org.PlanType).IsAnnual; async Task<bool> IsNonTaxableNonUSBusinessUseSubscription(Subscription localSubscription)
{
var familyPriceIds = (await Task.WhenAll(
pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually2019),
pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually)))
.Select(plan => plan.PasswordManager.StripePlanId);
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();
}
}
} }

View File

@ -15,7 +15,8 @@ public enum PolicyType : byte
DisablePersonalVaultExport = 10, DisablePersonalVaultExport = 10,
ActivateAutofill = 11, ActivateAutofill = 11,
AutomaticAppLogIn = 12, AutomaticAppLogIn = 12,
FreeFamiliesSponsorshipPolicy = 13 FreeFamiliesSponsorshipPolicy = 13,
RemoveUnlockWithPin = 14,
} }
public static class PolicyTypeExtensions public static class PolicyTypeExtensions
@ -41,7 +42,8 @@ public static class PolicyTypeExtensions
PolicyType.DisablePersonalVaultExport => "Remove individual vault export", PolicyType.DisablePersonalVaultExport => "Remove individual vault export",
PolicyType.ActivateAutofill => "Active auto-fill", PolicyType.ActivateAutofill => "Active auto-fill",
PolicyType.AutomaticAppLogIn => "Automatically log in users for allowed applications", PolicyType.AutomaticAppLogIn => "Automatically log in users for allowed applications",
PolicyType.FreeFamiliesSponsorshipPolicy => "Remove Free Bitwarden Families sponsorship" PolicyType.FreeFamiliesSponsorshipPolicy => "Remove Free Bitwarden Families sponsorship",
PolicyType.RemoveUnlockWithPin => "Remove unlock with PIN"
}; };
} }
} }

View File

@ -0,0 +1,39 @@
#nullable enable
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
using Bit.Core.Enums;
using Bit.Core.Models.Data;
using Bit.Core.Utilities;
namespace Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
/// <summary>
/// Represents an OrganizationUser and a Policy which *may* be enforced against them.
/// You may assume that the Policy is enabled and that the organization's plan supports policies.
/// This is consumed by <see cref="IPolicyRequirement"/> to create requirements for specific policy types.
/// </summary>
public class PolicyDetails
{
public Guid OrganizationUserId { get; set; }
public Guid OrganizationId { get; set; }
public PolicyType PolicyType { get; set; }
public string? PolicyData { get; set; }
public OrganizationUserType OrganizationUserType { get; set; }
public OrganizationUserStatusType OrganizationUserStatus { get; set; }
/// <summary>
/// Custom permissions for the organization user, if any. Use <see cref="GetOrganizationUserCustomPermissions"/>
/// to deserialize.
/// </summary>
public string? OrganizationUserPermissionsData { get; set; }
/// <summary>
/// True if the user is also a ProviderUser for the organization, false otherwise.
/// </summary>
public bool IsProvider { get; set; }
public T GetDataModel<T>() where T : IPolicyDataModel, new()
=> CoreHelpers.LoadClassFromJsonData<T>(PolicyData);
public Permissions GetOrganizationUserCustomPermissions()
=> CoreHelpers.LoadClassFromJsonData<Permissions>(OrganizationUserPermissionsData);
}

View File

@ -1,5 +1,6 @@
using System.Net; using System.Net;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using Bit.Core.Billing.Enums;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Utilities; using Bit.Core.Utilities;
@ -23,6 +24,7 @@ public class ProviderOrganizationOrganizationDetails
public int? OccupiedSeats { get; set; } public int? OccupiedSeats { get; set; }
public int? Seats { get; set; } public int? Seats { get; set; }
public string Plan { get; set; } public string Plan { get; set; }
public PlanType PlanType { get; set; }
public OrganizationStatusType Status { get; set; } public OrganizationStatusType Status { get; set; }
/// <summary> /// <summary>

View File

@ -2,6 +2,7 @@
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Enums; using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Pricing;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
@ -24,6 +25,7 @@ public class UpdateOrganizationUserCommand : IUpdateOrganizationUserCommand
private readonly ICollectionRepository _collectionRepository; private readonly ICollectionRepository _collectionRepository;
private readonly IGroupRepository _groupRepository; private readonly IGroupRepository _groupRepository;
private readonly IHasConfirmedOwnersExceptQuery _hasConfirmedOwnersExceptQuery; private readonly IHasConfirmedOwnersExceptQuery _hasConfirmedOwnersExceptQuery;
private readonly IPricingClient _pricingClient;
public UpdateOrganizationUserCommand( public UpdateOrganizationUserCommand(
IEventService eventService, IEventService eventService,
@ -34,7 +36,8 @@ public class UpdateOrganizationUserCommand : IUpdateOrganizationUserCommand
IUpdateSecretsManagerSubscriptionCommand updateSecretsManagerSubscriptionCommand, IUpdateSecretsManagerSubscriptionCommand updateSecretsManagerSubscriptionCommand,
ICollectionRepository collectionRepository, ICollectionRepository collectionRepository,
IGroupRepository groupRepository, IGroupRepository groupRepository,
IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery) IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery,
IPricingClient pricingClient)
{ {
_eventService = eventService; _eventService = eventService;
_organizationService = organizationService; _organizationService = organizationService;
@ -45,6 +48,7 @@ public class UpdateOrganizationUserCommand : IUpdateOrganizationUserCommand
_collectionRepository = collectionRepository; _collectionRepository = collectionRepository;
_groupRepository = groupRepository; _groupRepository = groupRepository;
_hasConfirmedOwnersExceptQuery = hasConfirmedOwnersExceptQuery; _hasConfirmedOwnersExceptQuery = hasConfirmedOwnersExceptQuery;
_pricingClient = pricingClient;
} }
/// <summary> /// <summary>
@ -59,10 +63,10 @@ public class UpdateOrganizationUserCommand : IUpdateOrganizationUserCommand
List<CollectionAccessSelection>? collectionAccess, IEnumerable<Guid>? groupAccess) List<CollectionAccessSelection>? collectionAccess, IEnumerable<Guid>? groupAccess)
{ {
// Avoid multiple enumeration // Avoid multiple enumeration
collectionAccess = collectionAccess?.ToList(); var collectionAccessList = collectionAccess?.ToList() ?? [];
groupAccess = groupAccess?.ToList(); groupAccess = groupAccess?.ToList();
if (organizationUser.Id.Equals(default(Guid))) if (organizationUser.Id.Equals(Guid.Empty))
{ {
throw new BadRequestException("Invite the user first."); throw new BadRequestException("Invite the user first.");
} }
@ -89,9 +93,9 @@ public class UpdateOrganizationUserCommand : IUpdateOrganizationUserCommand
} }
} }
if (collectionAccess?.Any() == true) if (collectionAccessList.Count != 0)
{ {
await ValidateCollectionAccessAsync(originalOrganizationUser, collectionAccess.ToList()); await ValidateCollectionAccessAsync(originalOrganizationUser, collectionAccessList);
} }
if (groupAccess?.Any() == true) if (groupAccess?.Any() == true)
@ -107,14 +111,15 @@ public class UpdateOrganizationUserCommand : IUpdateOrganizationUserCommand
await _organizationService.ValidateOrganizationCustomPermissionsEnabledAsync(organizationUser.OrganizationId, organizationUser.Type); await _organizationService.ValidateOrganizationCustomPermissionsEnabledAsync(organizationUser.OrganizationId, organizationUser.Type);
if (organizationUser.Type != OrganizationUserType.Owner && if (organizationUser.Type != OrganizationUserType.Owner &&
!await _hasConfirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync(organizationUser.OrganizationId, new[] { organizationUser.Id })) !await _hasConfirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync(organizationUser.OrganizationId,
[organizationUser.Id]))
{ {
throw new BadRequestException("Organization must have at least one confirmed owner."); throw new BadRequestException("Organization must have at least one confirmed owner.");
} }
if (collectionAccess?.Count > 0) if (collectionAccessList.Count > 0)
{ {
var invalidAssociations = collectionAccess.Where(cas => cas.Manage && (cas.ReadOnly || cas.HidePasswords)); var invalidAssociations = collectionAccessList.Where(cas => cas.Manage && (cas.ReadOnly || cas.HidePasswords));
if (invalidAssociations.Any()) if (invalidAssociations.Any())
{ {
throw new BadRequestException("The Manage property is mutually exclusive and cannot be true while the ReadOnly or HidePasswords properties are also true."); throw new BadRequestException("The Manage property is mutually exclusive and cannot be true while the ReadOnly or HidePasswords properties are also true.");
@ -128,13 +133,15 @@ public class UpdateOrganizationUserCommand : IUpdateOrganizationUserCommand
var additionalSmSeatsRequired = await _countNewSmSeatsRequiredQuery.CountNewSmSeatsRequiredAsync(organizationUser.OrganizationId, 1); var additionalSmSeatsRequired = await _countNewSmSeatsRequiredQuery.CountNewSmSeatsRequiredAsync(organizationUser.OrganizationId, 1);
if (additionalSmSeatsRequired > 0) if (additionalSmSeatsRequired > 0)
{ {
var update = new SecretsManagerSubscriptionUpdate(organization, true) // TODO: https://bitwarden.atlassian.net/browse/PM-17012
var plan = await _pricingClient.GetPlanOrThrow(organization.PlanType);
var update = new SecretsManagerSubscriptionUpdate(organization, plan, true)
.AdjustSeats(additionalSmSeatsRequired); .AdjustSeats(additionalSmSeatsRequired);
await _updateSecretsManagerSubscriptionCommand.UpdateSubscriptionAsync(update); await _updateSecretsManagerSubscriptionCommand.UpdateSubscriptionAsync(update);
} }
} }
await _organizationUserRepository.ReplaceAsync(organizationUser, collectionAccess); await _organizationUserRepository.ReplaceAsync(organizationUser, collectionAccessList);
if (groupAccess != null) if (groupAccess != null)
{ {

View File

@ -3,6 +3,7 @@ using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Services; using Bit.Core.AdminConsole.Services;
using Bit.Core.Billing.Enums; using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Models.Sales; using Bit.Core.Billing.Models.Sales;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Services; using Bit.Core.Billing.Services;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Entities; using Bit.Core.Entities;
@ -23,8 +24,7 @@ namespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
public record SignUpOrganizationResponse( public record SignUpOrganizationResponse(
Organization Organization, Organization Organization,
OrganizationUser OrganizationUser, OrganizationUser OrganizationUser);
Collection DefaultCollection);
public interface ICloudOrganizationSignUpCommand public interface ICloudOrganizationSignUpCommand
{ {
@ -33,7 +33,6 @@ public interface ICloudOrganizationSignUpCommand
public class CloudOrganizationSignUpCommand( public class CloudOrganizationSignUpCommand(
IOrganizationUserRepository organizationUserRepository, IOrganizationUserRepository organizationUserRepository,
IFeatureService featureService,
IOrganizationBillingService organizationBillingService, IOrganizationBillingService organizationBillingService,
IPaymentService paymentService, IPaymentService paymentService,
IPolicyService policyService, IPolicyService policyService,
@ -45,11 +44,12 @@ public class CloudOrganizationSignUpCommand(
IPushRegistrationService pushRegistrationService, IPushRegistrationService pushRegistrationService,
IPushNotificationService pushNotificationService, IPushNotificationService pushNotificationService,
ICollectionRepository collectionRepository, ICollectionRepository collectionRepository,
IDeviceRepository deviceRepository) : ICloudOrganizationSignUpCommand IDeviceRepository deviceRepository,
IPricingClient pricingClient) : ICloudOrganizationSignUpCommand
{ {
public async Task<SignUpOrganizationResponse> SignUpOrganizationAsync(OrganizationSignup signup) public async Task<SignUpOrganizationResponse> SignUpOrganizationAsync(OrganizationSignup signup)
{ {
var plan = StaticStore.GetPlan(signup.Plan); var plan = await pricingClient.GetPlanOrThrow(signup.Plan);
ValidatePasswordManagerPlan(plan, signup); ValidatePasswordManagerPlan(plan, signup);
@ -142,7 +142,7 @@ public class CloudOrganizationSignUpCommand(
// TODO: add reference events for SmSeats and Service Accounts - see AC-1481 // TODO: add reference events for SmSeats and Service Accounts - see AC-1481
}); });
return new SignUpOrganizationResponse(returnValue.organization, returnValue.organizationUser, returnValue.defaultCollection); return new SignUpOrganizationResponse(returnValue.organization, returnValue.organizationUser);
} }
public void ValidatePasswordManagerPlan(Plan plan, OrganizationUpgrade upgrade) public void ValidatePasswordManagerPlan(Plan plan, OrganizationUpgrade upgrade)

View File

@ -1,4 +1,5 @@
using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities;
using Bit.Core.Exceptions;
namespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces; namespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;

View File

@ -0,0 +1,14 @@
namespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;
/// <summary>
/// Command interface for disabling organizations.
/// </summary>
public interface IOrganizationDisableCommand
{
/// <summary>
/// Disables an organization with an optional expiration date.
/// </summary>
/// <param name="organizationId">The unique identifier of the organization to disable.</param>
/// <param name="expirationDate">Optional date when the disable status should expire.</param>
Task DisableAsync(Guid organizationId, DateTime? expirationDate);
}

View File

@ -0,0 +1,11 @@
namespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;
public interface IOrganizationEnableCommand
{
/// <summary>
/// Enables an organization that is currently disabled and has a gateway configured.
/// </summary>
/// <param name="organizationId">The unique identifier of the organization to enable.</param>
/// <param name="expirationDate">When provided, sets the date the organization's subscription will expire. If not provided, no expiration date will be set.</param>
Task EnableAsync(Guid organizationId, DateTime? expirationDate = null);
}

View File

@ -1,4 +1,5 @@
using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities;
using Bit.Core.Exceptions;
namespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces; namespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;

View File

@ -0,0 +1,33 @@
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;
using Bit.Core.Repositories;
using Bit.Core.Services;
namespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
public class OrganizationDisableCommand : IOrganizationDisableCommand
{
private readonly IOrganizationRepository _organizationRepository;
private readonly IApplicationCacheService _applicationCacheService;
public OrganizationDisableCommand(
IOrganizationRepository organizationRepository,
IApplicationCacheService applicationCacheService)
{
_organizationRepository = organizationRepository;
_applicationCacheService = applicationCacheService;
}
public async Task DisableAsync(Guid organizationId, DateTime? expirationDate)
{
var organization = await _organizationRepository.GetByIdAsync(organizationId);
if (organization is { Enabled: true })
{
organization.Enabled = false;
organization.ExpirationDate = expirationDate;
organization.RevisionDate = DateTime.UtcNow;
await _organizationRepository.ReplaceAsync(organization);
await _applicationCacheService.UpsertOrganizationAbilityAsync(organization);
}
}
}

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