1
0
mirror of https://github.com/bitwarden/server synced 2025-12-22 11:13:27 +00:00

Merge branch 'main' into billing/pm-27605/transition-migration

This commit is contained in:
cyprain-okeke
2025-11-19 17:01:49 +01:00
committed by GitHub
156 changed files with 14467 additions and 741 deletions

View File

@@ -1,6 +1,6 @@
name: Bitwarden Unified Deployment Bug Report name: Bitwarden Lite Deployment Bug Report
description: File a bug report description: File a bug report
labels: [bug, bw-unified-deploy] labels: [bug, bw-lite-deploy]
body: body:
- type: markdown - type: markdown
attributes: attributes:
@@ -74,7 +74,7 @@ body:
id: epic-label id: epic-label
attributes: attributes:
label: Issue-Link label: Issue-Link
description: Link to our pinned issue, tracking all Bitwarden Unified description: Link to our pinned issue, tracking all Bitwarden Lite
value: | value: |
https://github.com/bitwarden/server/issues/2480 https://github.com/bitwarden/server/issues/2480
validations: validations:

View File

@@ -22,7 +22,7 @@ env:
jobs: jobs:
lint: lint:
name: Lint name: Lint
runs-on: ubuntu-24.04 runs-on: ubuntu-22.04
steps: steps:
- name: Check out repo - name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
@@ -38,7 +38,7 @@ jobs:
build-artifacts: build-artifacts:
name: Build Docker images name: Build Docker images
runs-on: ubuntu-24.04 runs-on: ubuntu-22.04
needs: needs:
- lint - lint
outputs: outputs:
@@ -49,7 +49,6 @@ jobs:
timeout-minutes: 45 timeout-minutes: 45
strategy: strategy:
fail-fast: false fail-fast: false
max-parallel: 5
matrix: matrix:
include: include:
- project_name: Admin - project_name: Admin
@@ -292,7 +291,7 @@ jobs:
upload: upload:
name: Upload name: Upload
runs-on: ubuntu-24.04 runs-on: ubuntu-22.04
needs: build-artifacts needs: build-artifacts
permissions: permissions:
id-token: write id-token: write
@@ -410,7 +409,7 @@ jobs:
build-mssqlmigratorutility: build-mssqlmigratorutility:
name: Build MSSQL migrator utility name: Build MSSQL migrator utility
runs-on: ubuntu-24.04 runs-on: ubuntu-22.04
needs: needs:
- lint - lint
defaults: defaults:
@@ -467,7 +466,7 @@ jobs:
if: | if: |
github.event_name != 'pull_request' github.event_name != 'pull_request'
&& (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc') && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc')
runs-on: ubuntu-24.04 runs-on: ubuntu-22.04
needs: needs:
- build-artifacts - build-artifacts
permissions: permissions:
@@ -490,7 +489,7 @@ jobs:
- name: Log out from Azure - name: Log out from Azure
uses: bitwarden/gh-actions/azure-logout@main uses: bitwarden/gh-actions/azure-logout@main
- name: Trigger self-host build - name: Trigger Bitwarden Lite build
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with: with:
github-token: ${{ steps.retrieve-secret-pat.outputs.github-pat-bitwarden-devops-bot-repo-scope }} github-token: ${{ steps.retrieve-secret-pat.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
@@ -498,7 +497,7 @@ jobs:
await github.rest.actions.createWorkflowDispatch({ await github.rest.actions.createWorkflowDispatch({
owner: 'bitwarden', owner: 'bitwarden',
repo: 'self-host', repo: 'self-host',
workflow_id: 'build-unified.yml', workflow_id: 'build-bitwarden-lite.yml',
ref: 'main', ref: 'main',
inputs: { inputs: {
server_branch: process.env.GITHUB_REF server_branch: process.env.GITHUB_REF

View File

@@ -62,7 +62,7 @@ jobs:
docker compose --profile mssql --profile postgres --profile mysql up -d docker compose --profile mssql --profile postgres --profile mysql up -d
shell: pwsh shell: pwsh
- name: Add MariaDB for unified - name: Add MariaDB for Bitwarden Lite
# Use a different port than MySQL # Use a different port than MySQL
run: | run: |
docker run --detach --name mariadb --env MARIADB_ROOT_PASSWORD=mariadb-password -p 4306:3306 mariadb:10 docker run --detach --name mariadb --env MARIADB_ROOT_PASSWORD=mariadb-password -p 4306:3306 mariadb:10
@@ -133,7 +133,7 @@ jobs:
# Default Sqlite # Default Sqlite
BW_TEST_DATABASES__3__TYPE: "Sqlite" BW_TEST_DATABASES__3__TYPE: "Sqlite"
BW_TEST_DATABASES__3__CONNECTIONSTRING: "Data Source=${{ runner.temp }}/test.db" BW_TEST_DATABASES__3__CONNECTIONSTRING: "Data Source=${{ runner.temp }}/test.db"
# Unified MariaDB # Bitwarden Lite 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" /p:CoverletOutputFormatter="cobertura" --collect:"XPlat Code Coverage" run: dotnet test --logger "trx;LogFileName=infrastructure-test-results.trx" /p:CoverletOutputFormatter="cobertura" --collect:"XPlat Code Coverage"

View File

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

View File

@@ -30,6 +30,7 @@
}, },
"storage": { "storage": {
"connectionString": "UseDevelopmentStorage=true" "connectionString": "UseDevelopmentStorage=true"
} },
"pricingUri": "https://billingpricing.qa.bitwarden.pw"
} }
} }

View File

@@ -24,6 +24,7 @@
"storage": { "storage": {
"connectionString": "UseDevelopmentStorage=true" "connectionString": "UseDevelopmentStorage=true"
}, },
"developmentDirectory": "../../../dev" "developmentDirectory": "../../../dev",
"pricingUri": "https://billingpricing.qa.bitwarden.pw"
} }
} }

View File

@@ -13,7 +13,7 @@ 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 Bit.Core.Test.Billing.Mocks;
using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute; using NSubstitute;
@@ -207,7 +207,7 @@ public class RemoveOrganizationFromProviderCommandTests
organization.PlanType = PlanType.TeamsMonthly; organization.PlanType = PlanType.TeamsMonthly;
var teamsMonthlyPlan = StaticStore.GetPlan(PlanType.TeamsMonthly); var teamsMonthlyPlan = MockPlans.Get(PlanType.TeamsMonthly);
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(PlanType.TeamsMonthly).Returns(teamsMonthlyPlan); sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(PlanType.TeamsMonthly).Returns(teamsMonthlyPlan);
@@ -296,7 +296,7 @@ public class RemoveOrganizationFromProviderCommandTests
organization.PlanType = PlanType.TeamsMonthly; organization.PlanType = PlanType.TeamsMonthly;
var teamsMonthlyPlan = StaticStore.GetPlan(PlanType.TeamsMonthly); var teamsMonthlyPlan = MockPlans.Get(PlanType.TeamsMonthly);
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(PlanType.TeamsMonthly).Returns(teamsMonthlyPlan); sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(PlanType.TeamsMonthly).Returns(teamsMonthlyPlan);
@@ -416,7 +416,7 @@ public class RemoveOrganizationFromProviderCommandTests
organization.PlanType = PlanType.TeamsMonthly; organization.PlanType = PlanType.TeamsMonthly;
organization.Enabled = false; // Start with a disabled organization organization.Enabled = false; // Start with a disabled organization
var teamsMonthlyPlan = StaticStore.GetPlan(PlanType.TeamsMonthly); var teamsMonthlyPlan = MockPlans.Get(PlanType.TeamsMonthly);
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(PlanType.TeamsMonthly).Returns(teamsMonthlyPlan); sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(PlanType.TeamsMonthly).Returns(teamsMonthlyPlan);

View File

@@ -20,6 +20,7 @@ using Bit.Core.Models.Business;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Test.AutoFixture.OrganizationFixtures; using Bit.Core.Test.AutoFixture.OrganizationFixtures;
using Bit.Core.Test.Billing.Mocks;
using Bit.Core.Tokens; using Bit.Core.Tokens;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture;
@@ -811,12 +812,12 @@ public class ProviderServiceTests
organization.Plan = "Enterprise (Monthly)"; organization.Plan = "Enterprise (Monthly)";
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType) sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType)
.Returns(StaticStore.GetPlan(organization.PlanType)); .Returns(MockPlans.Get(organization.PlanType));
var expectedPlanType = PlanType.EnterpriseMonthly2020; var expectedPlanType = PlanType.EnterpriseMonthly2020;
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(expectedPlanType) sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(expectedPlanType)
.Returns(StaticStore.GetPlan(expectedPlanType)); .Returns(MockPlans.Get(expectedPlanType));
var expectedPlanId = "2020-enterprise-org-seat-monthly"; var expectedPlanId = "2020-enterprise-org-seat-monthly";

View File

@@ -18,6 +18,7 @@ using Bit.Core.Enums;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Settings; using Bit.Core.Settings;
using Bit.Core.Test.Billing.Mocks;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.AutoFixture.Attributes;
using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.DataProtection;
@@ -72,7 +73,7 @@ public class BusinessUnitConverterTests
{ {
organization.PlanType = PlanType.EnterpriseAnnually2020; organization.PlanType = PlanType.EnterpriseAnnually2020;
var enterpriseAnnually2020 = StaticStore.GetPlan(PlanType.EnterpriseAnnually2020); var enterpriseAnnually2020 = MockPlans.Get(PlanType.EnterpriseAnnually2020);
var subscription = new Subscription var subscription = new Subscription
{ {
@@ -134,7 +135,7 @@ public class BusinessUnitConverterTests
_pricingClient.GetPlanOrThrow(PlanType.EnterpriseAnnually2020) _pricingClient.GetPlanOrThrow(PlanType.EnterpriseAnnually2020)
.Returns(enterpriseAnnually2020); .Returns(enterpriseAnnually2020);
var enterpriseAnnually = StaticStore.GetPlan(PlanType.EnterpriseAnnually); var enterpriseAnnually = MockPlans.Get(PlanType.EnterpriseAnnually);
_pricingClient.GetPlanOrThrow(PlanType.EnterpriseAnnually) _pricingClient.GetPlanOrThrow(PlanType.EnterpriseAnnually)
.Returns(enterpriseAnnually); .Returns(enterpriseAnnually);
@@ -242,7 +243,7 @@ public class BusinessUnitConverterTests
argument.Status == ProviderStatusType.Pending && argument.Status == ProviderStatusType.Pending &&
argument.Type == ProviderType.BusinessUnit)).Returns(provider); argument.Type == ProviderType.BusinessUnit)).Returns(provider);
var plan = StaticStore.GetPlan(organization.PlanType); var plan = MockPlans.Get(organization.PlanType);
_pricingClient.GetPlanOrThrow(organization.PlanType).Returns(plan); _pricingClient.GetPlanOrThrow(organization.PlanType).Returns(plan);

View File

@@ -22,7 +22,7 @@ using Bit.Core.Exceptions;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Settings; using Bit.Core.Settings;
using Bit.Core.Utilities; using Bit.Core.Test.Billing.Mocks;
using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.AutoFixture.Attributes;
using Braintree; using Braintree;
@@ -140,7 +140,7 @@ public class ProviderBillingServiceTests
.Returns(existingPlan); .Returns(existingPlan);
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(existingPlan.PlanType) sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(existingPlan.PlanType)
.Returns(StaticStore.GetPlan(existingPlan.PlanType)); .Returns(MockPlans.Get(existingPlan.PlanType));
sutProvider.GetDependency<ISubscriberService>().GetSubscriptionOrThrow(provider) sutProvider.GetDependency<ISubscriberService>().GetSubscriptionOrThrow(provider)
.Returns(new Subscription .Returns(new Subscription
@@ -155,7 +155,7 @@ public class ProviderBillingServiceTests
Id = "si_ent_annual", Id = "si_ent_annual",
Price = new Price Price = new Price
{ {
Id = StaticStore.GetPlan(PlanType.EnterpriseAnnually).PasswordManager Id = MockPlans.Get(PlanType.EnterpriseAnnually).PasswordManager
.StripeProviderPortalSeatPlanId .StripeProviderPortalSeatPlanId
}, },
Quantity = 10 Quantity = 10
@@ -168,7 +168,7 @@ public class ProviderBillingServiceTests
new ChangeProviderPlanCommand(provider, providerPlanId, PlanType.EnterpriseMonthly); new ChangeProviderPlanCommand(provider, providerPlanId, PlanType.EnterpriseMonthly);
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(command.NewPlan) sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(command.NewPlan)
.Returns(StaticStore.GetPlan(command.NewPlan)); .Returns(MockPlans.Get(command.NewPlan));
// Act // Act
await sutProvider.Sut.ChangePlan(command); await sutProvider.Sut.ChangePlan(command);
@@ -185,7 +185,7 @@ public class ProviderBillingServiceTests
Arg.Is<SubscriptionUpdateOptions>(p => Arg.Is<SubscriptionUpdateOptions>(p =>
p.Items.Count(si => si.Id == "si_ent_annual" && si.Deleted == true) == 1)); p.Items.Count(si => si.Id == "si_ent_annual" && si.Deleted == true) == 1));
var newPlanCfg = StaticStore.GetPlan(command.NewPlan); var newPlanCfg = MockPlans.Get(command.NewPlan);
await stripeAdapter.Received(1) await stripeAdapter.Received(1)
.SubscriptionUpdateAsync( .SubscriptionUpdateAsync(
Arg.Is(provider.GatewaySubscriptionId), Arg.Is(provider.GatewaySubscriptionId),
@@ -491,7 +491,7 @@ public class ProviderBillingServiceTests
foreach (var plan in providerPlans) foreach (var plan in providerPlans)
{ {
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(plan.PlanType) sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(plan.PlanType)
.Returns(StaticStore.GetPlan(plan.PlanType)); .Returns(MockPlans.Get(plan.PlanType));
} }
sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id).Returns(providerPlans); sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id).Returns(providerPlans);
@@ -514,7 +514,7 @@ public class ProviderBillingServiceTests
sutProvider.GetDependency<ISubscriberService>().GetSubscriptionOrThrow(provider).Returns(subscription); sutProvider.GetDependency<ISubscriberService>().GetSubscriptionOrThrow(provider).Returns(subscription);
// 50 seats currently assigned with a seat minimum of 100 // 50 seats currently assigned with a seat minimum of 100
var teamsMonthlyPlan = StaticStore.GetPlan(PlanType.TeamsMonthly); var teamsMonthlyPlan = MockPlans.Get(PlanType.TeamsMonthly);
sutProvider.GetDependency<IProviderOrganizationRepository>().GetManyDetailsByProviderAsync(provider.Id).Returns( sutProvider.GetDependency<IProviderOrganizationRepository>().GetManyDetailsByProviderAsync(provider.Id).Returns(
[ [
@@ -573,7 +573,7 @@ public class ProviderBillingServiceTests
foreach (var plan in providerPlans) foreach (var plan in providerPlans)
{ {
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(plan.PlanType) sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(plan.PlanType)
.Returns(StaticStore.GetPlan(plan.PlanType)); .Returns(MockPlans.Get(plan.PlanType));
} }
var providerPlan = providerPlans.First(); var providerPlan = providerPlans.First();
@@ -598,7 +598,7 @@ public class ProviderBillingServiceTests
sutProvider.GetDependency<ISubscriberService>().GetSubscriptionOrThrow(provider).Returns(subscription); sutProvider.GetDependency<ISubscriberService>().GetSubscriptionOrThrow(provider).Returns(subscription);
// 95 seats currently assigned with a seat minimum of 100 // 95 seats currently assigned with a seat minimum of 100
var teamsMonthlyPlan = StaticStore.GetPlan(PlanType.TeamsMonthly); var teamsMonthlyPlan = MockPlans.Get(PlanType.TeamsMonthly);
sutProvider.GetDependency<IProviderOrganizationRepository>().GetManyDetailsByProviderAsync(provider.Id).Returns( sutProvider.GetDependency<IProviderOrganizationRepository>().GetManyDetailsByProviderAsync(provider.Id).Returns(
[ [
@@ -661,7 +661,7 @@ public class ProviderBillingServiceTests
foreach (var plan in providerPlans) foreach (var plan in providerPlans)
{ {
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(plan.PlanType) sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(plan.PlanType)
.Returns(StaticStore.GetPlan(plan.PlanType)); .Returns(MockPlans.Get(plan.PlanType));
} }
var providerPlan = providerPlans.First(); var providerPlan = providerPlans.First();
@@ -686,7 +686,7 @@ public class ProviderBillingServiceTests
sutProvider.GetDependency<ISubscriberService>().GetSubscriptionOrThrow(provider).Returns(subscription); sutProvider.GetDependency<ISubscriberService>().GetSubscriptionOrThrow(provider).Returns(subscription);
// 110 seats currently assigned with a seat minimum of 100 // 110 seats currently assigned with a seat minimum of 100
var teamsMonthlyPlan = StaticStore.GetPlan(PlanType.TeamsMonthly); var teamsMonthlyPlan = MockPlans.Get(PlanType.TeamsMonthly);
sutProvider.GetDependency<IProviderOrganizationRepository>().GetManyDetailsByProviderAsync(provider.Id).Returns( sutProvider.GetDependency<IProviderOrganizationRepository>().GetManyDetailsByProviderAsync(provider.Id).Returns(
[ [
@@ -749,7 +749,7 @@ public class ProviderBillingServiceTests
foreach (var plan in providerPlans) foreach (var plan in providerPlans)
{ {
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(plan.PlanType) sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(plan.PlanType)
.Returns(StaticStore.GetPlan(plan.PlanType)); .Returns(MockPlans.Get(plan.PlanType));
} }
var providerPlan = providerPlans.First(); var providerPlan = providerPlans.First();
@@ -774,7 +774,7 @@ public class ProviderBillingServiceTests
sutProvider.GetDependency<ISubscriberService>().GetSubscriptionOrThrow(provider).Returns(subscription); sutProvider.GetDependency<ISubscriberService>().GetSubscriptionOrThrow(provider).Returns(subscription);
// 110 seats currently assigned with a seat minimum of 100 // 110 seats currently assigned with a seat minimum of 100
var teamsMonthlyPlan = StaticStore.GetPlan(PlanType.TeamsMonthly); var teamsMonthlyPlan = MockPlans.Get(PlanType.TeamsMonthly);
sutProvider.GetDependency<IProviderOrganizationRepository>().GetManyDetailsByProviderAsync(provider.Id).Returns( sutProvider.GetDependency<IProviderOrganizationRepository>().GetManyDetailsByProviderAsync(provider.Id).Returns(
[ [
@@ -827,13 +827,13 @@ public class ProviderBillingServiceTests
} }
]); ]);
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(planType).Returns(StaticStore.GetPlan(planType)); sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(planType).Returns(MockPlans.Get(planType));
sutProvider.GetDependency<IProviderOrganizationRepository>().GetManyDetailsByProviderAsync(provider.Id).Returns( sutProvider.GetDependency<IProviderOrganizationRepository>().GetManyDetailsByProviderAsync(provider.Id).Returns(
[ [
new ProviderOrganizationOrganizationDetails new ProviderOrganizationOrganizationDetails
{ {
Plan = StaticStore.GetPlan(planType).Name, Plan = MockPlans.Get(planType).Name,
Status = OrganizationStatusType.Managed, Status = OrganizationStatusType.Managed,
Seats = 5 Seats = 5
} }
@@ -865,13 +865,13 @@ public class ProviderBillingServiceTests
} }
]); ]);
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(planType).Returns(StaticStore.GetPlan(planType)); sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(planType).Returns(MockPlans.Get(planType));
sutProvider.GetDependency<IProviderOrganizationRepository>().GetManyDetailsByProviderAsync(provider.Id).Returns( sutProvider.GetDependency<IProviderOrganizationRepository>().GetManyDetailsByProviderAsync(provider.Id).Returns(
[ [
new ProviderOrganizationOrganizationDetails new ProviderOrganizationOrganizationDetails
{ {
Plan = StaticStore.GetPlan(planType).Name, Plan = MockPlans.Get(planType).Name,
Status = OrganizationStatusType.Managed, Status = OrganizationStatusType.Managed,
Seats = 15 Seats = 15
} }
@@ -1238,7 +1238,7 @@ public class ProviderBillingServiceTests
.Returns(providerPlans); .Returns(providerPlans);
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(PlanType.EnterpriseMonthly) sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(PlanType.EnterpriseMonthly)
.Returns(StaticStore.GetPlan(PlanType.EnterpriseMonthly)); .Returns(MockPlans.Get(PlanType.EnterpriseMonthly));
await ThrowsBillingExceptionAsync(() => sutProvider.Sut.SetupSubscription(provider)); await ThrowsBillingExceptionAsync(() => sutProvider.Sut.SetupSubscription(provider));
@@ -1266,7 +1266,7 @@ public class ProviderBillingServiceTests
.Returns(providerPlans); .Returns(providerPlans);
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(PlanType.TeamsMonthly) sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(PlanType.TeamsMonthly)
.Returns(StaticStore.GetPlan(PlanType.TeamsMonthly)); .Returns(MockPlans.Get(PlanType.TeamsMonthly));
await ThrowsBillingExceptionAsync(() => sutProvider.Sut.SetupSubscription(provider)); await ThrowsBillingExceptionAsync(() => sutProvider.Sut.SetupSubscription(provider));
@@ -1317,7 +1317,7 @@ public class ProviderBillingServiceTests
foreach (var plan in providerPlans) foreach (var plan in providerPlans)
{ {
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(plan.PlanType) sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(plan.PlanType)
.Returns(StaticStore.GetPlan(plan.PlanType)); .Returns(MockPlans.Get(plan.PlanType));
} }
sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id) sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id)
@@ -1373,7 +1373,7 @@ public class ProviderBillingServiceTests
foreach (var plan in providerPlans) foreach (var plan in providerPlans)
{ {
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(plan.PlanType) sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(plan.PlanType)
.Returns(StaticStore.GetPlan(plan.PlanType)); .Returns(MockPlans.Get(plan.PlanType));
} }
sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id) sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id)
@@ -1449,7 +1449,7 @@ public class ProviderBillingServiceTests
foreach (var plan in providerPlans) foreach (var plan in providerPlans)
{ {
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(plan.PlanType) sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(plan.PlanType)
.Returns(StaticStore.GetPlan(plan.PlanType)); .Returns(MockPlans.Get(plan.PlanType));
} }
sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id) sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id)
@@ -1525,7 +1525,7 @@ public class ProviderBillingServiceTests
foreach (var plan in providerPlans) foreach (var plan in providerPlans)
{ {
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(plan.PlanType) sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(plan.PlanType)
.Returns(StaticStore.GetPlan(plan.PlanType)); .Returns(MockPlans.Get(plan.PlanType));
} }
sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id) sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id)
@@ -1626,7 +1626,7 @@ public class ProviderBillingServiceTests
foreach (var plan in providerPlans) foreach (var plan in providerPlans)
{ {
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(plan.PlanType) sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(plan.PlanType)
.Returns(StaticStore.GetPlan(plan.PlanType)); .Returns(MockPlans.Get(plan.PlanType));
} }
sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id) sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id)
@@ -1704,7 +1704,7 @@ public class ProviderBillingServiceTests
foreach (var plan in providerPlans) foreach (var plan in providerPlans)
{ {
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(plan.PlanType) sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(plan.PlanType)
.Returns(StaticStore.GetPlan(plan.PlanType)); .Returns(MockPlans.Get(plan.PlanType));
} }
sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id) sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id)
@@ -1772,8 +1772,8 @@ public class ProviderBillingServiceTests
const string enterpriseLineItemId = "enterprise_line_item_id"; const string enterpriseLineItemId = "enterprise_line_item_id";
const string teamsLineItemId = "teams_line_item_id"; const string teamsLineItemId = "teams_line_item_id";
var enterprisePriceId = StaticStore.GetPlan(PlanType.EnterpriseMonthly).PasswordManager.StripeProviderPortalSeatPlanId; var enterprisePriceId = MockPlans.Get(PlanType.EnterpriseMonthly).PasswordManager.StripeProviderPortalSeatPlanId;
var teamsPriceId = StaticStore.GetPlan(PlanType.TeamsMonthly).PasswordManager.StripeProviderPortalSeatPlanId; var teamsPriceId = MockPlans.Get(PlanType.TeamsMonthly).PasswordManager.StripeProviderPortalSeatPlanId;
var subscription = new Subscription var subscription = new Subscription
{ {
@@ -1806,7 +1806,7 @@ public class ProviderBillingServiceTests
foreach (var plan in providerPlans) foreach (var plan in providerPlans)
{ {
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(plan.PlanType) sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(plan.PlanType)
.Returns(StaticStore.GetPlan(plan.PlanType)); .Returns(MockPlans.Get(plan.PlanType));
} }
providerPlanRepository.GetByProviderId(provider.Id).Returns(providerPlans); providerPlanRepository.GetByProviderId(provider.Id).Returns(providerPlans);
@@ -1852,8 +1852,8 @@ public class ProviderBillingServiceTests
const string enterpriseLineItemId = "enterprise_line_item_id"; const string enterpriseLineItemId = "enterprise_line_item_id";
const string teamsLineItemId = "teams_line_item_id"; const string teamsLineItemId = "teams_line_item_id";
var enterprisePriceId = StaticStore.GetPlan(PlanType.EnterpriseMonthly).PasswordManager.StripeProviderPortalSeatPlanId; var enterprisePriceId = MockPlans.Get(PlanType.EnterpriseMonthly).PasswordManager.StripeProviderPortalSeatPlanId;
var teamsPriceId = StaticStore.GetPlan(PlanType.TeamsMonthly).PasswordManager.StripeProviderPortalSeatPlanId; var teamsPriceId = MockPlans.Get(PlanType.TeamsMonthly).PasswordManager.StripeProviderPortalSeatPlanId;
var subscription = new Subscription var subscription = new Subscription
{ {
@@ -1886,7 +1886,7 @@ public class ProviderBillingServiceTests
foreach (var plan in providerPlans) foreach (var plan in providerPlans)
{ {
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(plan.PlanType) sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(plan.PlanType)
.Returns(StaticStore.GetPlan(plan.PlanType)); .Returns(MockPlans.Get(plan.PlanType));
} }
providerPlanRepository.GetByProviderId(provider.Id).Returns(providerPlans); providerPlanRepository.GetByProviderId(provider.Id).Returns(providerPlans);
@@ -1932,8 +1932,8 @@ public class ProviderBillingServiceTests
const string enterpriseLineItemId = "enterprise_line_item_id"; const string enterpriseLineItemId = "enterprise_line_item_id";
const string teamsLineItemId = "teams_line_item_id"; const string teamsLineItemId = "teams_line_item_id";
var enterprisePriceId = StaticStore.GetPlan(PlanType.EnterpriseMonthly).PasswordManager.StripeProviderPortalSeatPlanId; var enterprisePriceId = MockPlans.Get(PlanType.EnterpriseMonthly).PasswordManager.StripeProviderPortalSeatPlanId;
var teamsPriceId = StaticStore.GetPlan(PlanType.TeamsMonthly).PasswordManager.StripeProviderPortalSeatPlanId; var teamsPriceId = MockPlans.Get(PlanType.TeamsMonthly).PasswordManager.StripeProviderPortalSeatPlanId;
var subscription = new Subscription var subscription = new Subscription
{ {
@@ -1966,7 +1966,7 @@ public class ProviderBillingServiceTests
foreach (var plan in providerPlans) foreach (var plan in providerPlans)
{ {
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(plan.PlanType) sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(plan.PlanType)
.Returns(StaticStore.GetPlan(plan.PlanType)); .Returns(MockPlans.Get(plan.PlanType));
} }
providerPlanRepository.GetByProviderId(provider.Id).Returns(providerPlans); providerPlanRepository.GetByProviderId(provider.Id).Returns(providerPlans);
@@ -2006,8 +2006,8 @@ public class ProviderBillingServiceTests
const string enterpriseLineItemId = "enterprise_line_item_id"; const string enterpriseLineItemId = "enterprise_line_item_id";
const string teamsLineItemId = "teams_line_item_id"; const string teamsLineItemId = "teams_line_item_id";
var enterprisePriceId = StaticStore.GetPlan(PlanType.EnterpriseMonthly).PasswordManager.StripeProviderPortalSeatPlanId; var enterprisePriceId = MockPlans.Get(PlanType.EnterpriseMonthly).PasswordManager.StripeProviderPortalSeatPlanId;
var teamsPriceId = StaticStore.GetPlan(PlanType.TeamsMonthly).PasswordManager.StripeProviderPortalSeatPlanId; var teamsPriceId = MockPlans.Get(PlanType.TeamsMonthly).PasswordManager.StripeProviderPortalSeatPlanId;
var subscription = new Subscription var subscription = new Subscription
{ {
@@ -2040,7 +2040,7 @@ public class ProviderBillingServiceTests
foreach (var plan in providerPlans) foreach (var plan in providerPlans)
{ {
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(plan.PlanType) sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(plan.PlanType)
.Returns(StaticStore.GetPlan(plan.PlanType)); .Returns(MockPlans.Get(plan.PlanType));
} }
providerPlanRepository.GetByProviderId(provider.Id).Returns(providerPlans); providerPlanRepository.GetByProviderId(provider.Id).Returns(providerPlans);
@@ -2086,8 +2086,8 @@ public class ProviderBillingServiceTests
const string enterpriseLineItemId = "enterprise_line_item_id"; const string enterpriseLineItemId = "enterprise_line_item_id";
const string teamsLineItemId = "teams_line_item_id"; const string teamsLineItemId = "teams_line_item_id";
var enterprisePriceId = StaticStore.GetPlan(PlanType.EnterpriseMonthly).PasswordManager.StripeProviderPortalSeatPlanId; var enterprisePriceId = MockPlans.Get(PlanType.EnterpriseMonthly).PasswordManager.StripeProviderPortalSeatPlanId;
var teamsPriceId = StaticStore.GetPlan(PlanType.TeamsMonthly).PasswordManager.StripeProviderPortalSeatPlanId; var teamsPriceId = MockPlans.Get(PlanType.TeamsMonthly).PasswordManager.StripeProviderPortalSeatPlanId;
var subscription = new Subscription var subscription = new Subscription
{ {
@@ -2120,7 +2120,7 @@ public class ProviderBillingServiceTests
foreach (var plan in providerPlans) foreach (var plan in providerPlans)
{ {
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(plan.PlanType) sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(plan.PlanType)
.Returns(StaticStore.GetPlan(plan.PlanType)); .Returns(MockPlans.Get(plan.PlanType));
} }
providerPlanRepository.GetByProviderId(provider.Id).Returns(providerPlans); providerPlanRepository.GetByProviderId(provider.Id).Returns(providerPlans);

View File

@@ -6,7 +6,7 @@ using Bit.Core.Exceptions;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.SecretsManager.Repositories; using Bit.Core.SecretsManager.Repositories;
using Bit.Core.Settings; using Bit.Core.Settings;
using Bit.Core.Utilities; using Bit.Core.Test.Billing.Mocks;
using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute; using NSubstitute;
@@ -69,7 +69,7 @@ public class MaxProjectsQueryTests
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization); sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
sutProvider.GetDependency<IPricingClient>().GetPlan(organization.PlanType) sutProvider.GetDependency<IPricingClient>().GetPlan(organization.PlanType)
.Returns(StaticStore.GetPlan(organization.PlanType)); .Returns(MockPlans.Get(organization.PlanType));
var (limit, overLimit) = await sutProvider.Sut.GetByOrgIdAsync(organization.Id, 1); var (limit, overLimit) = await sutProvider.Sut.GetByOrgIdAsync(organization.Id, 1);
@@ -114,7 +114,7 @@ public class MaxProjectsQueryTests
.Returns(projects); .Returns(projects);
sutProvider.GetDependency<IPricingClient>().GetPlan(organization.PlanType) sutProvider.GetDependency<IPricingClient>().GetPlan(organization.PlanType)
.Returns(StaticStore.GetPlan(organization.PlanType)); .Returns(MockPlans.Get(organization.PlanType));
var (max, overMax) = await sutProvider.Sut.GetByOrgIdAsync(organization.Id, projectsToAdd); var (max, overMax) = await sutProvider.Sut.GetByOrgIdAsync(organization.Id, projectsToAdd);

View File

@@ -0,0 +1,36 @@
{
"globalSettings": {
"baseServiceUri": {
"vault": "https://localhost:8080",
"api": "http://localhost:4000",
"identity": "http://localhost:33656",
"admin": "http://localhost:62911",
"notifications": "http://localhost:61840",
"sso": "http://localhost:51822",
"internalNotifications": "http://localhost:61840",
"internalAdmin": "http://localhost:62911",
"internalIdentity": "http://localhost:33656",
"internalApi": "http://localhost:4000",
"internalVault": "https://localhost:8080",
"internalSso": "http://localhost:51822",
"internalScim": "http://localhost:44559"
},
"mail": {
"smtp": {
"host": "localhost",
"port": 10250
}
},
"attachment": {
"connectionString": "UseDevelopmentStorage=true",
"baseUrl": "http://localhost:4000/attachments/"
},
"events": {
"connectionString": "UseDevelopmentStorage=true"
},
"storage": {
"connectionString": "UseDevelopmentStorage=true"
},
"pricingUri": "https://billingpricing.qa.bitwarden.pw"
}
}

View File

@@ -57,7 +57,6 @@ services:
mysql: mysql:
image: mysql:8.0 image: mysql:8.0
container_name: bw-mysql
ports: ports:
- "3306:3306" - "3306:3306"
command: command:
@@ -88,7 +87,6 @@ services:
idp: idp:
image: kenchan0130/simplesamlphp:1.19.8 image: kenchan0130/simplesamlphp:1.19.8
container_name: idp
ports: ports:
- "8090:8080" - "8090:8080"
environment: environment:
@@ -102,7 +100,6 @@ services:
rabbitmq: rabbitmq:
image: rabbitmq:4.1.3-management image: rabbitmq:4.1.3-management
container_name: rabbitmq
ports: ports:
- "5672:5672" - "5672:5672"
- "15672:15672" - "15672:15672"
@@ -116,7 +113,6 @@ services:
reverse-proxy: reverse-proxy:
image: nginx:alpine image: nginx:alpine
container_name: reverse-proxy
volumes: volumes:
- "./reverse-proxy.conf:/etc/nginx/conf.d/default.conf" - "./reverse-proxy.conf:/etc/nginx/conf.d/default.conf"
ports: ports:
@@ -126,7 +122,6 @@ services:
- proxy - proxy
service-bus: service-bus:
container_name: service-bus
image: mcr.microsoft.com/azure-messaging/servicebus-emulator:latest image: mcr.microsoft.com/azure-messaging/servicebus-emulator:latest
pull_policy: always pull_policy: always
volumes: volumes:
@@ -142,7 +137,6 @@ services:
redis: redis:
image: redis:alpine image: redis:alpine
container_name: bw-redis
ports: ports:
- "6379:6379" - "6379:6379"
volumes: volumes:

View File

@@ -27,6 +27,7 @@
}, },
"storage": { "storage": {
"connectionString": "UseDevelopmentStorage=true" "connectionString": "UseDevelopmentStorage=true"
} },
"pricingUri": "https://billingpricing.qa.bitwarden.pw"
} }
} }

View File

@@ -0,0 +1,26 @@
using Bit.Core.AdminConsole.Utilities.v2;
using Bit.Core.AdminConsole.Utilities.v2.Results;
using Bit.Core.Models.Api;
using Microsoft.AspNetCore.Mvc;
namespace Bit.Api.AdminConsole.Controllers;
public abstract class BaseAdminConsoleController : Controller
{
protected static IResult Handle(CommandResult commandResult) =>
commandResult.Match<IResult>(
error => error switch
{
BadRequestError badRequest => TypedResults.BadRequest(new ErrorResponseModel(badRequest.Message)),
NotFoundError notFound => TypedResults.NotFound(new ErrorResponseModel(notFound.Message)),
InternalError internalError => TypedResults.Json(
new ErrorResponseModel(internalError.Message),
statusCode: StatusCodes.Status500InternalServerError),
_ => TypedResults.Json(
new ErrorResponseModel(error.Message),
statusCode: StatusCodes.Status500InternalServerError
)
},
_ => TypedResults.NoContent()
);
}

View File

@@ -11,8 +11,10 @@ using Bit.Api.Models.Response;
using Bit.Api.Vault.AuthorizationHandlers.Collections; using Bit.Api.Vault.AuthorizationHandlers.Collections;
using Bit.Core; using Bit.Core;
using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Models.Data;
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.AccountRecovery; using Bit.Core.AdminConsole.OrganizationFeatures.AccountRecovery;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUser;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccount; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccount;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;
@@ -20,6 +22,7 @@ using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RestoreUser.v
using Bit.Core.AdminConsole.OrganizationFeatures.Policies; using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Utilities.v2;
using Bit.Core.Auth.Enums; using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Repositories; using Bit.Core.Auth.Repositories;
using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Pricing;
@@ -43,7 +46,7 @@ namespace Bit.Api.AdminConsole.Controllers;
[Route("organizations/{orgId}/users")] [Route("organizations/{orgId}/users")]
[Authorize("Application")] [Authorize("Application")]
public class OrganizationUsersController : Controller public class OrganizationUsersController : BaseAdminConsoleController
{ {
private readonly IOrganizationRepository _organizationRepository; private readonly IOrganizationRepository _organizationRepository;
private readonly IOrganizationUserRepository _organizationUserRepository; private readonly IOrganizationUserRepository _organizationUserRepository;
@@ -68,6 +71,7 @@ public class OrganizationUsersController : Controller
private readonly IFeatureService _featureService; private readonly IFeatureService _featureService;
private readonly IPricingClient _pricingClient; private readonly IPricingClient _pricingClient;
private readonly IResendOrganizationInviteCommand _resendOrganizationInviteCommand; private readonly IResendOrganizationInviteCommand _resendOrganizationInviteCommand;
private readonly IAutomaticallyConfirmOrganizationUserCommand _automaticallyConfirmOrganizationUserCommand;
private readonly IConfirmOrganizationUserCommand _confirmOrganizationUserCommand; private readonly IConfirmOrganizationUserCommand _confirmOrganizationUserCommand;
private readonly IRestoreOrganizationUserCommand _restoreOrganizationUserCommand; private readonly IRestoreOrganizationUserCommand _restoreOrganizationUserCommand;
private readonly IInitPendingOrganizationCommand _initPendingOrganizationCommand; private readonly IInitPendingOrganizationCommand _initPendingOrganizationCommand;
@@ -101,7 +105,8 @@ public class OrganizationUsersController : Controller
IInitPendingOrganizationCommand initPendingOrganizationCommand, IInitPendingOrganizationCommand initPendingOrganizationCommand,
IRevokeOrganizationUserCommand revokeOrganizationUserCommand, IRevokeOrganizationUserCommand revokeOrganizationUserCommand,
IResendOrganizationInviteCommand resendOrganizationInviteCommand, IResendOrganizationInviteCommand resendOrganizationInviteCommand,
IAdminRecoverAccountCommand adminRecoverAccountCommand) IAdminRecoverAccountCommand adminRecoverAccountCommand,
IAutomaticallyConfirmOrganizationUserCommand automaticallyConfirmOrganizationUserCommand)
{ {
_organizationRepository = organizationRepository; _organizationRepository = organizationRepository;
_organizationUserRepository = organizationUserRepository; _organizationUserRepository = organizationUserRepository;
@@ -126,6 +131,7 @@ public class OrganizationUsersController : Controller
_featureService = featureService; _featureService = featureService;
_pricingClient = pricingClient; _pricingClient = pricingClient;
_resendOrganizationInviteCommand = resendOrganizationInviteCommand; _resendOrganizationInviteCommand = resendOrganizationInviteCommand;
_automaticallyConfirmOrganizationUserCommand = automaticallyConfirmOrganizationUserCommand;
_confirmOrganizationUserCommand = confirmOrganizationUserCommand; _confirmOrganizationUserCommand = confirmOrganizationUserCommand;
_restoreOrganizationUserCommand = restoreOrganizationUserCommand; _restoreOrganizationUserCommand = restoreOrganizationUserCommand;
_initPendingOrganizationCommand = initPendingOrganizationCommand; _initPendingOrganizationCommand = initPendingOrganizationCommand;
@@ -738,6 +744,31 @@ public class OrganizationUsersController : Controller
await BulkEnableSecretsManagerAsync(orgId, model); await BulkEnableSecretsManagerAsync(orgId, model);
} }
[HttpPost("{id}/auto-confirm")]
[Authorize<ManageUsersRequirement>]
[RequireFeature(FeatureFlagKeys.AutomaticConfirmUsers)]
public async Task<IResult> AutomaticallyConfirmOrganizationUserAsync([FromRoute] Guid orgId,
[FromRoute] Guid id,
[FromBody] OrganizationUserConfirmRequestModel model)
{
var userId = _userService.GetProperUserId(User);
if (userId is null || userId.Value == Guid.Empty)
{
return TypedResults.Unauthorized();
}
return Handle(await _automaticallyConfirmOrganizationUserCommand.AutomaticallyConfirmOrganizationUserAsync(
new AutomaticallyConfirmOrganizationUserRequest
{
OrganizationId = orgId,
OrganizationUserId = id,
Key = model.Key,
DefaultUserCollectionName = model.DefaultUserCollectionName,
PerformedBy = new StandardUser(userId.Value, await _currentContext.OrganizationOwner(orgId)),
}));
}
private async Task RestoreOrRevokeUserAsync( private async Task RestoreOrRevokeUserAsync(
Guid orgId, Guid orgId,
Guid id, Guid id,

View File

@@ -1,4 +1,5 @@
using Bit.Core.Enums; using Bit.Core.Billing.Models;
using Bit.Core.Enums;
using Bit.Core.Models.Data; using Bit.Core.Models.Data;
using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.Utilities; using Bit.Core.Utilities;
@@ -27,7 +28,7 @@ public class ProfileOrganizationResponseModel : BaseProfileOrganizationResponseM
FamilySponsorshipToDelete = organizationDetails.FamilySponsorshipToDelete; FamilySponsorshipToDelete = organizationDetails.FamilySponsorshipToDelete;
FamilySponsorshipValidUntil = organizationDetails.FamilySponsorshipValidUntil; FamilySponsorshipValidUntil = organizationDetails.FamilySponsorshipValidUntil;
FamilySponsorshipAvailable = (organizationDetails.FamilySponsorshipFriendlyName == null || IsAdminInitiated) && FamilySponsorshipAvailable = (organizationDetails.FamilySponsorshipFriendlyName == null || IsAdminInitiated) &&
StaticStore.GetSponsoredPlan(PlanSponsorshipType.FamiliesForEnterprise) SponsoredPlans.Get(PlanSponsorshipType.FamiliesForEnterprise)
.UsersCanSponsor(organizationDetails); .UsersCanSponsor(organizationDetails);
AccessSecretsManager = organizationDetails.AccessSecretsManager; AccessSecretsManager = organizationDetails.AccessSecretsManager;
} }

View File

@@ -0,0 +1,35 @@
using Bit.Api.AdminConsole.Authorization;
using Bit.Api.AdminConsole.Authorization.Requirements;
using Bit.Api.Billing.Attributes;
using Bit.Core;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Billing.Organizations.Queries;
using Bit.Core.Utilities;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
namespace Bit.Api.Billing.Controllers.VNext;
[Authorize("Application")]
[Route("organizations/{organizationId:guid}/billing/vnext/self-host")]
[SelfHosted(SelfHostedOnly = true)]
public class SelfHostedBillingController(
IGetOrganizationMetadataQuery getOrganizationMetadataQuery) : BaseBillingController
{
[Authorize<MemberOrProviderRequirement>]
[HttpGet("metadata")]
[RequireFeature(FeatureFlagKeys.PM25379_UseNewOrganizationMetadataStructure)]
[InjectOrganization]
public async Task<IResult> GetMetadataAsync([BindNever] Organization organization)
{
var metadata = await getOrganizationMetadataQuery.Run(organization);
if (metadata == null)
{
return TypedResults.NotFound();
}
return TypedResults.Ok(metadata);
}
}

View File

@@ -1422,11 +1422,9 @@ public class CiphersController : Controller
throw new NotFoundException(); throw new NotFoundException();
} }
// Extract lastKnownRevisionDate from form data if present
DateTime? lastKnownRevisionDate = GetLastKnownRevisionDateFromForm();
await Request.GetFileAsync(async (stream) => await Request.GetFileAsync(async (stream) =>
{ {
await _cipherService.UploadFileForExistingAttachmentAsync(stream, cipher, attachmentData, lastKnownRevisionDate); await _cipherService.UploadFileForExistingAttachmentAsync(stream, cipher, attachmentData);
}); });
} }
@@ -1525,13 +1523,10 @@ public class CiphersController : Controller
throw new NotFoundException(); throw new NotFoundException();
} }
// Extract lastKnownRevisionDate from form data if present
DateTime? lastKnownRevisionDate = GetLastKnownRevisionDateFromForm();
await Request.GetFileAsync(async (stream, fileName, key) => await Request.GetFileAsync(async (stream, fileName, key) =>
{ {
await _cipherService.CreateAttachmentShareAsync(cipher, stream, fileName, key, await _cipherService.CreateAttachmentShareAsync(cipher, stream, fileName, key,
Request.ContentLength.GetValueOrDefault(0), attachmentId, organizationId, lastKnownRevisionDate); Request.ContentLength.GetValueOrDefault(0), attachmentId, organizationId);
}); });
} }

View File

@@ -41,6 +41,7 @@
"phishingDomain": { "phishingDomain": {
"updateUrl": "https://phish.co.za/latest/phishing-domains-ACTIVE.txt", "updateUrl": "https://phish.co.za/latest/phishing-domains-ACTIVE.txt",
"checksumUrl": "https://raw.githubusercontent.com/Phishing-Database/checksums/refs/heads/master/phishing-domains-ACTIVE.txt.sha256" "checksumUrl": "https://raw.githubusercontent.com/Phishing-Database/checksums/refs/heads/master/phishing-domains-ACTIVE.txt.sha256"
} },
"pricingUri": "https://billingpricing.qa.bitwarden.pw"
} }
} }

View File

@@ -3,12 +3,12 @@
using Bit.Billing.Constants; using Bit.Billing.Constants;
using Bit.Core.Billing.Constants; using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Models;
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.Settings; using Bit.Core.Settings;
using Bit.Core.Utilities;
using Braintree; using Braintree;
using Stripe; using Stripe;
using Customer = Stripe.Customer; using Customer = Stripe.Customer;
@@ -112,7 +112,7 @@ public class StripeEventUtilityService : IStripeEventUtilityService
} }
public bool IsSponsoredSubscription(Subscription subscription) => public bool IsSponsoredSubscription(Subscription subscription) =>
StaticStore.SponsoredPlans SponsoredPlans.All
.Any(p => subscription.Items .Any(p => subscription.Items
.Any(i => i.Plan.Id == p.StripePlanId)); .Any(i => i.Plan.Id == p.StripePlanId));

View File

@@ -195,40 +195,48 @@ public class UpcomingInvoiceHandler(
Plan plan, Plan plan,
bool milestone3) bool milestone3)
{ {
if (milestone3 && plan.Type == PlanType.FamiliesAnnually2019) // currently these are the only plans that need aligned and both require the same flag and share most of the logic
if (!milestone3 || plan.Type is not (PlanType.FamiliesAnnually2019 or PlanType.FamiliesAnnually2025))
{ {
var passwordManagerItem = return;
subscription.Items.FirstOrDefault(item => item.Price.Id == plan.PasswordManager.StripePlanId); }
if (passwordManagerItem == null) var passwordManagerItem =
{ subscription.Items.FirstOrDefault(item => item.Price.Id == plan.PasswordManager.StripePlanId);
logger.LogWarning("Could not find Organization's ({OrganizationId}) password manager item while processing '{EventType}' event ({EventID})",
organization.Id, @event.Type, @event.Id);
return;
}
var families = await pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually); if (passwordManagerItem == null)
{
logger.LogWarning("Could not find Organization's ({OrganizationId}) password manager item while processing '{EventType}' event ({EventID})",
organization.Id, @event.Type, @event.Id);
return;
}
organization.PlanType = families.Type; var families = await pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually);
organization.Plan = families.Name;
organization.UsersGetPremium = families.UsersGetPremium;
organization.Seats = families.PasswordManager.BaseSeats;
var options = new SubscriptionUpdateOptions organization.PlanType = families.Type;
{ organization.Plan = families.Name;
Items = organization.UsersGetPremium = families.UsersGetPremium;
[ organization.Seats = families.PasswordManager.BaseSeats;
new SubscriptionItemOptions
{ var options = new SubscriptionUpdateOptions
Id = passwordManagerItem.Id, Price = families.PasswordManager.StripePlanId {
} Items =
], [
Discounts = new SubscriptionItemOptions
[ {
new SubscriptionDiscountOptions { Coupon = CouponIDs.Milestone3SubscriptionDiscount } Id = passwordManagerItem.Id,
], Price = families.PasswordManager.StripePlanId
ProrationBehavior = ProrationBehavior.None }
}; ],
ProrationBehavior = ProrationBehavior.None
};
if (plan.Type == PlanType.FamiliesAnnually2019)
{
options.Discounts =
[
new SubscriptionDiscountOptions { Coupon = CouponIDs.Milestone3SubscriptionDiscount }
];
var premiumAccessAddOnItem = subscription.Items.FirstOrDefault(item => var premiumAccessAddOnItem = subscription.Items.FirstOrDefault(item =>
item.Price.Id == plan.PasswordManager.StripePremiumAccessPlanId); item.Price.Id == plan.PasswordManager.StripePremiumAccessPlanId);
@@ -242,21 +250,32 @@ public class UpcomingInvoiceHandler(
}); });
} }
try var seatAddOnItem = subscription.Items.FirstOrDefault(item => item.Price.Id == "personal-org-seat-annually");
if (seatAddOnItem != null)
{ {
await organizationRepository.ReplaceAsync(organization); options.Items.Add(new SubscriptionItemOptions
await stripeFacade.UpdateSubscription(subscription.Id, options); {
} Id = seatAddOnItem.Id,
catch (Exception exception) Deleted = true
{ });
logger.LogError(
exception,
"Failed to align subscription concerns for Organization ({OrganizationID}) while processing '{EventType}' event ({EventID})",
organization.Id,
@event.Type,
@event.Id);
} }
} }
try
{
await organizationRepository.ReplaceAsync(organization);
await stripeFacade.UpdateSubscription(subscription.Id, options);
}
catch (Exception exception)
{
logger.LogError(
exception,
"Failed to align subscription concerns for Organization ({OrganizationID}) while processing '{EventType}' event ({EventID})",
organization.Id,
@event.Type,
@event.Id);
}
} }
#endregion #endregion

View File

@@ -36,5 +36,6 @@
"onyx": { "onyx": {
"personaId": 68 "personaId": 68
} }
} },
"pricingUri": "https://billingpricing.qa.bitwarden.pw"
} }

View File

@@ -60,6 +60,7 @@ public enum EventType : int
OrganizationUser_RejectedAuthRequest = 1514, OrganizationUser_RejectedAuthRequest = 1514,
OrganizationUser_Deleted = 1515, // Both user and organization user data were deleted OrganizationUser_Deleted = 1515, // Both user and organization user data were deleted
OrganizationUser_Left = 1516, // User voluntarily left the organization OrganizationUser_Left = 1516, // User voluntarily left the organization
OrganizationUser_AutomaticallyConfirmed = 1517,
Organization_Updated = 1600, Organization_Updated = 1600,
Organization_PurgedVault = 1601, Organization_PurgedVault = 1601,

View File

@@ -0,0 +1,8 @@
namespace Bit.Core.AdminConsole.Models.Data.OrganizationUsers;
public record AcceptedOrganizationUserToConfirm
{
public required Guid OrganizationUserId { get; init; }
public required Guid UserId { get; init; }
public required string Key { get; init; }
}

View File

@@ -0,0 +1,186 @@
using Bit.Core.AdminConsole.Models.Data.OrganizationUsers;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Models.Data;
using Bit.Core.Platform.Push;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Microsoft.Extensions.Logging;
using OneOf.Types;
using CommandResult = Bit.Core.AdminConsole.Utilities.v2.Results.CommandResult;
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUser;
public class AutomaticallyConfirmOrganizationUserCommand(IOrganizationUserRepository organizationUserRepository,
IOrganizationRepository organizationRepository,
IAutomaticallyConfirmOrganizationUsersValidator validator,
IEventService eventService,
IMailService mailService,
IUserRepository userRepository,
IPushRegistrationService pushRegistrationService,
IDeviceRepository deviceRepository,
IPushNotificationService pushNotificationService,
IPolicyRequirementQuery policyRequirementQuery,
ICollectionRepository collectionRepository,
TimeProvider timeProvider,
ILogger<AutomaticallyConfirmOrganizationUserCommand> logger) : IAutomaticallyConfirmOrganizationUserCommand
{
public async Task<CommandResult> AutomaticallyConfirmOrganizationUserAsync(AutomaticallyConfirmOrganizationUserRequest request)
{
var validatorRequest = await RetrieveDataAsync(request);
var validatedData = await validator.ValidateAsync(validatorRequest);
return await validatedData.Match<Task<CommandResult>>(
error => Task.FromResult(new CommandResult(error)),
async _ =>
{
var userToConfirm = new AcceptedOrganizationUserToConfirm
{
OrganizationUserId = validatedData.Request.OrganizationUser!.Id,
UserId = validatedData.Request.OrganizationUser.UserId!.Value,
Key = validatedData.Request.Key
};
// This operation is idempotent. If false, the user is already confirmed and no additional side effects are required.
if (!await organizationUserRepository.ConfirmOrganizationUserAsync(userToConfirm))
{
return new None();
}
await CreateDefaultCollectionsAsync(validatedData.Request);
await Task.WhenAll(
LogOrganizationUserConfirmedEventAsync(validatedData.Request),
SendConfirmedOrganizationUserEmailAsync(validatedData.Request),
SyncOrganizationKeysAsync(validatedData.Request)
);
return new None();
}
);
}
private async Task SyncOrganizationKeysAsync(AutomaticallyConfirmOrganizationUserValidationRequest request)
{
await DeleteDeviceRegistrationAsync(request);
await PushSyncOrganizationKeysAsync(request);
}
private async Task CreateDefaultCollectionsAsync(AutomaticallyConfirmOrganizationUserValidationRequest request)
{
try
{
if (!await ShouldCreateDefaultCollectionAsync(request))
{
return;
}
await collectionRepository.CreateAsync(
new Collection
{
OrganizationId = request.Organization!.Id,
Name = request.DefaultUserCollectionName,
Type = CollectionType.DefaultUserCollection
},
groups: null,
[new CollectionAccessSelection
{
Id = request.OrganizationUser!.Id,
Manage = true
}]);
}
catch (Exception ex)
{
logger.LogError(ex, "Failed to create default collection for user.");
}
}
/// <summary>
/// Determines whether a default collection should be created for an organization user during the confirmation process.
/// </summary>
/// <param name="request">
/// The validation request containing information about the user, organization, and collection settings.
/// </param>
/// <returns>The result is a boolean value indicating whether a default collection should be created.</returns>
private async Task<bool> ShouldCreateDefaultCollectionAsync(AutomaticallyConfirmOrganizationUserValidationRequest request) =>
!string.IsNullOrWhiteSpace(request.DefaultUserCollectionName)
&& (await policyRequirementQuery.GetAsync<OrganizationDataOwnershipPolicyRequirement>(request.OrganizationUser!.UserId!.Value))
.RequiresDefaultCollectionOnConfirm(request.Organization!.Id);
private async Task PushSyncOrganizationKeysAsync(AutomaticallyConfirmOrganizationUserValidationRequest request)
{
try
{
await pushNotificationService.PushSyncOrgKeysAsync(request.OrganizationUser!.UserId!.Value);
}
catch (Exception ex)
{
logger.LogError(ex, "Failed to push organization keys.");
}
}
private async Task LogOrganizationUserConfirmedEventAsync(AutomaticallyConfirmOrganizationUserValidationRequest request)
{
try
{
await eventService.LogOrganizationUserEventAsync(request.OrganizationUser,
EventType.OrganizationUser_AutomaticallyConfirmed,
timeProvider.GetUtcNow().UtcDateTime);
}
catch (Exception ex)
{
logger.LogError(ex, "Failed to log OrganizationUser_AutomaticallyConfirmed event.");
}
}
private async Task SendConfirmedOrganizationUserEmailAsync(AutomaticallyConfirmOrganizationUserValidationRequest request)
{
try
{
var user = await userRepository.GetByIdAsync(request.OrganizationUser!.UserId!.Value);
await mailService.SendOrganizationConfirmedEmailAsync(request.Organization!.Name,
user!.Email,
request.OrganizationUser.AccessSecretsManager);
}
catch (Exception ex)
{
logger.LogError(ex, "Failed to send OrganizationUserConfirmed.");
}
}
private async Task DeleteDeviceRegistrationAsync(AutomaticallyConfirmOrganizationUserValidationRequest request)
{
try
{
var devices = (await deviceRepository.GetManyByUserIdAsync(request.OrganizationUser!.UserId!.Value))
.Where(d => !string.IsNullOrWhiteSpace(d.PushToken))
.Select(d => d.Id.ToString());
await pushRegistrationService.DeleteUserRegistrationOrganizationAsync(devices, request.Organization!.Id.ToString());
}
catch (Exception ex)
{
logger.LogError(ex, "Failed to delete device registration.");
}
}
private async Task<AutomaticallyConfirmOrganizationUserValidationRequest> RetrieveDataAsync(
AutomaticallyConfirmOrganizationUserRequest request)
{
return new AutomaticallyConfirmOrganizationUserValidationRequest
{
OrganizationUserId = request.OrganizationUserId,
OrganizationId = request.OrganizationId,
Key = request.Key,
DefaultUserCollectionName = request.DefaultUserCollectionName,
PerformedBy = request.PerformedBy,
OrganizationUser = await organizationUserRepository.GetByIdAsync(request.OrganizationUserId),
Organization = await organizationRepository.GetByIdAsync(request.OrganizationId)
};
}
}

View File

@@ -0,0 +1,29 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Models.Data;
using Bit.Core.Entities;
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUser;
/// <summary>
/// Automatically Confirm User Command Request
/// </summary>
public record AutomaticallyConfirmOrganizationUserRequest
{
public required Guid OrganizationUserId { get; init; }
public required Guid OrganizationId { get; init; }
public required string Key { get; init; }
public required string DefaultUserCollectionName { get; init; }
public required IActingUser PerformedBy { get; init; }
}
/// <summary>
/// Automatically Confirm User Validation Request
/// </summary>
/// <remarks>
/// This is used to hold retrieved data and pass it to the validator
/// </remarks>
public record AutomaticallyConfirmOrganizationUserValidationRequest : AutomaticallyConfirmOrganizationUserRequest
{
public OrganizationUser? OrganizationUser { get; set; }
public Organization? Organization { get; set; }
}

View File

@@ -0,0 +1,116 @@
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccount;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Utilities.v2;
using Bit.Core.AdminConsole.Utilities.v2.Validation;
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
using Bit.Core.Enums;
using Bit.Core.Repositories;
using static Bit.Core.AdminConsole.Utilities.v2.Validation.ValidationResultHelpers;
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUser;
public class AutomaticallyConfirmOrganizationUsersValidator(
IOrganizationUserRepository organizationUserRepository,
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
IPolicyRequirementQuery policyRequirementQuery,
IPolicyRepository policyRepository) : IAutomaticallyConfirmOrganizationUsersValidator
{
public async Task<ValidationResult<AutomaticallyConfirmOrganizationUserValidationRequest>> ValidateAsync(
AutomaticallyConfirmOrganizationUserValidationRequest request)
{
// User must exist
if (request is { OrganizationUser: null } || request.OrganizationUser is { UserId: null })
{
return Invalid(request, new UserNotFoundError());
}
// Organization must exist
if (request is { Organization: null })
{
return Invalid(request, new OrganizationNotFound());
}
// User must belong to the organization
if (request.OrganizationUser.OrganizationId != request.Organization.Id)
{
return Invalid(request, new OrganizationUserIdIsInvalid());
}
// User must be accepted
if (request is { OrganizationUser.Status: not OrganizationUserStatusType.Accepted })
{
return Invalid(request, new UserIsNotAccepted());
}
// User must be of type User
if (request is { OrganizationUser.Type: not OrganizationUserType.User })
{
return Invalid(request, new UserIsNotUserType());
}
if (!await OrganizationHasAutomaticallyConfirmUsersPolicyEnabledAsync(request))
{
return Invalid(request, new AutomaticallyConfirmUsersPolicyIsNotEnabled());
}
if (!await OrganizationUserConformsToTwoFactorRequiredPolicyAsync(request))
{
return Invalid(request, new UserDoesNotHaveTwoFactorEnabled());
}
if (await OrganizationUserConformsToSingleOrgPolicyAsync(request) is { } error)
{
return Invalid(request, error);
}
return Valid(request);
}
private async Task<bool> OrganizationHasAutomaticallyConfirmUsersPolicyEnabledAsync(
AutomaticallyConfirmOrganizationUserValidationRequest request) =>
await policyRepository.GetByOrganizationIdTypeAsync(request.OrganizationId,
PolicyType.AutomaticUserConfirmation) is { Enabled: true }
&& request.Organization is { UseAutomaticUserConfirmation: true };
private async Task<bool> OrganizationUserConformsToTwoFactorRequiredPolicyAsync(AutomaticallyConfirmOrganizationUserValidationRequest request)
{
if ((await twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync([request.OrganizationUser!.UserId!.Value]))
.Any(x => x.userId == request.OrganizationUser.UserId && x.twoFactorIsEnabled))
{
return true;
}
return !(await policyRequirementQuery.GetAsync<RequireTwoFactorPolicyRequirement>(request.OrganizationUser.UserId!.Value))
.IsTwoFactorRequiredForOrganization(request.Organization!.Id);
}
private async Task<Error?> OrganizationUserConformsToSingleOrgPolicyAsync(
AutomaticallyConfirmOrganizationUserValidationRequest request)
{
var allOrganizationUsersForUser = await organizationUserRepository
.GetManyByUserAsync(request.OrganizationUser!.UserId!.Value);
if (allOrganizationUsersForUser.Count == 1)
{
return null;
}
var policyRequirement = await policyRequirementQuery
.GetAsync<SingleOrganizationPolicyRequirement>(request.OrganizationUser!.UserId!.Value);
if (policyRequirement.IsSingleOrgEnabledForThisOrganization(request.Organization!.Id))
{
return new OrganizationEnforcesSingleOrgPolicy();
}
if (policyRequirement.IsSingleOrgEnabledForOrganizationsOtherThan(request.Organization.Id))
{
return new OtherOrganizationEnforcesSingleOrgPolicy();
}
return null;
}
}

View File

@@ -0,0 +1,13 @@
using Bit.Core.AdminConsole.Utilities.v2;
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUser;
public record OrganizationNotFound() : NotFoundError("Invalid organization");
public record FailedToWriteToEventLog() : InternalError("Failed to write to event log");
public record UserIsNotUserType() : BadRequestError("Only organization users with the User role can be automatically confirmed");
public record UserIsNotAccepted() : BadRequestError("Cannot confirm user that has not accepted the invitation.");
public record OrganizationUserIdIsInvalid() : BadRequestError("Invalid organization user id.");
public record UserDoesNotHaveTwoFactorEnabled() : BadRequestError("User does not have two-step login enabled.");
public record OrganizationEnforcesSingleOrgPolicy() : BadRequestError("Cannot confirm this member to the organization until they leave or remove all other organizations");
public record OtherOrganizationEnforcesSingleOrgPolicy() : BadRequestError("Cannot confirm this member to the organization because they are in another organization which forbids it.");
public record AutomaticallyConfirmUsersPolicyIsNotEnabled() : BadRequestError("Cannot confirm this member because the Automatically Confirm Users policy is not enabled.");

View File

@@ -0,0 +1,9 @@
using Bit.Core.AdminConsole.Utilities.v2.Validation;
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUser;
public interface IAutomaticallyConfirmOrganizationUsersValidator
{
Task<ValidationResult<AutomaticallyConfirmOrganizationUserValidationRequest>> ValidateAsync(
AutomaticallyConfirmOrganizationUserValidationRequest request);
}

View File

@@ -1,4 +1,6 @@
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.AdminConsole.Utilities.v2.Results;
using Bit.Core.AdminConsole.Utilities.v2.Validation;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;

View File

@@ -1,8 +1,9 @@
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Utilities.v2.Validation;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using static Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccount.ValidationResultHelpers; using static Bit.Core.AdminConsole.Utilities.v2.Validation.ValidationResultHelpers;
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccount; namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccount;

View File

@@ -1,15 +1,6 @@
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccount; using Bit.Core.AdminConsole.Utilities.v2;
/// <summary> namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccount;
/// A strongly typed error containing a reason that an action failed.
/// This is used for business logic validation and other expected errors, not exceptions.
/// </summary>
public abstract record Error(string Message);
/// <summary>
/// An <see cref="Error"/> type that maps to a NotFoundResult at the api layer.
/// </summary>
/// <param name="Message"></param>
public abstract record NotFoundError(string Message) : Error(Message);
public record UserNotFoundError() : NotFoundError("Invalid user."); public record UserNotFoundError() : NotFoundError("Invalid user.");
public record UserNotClaimedError() : Error("Member is not claimed by the organization."); public record UserNotClaimedError() : Error("Member is not claimed by the organization.");

View File

@@ -1,4 +1,6 @@
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccount; using Bit.Core.AdminConsole.Utilities.v2.Results;
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccount;
public interface IDeleteClaimedOrganizationUserAccountCommand public interface IDeleteClaimedOrganizationUserAccountCommand
{ {

View File

@@ -1,4 +1,6 @@
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccount; using Bit.Core.AdminConsole.Utilities.v2.Validation;
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccount;
public interface IDeleteClaimedOrganizationUserAccountValidator public interface IDeleteClaimedOrganizationUserAccountValidator
{ {

View File

@@ -0,0 +1,40 @@
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUser;
using Bit.Core.AdminConsole.Utilities.v2.Results;
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
/// <summary>
/// Command to automatically confirm an organization user.
/// </summary>
/// <remarks>
/// The auto-confirm feature enables eligible client apps to confirm OrganizationUsers
/// automatically via push notifications, eliminating the need for manual administrator
/// intervention. Client apps receive a push notification, perform the required key exchange,
/// and submit an auto-confirm request to the server. This command processes those
/// client-initiated requests and should only be used in that specific context.
/// </remarks>
public interface IAutomaticallyConfirmOrganizationUserCommand
{
/// <summary>
/// Automatically confirms the organization user based on the provided request data.
/// </summary>
/// <param name="request">The request containing necessary information to confirm the organization user.</param>
/// <remarks>
/// This action has side effects. The side effects are
/// <ul>
/// <li>Creating an event log entry.</li>
/// <li>Syncing organization keys with the user.</li>
/// <li>Deleting any registered user devices for the organization.</li>
/// <li>Sending an email to the confirmed user.</li>
/// <li>Creating the default collection if applicable.</li>
/// </ul>
///
/// Each of these actions is performed independently of each other and not guaranteed to be performed in any order.
/// Errors will be reported back for the actions that failed in a consolidated error message.
/// </remarks>
/// <returns>
/// The result of the command. If there was an error, the result will contain a typed error describing the problem
/// that occurred.
/// </returns>
Task<CommandResult> AutomaticallyConfirmOrganizationUserAsync(AutomaticallyConfirmOrganizationUserRequest request);
}

View File

@@ -75,8 +75,7 @@ public class CloudOrganizationSignUpCommand(
PlanType = plan!.Type, PlanType = plan!.Type,
Seats = (short)(plan.PasswordManager.BaseSeats + signup.AdditionalSeats), Seats = (short)(plan.PasswordManager.BaseSeats + signup.AdditionalSeats),
MaxCollections = plan.PasswordManager.MaxCollections, MaxCollections = plan.PasswordManager.MaxCollections,
MaxStorageGb = !plan.PasswordManager.BaseStorageGb.HasValue ? MaxStorageGb = (short)(plan.PasswordManager.BaseStorageGb + signup.AdditionalStorageGb),
(short?)null : (short)(plan.PasswordManager.BaseStorageGb.Value + signup.AdditionalStorageGb),
UsePolicies = plan.HasPolicies, UsePolicies = plan.HasPolicies,
UseSso = plan.HasSso, UseSso = plan.HasSso,
UseGroups = plan.HasGroups, UseGroups = plan.HasGroups,

View File

@@ -73,7 +73,7 @@ public class ProviderClientOrganizationSignUpCommand : IProviderClientOrganizati
PlanType = plan!.Type, PlanType = plan!.Type,
Seats = signup.AdditionalSeats, Seats = signup.AdditionalSeats,
MaxCollections = plan.PasswordManager.MaxCollections, MaxCollections = plan.PasswordManager.MaxCollections,
MaxStorageGb = 1, MaxStorageGb = plan.PasswordManager.BaseStorageGb,
UsePolicies = plan.HasPolicies, UsePolicies = plan.HasPolicies,
UseSso = plan.HasSso, UseSso = plan.HasSso,
UseOrganizationDomains = plan.HasOrganizationDomains, UseOrganizationDomains = plan.HasOrganizationDomains,

View File

@@ -0,0 +1,21 @@
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
public class SingleOrganizationPolicyRequirement(IEnumerable<PolicyDetails> policyDetails) : IPolicyRequirement
{
public bool IsSingleOrgEnabledForThisOrganization(Guid organizationId) =>
policyDetails.Any(p => p.OrganizationId == organizationId);
public bool IsSingleOrgEnabledForOrganizationsOtherThan(Guid organizationId) =>
policyDetails.Any(p => p.OrganizationId != organizationId);
}
public class SingleOrganizationPolicyRequirementFactory : BasePolicyRequirementFactory<SingleOrganizationPolicyRequirement>
{
public override PolicyType PolicyType => PolicyType.SingleOrg;
public override SingleOrganizationPolicyRequirement Create(IEnumerable<PolicyDetails> policyDetails) =>
new(policyDetails);
}

View File

@@ -65,5 +65,6 @@ public static class PolicyServiceCollectionExtensions
services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, RequireSsoPolicyRequirementFactory>(); services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, RequireSsoPolicyRequirementFactory>();
services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, RequireTwoFactorPolicyRequirementFactory>(); services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, RequireTwoFactorPolicyRequirementFactory>();
services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, MasterPasswordPolicyRequirementFactory>(); services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, MasterPasswordPolicyRequirementFactory>();
services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, SingleOrganizationPolicyRequirementFactory>();
} }
} }

View File

@@ -1,6 +1,4 @@
#nullable enable using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Models.Data; using Bit.Core.AdminConsole.Models.Data;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;
@@ -29,8 +27,6 @@ public class SingleOrgPolicyValidator : IPolicyValidator, IPolicyValidationEvent
private readonly IOrganizationRepository _organizationRepository; private readonly IOrganizationRepository _organizationRepository;
private readonly ISsoConfigRepository _ssoConfigRepository; private readonly ISsoConfigRepository _ssoConfigRepository;
private readonly ICurrentContext _currentContext; private readonly ICurrentContext _currentContext;
private readonly IFeatureService _featureService;
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
private readonly IOrganizationHasVerifiedDomainsQuery _organizationHasVerifiedDomainsQuery; private readonly IOrganizationHasVerifiedDomainsQuery _organizationHasVerifiedDomainsQuery;
private readonly IRevokeNonCompliantOrganizationUserCommand _revokeNonCompliantOrganizationUserCommand; private readonly IRevokeNonCompliantOrganizationUserCommand _revokeNonCompliantOrganizationUserCommand;
@@ -40,8 +36,6 @@ public class SingleOrgPolicyValidator : IPolicyValidator, IPolicyValidationEvent
IOrganizationRepository organizationRepository, IOrganizationRepository organizationRepository,
ISsoConfigRepository ssoConfigRepository, ISsoConfigRepository ssoConfigRepository,
ICurrentContext currentContext, ICurrentContext currentContext,
IFeatureService featureService,
IRemoveOrganizationUserCommand removeOrganizationUserCommand,
IOrganizationHasVerifiedDomainsQuery organizationHasVerifiedDomainsQuery, IOrganizationHasVerifiedDomainsQuery organizationHasVerifiedDomainsQuery,
IRevokeNonCompliantOrganizationUserCommand revokeNonCompliantOrganizationUserCommand) IRevokeNonCompliantOrganizationUserCommand revokeNonCompliantOrganizationUserCommand)
{ {
@@ -50,8 +44,6 @@ public class SingleOrgPolicyValidator : IPolicyValidator, IPolicyValidationEvent
_organizationRepository = organizationRepository; _organizationRepository = organizationRepository;
_ssoConfigRepository = ssoConfigRepository; _ssoConfigRepository = ssoConfigRepository;
_currentContext = currentContext; _currentContext = currentContext;
_featureService = featureService;
_removeOrganizationUserCommand = removeOrganizationUserCommand;
_organizationHasVerifiedDomainsQuery = organizationHasVerifiedDomainsQuery; _organizationHasVerifiedDomainsQuery = organizationHasVerifiedDomainsQuery;
_revokeNonCompliantOrganizationUserCommand = revokeNonCompliantOrganizationUserCommand; _revokeNonCompliantOrganizationUserCommand = revokeNonCompliantOrganizationUserCommand;
} }

View File

@@ -1,4 +1,5 @@
using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Models.Data.OrganizationUsers;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
@@ -93,7 +94,7 @@ public interface IOrganizationUserRepository : IRepository<OrganizationUser, Gui
/// ///
/// This is an idempotent operation. /// This is an idempotent operation.
/// </summary> /// </summary>
/// <param name="organizationUser">Accepted OrganizationUser to confirm</param> /// <param name="organizationUserToConfirm">Accepted OrganizationUser to confirm</param>
/// <returns>True, if the user was updated. False, if not performed.</returns> /// <returns>True, if the user was updated. False, if not performed.</returns>
Task<bool> ConfirmOrganizationUserAsync(OrganizationUser organizationUser); Task<bool> ConfirmOrganizationUserAsync(AcceptedOrganizationUserToConfirm organizationUserToConfirm);
} }

View File

@@ -148,7 +148,7 @@ public class OrganizationService : IOrganizationService
} }
var secret = await BillingHelpers.AdjustStorageAsync(_paymentService, organization, storageAdjustmentGb, var secret = await BillingHelpers.AdjustStorageAsync(_paymentService, organization, storageAdjustmentGb,
plan.PasswordManager.StripeStoragePlanId); plan.PasswordManager.StripeStoragePlanId, plan.PasswordManager.BaseStorageGb);
await ReplaceAndUpdateCacheAsync(organization); await ReplaceAndUpdateCacheAsync(organization);
return secret; return secret;
} }

View File

@@ -0,0 +1,15 @@
namespace Bit.Core.AdminConsole.Utilities.v2;
/// <summary>
/// A strongly typed error containing a reason that an action failed.
/// This is used for business logic validation and other expected errors, not exceptions.
/// </summary>
public abstract record Error(string Message);
/// <summary>
/// An <see cref="Error"/> type that maps to a NotFoundResult at the api layer.
/// </summary>
/// <param name="Message"></param>
public abstract record NotFoundError(string Message) : Error(Message);
public abstract record BadRequestError(string Message) : Error(Message);
public abstract record InternalError(string Message) : Error(Message);

View File

@@ -1,7 +1,7 @@
using OneOf; using OneOf;
using OneOf.Types; using OneOf.Types;
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccount; namespace Bit.Core.AdminConsole.Utilities.v2.Results;
/// <summary> /// <summary>
/// Represents the result of a command. /// Represents the result of a command.
@@ -39,4 +39,3 @@ public record BulkCommandResult<T>(Guid Id, CommandResult<T> Result);
/// A wrapper for <see cref="CommandResult"/> with an ID, to identify the result in bulk operations. /// A wrapper for <see cref="CommandResult"/> with an ID, to identify the result in bulk operations.
/// </summary> /// </summary>
public record BulkCommandResult(Guid Id, CommandResult Result); public record BulkCommandResult(Guid Id, CommandResult Result);

View File

@@ -1,7 +1,7 @@
using OneOf; using OneOf;
using OneOf.Types; using OneOf.Types;
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccount; namespace Bit.Core.AdminConsole.Utilities.v2.Validation;
/// <summary> /// <summary>
/// Represents the result of validating a request. /// Represents the result of validating a request.

View File

@@ -0,0 +1,25 @@
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Extensions;
using Bit.Core.Enums;
using Bit.Core.Models.StaticStore;
namespace Bit.Core.Billing.Models;
public class SponsoredPlans
{
public static IEnumerable<SponsoredPlan> All { get; set; } =
[
new()
{
PlanSponsorshipType = PlanSponsorshipType.FamiliesForEnterprise,
SponsoredProductTierType = ProductTierType.Families,
SponsoringProductTierType = ProductTierType.Enterprise,
StripePlanId = "2021-family-for-enterprise-annually",
UsersCanSponsor = org =>
org.PlanType.GetProductTier() == ProductTierType.Enterprise,
}
];
public static SponsoredPlan Get(PlanSponsorshipType planSponsorshipType) =>
All.FirstOrDefault(p => p.PlanSponsorshipType == planSponsorshipType)!;
}

View File

@@ -97,7 +97,7 @@ public abstract record Plan
public decimal PremiumAccessOptionPrice { get; init; } public decimal PremiumAccessOptionPrice { get; init; }
public short? MaxSeats { get; init; } public short? MaxSeats { get; init; }
// Storage // Storage
public short? BaseStorageGb { get; init; } public short BaseStorageGb { get; init; }
public bool HasAdditionalStorageOption { get; init; } public bool HasAdditionalStorageOption { get; init; }
public decimal AdditionalStoragePricePerGb { get; init; } public decimal AdditionalStoragePricePerGb { get; init; }
public string StripeStoragePlanId { get; init; } public string StripeStoragePlanId { get; init; }

View File

@@ -3,12 +3,12 @@ using Bit.Core.Billing.Commands;
using Bit.Core.Billing.Constants; using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Enums; using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Extensions;
using Bit.Core.Billing.Models;
using Bit.Core.Billing.Organizations.Models; using Bit.Core.Billing.Organizations.Models;
using Bit.Core.Billing.Payment.Models; using Bit.Core.Billing.Payment.Models;
using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Pricing;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Utilities;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using OneOf; using OneOf;
using Stripe; using Stripe;
@@ -54,7 +54,7 @@ public class PreviewOrganizationTaxCommand(
switch (purchase) switch (purchase)
{ {
case { PasswordManager.Sponsored: true }: case { PasswordManager.Sponsored: true }:
var sponsoredPlan = StaticStore.GetSponsoredPlan(PlanSponsorshipType.FamiliesForEnterprise); var sponsoredPlan = SponsoredPlans.Get(PlanSponsorshipType.FamiliesForEnterprise);
items.Add(new InvoiceSubscriptionDetailsItemOptions items.Add(new InvoiceSubscriptionDetailsItemOptions
{ {
Price = sponsoredPlan.StripePlanId, Price = sponsoredPlan.StripePlanId,

View File

@@ -1,6 +1,7 @@
// FIXME: Update this file to be null safe and then delete the line below // FIXME: Update this file to be null safe and then delete the line below
#nullable disable #nullable disable
using Bit.Core.Billing.Models;
using Bit.Core.Models.Business; using Bit.Core.Models.Business;
using Stripe; using Stripe;
@@ -17,7 +18,7 @@ public class SponsorOrganizationSubscriptionUpdate : SubscriptionUpdate
{ {
_existingPlanStripeId = existingPlan.PasswordManager.StripePlanId; _existingPlanStripeId = existingPlan.PasswordManager.StripePlanId;
_sponsoredPlanStripeId = sponsoredPlan?.StripePlanId _sponsoredPlanStripeId = sponsoredPlan?.StripePlanId
?? Core.Utilities.StaticStore.SponsoredPlans.FirstOrDefault()?.StripePlanId; ?? SponsoredPlans.All.FirstOrDefault()?.StripePlanId;
_applySponsorship = applySponsorship; _applySponsorship = applySponsorship;
} }

View File

@@ -80,6 +80,8 @@ public class CreatePremiumCloudHostedSubscriptionCommand(
return new BadRequest("Additional storage must be greater than 0."); return new BadRequest("Additional storage must be greater than 0.");
} }
var premiumPlan = await pricingClient.GetAvailablePremiumPlan();
Customer? customer; Customer? customer;
/* /*
@@ -107,7 +109,7 @@ public class CreatePremiumCloudHostedSubscriptionCommand(
customer = await ReconcileBillingLocationAsync(customer, billingAddress); customer = await ReconcileBillingLocationAsync(customer, billingAddress);
var subscription = await CreateSubscriptionAsync(user.Id, customer, additionalStorageGb > 0 ? additionalStorageGb : null); var subscription = await CreateSubscriptionAsync(user.Id, customer, premiumPlan, additionalStorageGb > 0 ? additionalStorageGb : null);
paymentMethod.Switch( paymentMethod.Switch(
tokenized => tokenized =>
@@ -140,7 +142,7 @@ public class CreatePremiumCloudHostedSubscriptionCommand(
user.Gateway = GatewayType.Stripe; user.Gateway = GatewayType.Stripe;
user.GatewayCustomerId = customer.Id; user.GatewayCustomerId = customer.Id;
user.GatewaySubscriptionId = subscription.Id; user.GatewaySubscriptionId = subscription.Id;
user.MaxStorageGb = (short)(1 + additionalStorageGb); user.MaxStorageGb = (short)(premiumPlan.Storage.Provided + additionalStorageGb);
user.LicenseKey = CoreHelpers.SecureRandomString(20); user.LicenseKey = CoreHelpers.SecureRandomString(20);
user.RevisionDate = DateTime.UtcNow; user.RevisionDate = DateTime.UtcNow;
@@ -304,9 +306,9 @@ public class CreatePremiumCloudHostedSubscriptionCommand(
private async Task<Subscription> CreateSubscriptionAsync( private async Task<Subscription> CreateSubscriptionAsync(
Guid userId, Guid userId,
Customer customer, Customer customer,
Pricing.Premium.Plan premiumPlan,
int? storage) int? storage)
{ {
var premiumPlan = await pricingClient.GetAvailablePremiumPlan();
var subscriptionItemOptionsList = new List<SubscriptionItemOptions> var subscriptionItemOptionsList = new List<SubscriptionItemOptions>
{ {

View File

@@ -99,7 +99,7 @@ public record PlanAdapter : Core.Models.StaticStore.Plan
_ => true); _ => true);
var baseSeats = GetBaseSeats(plan.Seats); var baseSeats = GetBaseSeats(plan.Seats);
var maxSeats = GetMaxSeats(plan.Seats); var maxSeats = GetMaxSeats(plan.Seats);
var baseStorageGb = (short?)plan.Storage?.Provided; var baseStorageGb = (short)(plan.Storage?.Provided ?? 0);
var hasAdditionalStorageOption = plan.Storage != null; var hasAdditionalStorageOption = plan.Storage != null;
var additionalStoragePricePerGb = plan.Storage?.Price ?? 0; var additionalStoragePricePerGb = plan.Storage?.Price ?? 0;
var stripeStoragePlanId = plan.Storage?.StripePriceId; var stripeStoragePlanId = plan.Storage?.StripePriceId;

View File

@@ -4,4 +4,5 @@ public class Purchasable
{ {
public string StripePriceId { get; init; } = null!; public string StripePriceId { get; init; } = null!;
public decimal Price { get; init; } public decimal Price { get; init; }
public int Provided { get; init; }
} }

View File

@@ -6,7 +6,6 @@ using Bit.Core.Billing.Pricing.Organizations;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Settings; using Bit.Core.Settings;
using Bit.Core.Utilities;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace Bit.Core.Billing.Pricing; namespace Bit.Core.Billing.Pricing;
@@ -28,13 +27,6 @@ public class PricingClient(
return null; return null;
} }
var usePricingService = featureService.IsEnabled(FeatureFlagKeys.UsePricingService);
if (!usePricingService)
{
return StaticStore.GetPlan(planType);
}
var lookupKey = GetLookupKey(planType); var lookupKey = GetLookupKey(planType);
if (lookupKey == null) if (lookupKey == null)
@@ -77,13 +69,6 @@ public class PricingClient(
return []; return [];
} }
var usePricingService = featureService.IsEnabled(FeatureFlagKeys.UsePricingService);
if (!usePricingService)
{
return StaticStore.Plans.ToList();
}
var response = await httpClient.GetAsync("plans/organization"); var response = await httpClient.GetAsync("plans/organization");
if (response.IsSuccessStatusCode) if (response.IsSuccessStatusCode)
@@ -114,11 +99,10 @@ public class PricingClient(
return []; return [];
} }
var usePricingService = featureService.IsEnabled(FeatureFlagKeys.UsePricingService);
var fetchPremiumPriceFromPricingService = var fetchPremiumPriceFromPricingService =
featureService.IsEnabled(FeatureFlagKeys.PM26793_FetchPremiumPriceFromPricingService); featureService.IsEnabled(FeatureFlagKeys.PM26793_FetchPremiumPriceFromPricingService);
if (!usePricingService || !fetchPremiumPriceFromPricingService) if (!fetchPremiumPriceFromPricingService)
{ {
return [CurrentPremiumPlan]; return [CurrentPremiumPlan];
} }
@@ -186,6 +170,6 @@ public class PricingClient(
Available = true, Available = true,
LegacyYear = null, LegacyYear = null,
Seat = new Purchasable { Price = 10M, StripePriceId = StripeConstants.Prices.PremiumAnnually }, Seat = new Purchasable { Price = 10M, StripePriceId = StripeConstants.Prices.PremiumAnnually },
Storage = new Purchasable { Price = 4M, StripePriceId = StripeConstants.Prices.StoragePlanPersonal } Storage = new Purchasable { Price = 4M, StripePriceId = StripeConstants.Prices.StoragePlanPersonal, Provided = 1 }
}; };
} }

View File

@@ -101,7 +101,9 @@ public class PremiumUserBillingService(
*/ */
customer = await ReconcileBillingLocationAsync(customer, customerSetup.TaxInformation); customer = await ReconcileBillingLocationAsync(customer, customerSetup.TaxInformation);
var subscription = await CreateSubscriptionAsync(user.Id, customer, storage); var premiumPlan = await pricingClient.GetAvailablePremiumPlan();
var subscription = await CreateSubscriptionAsync(user.Id, customer, premiumPlan, storage);
switch (customerSetup.TokenizedPaymentSource) switch (customerSetup.TokenizedPaymentSource)
{ {
@@ -119,6 +121,7 @@ public class PremiumUserBillingService(
user.Gateway = GatewayType.Stripe; user.Gateway = GatewayType.Stripe;
user.GatewayCustomerId = customer.Id; user.GatewayCustomerId = customer.Id;
user.GatewaySubscriptionId = subscription.Id; user.GatewaySubscriptionId = subscription.Id;
user.MaxStorageGb = (short)(premiumPlan.Storage.Provided + (storage ?? 0));
await userRepository.ReplaceAsync(user); await userRepository.ReplaceAsync(user);
} }
@@ -301,9 +304,9 @@ public class PremiumUserBillingService(
private async Task<Subscription> CreateSubscriptionAsync( private async Task<Subscription> CreateSubscriptionAsync(
Guid userId, Guid userId,
Customer customer, Customer customer,
Pricing.Premium.Plan premiumPlan,
int? storage) int? storage)
{ {
var premiumPlan = await pricingClient.GetAvailablePremiumPlan();
var subscriptionItemOptionsList = new List<SubscriptionItemOptions> var subscriptionItemOptionsList = new List<SubscriptionItemOptions>
{ {

View File

@@ -163,6 +163,7 @@ public static class FeatureFlagKeys
public const string RecoveryCodeSupportForSsoRequiredUsers = "pm-21153-recovery-code-support-for-sso-required"; public const string RecoveryCodeSupportForSsoRequiredUsers = "pm-21153-recovery-code-support-for-sso-required";
public const string MJMLBasedEmailTemplates = "mjml-based-email-templates"; public const string MJMLBasedEmailTemplates = "mjml-based-email-templates";
public const string MjmlWelcomeEmailTemplates = "mjml-welcome-email-templates"; public const string MjmlWelcomeEmailTemplates = "mjml-welcome-email-templates";
public const string MarketingInitiatedPremiumFlow = "pm-26140-marketing-initiated-premium-flow";
/* Autofill Team */ /* Autofill Team */
public const string IdpAutoSubmitLogin = "idp-auto-submit-login"; public const string IdpAutoSubmitLogin = "idp-auto-submit-login";
@@ -184,7 +185,6 @@ public static class FeatureFlagKeys
/* Billing Team */ /* Billing Team */
public const string TrialPayment = "PM-8163-trial-payment"; public const string TrialPayment = "PM-8163-trial-payment";
public const string UsePricingService = "use-pricing-service";
public const string PM19422_AllowAutomaticTaxUpdates = "pm-19422-allow-automatic-tax-updates"; public const string PM19422_AllowAutomaticTaxUpdates = "pm-19422-allow-automatic-tax-updates";
public const string PM21821_ProviderPortalTakeover = "pm-21821-provider-portal-takeover"; public const string PM21821_ProviderPortalTakeover = "pm-21821-provider-portal-takeover";
public const string PM22415_TaxIDWarnings = "pm-22415-tax-id-warnings"; public const string PM22415_TaxIDWarnings = "pm-22415-tax-id-warnings";

View File

@@ -299,7 +299,7 @@ public class CompleteSubscriptionUpdate : SubscriptionUpdate
? organization.SmServiceAccounts - plan.SecretsManager.BaseServiceAccount ? organization.SmServiceAccounts - plan.SecretsManager.BaseServiceAccount
: 0, : 0,
PurchasedAdditionalStorage = organization.MaxStorageGb.HasValue PurchasedAdditionalStorage = organization.MaxStorageGb.HasValue
? organization.MaxStorageGb.Value - (plan.PasswordManager.BaseStorageGb ?? 0) : ? organization.MaxStorageGb.Value - plan.PasswordManager.BaseStorageGb :
0 0
}; };
} }

View File

@@ -1,4 +1,5 @@
using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Extensions;
using Bit.Core.Billing.Models;
using Stripe; using Stripe;
#nullable enable #nullable enable
@@ -150,7 +151,7 @@ public class SubscriptionInfo
} }
Quantity = (int)item.Quantity; Quantity = (int)item.Quantity;
SponsoredSubscriptionItem = item.Plan != null && Utilities.StaticStore.SponsoredPlans.Any(p => p.StripePlanId == item.Plan.Id); SponsoredSubscriptionItem = item.Plan != null && SponsoredPlans.All.Any(p => p.StripePlanId == item.Plan.Id);
} }
public bool AddonSubscriptionItem { get; set; } public bool AddonSubscriptionItem { get; set; }

View File

@@ -14,6 +14,7 @@ using Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Authorization; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Authorization;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUser;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccount; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccount;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;
@@ -135,6 +136,8 @@ public static class OrganizationServiceCollectionExtensions
services.AddScoped<IUpdateOrganizationUserGroupsCommand, UpdateOrganizationUserGroupsCommand>(); services.AddScoped<IUpdateOrganizationUserGroupsCommand, UpdateOrganizationUserGroupsCommand>();
services.AddScoped<IConfirmOrganizationUserCommand, ConfirmOrganizationUserCommand>(); services.AddScoped<IConfirmOrganizationUserCommand, ConfirmOrganizationUserCommand>();
services.AddScoped<IAdminRecoverAccountCommand, AdminRecoverAccountCommand>(); services.AddScoped<IAdminRecoverAccountCommand, AdminRecoverAccountCommand>();
services.AddScoped<IAutomaticallyConfirmOrganizationUserCommand, AutomaticallyConfirmOrganizationUserCommand>();
services.AddScoped<IAutomaticallyConfirmOrganizationUsersValidator, AutomaticallyConfirmOrganizationUsersValidator>();
services.AddScoped<IDeleteClaimedOrganizationUserAccountCommand, DeleteClaimedOrganizationUserAccountCommand>(); services.AddScoped<IDeleteClaimedOrganizationUserAccountCommand, DeleteClaimedOrganizationUserAccountCommand>();
services.AddScoped<IDeleteClaimedOrganizationUserAccountValidator, DeleteClaimedOrganizationUserAccountValidator>(); services.AddScoped<IDeleteClaimedOrganizationUserAccountValidator, DeleteClaimedOrganizationUserAccountValidator>();

View File

@@ -1,5 +1,6 @@
using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities;
using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Extensions;
using Bit.Core.Billing.Models;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
@@ -7,7 +8,6 @@ using Bit.Core.Models.Data.Organizations.OrganizationSponsorships;
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces; using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Utilities;
namespace Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Cloud; namespace Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Cloud;
@@ -54,10 +54,9 @@ public class CloudSyncSponsorshipsCommand : ICloudSyncSponsorshipsCommand
foreach (var selfHostedSponsorship in sponsorshipsData) foreach (var selfHostedSponsorship in sponsorshipsData)
{ {
var requiredSponsoringProductType = StaticStore.GetSponsoredPlan(selfHostedSponsorship.PlanSponsorshipType)?.SponsoringProductTierType; var requiredSponsoringProductType = SponsoredPlans.Get(selfHostedSponsorship.PlanSponsorshipType).SponsoringProductTierType;
var sponsoringOrgProductTier = sponsoringOrg.PlanType.GetProductTier(); var sponsoringOrgProductTier = sponsoringOrg.PlanType.GetProductTier();
if (requiredSponsoringProductType == null if (sponsoringOrgProductTier != requiredSponsoringProductType)
|| sponsoringOrgProductTier != requiredSponsoringProductType.Value)
{ {
continue; // prevent unsupported sponsorships continue; // prevent unsupported sponsorships
} }

View File

@@ -1,11 +1,11 @@
using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities;
using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Extensions;
using Bit.Core.Billing.Models;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
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;
namespace Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Cloud; namespace Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Cloud;
@@ -50,11 +50,10 @@ public class SetUpSponsorshipCommand : ISetUpSponsorshipCommand
} }
// Check org to sponsor's product type // Check org to sponsor's product type
var requiredSponsoredProductType = StaticStore.GetSponsoredPlan(sponsorship.PlanSponsorshipType.Value)?.SponsoredProductTierType; var requiredSponsoredProductType = SponsoredPlans.Get(sponsorship.PlanSponsorshipType.Value).SponsoredProductTierType;
var sponsoredOrganizationProductTier = sponsoredOrganization.PlanType.GetProductTier(); var sponsoredOrganizationProductTier = sponsoredOrganization.PlanType.GetProductTier();
if (requiredSponsoredProductType == null || if (sponsoredOrganizationProductTier != requiredSponsoredProductType)
sponsoredOrganizationProductTier != requiredSponsoredProductType.Value)
{ {
throw new BadRequestException("Can only redeem sponsorship offer on families organizations."); throw new BadRequestException("Can only redeem sponsorship offer on families organizations.");
} }

View File

@@ -3,6 +3,7 @@
using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities;
using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Extensions;
using Bit.Core.Billing.Models;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces; using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
using Bit.Core.Repositories; using Bit.Core.Repositories;
@@ -95,7 +96,7 @@ public class ValidateSponsorshipCommand : CancelSponsorshipCommand, IValidateSpo
return false; return false;
} }
var sponsoredPlan = Utilities.StaticStore.GetSponsoredPlan(existingSponsorship.PlanSponsorshipType.Value); var sponsoredPlan = SponsoredPlans.Get(existingSponsorship.PlanSponsorshipType.Value);
var sponsoringOrganization = await _organizationRepository var sponsoringOrganization = await _organizationRepository
.GetByIdAsync(existingSponsorship.SponsoringOrganizationId.Value); .GetByIdAsync(existingSponsorship.SponsoringOrganizationId.Value);

View File

@@ -1,5 +1,6 @@
using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities;
using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Extensions;
using Bit.Core.Billing.Models;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
@@ -7,7 +8,6 @@ using Bit.Core.Exceptions;
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;
namespace Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise; namespace Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise;
@@ -34,11 +34,10 @@ public class CreateSponsorshipCommand(
throw new BadRequestException("Cannot offer a Families Organization Sponsorship to yourself. Choose a different email."); throw new BadRequestException("Cannot offer a Families Organization Sponsorship to yourself. Choose a different email.");
} }
var requiredSponsoringProductType = StaticStore.GetSponsoredPlan(sponsorshipType)?.SponsoringProductTierType; var requiredSponsoringProductType = SponsoredPlans.Get(sponsorshipType).SponsoringProductTierType;
var sponsoringOrgProductTier = sponsoringOrganization.PlanType.GetProductTier(); var sponsoringOrgProductTier = sponsoringOrganization.PlanType.GetProductTier();
if (requiredSponsoringProductType == null || if (sponsoringOrgProductTier != requiredSponsoringProductType)
sponsoringOrgProductTier != requiredSponsoringProductType.Value)
{ {
throw new BadRequestException("Specified Organization cannot sponsor other organizations."); throw new BadRequestException("Specified Organization cannot sponsor other organizations.");
} }

View File

@@ -254,9 +254,7 @@ public class UpgradeOrganizationPlanCommand : IUpgradeOrganizationPlanCommand
organization.UseApi = newPlan.HasApi; organization.UseApi = newPlan.HasApi;
organization.SelfHost = newPlan.HasSelfHost; organization.SelfHost = newPlan.HasSelfHost;
organization.UsePolicies = newPlan.HasPolicies; organization.UsePolicies = newPlan.HasPolicies;
organization.MaxStorageGb = !newPlan.PasswordManager.BaseStorageGb.HasValue organization.MaxStorageGb = (short)(newPlan.PasswordManager.BaseStorageGb + upgrade.AdditionalStorageGb);
? (short?)null
: (short)(newPlan.PasswordManager.BaseStorageGb.Value + upgrade.AdditionalStorageGb);
organization.UseGroups = newPlan.HasGroups; organization.UseGroups = newPlan.HasGroups;
organization.UseDirectory = newPlan.HasDirectory; organization.UseDirectory = newPlan.HasDirectory;
organization.UseEvents = newPlan.HasEvents; organization.UseEvents = newPlan.HasEvents;

View File

@@ -67,7 +67,7 @@ public class StripePaymentService : IPaymentService
{ {
var existingPlan = await _pricingClient.GetPlanOrThrow(org.PlanType); var existingPlan = await _pricingClient.GetPlanOrThrow(org.PlanType);
var sponsoredPlan = sponsorship?.PlanSponsorshipType != null var sponsoredPlan = sponsorship?.PlanSponsorshipType != null
? Utilities.StaticStore.GetSponsoredPlan(sponsorship.PlanSponsorshipType.Value) ? SponsoredPlans.Get(sponsorship.PlanSponsorshipType.Value)
: null; : null;
var subscriptionUpdate = var subscriptionUpdate =
new SponsorOrganizationSubscriptionUpdate(existingPlan, sponsoredPlan, applySponsorship); new SponsorOrganizationSubscriptionUpdate(existingPlan, sponsoredPlan, applySponsorship);
@@ -1072,7 +1072,7 @@ public class StripePaymentService : IPaymentService
if (isSponsored) if (isSponsored)
{ {
var sponsoredPlan = Utilities.StaticStore.GetSponsoredPlan(parameters.PasswordManager.SponsoredPlan.Value); var sponsoredPlan = SponsoredPlans.Get(parameters.PasswordManager.SponsoredPlan.Value);
options.SubscriptionDetails.Items.Add( options.SubscriptionDetails.Items.Add(
new InvoiceSubscriptionDetailsItemOptions { Quantity = 1, Plan = sponsoredPlan.StripePlanId } new InvoiceSubscriptionDetailsItemOptions { Quantity = 1, Plan = sponsoredPlan.StripePlanId }
); );

View File

@@ -904,7 +904,6 @@ public class UserService : UserManager<User>, IUserService
} }
else else
{ {
user.MaxStorageGb = (short)(1 + additionalStorageGb);
user.LicenseKey = CoreHelpers.SecureRandomString(20); user.LicenseKey = CoreHelpers.SecureRandomString(20);
} }
@@ -977,7 +976,8 @@ public class UserService : UserManager<User>, IUserService
var premiumPlan = await _pricingClient.GetAvailablePremiumPlan(); var premiumPlan = await _pricingClient.GetAvailablePremiumPlan();
var secret = await BillingHelpers.AdjustStorageAsync(_paymentService, user, storageAdjustmentGb, premiumPlan.Storage.StripePriceId); var baseStorageGb = (short)premiumPlan.Storage.Provided;
var secret = await BillingHelpers.AdjustStorageAsync(_paymentService, user, storageAdjustmentGb, premiumPlan.Storage.StripePriceId, baseStorageGb);
await SaveUserAsync(user); await SaveUserAsync(user);
return secret; return secret;
} }

View File

@@ -21,7 +21,7 @@ public class GlobalSettings : IGlobalSettings
} }
public bool SelfHosted { get; set; } public bool SelfHosted { get; set; }
public bool UnifiedDeployment { get; set; } public bool LiteDeployment { get; set; }
public virtual string KnownProxies { get; set; } public virtual string KnownProxies { get; set; }
public virtual string SiteName { get; set; } public virtual string SiteName { get; set; }
public virtual string ProjectName { get; set; } public virtual string ProjectName { get; set; }

View File

@@ -6,7 +6,7 @@ public interface IGlobalSettings
{ {
// This interface exists for testing. Add settings here as needed for testing // This interface exists for testing. Add settings here as needed for testing
bool SelfHosted { get; set; } bool SelfHosted { get; set; }
bool UnifiedDeployment { get; set; } bool LiteDeployment { get; set; }
string KnownProxies { get; set; } string KnownProxies { get; set; }
string ProjectName { get; set; } string ProjectName { get; set; }
bool EnableCloudCommunication { get; set; } bool EnableCloudCommunication { get; set; }

View File

@@ -150,17 +150,34 @@ public class ImportCiphersCommand : IImportCiphersCommand
foreach (var collection in collections) foreach (var collection in collections)
{ {
if (!organizationCollectionsIds.Contains(collection.Id)) // If the collection already exists, skip it
if (organizationCollectionsIds.Contains(collection.Id))
{ {
collection.SetNewId(); continue;
newCollections.Add(collection);
newCollectionUsers.Add(new CollectionUser
{
CollectionId = collection.Id,
OrganizationUserId = importingOrgUser.Id,
Manage = true
});
} }
// Create new collections if not already present
collection.SetNewId();
newCollections.Add(collection);
/*
* If the organization was created by a Provider, the organization may have zero members (users)
* In this situation importingOrgUser will be null, and accessing importingOrgUser.Id will
* result in a null reference exception.
*
* Avoid user assignment, but proceed with adding the collection.
*/
if (importingOrgUser == null)
{
continue;
}
newCollectionUsers.Add(new CollectionUser
{
CollectionId = collection.Id,
OrganizationUserId = importingOrgUser.Id,
Manage = true
});
} }
// Create associations based on the newly assigned ids // Create associations based on the newly assigned ids

View File

@@ -7,7 +7,7 @@ namespace Bit.Core.Utilities;
public static class BillingHelpers public static class BillingHelpers
{ {
internal static async Task<string> AdjustStorageAsync(IPaymentService paymentService, IStorableSubscriber storableSubscriber, internal static async Task<string> AdjustStorageAsync(IPaymentService paymentService, IStorableSubscriber storableSubscriber,
short storageAdjustmentGb, string storagePlanId) short storageAdjustmentGb, string storagePlanId, short baseStorageGb)
{ {
if (storableSubscriber == null) if (storableSubscriber == null)
{ {
@@ -30,9 +30,9 @@ public static class BillingHelpers
} }
var newStorageGb = (short)(storableSubscriber.MaxStorageGb.Value + storageAdjustmentGb); var newStorageGb = (short)(storableSubscriber.MaxStorageGb.Value + storageAdjustmentGb);
if (newStorageGb < 1) if (newStorageGb < baseStorageGb)
{ {
newStorageGb = 1; newStorageGb = baseStorageGb;
} }
if (newStorageGb > 100) if (newStorageGb > 100)
@@ -48,7 +48,7 @@ public static class BillingHelpers
"Delete some stored data first."); "Delete some stored data first.");
} }
var additionalStorage = newStorageGb - 1; var additionalStorage = newStorageGb - baseStorageGb;
var paymentIntentClientSecret = await paymentService.AdjustStorageAsync(storableSubscriber, var paymentIntentClientSecret = await paymentService.AdjustStorageAsync(storableSubscriber,
additionalStorage, storagePlanId); additionalStorage, storagePlanId);
storableSubscriber.MaxStorageGb = newStorageGb; storableSubscriber.MaxStorageGb = newStorageGb;

View File

@@ -1,13 +1,7 @@
// FIXME: Update this file to be null safe and then delete the line below // FIXME: Update this file to be null safe and then delete the line below
#nullable disable #nullable disable
using System.Collections.Immutable;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Extensions;
using Bit.Core.Billing.Models.StaticStore.Plans;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.Models.StaticStore;
namespace Bit.Core.Utilities; namespace Bit.Core.Utilities;
@@ -110,56 +104,7 @@ public static class StaticStore
GlobalDomains.Add(GlobalEquivalentDomainsType.Atlassian, new List<string> { "atlassian.com", "bitbucket.org", "trello.com", "statuspage.io", "atlassian.net", "jira.com" }); GlobalDomains.Add(GlobalEquivalentDomainsType.Atlassian, new List<string> { "atlassian.com", "bitbucket.org", "trello.com", "statuspage.io", "atlassian.net", "jira.com" });
GlobalDomains.Add(GlobalEquivalentDomainsType.Pinterest, new List<string> { "pinterest.com", "pinterest.com.au", "pinterest.cl", "pinterest.de", "pinterest.dk", "pinterest.es", "pinterest.fr", "pinterest.co.uk", "pinterest.jp", "pinterest.co.kr", "pinterest.nz", "pinterest.pt", "pinterest.se" }); GlobalDomains.Add(GlobalEquivalentDomainsType.Pinterest, new List<string> { "pinterest.com", "pinterest.com.au", "pinterest.cl", "pinterest.de", "pinterest.dk", "pinterest.es", "pinterest.fr", "pinterest.co.uk", "pinterest.jp", "pinterest.co.kr", "pinterest.nz", "pinterest.pt", "pinterest.se" });
#endregion #endregion
Plans = new List<Plan>
{
new EnterprisePlan(true),
new EnterprisePlan(false),
new TeamsStarterPlan(),
new TeamsPlan(true),
new TeamsPlan(false),
new Enterprise2023Plan(true),
new Enterprise2023Plan(false),
new Enterprise2020Plan(true),
new Enterprise2020Plan(false),
new TeamsStarterPlan2023(),
new Teams2023Plan(true),
new Teams2023Plan(false),
new Teams2020Plan(true),
new Teams2020Plan(false),
new FamiliesPlan(),
new FreePlan(),
new CustomPlan(),
new Enterprise2019Plan(true),
new Enterprise2019Plan(false),
new Teams2019Plan(true),
new Teams2019Plan(false),
new Families2019Plan(),
new Families2025Plan()
}.ToImmutableList();
} }
public static IDictionary<GlobalEquivalentDomainsType, IEnumerable<string>> GlobalDomains { get; set; } public static IDictionary<GlobalEquivalentDomainsType, IEnumerable<string>> GlobalDomains { get; set; }
[Obsolete("Use PricingClient.ListPlans to retrieve all plans.")]
public static IEnumerable<Plan> Plans { get; }
public static IEnumerable<SponsoredPlan> SponsoredPlans { get; set; } = new[]
{
new SponsoredPlan
{
PlanSponsorshipType = PlanSponsorshipType.FamiliesForEnterprise,
SponsoredProductTierType = ProductTierType.Families,
SponsoringProductTierType = ProductTierType.Enterprise,
StripePlanId = "2021-family-for-enterprise-annually",
UsersCanSponsor = (OrganizationUserOrganizationDetails org) =>
org.PlanType.GetProductTier() == ProductTierType.Enterprise,
}
};
[Obsolete("Use PricingClient.GetPlan to retrieve a plan.")]
public static Plan GetPlan(PlanType planType) => Plans.SingleOrDefault(p => p.Type == planType);
public static SponsoredPlan GetSponsoredPlan(PlanSponsorshipType planSponsorshipType) =>
SponsoredPlans.FirstOrDefault(p => p.PlanSponsorshipType == planSponsorshipType);
} }

View File

@@ -17,7 +17,7 @@ public interface ICipherService
Task CreateAttachmentAsync(Cipher cipher, Stream stream, string fileName, string key, Task CreateAttachmentAsync(Cipher cipher, Stream stream, string fileName, string key,
long requestLength, Guid savingUserId, bool orgAdmin = false, DateTime? lastKnownRevisionDate = null); long requestLength, Guid savingUserId, bool orgAdmin = false, DateTime? lastKnownRevisionDate = null);
Task CreateAttachmentShareAsync(Cipher cipher, Stream stream, string fileName, string key, long requestLength, Task CreateAttachmentShareAsync(Cipher cipher, Stream stream, string fileName, string key, long requestLength,
string attachmentId, Guid organizationShareId, DateTime? lastKnownRevisionDate = null); string attachmentId, Guid organizationShareId);
Task DeleteAsync(CipherDetails cipherDetails, Guid deletingUserId, bool orgAdmin = false); Task DeleteAsync(CipherDetails cipherDetails, Guid deletingUserId, bool orgAdmin = false);
Task DeleteManyAsync(IEnumerable<Guid> cipherIds, Guid deletingUserId, Guid? organizationId = null, bool orgAdmin = false); Task DeleteManyAsync(IEnumerable<Guid> cipherIds, Guid deletingUserId, Guid? organizationId = null, bool orgAdmin = false);
Task<DeleteAttachmentResponseData> DeleteAttachmentAsync(Cipher cipher, string attachmentId, Guid deletingUserId, bool orgAdmin = false); Task<DeleteAttachmentResponseData> DeleteAttachmentAsync(Cipher cipher, string attachmentId, Guid deletingUserId, bool orgAdmin = false);
@@ -34,7 +34,7 @@ public interface ICipherService
Task SoftDeleteManyAsync(IEnumerable<Guid> cipherIds, Guid deletingUserId, Guid? organizationId = null, bool orgAdmin = false); Task SoftDeleteManyAsync(IEnumerable<Guid> cipherIds, Guid deletingUserId, Guid? organizationId = null, bool orgAdmin = false);
Task RestoreAsync(CipherDetails cipherDetails, Guid restoringUserId, bool orgAdmin = false); Task RestoreAsync(CipherDetails cipherDetails, Guid restoringUserId, bool orgAdmin = false);
Task<ICollection<CipherOrganizationDetails>> RestoreManyAsync(IEnumerable<Guid> cipherIds, Guid restoringUserId, Guid? organizationId = null, bool orgAdmin = false); Task<ICollection<CipherOrganizationDetails>> RestoreManyAsync(IEnumerable<Guid> cipherIds, Guid restoringUserId, Guid? organizationId = null, bool orgAdmin = false);
Task UploadFileForExistingAttachmentAsync(Stream stream, Cipher cipher, CipherAttachment.MetaData attachmentId, DateTime? lastKnownRevisionDate = null); Task UploadFileForExistingAttachmentAsync(Stream stream, Cipher cipher, CipherAttachment.MetaData attachmentId);
Task<AttachmentResponseData> GetAttachmentDownloadDataAsync(Cipher cipher, string attachmentId); Task<AttachmentResponseData> GetAttachmentDownloadDataAsync(Cipher cipher, string attachmentId);
Task<bool> ValidateCipherAttachmentFile(Cipher cipher, CipherAttachment.MetaData attachmentData); Task<bool> ValidateCipherAttachmentFile(Cipher cipher, CipherAttachment.MetaData attachmentData);
Task ValidateBulkCollectionAssignmentAsync(IEnumerable<Guid> collectionIds, IEnumerable<Guid> cipherIds, Guid userId); Task ValidateBulkCollectionAssignmentAsync(IEnumerable<Guid> collectionIds, IEnumerable<Guid> cipherIds, Guid userId);

View File

@@ -183,9 +183,8 @@ public class CipherService : ICipherService
} }
} }
public async Task UploadFileForExistingAttachmentAsync(Stream stream, Cipher cipher, CipherAttachment.MetaData attachment, DateTime? lastKnownRevisionDate = null) public async Task UploadFileForExistingAttachmentAsync(Stream stream, Cipher cipher, CipherAttachment.MetaData attachment)
{ {
ValidateCipherLastKnownRevisionDate(cipher, lastKnownRevisionDate);
if (attachment == null) if (attachment == null)
{ {
throw new BadRequestException("Cipher attachment does not exist"); throw new BadRequestException("Cipher attachment does not exist");
@@ -290,11 +289,10 @@ public class CipherService : ICipherService
} }
public async Task CreateAttachmentShareAsync(Cipher cipher, Stream stream, string fileName, string key, public async Task CreateAttachmentShareAsync(Cipher cipher, Stream stream, string fileName, string key,
long requestLength, string attachmentId, Guid organizationId, DateTime? lastKnownRevisionDate = null) long requestLength, string attachmentId, Guid organizationId)
{ {
try try
{ {
ValidateCipherLastKnownRevisionDate(cipher, lastKnownRevisionDate);
if (requestLength < 1) if (requestLength < 1)
{ {
throw new BadRequestException("No data to attach."); throw new BadRequestException("No data to attach.");

View File

@@ -2,6 +2,7 @@
using System.Text.Json; using System.Text.Json;
using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Models.Data.OrganizationUsers;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
using Bit.Core.AdminConsole.Utilities.DebuggingInstruments; using Bit.Core.AdminConsole.Utilities.DebuggingInstruments;
using Bit.Core.Entities; using Bit.Core.Entities;
@@ -671,7 +672,7 @@ public class OrganizationUserRepository : Repository<OrganizationUser, Guid>, IO
commandType: CommandType.StoredProcedure); commandType: CommandType.StoredProcedure);
} }
public async Task<bool> ConfirmOrganizationUserAsync(OrganizationUser organizationUser) public async Task<bool> ConfirmOrganizationUserAsync(AcceptedOrganizationUserToConfirm organizationUserToConfirm)
{ {
await using var connection = new SqlConnection(_marsConnectionString); await using var connection = new SqlConnection(_marsConnectionString);
@@ -679,10 +680,10 @@ public class OrganizationUserRepository : Repository<OrganizationUser, Guid>, IO
$"[{Schema}].[OrganizationUser_ConfirmById]", $"[{Schema}].[OrganizationUser_ConfirmById]",
new new
{ {
organizationUser.Id, Id = organizationUserToConfirm.OrganizationUserId,
organizationUser.UserId, UserId = organizationUserToConfirm.UserId,
RevisionDate = DateTime.UtcNow.Date, RevisionDate = DateTime.UtcNow.Date,
Key = organizationUser.Key Key = organizationUserToConfirm.Key
}); });
return rowCount > 0; return rowCount > 0;

View File

@@ -3,6 +3,7 @@
using AutoMapper; using AutoMapper;
using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Models.Data.OrganizationUsers;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
@@ -943,23 +944,24 @@ public class OrganizationUserRepository : Repository<Core.Entities.OrganizationU
await dbContext.SaveChangesAsync(); await dbContext.SaveChangesAsync();
} }
public async Task<bool> ConfirmOrganizationUserAsync(Core.Entities.OrganizationUser organizationUser) public async Task<bool> ConfirmOrganizationUserAsync(AcceptedOrganizationUserToConfirm organizationUserToConfirm)
{ {
using var scope = ServiceScopeFactory.CreateScope(); using var scope = ServiceScopeFactory.CreateScope();
await using var dbContext = GetDatabaseContext(scope); await using var dbContext = GetDatabaseContext(scope);
var result = await dbContext.OrganizationUsers var result = await dbContext.OrganizationUsers
.Where(ou => ou.Id == organizationUser.Id && ou.Status == OrganizationUserStatusType.Accepted) .Where(ou => ou.Id == organizationUserToConfirm.OrganizationUserId
&& ou.Status == OrganizationUserStatusType.Accepted)
.ExecuteUpdateAsync(x => x .ExecuteUpdateAsync(x => x
.SetProperty(y => y.Status, OrganizationUserStatusType.Confirmed) .SetProperty(y => y.Status, OrganizationUserStatusType.Confirmed)
.SetProperty(y => y.Key, organizationUser.Key)); .SetProperty(y => y.Key, organizationUserToConfirm.Key));
if (result <= 0) if (result <= 0)
{ {
return false; return false;
} }
await dbContext.UserBumpAccountRevisionDateByOrganizationUserIdAsync(organizationUser.Id); await dbContext.UserBumpAccountRevisionDateByOrganizationUserIdAsync(organizationUserToConfirm.OrganizationUserId);
return true; return true;
} }

View File

@@ -217,7 +217,7 @@ public class PolicyRepository : Repository<AdminConsoleEntities.Policy, Policy,
UserId = u.Id UserId = u.Id
}).ToListAsync(); }).ToListAsync();
// Combine results with provder lookup // Combine results with the provider lookup
var allResults = acceptedUsers.Concat(invitedUsers) var allResults = acceptedUsers.Concat(invitedUsers)
.Select(item => new OrganizationPolicyDetails .Select(item => new OrganizationPolicyDetails
{ {

View File

@@ -645,7 +645,7 @@ public static class ServiceCollectionExtensions
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
}; };
if (!globalSettings.UnifiedDeployment) if (!globalSettings.LiteDeployment)
{ {
// Trust the X-Forwarded-Host header of the nginx docker container // Trust the X-Forwarded-Host header of the nginx docker container
try try

View File

@@ -60,6 +60,7 @@ CREATE TABLE [dbo].[Organization] (
[UseAdminSponsoredFamilies] BIT NOT NULL CONSTRAINT [DF_Organization_UseAdminSponsoredFamilies] DEFAULT (0), [UseAdminSponsoredFamilies] BIT NOT NULL CONSTRAINT [DF_Organization_UseAdminSponsoredFamilies] DEFAULT (0),
[SyncSeats] BIT NOT NULL CONSTRAINT [DF_Organization_SyncSeats] DEFAULT (0), [SyncSeats] BIT NOT NULL CONSTRAINT [DF_Organization_SyncSeats] DEFAULT (0),
[UseAutomaticUserConfirmation] BIT NOT NULL CONSTRAINT [DF_Organization_UseAutomaticUserConfirmation] DEFAULT (0), [UseAutomaticUserConfirmation] BIT NOT NULL CONSTRAINT [DF_Organization_UseAutomaticUserConfirmation] DEFAULT (0),
[MaxStorageGbIncreased] SMALLINT NULL,
CONSTRAINT [PK_Organization] PRIMARY KEY CLUSTERED ([Id] ASC) CONSTRAINT [PK_Organization] PRIMARY KEY CLUSTERED ([Id] ASC)
); );

View File

@@ -45,6 +45,7 @@
[SecurityState] VARCHAR (MAX) NULL, [SecurityState] VARCHAR (MAX) NULL,
[SecurityVersion] INT NULL, [SecurityVersion] INT NULL,
[SignedPublicKey] VARCHAR (MAX) NULL, [SignedPublicKey] VARCHAR (MAX) NULL,
[MaxStorageGbIncreased] SMALLINT NULL,
CONSTRAINT [PK_User] PRIMARY KEY CLUSTERED ([Id] ASC) CONSTRAINT [PK_User] PRIMARY KEY CLUSTERED ([Id] ASC)
); );

View File

@@ -24,7 +24,7 @@ SELECT
O.[UseSecretsManager], O.[UseSecretsManager],
O.[Seats], O.[Seats],
O.[MaxCollections], O.[MaxCollections],
O.[MaxStorageGb], COALESCE(O.[MaxStorageGbIncreased], O.[MaxStorageGb]) AS [MaxStorageGb],
O.[Identifier], O.[Identifier],
OU.[Key], OU.[Key],
OU.[ResetPasswordKey], OU.[ResetPasswordKey],

View File

@@ -1,6 +1,66 @@
CREATE VIEW [dbo].[OrganizationView] CREATE VIEW [dbo].[OrganizationView]
AS AS
SELECT SELECT
* [Id],
[Identifier],
[Name],
[BusinessName],
[BusinessAddress1],
[BusinessAddress2],
[BusinessAddress3],
[BusinessCountry],
[BusinessTaxNumber],
[BillingEmail],
[Plan],
[PlanType],
[Seats],
[MaxCollections],
[UsePolicies],
[UseSso],
[UseGroups],
[UseDirectory],
[UseEvents],
[UseTotp],
[Use2fa],
[UseApi],
[UseResetPassword],
[SelfHost],
[UsersGetPremium],
[Storage],
COALESCE([MaxStorageGbIncreased], [MaxStorageGb]) AS [MaxStorageGb],
[Gateway],
[GatewayCustomerId],
[GatewaySubscriptionId],
[ReferenceData],
[Enabled],
[LicenseKey],
[PublicKey],
[PrivateKey],
[TwoFactorProviders],
[ExpirationDate],
[CreationDate],
[RevisionDate],
[OwnersNotifiedOfAutoscaling],
[MaxAutoscaleSeats],
[UseKeyConnector],
[UseScim],
[UseCustomPermissions],
[UseSecretsManager],
[Status],
[UsePasswordManager],
[SmSeats],
[SmServiceAccounts],
[MaxAutoscaleSmSeats],
[MaxAutoscaleSmServiceAccounts],
[SecretsManagerBeta],
[LimitCollectionCreation],
[LimitCollectionDeletion],
[LimitItemDeletion],
[AllowAdminAccessToAllCollectionItems],
[UseRiskInsights],
[UseOrganizationDomains],
[UseAdminSponsoredFamilies],
[SyncSeats],
[UseAutomaticUserConfirmation]
FROM FROM
[dbo].[Organization] [dbo].[Organization]

View File

@@ -23,7 +23,7 @@ SELECT
O.[UseCustomPermissions], O.[UseCustomPermissions],
O.[Seats], O.[Seats],
O.[MaxCollections], O.[MaxCollections],
O.[MaxStorageGb], COALESCE(O.[MaxStorageGbIncreased], O.[MaxStorageGb]) AS [MaxStorageGb],
O.[Identifier], O.[Identifier],
PO.[Key], PO.[Key],
O.[PublicKey], O.[PublicKey],

View File

@@ -1,6 +1,51 @@
CREATE VIEW [dbo].[UserView] CREATE VIEW [dbo].[UserView]
AS AS
SELECT SELECT
* [Id],
[Name],
[Email],
[EmailVerified],
[MasterPassword],
[MasterPasswordHint],
[Culture],
[SecurityStamp],
[TwoFactorProviders],
[TwoFactorRecoveryCode],
[EquivalentDomains],
[ExcludedGlobalEquivalentDomains],
[AccountRevisionDate],
[Key],
[PublicKey],
[PrivateKey],
[Premium],
[PremiumExpirationDate],
[RenewalReminderDate],
[Storage],
COALESCE([MaxStorageGbIncreased], [MaxStorageGb]) AS [MaxStorageGb],
[Gateway],
[GatewayCustomerId],
[GatewaySubscriptionId],
[ReferenceData],
[LicenseKey],
[ApiKey],
[Kdf],
[KdfIterations],
[KdfMemory],
[KdfParallelism],
[CreationDate],
[RevisionDate],
[ForcePasswordReset],
[UsesKeyConnector],
[FailedLoginCount],
[LastFailedLoginDate],
[AvatarColor],
[LastPasswordChangeDate],
[LastKdfChangeDate],
[LastKeyRotationDate],
[LastEmailChangeDate],
[VerifyDevices],
[SecurityState],
[SecurityVersion],
[SignedPublicKey]
FROM FROM
[dbo].[User] [dbo].[User]

View File

@@ -0,0 +1,225 @@
using System.Net;
using Bit.Api.AdminConsole.Models.Request.Organizations;
using Bit.Api.IntegrationTest.Factories;
using Bit.Api.IntegrationTest.Helpers;
using Bit.Core;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Enums;
using Bit.Core.Enums;
using Bit.Core.Models.Data;
using Bit.Core.Repositories;
using Bit.Core.Services;
using NSubstitute;
using Xunit;
namespace Bit.Api.IntegrationTest.AdminConsole.Controllers;
public class OrganizationUserControllerAutoConfirmTests : IClassFixture<ApiApplicationFactory>, IAsyncLifetime
{
private const string _mockEncryptedString = "2.AOs41Hd8OQiCPXjyJKCiDA==|O6OHgt2U2hJGBSNGnimJmg==|iD33s8B69C8JhYYhSa4V1tArjvLr8eEaGqOV7BRo5Jk=";
private readonly HttpClient _client;
private readonly ApiApplicationFactory _factory;
private readonly LoginHelper _loginHelper;
private string _ownerEmail = null!;
public OrganizationUserControllerAutoConfirmTests(ApiApplicationFactory apiFactory)
{
_factory = apiFactory;
_factory.SubstituteService<IFeatureService>(featureService =>
{
featureService
.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers)
.Returns(true);
});
_client = _factory.CreateClient();
_loginHelper = new LoginHelper(_factory, _client);
}
public async Task InitializeAsync()
{
_ownerEmail = $"org-owner-{Guid.NewGuid()}@example.com";
await _factory.LoginWithNewAccount(_ownerEmail);
}
[Fact]
public async Task AutoConfirm_WhenUserCannotManageOtherUsers_ThenShouldReturnForbidden()
{
var (organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, plan: PlanType.EnterpriseAnnually,
ownerEmail: _ownerEmail, passwordManagerSeats: 5, paymentMethod: PaymentMethodType.Card);
organization.UseAutomaticUserConfirmation = true;
await _factory.GetService<IOrganizationRepository>()
.UpsertAsync(organization);
var testKey = $"test-key-{Guid.NewGuid()}";
var userToConfirmEmail = $"org-user-to-confirm-{Guid.NewGuid()}@example.com";
await _factory.LoginWithNewAccount(userToConfirmEmail);
var (confirmingUserEmail, _) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, organization.Id, OrganizationUserType.User);
await _loginHelper.LoginAsync(confirmingUserEmail);
var organizationUser = await OrganizationTestHelpers.CreateUserAsync(
_factory,
organization.Id,
userToConfirmEmail,
OrganizationUserType.User,
false,
new Permissions { ManageUsers = false },
OrganizationUserStatusType.Accepted);
var result = await _client.PostAsJsonAsync($"organizations/{organization.Id}/users/{organizationUser.Id}/auto-confirm",
new OrganizationUserConfirmRequestModel
{
Key = testKey,
DefaultUserCollectionName = _mockEncryptedString
});
Assert.Equal(HttpStatusCode.Forbidden, result.StatusCode);
await _factory.GetService<IOrganizationRepository>().DeleteAsync(organization);
}
[Fact]
public async Task AutoConfirm_WhenOwnerConfirmsValidUser_ThenShouldReturnNoContent()
{
var (organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, plan: PlanType.EnterpriseAnnually,
ownerEmail: _ownerEmail, passwordManagerSeats: 5, paymentMethod: PaymentMethodType.Card);
organization.UseAutomaticUserConfirmation = true;
await _factory.GetService<IOrganizationRepository>()
.UpsertAsync(organization);
var testKey = $"test-key-{Guid.NewGuid()}";
await _factory.GetService<IPolicyRepository>().CreateAsync(new Policy
{
OrganizationId = organization.Id,
Type = PolicyType.AutomaticUserConfirmation,
Enabled = true
});
await _factory.GetService<IPolicyRepository>().CreateAsync(new Policy
{
OrganizationId = organization.Id,
Type = PolicyType.OrganizationDataOwnership,
Enabled = true
});
var userToConfirmEmail = $"org-user-to-confirm-{Guid.NewGuid()}@example.com";
await _factory.LoginWithNewAccount(userToConfirmEmail);
await _loginHelper.LoginAsync(_ownerEmail);
var organizationUser = await OrganizationTestHelpers.CreateUserAsync(
_factory,
organization.Id,
userToConfirmEmail,
OrganizationUserType.User,
false,
new Permissions(),
OrganizationUserStatusType.Accepted);
var result = await _client.PostAsJsonAsync($"organizations/{organization.Id}/users/{organizationUser.Id}/auto-confirm",
new OrganizationUserConfirmRequestModel
{
Key = testKey,
DefaultUserCollectionName = _mockEncryptedString
});
Assert.Equal(HttpStatusCode.NoContent, result.StatusCode);
var orgUserRepository = _factory.GetService<IOrganizationUserRepository>();
var confirmedUser = await orgUserRepository.GetByIdAsync(organizationUser.Id);
Assert.NotNull(confirmedUser);
Assert.Equal(OrganizationUserStatusType.Confirmed, confirmedUser.Status);
Assert.Equal(testKey, confirmedUser.Key);
var collectionRepository = _factory.GetService<ICollectionRepository>();
var collections = await collectionRepository.GetManyByUserIdAsync(organizationUser.UserId!.Value);
Assert.NotEmpty(collections);
Assert.Single(collections.Where(c => c.Type == CollectionType.DefaultUserCollection));
await _factory.GetService<IOrganizationRepository>().DeleteAsync(organization);
}
[Fact]
public async Task AutoConfirm_WhenUserIsConfirmedMultipleTimes_ThenShouldSuccessAndOnlyConfirmOneUser()
{
var (organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, plan: PlanType.EnterpriseAnnually,
ownerEmail: _ownerEmail, passwordManagerSeats: 5, paymentMethod: PaymentMethodType.Card);
organization.UseAutomaticUserConfirmation = true;
await _factory.GetService<IOrganizationRepository>()
.UpsertAsync(organization);
var testKey = $"test-key-{Guid.NewGuid()}";
var userToConfirmEmail = $"org-user-to-confirm-{Guid.NewGuid()}@example.com";
await _factory.LoginWithNewAccount(userToConfirmEmail);
await _factory.GetService<IPolicyRepository>().CreateAsync(new Policy
{
OrganizationId = organization.Id,
Type = PolicyType.AutomaticUserConfirmation,
Enabled = true
});
await _factory.GetService<IPolicyRepository>().CreateAsync(new Policy
{
OrganizationId = organization.Id,
Type = PolicyType.OrganizationDataOwnership,
Enabled = true
});
await _loginHelper.LoginAsync(_ownerEmail);
var organizationUser = await OrganizationTestHelpers.CreateUserAsync(
_factory,
organization.Id,
userToConfirmEmail,
OrganizationUserType.User,
false,
new Permissions(),
OrganizationUserStatusType.Accepted);
var tenRequests = Enumerable.Range(0, 10)
.Select(_ => _client.PostAsJsonAsync($"organizations/{organization.Id}/users/{organizationUser.Id}/auto-confirm",
new OrganizationUserConfirmRequestModel
{
Key = testKey,
DefaultUserCollectionName = _mockEncryptedString
})).ToList();
var results = await Task.WhenAll(tenRequests);
Assert.Contains(results, r => r.StatusCode == HttpStatusCode.NoContent);
var orgUserRepository = _factory.GetService<IOrganizationUserRepository>();
var confirmedUser = await orgUserRepository.GetByIdAsync(organizationUser.Id);
Assert.NotNull(confirmedUser);
Assert.Equal(OrganizationUserStatusType.Confirmed, confirmedUser.Status);
Assert.Equal(testKey, confirmedUser.Key);
var collections = await _factory.GetService<ICollectionRepository>()
.GetManyByUserIdAsync(organizationUser.UserId!.Value);
Assert.NotEmpty(collections);
// validates user only received one default collection
Assert.Single(collections.Where(c => c.Type == CollectionType.DefaultUserCollection));
await _factory.GetService<IOrganizationRepository>().DeleteAsync(organization);
}
public Task DisposeAsync()
{
_client.Dispose();
return Task.CompletedTask;
}
}

View File

@@ -218,7 +218,7 @@ public class OrganizationUserControllerTests : IClassFixture<ApiApplicationFacto
_ownerEmail = $"org-user-integration-test-{Guid.NewGuid()}@bitwarden.com"; _ownerEmail = $"org-user-integration-test-{Guid.NewGuid()}@bitwarden.com";
await _factory.LoginWithNewAccount(_ownerEmail); await _factory.LoginWithNewAccount(_ownerEmail);
(_organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, plan: PlanType.EnterpriseAnnually2023, (_organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, plan: PlanType.EnterpriseAnnually,
ownerEmail: _ownerEmail, passwordManagerSeats: 5, paymentMethod: PaymentMethodType.Card); ownerEmail: _ownerEmail, passwordManagerSeats: 5, paymentMethod: PaymentMethodType.Card);
} }

View File

@@ -47,7 +47,7 @@ public class OrganizationUsersControllerPutResetPasswordTests : IClassFixture<Ap
_ownerEmail = $"reset-password-test-{Guid.NewGuid()}@example.com"; _ownerEmail = $"reset-password-test-{Guid.NewGuid()}@example.com";
await _factory.LoginWithNewAccount(_ownerEmail); await _factory.LoginWithNewAccount(_ownerEmail);
(_organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, plan: PlanType.EnterpriseAnnually2023, (_organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, plan: PlanType.EnterpriseAnnually,
ownerEmail: _ownerEmail, passwordManagerSeats: 5, paymentMethod: PaymentMethodType.Card); ownerEmail: _ownerEmail, passwordManagerSeats: 5, paymentMethod: PaymentMethodType.Card);
// Enable reset password and policies for the organization // Enable reset password and policies for the organization

View File

@@ -33,7 +33,7 @@ public class ImportOrganizationUsersAndGroupsCommandTests : IClassFixture<ApiApp
await _factory.LoginWithNewAccount(_ownerEmail); await _factory.LoginWithNewAccount(_ownerEmail);
// Create the organization // Create the organization
(_organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, plan: PlanType.EnterpriseAnnually2023, (_organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, plan: PlanType.EnterpriseAnnually,
ownerEmail: _ownerEmail, passwordManagerSeats: 10, paymentMethod: PaymentMethodType.Card); ownerEmail: _ownerEmail, passwordManagerSeats: 10, paymentMethod: PaymentMethodType.Card);
// Authorize with the organization api key // Authorize with the organization api key

View File

@@ -39,7 +39,7 @@ public class MembersControllerTests : IClassFixture<ApiApplicationFactory>, IAsy
await _factory.LoginWithNewAccount(_ownerEmail); await _factory.LoginWithNewAccount(_ownerEmail);
// Create the organization // Create the organization
(_organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, plan: PlanType.EnterpriseAnnually2023, (_organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, plan: PlanType.EnterpriseAnnually,
ownerEmail: _ownerEmail, passwordManagerSeats: 10, paymentMethod: PaymentMethodType.Card); ownerEmail: _ownerEmail, passwordManagerSeats: 10, paymentMethod: PaymentMethodType.Card);
// Authorize with the organization api key // Authorize with the organization api key

View File

@@ -39,7 +39,7 @@ public class PoliciesControllerTests : IClassFixture<ApiApplicationFactory>, IAs
await _factory.LoginWithNewAccount(_ownerEmail); await _factory.LoginWithNewAccount(_ownerEmail);
// Create the organization // Create the organization
(_organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, plan: PlanType.EnterpriseAnnually2023, (_organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, plan: PlanType.EnterpriseAnnually,
ownerEmail: _ownerEmail, passwordManagerSeats: 10, paymentMethod: PaymentMethodType.Card); ownerEmail: _ownerEmail, passwordManagerSeats: 10, paymentMethod: PaymentMethodType.Card);
// Authorize with the organization api key // Authorize with the organization api key

View File

@@ -9,10 +9,12 @@ using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.AccountRecovery; using Bit.Core.AdminConsole.OrganizationFeatures.AccountRecovery;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUser;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies; using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Utilities.v2.Results;
using Bit.Core.Auth.Entities; using Bit.Core.Auth.Entities;
using Bit.Core.Auth.Repositories; using Bit.Core.Auth.Repositories;
using Bit.Core.Context; using Bit.Core.Context;
@@ -33,9 +35,11 @@ using Bit.Test.Common.AutoFixture.Attributes;
using Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests; using Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding;
using NSubstitute; using NSubstitute;
using OneOf.Types;
using Xunit; using Xunit;
namespace Bit.Api.Test.AdminConsole.Controllers; namespace Bit.Api.Test.AdminConsole.Controllers;
@@ -476,7 +480,7 @@ public class OrganizationUsersControllerTests
var result = await sutProvider.Sut.PutResetPassword(orgId, orgUserId, model); var result = await sutProvider.Sut.PutResetPassword(orgId, orgUserId, model);
Assert.IsType<NotFound>(result); Assert.IsType<Microsoft.AspNetCore.Http.HttpResults.NotFound>(result);
} }
[Theory] [Theory]
@@ -506,7 +510,7 @@ public class OrganizationUsersControllerTests
var result = await sutProvider.Sut.PutResetPassword(orgId, orgUserId, model); var result = await sutProvider.Sut.PutResetPassword(orgId, orgUserId, model);
Assert.IsType<NotFound>(result); Assert.IsType<Microsoft.AspNetCore.Http.HttpResults.NotFound>(result);
} }
[Theory] [Theory]
@@ -521,7 +525,7 @@ public class OrganizationUsersControllerTests
var result = await sutProvider.Sut.PutResetPassword(orgId, orgUserId, model); var result = await sutProvider.Sut.PutResetPassword(orgId, orgUserId, model);
Assert.IsType<NotFound>(result); Assert.IsType<Microsoft.AspNetCore.Http.HttpResults.NotFound>(result);
} }
[Theory] [Theory]
@@ -594,4 +598,190 @@ public class OrganizationUsersControllerTests
Assert.IsType<BadRequest<ModelStateDictionary>>(result); Assert.IsType<BadRequest<ModelStateDictionary>>(result);
} }
[Theory]
[BitAutoData]
public async Task AutomaticallyConfirmOrganizationUserAsync_UserIdNull_ReturnsUnauthorized(
Guid orgId,
Guid orgUserId,
OrganizationUserConfirmRequestModel model,
SutProvider<OrganizationUsersController> sutProvider)
{
// Arrange
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers)
.Returns(true);
sutProvider.GetDependency<IUserService>()
.GetProperUserId(Arg.Any<ClaimsPrincipal>())
.Returns((Guid?)null);
// Act
var result = await sutProvider.Sut.AutomaticallyConfirmOrganizationUserAsync(orgId, orgUserId, model);
// Assert
Assert.IsType<UnauthorizedHttpResult>(result);
}
[Theory]
[BitAutoData]
public async Task AutomaticallyConfirmOrganizationUserAsync_UserIdEmpty_ReturnsUnauthorized(
Guid orgId,
Guid orgUserId,
OrganizationUserConfirmRequestModel model,
SutProvider<OrganizationUsersController> sutProvider)
{
// Arrange
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers)
.Returns(true);
sutProvider.GetDependency<IUserService>()
.GetProperUserId(Arg.Any<ClaimsPrincipal>())
.Returns(Guid.Empty);
// Act
var result = await sutProvider.Sut.AutomaticallyConfirmOrganizationUserAsync(orgId, orgUserId, model);
// Assert
Assert.IsType<UnauthorizedHttpResult>(result);
}
[Theory]
[BitAutoData]
public async Task AutomaticallyConfirmOrganizationUserAsync_Success_ReturnsOk(
Guid orgId,
Guid orgUserId,
Guid userId,
OrganizationUserConfirmRequestModel model,
SutProvider<OrganizationUsersController> sutProvider)
{
// Arrange
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers)
.Returns(true);
sutProvider.GetDependency<IUserService>()
.GetProperUserId(Arg.Any<ClaimsPrincipal>())
.Returns(userId);
sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(orgId)
.Returns(true);
sutProvider.GetDependency<IAutomaticallyConfirmOrganizationUserCommand>()
.AutomaticallyConfirmOrganizationUserAsync(Arg.Any<AutomaticallyConfirmOrganizationUserRequest>())
.Returns(new CommandResult(new None()));
// Act
var result = await sutProvider.Sut.AutomaticallyConfirmOrganizationUserAsync(orgId, orgUserId, model);
// Assert
Assert.IsType<NoContent>(result);
}
[Theory]
[BitAutoData]
public async Task AutomaticallyConfirmOrganizationUserAsync_NotFoundError_ReturnsNotFound(
Guid orgId,
Guid orgUserId,
Guid userId,
OrganizationUserConfirmRequestModel model,
SutProvider<OrganizationUsersController> sutProvider)
{
// Arrange
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers)
.Returns(true);
sutProvider.GetDependency<IUserService>()
.GetProperUserId(Arg.Any<ClaimsPrincipal>())
.Returns(userId);
sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(orgId)
.Returns(false);
var notFoundError = new OrganizationNotFound();
sutProvider.GetDependency<IAutomaticallyConfirmOrganizationUserCommand>()
.AutomaticallyConfirmOrganizationUserAsync(Arg.Any<AutomaticallyConfirmOrganizationUserRequest>())
.Returns(new CommandResult(notFoundError));
// Act
var result = await sutProvider.Sut.AutomaticallyConfirmOrganizationUserAsync(orgId, orgUserId, model);
// Assert
var notFoundResult = Assert.IsType<NotFound<ErrorResponseModel>>(result);
Assert.Equal(notFoundError.Message, notFoundResult.Value.Message);
}
[Theory]
[BitAutoData]
public async Task AutomaticallyConfirmOrganizationUserAsync_BadRequestError_ReturnsBadRequest(
Guid orgId,
Guid orgUserId,
Guid userId,
OrganizationUserConfirmRequestModel model,
SutProvider<OrganizationUsersController> sutProvider)
{
// Arrange
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers)
.Returns(true);
sutProvider.GetDependency<IUserService>()
.GetProperUserId(Arg.Any<ClaimsPrincipal>())
.Returns(userId);
sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(orgId)
.Returns(true);
var badRequestError = new UserIsNotAccepted();
sutProvider.GetDependency<IAutomaticallyConfirmOrganizationUserCommand>()
.AutomaticallyConfirmOrganizationUserAsync(Arg.Any<AutomaticallyConfirmOrganizationUserRequest>())
.Returns(new CommandResult(badRequestError));
// Act
var result = await sutProvider.Sut.AutomaticallyConfirmOrganizationUserAsync(orgId, orgUserId, model);
// Assert
var badRequestResult = Assert.IsType<BadRequest<ErrorResponseModel>>(result);
Assert.Equal(badRequestError.Message, badRequestResult.Value.Message);
}
[Theory]
[BitAutoData]
public async Task AutomaticallyConfirmOrganizationUserAsync_InternalError_ReturnsProblem(
Guid orgId,
Guid orgUserId,
Guid userId,
OrganizationUserConfirmRequestModel model,
SutProvider<OrganizationUsersController> sutProvider)
{
// Arrange
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers)
.Returns(true);
sutProvider.GetDependency<IUserService>()
.GetProperUserId(Arg.Any<ClaimsPrincipal>())
.Returns(userId);
sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(orgId)
.Returns(true);
var internalError = new FailedToWriteToEventLog();
sutProvider.GetDependency<IAutomaticallyConfirmOrganizationUserCommand>()
.AutomaticallyConfirmOrganizationUserAsync(Arg.Any<AutomaticallyConfirmOrganizationUserRequest>())
.Returns(new CommandResult(internalError));
// Act
var result = await sutProvider.Sut.AutomaticallyConfirmOrganizationUserAsync(orgId, orgUserId, model);
// Assert
var problemResult = Assert.IsType<JsonHttpResult<ErrorResponseModel>>(result);
Assert.Equal(StatusCodes.Status500InternalServerError, problemResult.StatusCode);
}
} }

View File

@@ -30,8 +30,8 @@ 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.Test.Billing.Mocks;
using Bit.Core.Tokens; using Bit.Core.Tokens;
using Bit.Core.Utilities;
using Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider; using Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider;
using NSubstitute; using NSubstitute;
using Xunit; using Xunit;
@@ -305,7 +305,7 @@ public class OrganizationsControllerTests : IDisposable
// Arrange // Arrange
_currentContext.OrganizationOwner(organization.Id).Returns(true); _currentContext.OrganizationOwner(organization.Id).Returns(true);
var plan = StaticStore.GetPlan(PlanType.EnterpriseAnnually); var plan = MockPlans.Get(PlanType.EnterpriseAnnually);
_pricingClient.GetPlan(Arg.Any<PlanType>()).Returns(plan); _pricingClient.GetPlan(Arg.Any<PlanType>()).Returns(plan);
_organizationService _organizationService

View File

@@ -10,7 +10,7 @@ using Bit.Core.Models.Data;
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 Bit.Core.Test.Billing.Mocks;
using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute; using NSubstitute;
@@ -24,11 +24,11 @@ namespace Bit.Api.Test.Billing.Controllers;
public class OrganizationSponsorshipsControllerTests public class OrganizationSponsorshipsControllerTests
{ {
public static IEnumerable<object[]> EnterprisePlanTypes => public static IEnumerable<object[]> EnterprisePlanTypes =>
Enum.GetValues<PlanType>().Where(p => StaticStore.GetPlan(p).ProductTier == ProductTierType.Enterprise).Select(p => new object[] { p }); Enum.GetValues<PlanType>().Where(p => MockPlans.Get(p).ProductTier == ProductTierType.Enterprise).Select(p => new object[] { p });
public static IEnumerable<object[]> NonEnterprisePlanTypes => public static IEnumerable<object[]> NonEnterprisePlanTypes =>
Enum.GetValues<PlanType>().Where(p => StaticStore.GetPlan(p).ProductTier != ProductTierType.Enterprise).Select(p => new object[] { p }); Enum.GetValues<PlanType>().Where(p => MockPlans.Get(p).ProductTier != ProductTierType.Enterprise).Select(p => new object[] { p });
public static IEnumerable<object[]> NonFamiliesPlanTypes => public static IEnumerable<object[]> NonFamiliesPlanTypes =>
Enum.GetValues<PlanType>().Where(p => StaticStore.GetPlan(p).ProductTier != ProductTierType.Families).Select(p => new object[] { p }); Enum.GetValues<PlanType>().Where(p => MockPlans.Get(p).ProductTier != ProductTierType.Families).Select(p => new object[] { p });
public static IEnumerable<object[]> NonConfirmedOrganizationUsersStatuses => public static IEnumerable<object[]> NonConfirmedOrganizationUsersStatuses =>
Enum.GetValues<OrganizationUserStatusType>() Enum.GetValues<OrganizationUserStatusType>()

View File

@@ -17,7 +17,7 @@ using Bit.Core.Context;
using Bit.Core.Models.Api; using Bit.Core.Models.Api;
using Bit.Core.Models.BitStripe; using Bit.Core.Models.BitStripe;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Utilities; using Bit.Core.Test.Billing.Mocks;
using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.AutoFixture.Attributes;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
@@ -351,7 +351,7 @@ public class ProviderBillingControllerTests
foreach (var providerPlan in providerPlans) foreach (var providerPlan in providerPlans)
{ {
var plan = StaticStore.GetPlan(providerPlan.PlanType); var plan = MockPlans.Get(providerPlan.PlanType);
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(providerPlan.PlanType).Returns(plan); sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(providerPlan.PlanType).Returns(plan);
var priceId = ProviderPriceAdapter.GetPriceId(provider, subscription, providerPlan.PlanType); var priceId = ProviderPriceAdapter.GetPriceId(provider, subscription, providerPlan.PlanType);
sutProvider.GetDependency<IStripeAdapter>().PriceGetAsync(priceId) sutProvider.GetDependency<IStripeAdapter>().PriceGetAsync(priceId)
@@ -372,7 +372,7 @@ public class ProviderBillingControllerTests
Assert.Equal(subscription.Customer!.Discount!.Coupon!.PercentOff, response.DiscountPercentage); Assert.Equal(subscription.Customer!.Discount!.Coupon!.PercentOff, response.DiscountPercentage);
Assert.Equal(subscription.CollectionMethod, response.CollectionMethod); Assert.Equal(subscription.CollectionMethod, response.CollectionMethod);
var teamsPlan = StaticStore.GetPlan(PlanType.TeamsMonthly); var teamsPlan = MockPlans.Get(PlanType.TeamsMonthly);
var providerTeamsPlan = response.Plans.FirstOrDefault(plan => plan.PlanName == teamsPlan.Name); var providerTeamsPlan = response.Plans.FirstOrDefault(plan => plan.PlanName == teamsPlan.Name);
Assert.NotNull(providerTeamsPlan); Assert.NotNull(providerTeamsPlan);
Assert.Equal(50, providerTeamsPlan.SeatMinimum); Assert.Equal(50, providerTeamsPlan.SeatMinimum);
@@ -381,7 +381,7 @@ public class ProviderBillingControllerTests
Assert.Equal(60 * teamsPlan.PasswordManager.ProviderPortalSeatPrice, providerTeamsPlan.Cost); Assert.Equal(60 * teamsPlan.PasswordManager.ProviderPortalSeatPrice, providerTeamsPlan.Cost);
Assert.Equal("Monthly", providerTeamsPlan.Cadence); Assert.Equal("Monthly", providerTeamsPlan.Cadence);
var enterprisePlan = StaticStore.GetPlan(PlanType.EnterpriseMonthly); var enterprisePlan = MockPlans.Get(PlanType.EnterpriseMonthly);
var providerEnterprisePlan = response.Plans.FirstOrDefault(plan => plan.PlanName == enterprisePlan.Name); var providerEnterprisePlan = response.Plans.FirstOrDefault(plan => plan.PlanName == enterprisePlan.Name);
Assert.NotNull(providerEnterprisePlan); Assert.NotNull(providerEnterprisePlan);
Assert.Equal(100, providerEnterprisePlan.SeatMinimum); Assert.Equal(100, providerEnterprisePlan.SeatMinimum);
@@ -498,7 +498,7 @@ public class ProviderBillingControllerTests
foreach (var providerPlan in providerPlans) foreach (var providerPlan in providerPlans)
{ {
var plan = StaticStore.GetPlan(providerPlan.PlanType); var plan = MockPlans.Get(providerPlan.PlanType);
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(providerPlan.PlanType).Returns(plan); sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(providerPlan.PlanType).Returns(plan);
var priceId = ProviderPriceAdapter.GetPriceId(provider, subscription, providerPlan.PlanType); var priceId = ProviderPriceAdapter.GetPriceId(provider, subscription, providerPlan.PlanType);
sutProvider.GetDependency<IStripeAdapter>().PriceGetAsync(priceId) sutProvider.GetDependency<IStripeAdapter>().PriceGetAsync(priceId)

View File

@@ -16,7 +16,7 @@ using Bit.Core.SecretsManager.Models.Data;
using Bit.Core.SecretsManager.Queries.ServiceAccounts.Interfaces; using Bit.Core.SecretsManager.Queries.ServiceAccounts.Interfaces;
using Bit.Core.SecretsManager.Repositories; using Bit.Core.SecretsManager.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Utilities; using Bit.Core.Test.Billing.Mocks;
using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.AutoFixture.Attributes;
using Bit.Test.Common.Helpers; using Bit.Test.Common.Helpers;
@@ -121,7 +121,7 @@ public class ServiceAccountsControllerTests
{ {
ArrangeCreateServiceAccountAutoScalingTest(newSlotsRequired, sutProvider, data, organization); ArrangeCreateServiceAccountAutoScalingTest(newSlotsRequired, sutProvider, data, organization);
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType).Returns(StaticStore.GetPlan(organization.PlanType)); sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType).Returns(MockPlans.Get(organization.PlanType));
await sutProvider.Sut.CreateAsync(organization.Id, data); await sutProvider.Sut.CreateAsync(organization.Id, data);

View File

@@ -18,9 +18,9 @@ using Bit.Core.Models.Data;
using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Test.Billing.Mocks;
using Bit.Core.Tools.Entities; using Bit.Core.Tools.Entities;
using Bit.Core.Tools.Repositories; using Bit.Core.Tools.Repositories;
using Bit.Core.Utilities;
using Bit.Core.Vault.Entities; using Bit.Core.Vault.Entities;
using Bit.Core.Vault.Models.Data; using Bit.Core.Vault.Models.Data;
using Bit.Core.Vault.Repositories; using Bit.Core.Vault.Repositories;
@@ -335,7 +335,7 @@ public class SyncControllerTests
if (matchedProviderUserOrgDetails != null) if (matchedProviderUserOrgDetails != null)
{ {
var providerOrgProductType = StaticStore.GetPlan(matchedProviderUserOrgDetails.PlanType).ProductTier; var providerOrgProductType = MockPlans.Get(matchedProviderUserOrgDetails.PlanType).ProductTier;
Assert.Equal(providerOrgProductType, profProviderOrg.ProductTierType); Assert.Equal(providerOrgProductType, profProviderOrg.ProductTierType);
} }
} }

View File

@@ -24,6 +24,7 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\src\Billing\Billing.csproj" /> <ProjectReference Include="..\..\src\Billing\Billing.csproj" />
<ProjectReference Include="..\Common\Common.csproj" /> <ProjectReference Include="..\Common\Common.csproj" />
<ProjectReference Include="..\Core.Test\Core.Test.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -9,7 +9,7 @@ using Bit.Core.Billing.Providers.Entities;
using Bit.Core.Billing.Providers.Repositories; using Bit.Core.Billing.Providers.Repositories;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Utilities; using Bit.Core.Test.Billing.Mocks;
using NSubstitute; using NSubstitute;
using Stripe; using Stripe;
using Xunit; using Xunit;
@@ -237,7 +237,7 @@ public class ProviderEventServiceTests
foreach (var providerPlan in providerPlans) foreach (var providerPlan in providerPlans)
{ {
_pricingClient.GetPlanOrThrow(providerPlan.PlanType).Returns(StaticStore.GetPlan(providerPlan.PlanType)); _pricingClient.GetPlanOrThrow(providerPlan.PlanType).Returns(MockPlans.Get(providerPlan.PlanType));
} }
_providerPlanRepository.GetByProviderId(providerId).Returns(providerPlans); _providerPlanRepository.GetByProviderId(providerId).Returns(providerPlans);
@@ -246,8 +246,8 @@ public class ProviderEventServiceTests
await _providerEventService.TryRecordInvoiceLineItems(stripeEvent); await _providerEventService.TryRecordInvoiceLineItems(stripeEvent);
// Assert // Assert
var teamsPlan = StaticStore.GetPlan(PlanType.TeamsMonthly); var teamsPlan = MockPlans.Get(PlanType.TeamsMonthly);
var enterprisePlan = StaticStore.GetPlan(PlanType.EnterpriseMonthly); var enterprisePlan = MockPlans.Get(PlanType.EnterpriseMonthly);
await _providerInvoiceItemRepository.Received(1).CreateAsync(Arg.Is<ProviderInvoiceItem>( await _providerInvoiceItemRepository.Received(1).CreateAsync(Arg.Is<ProviderInvoiceItem>(
options => options =>

View File

@@ -8,11 +8,11 @@ using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Services; using Bit.Core.AdminConsole.Services;
using Bit.Core.Billing.Enums; using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Models.StaticStore.Plans;
using Bit.Core.Billing.Pricing; 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.Test.Billing.Mocks.Plans;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using NSubstitute; using NSubstitute;

View File

@@ -5,7 +5,6 @@ using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Enums; using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Models.StaticStore.Plans;
using Bit.Core.Billing.Payment.Models; using Bit.Core.Billing.Payment.Models;
using Bit.Core.Billing.Payment.Queries; using Bit.Core.Billing.Payment.Queries;
using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Pricing;
@@ -16,6 +15,7 @@ using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterpri
using Bit.Core.Platform.Mail.Mailer; using Bit.Core.Platform.Mail.Mailer;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Test.Billing.Mocks.Plans;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using NSubstitute; using NSubstitute;
using NSubstitute.ExceptionExtensions; using NSubstitute.ExceptionExtensions;
@@ -1141,7 +1141,7 @@ public class UpcomingInvoiceHandlerTests
} }
[Fact] [Fact]
public async Task HandleAsync_WhenMilestone3Disabled_DoesNotUpdateSubscription() public async Task HandleAsync_WhenMilestone3Disabled_AndFamilies2019Plan_DoesNotUpdateSubscription()
{ {
// Arrange // Arrange
var parsedEvent = new Event { Id = "evt_123", Type = "invoice.upcoming" }; var parsedEvent = new Event { Id = "evt_123", Type = "invoice.upcoming" };
@@ -1469,4 +1469,490 @@ public class UpcomingInvoiceHandlerTests
email.ToEmails.Contains("org@example.com") && email.ToEmails.Contains("org@example.com") &&
email.Subject == "Your Subscription Will Renew Soon")); email.Subject == "Your Subscription Will Renew Soon"));
} }
[Fact]
public async Task HandleAsync_WhenMilestone3Enabled_AndSeatAddOnExists_DeletesItem()
{
// Arrange
var parsedEvent = new Event { Id = "evt_123", Type = "invoice.upcoming" };
var customerId = "cus_123";
var subscriptionId = "sub_123";
var passwordManagerItemId = "si_pm_123";
var seatAddOnItemId = "si_seat_123";
var invoice = new Invoice
{
CustomerId = customerId,
AmountDue = 40000,
NextPaymentAttempt = DateTime.UtcNow.AddDays(7),
Lines = new StripeList<InvoiceLineItem>
{
Data = new List<InvoiceLineItem> { new() { Description = "Test Item" } }
}
};
var families2019Plan = new Families2019Plan();
var familiesPlan = new FamiliesPlan();
var subscription = new Subscription
{
Id = subscriptionId,
CustomerId = customerId,
Items = new StripeList<SubscriptionItem>
{
Data = new List<SubscriptionItem>
{
new()
{
Id = passwordManagerItemId,
Price = new Price { Id = families2019Plan.PasswordManager.StripePlanId }
},
new()
{
Id = seatAddOnItemId,
Price = new Price { Id = "personal-org-seat-annually" },
Quantity = 3
}
}
},
AutomaticTax = new SubscriptionAutomaticTax { Enabled = true },
Metadata = new Dictionary<string, string>()
};
var customer = new Customer
{
Id = customerId,
Subscriptions = new StripeList<Subscription> { Data = new List<Subscription> { subscription } },
Address = new Address { Country = "US" }
};
var organization = new Organization
{
Id = _organizationId,
BillingEmail = "org@example.com",
PlanType = PlanType.FamiliesAnnually2019
};
_stripeEventService.GetInvoice(parsedEvent).Returns(invoice);
_stripeFacade.GetCustomer(customerId, Arg.Any<CustomerGetOptions>()).Returns(customer);
_stripeEventUtilityService
.GetIdsFromMetadata(subscription.Metadata)
.Returns(new Tuple<Guid?, Guid?, Guid?>(_organizationId, null, null));
_organizationRepository.GetByIdAsync(_organizationId).Returns(organization);
_pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually2019).Returns(families2019Plan);
_pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually).Returns(familiesPlan);
_featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3).Returns(true);
_stripeEventUtilityService.IsSponsoredSubscription(subscription).Returns(false);
// Act
await _sut.HandleAsync(parsedEvent);
// Assert
await _stripeFacade.Received(1).UpdateSubscription(
Arg.Is(subscriptionId),
Arg.Is<SubscriptionUpdateOptions>(o =>
o.Items.Count == 2 &&
o.Items[0].Id == passwordManagerItemId &&
o.Items[0].Price == familiesPlan.PasswordManager.StripePlanId &&
o.Items[1].Id == seatAddOnItemId &&
o.Items[1].Deleted == true &&
o.Discounts.Count == 1 &&
o.Discounts[0].Coupon == CouponIDs.Milestone3SubscriptionDiscount &&
o.ProrationBehavior == ProrationBehavior.None));
await _organizationRepository.Received(1).ReplaceAsync(
Arg.Is<Organization>(org =>
org.Id == _organizationId &&
org.PlanType == PlanType.FamiliesAnnually &&
org.Plan == familiesPlan.Name &&
org.UsersGetPremium == familiesPlan.UsersGetPremium &&
org.Seats == familiesPlan.PasswordManager.BaseSeats));
await _mailer.Received(1).SendEmail(
Arg.Is<UpdatedInvoiceUpcomingMail>(email =>
email.ToEmails.Contains("org@example.com") &&
email.Subject == "Your Subscription Will Renew Soon"));
}
[Fact]
public async Task HandleAsync_WhenMilestone3Enabled_AndSeatAddOnWithQuantityOne_DeletesItem()
{
// Arrange
var parsedEvent = new Event { Id = "evt_123", Type = "invoice.upcoming" };
var customerId = "cus_123";
var subscriptionId = "sub_123";
var passwordManagerItemId = "si_pm_123";
var seatAddOnItemId = "si_seat_123";
var invoice = new Invoice
{
CustomerId = customerId,
AmountDue = 40000,
NextPaymentAttempt = DateTime.UtcNow.AddDays(7),
Lines = new StripeList<InvoiceLineItem>
{
Data = new List<InvoiceLineItem> { new() { Description = "Test Item" } }
}
};
var families2019Plan = new Families2019Plan();
var familiesPlan = new FamiliesPlan();
var subscription = new Subscription
{
Id = subscriptionId,
CustomerId = customerId,
Items = new StripeList<SubscriptionItem>
{
Data = new List<SubscriptionItem>
{
new()
{
Id = passwordManagerItemId,
Price = new Price { Id = families2019Plan.PasswordManager.StripePlanId }
},
new()
{
Id = seatAddOnItemId,
Price = new Price { Id = "personal-org-seat-annually" },
Quantity = 1
}
}
},
AutomaticTax = new SubscriptionAutomaticTax { Enabled = true },
Metadata = new Dictionary<string, string>()
};
var customer = new Customer
{
Id = customerId,
Subscriptions = new StripeList<Subscription> { Data = new List<Subscription> { subscription } },
Address = new Address { Country = "US" }
};
var organization = new Organization
{
Id = _organizationId,
BillingEmail = "org@example.com",
PlanType = PlanType.FamiliesAnnually2019
};
_stripeEventService.GetInvoice(parsedEvent).Returns(invoice);
_stripeFacade.GetCustomer(customerId, Arg.Any<CustomerGetOptions>()).Returns(customer);
_stripeEventUtilityService
.GetIdsFromMetadata(subscription.Metadata)
.Returns(new Tuple<Guid?, Guid?, Guid?>(_organizationId, null, null));
_organizationRepository.GetByIdAsync(_organizationId).Returns(organization);
_pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually2019).Returns(families2019Plan);
_pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually).Returns(familiesPlan);
_featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3).Returns(true);
_stripeEventUtilityService.IsSponsoredSubscription(subscription).Returns(false);
// Act
await _sut.HandleAsync(parsedEvent);
// Assert
await _stripeFacade.Received(1).UpdateSubscription(
Arg.Is(subscriptionId),
Arg.Is<SubscriptionUpdateOptions>(o =>
o.Items.Count == 2 &&
o.Items[0].Id == passwordManagerItemId &&
o.Items[0].Price == familiesPlan.PasswordManager.StripePlanId &&
o.Items[1].Id == seatAddOnItemId &&
o.Items[1].Deleted == true &&
o.Discounts.Count == 1 &&
o.Discounts[0].Coupon == CouponIDs.Milestone3SubscriptionDiscount &&
o.ProrationBehavior == ProrationBehavior.None));
await _organizationRepository.Received(1).ReplaceAsync(
Arg.Is<Organization>(org =>
org.Id == _organizationId &&
org.PlanType == PlanType.FamiliesAnnually &&
org.Plan == familiesPlan.Name &&
org.UsersGetPremium == familiesPlan.UsersGetPremium &&
org.Seats == familiesPlan.PasswordManager.BaseSeats));
await _mailer.Received(1).SendEmail(
Arg.Is<UpdatedInvoiceUpcomingMail>(email =>
email.ToEmails.Contains("org@example.com") &&
email.Subject == "Your Subscription Will Renew Soon"));
}
[Fact]
public async Task HandleAsync_WhenMilestone3Enabled_WithPremiumAccessAndSeatAddOn_UpdatesBothItems()
{
// Arrange
var parsedEvent = new Event { Id = "evt_123", Type = "invoice.upcoming" };
var customerId = "cus_123";
var subscriptionId = "sub_123";
var passwordManagerItemId = "si_pm_123";
var premiumAccessItemId = "si_premium_123";
var seatAddOnItemId = "si_seat_123";
var invoice = new Invoice
{
CustomerId = customerId,
AmountDue = 40000,
NextPaymentAttempt = DateTime.UtcNow.AddDays(7),
Lines = new StripeList<InvoiceLineItem>
{
Data = new List<InvoiceLineItem> { new() { Description = "Test Item" } }
}
};
var families2019Plan = new Families2019Plan();
var familiesPlan = new FamiliesPlan();
var subscription = new Subscription
{
Id = subscriptionId,
CustomerId = customerId,
Items = new StripeList<SubscriptionItem>
{
Data = new List<SubscriptionItem>
{
new()
{
Id = passwordManagerItemId,
Price = new Price { Id = families2019Plan.PasswordManager.StripePlanId }
},
new()
{
Id = premiumAccessItemId,
Price = new Price { Id = families2019Plan.PasswordManager.StripePremiumAccessPlanId }
},
new()
{
Id = seatAddOnItemId,
Price = new Price { Id = "personal-org-seat-annually" },
Quantity = 2
}
}
},
AutomaticTax = new SubscriptionAutomaticTax { Enabled = true },
Metadata = new Dictionary<string, string>()
};
var customer = new Customer
{
Id = customerId,
Subscriptions = new StripeList<Subscription> { Data = new List<Subscription> { subscription } },
Address = new Address { Country = "US" }
};
var organization = new Organization
{
Id = _organizationId,
BillingEmail = "org@example.com",
PlanType = PlanType.FamiliesAnnually2019
};
_stripeEventService.GetInvoice(parsedEvent).Returns(invoice);
_stripeFacade.GetCustomer(customerId, Arg.Any<CustomerGetOptions>()).Returns(customer);
_stripeEventUtilityService
.GetIdsFromMetadata(subscription.Metadata)
.Returns(new Tuple<Guid?, Guid?, Guid?>(_organizationId, null, null));
_organizationRepository.GetByIdAsync(_organizationId).Returns(organization);
_pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually2019).Returns(families2019Plan);
_pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually).Returns(familiesPlan);
_featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3).Returns(true);
_stripeEventUtilityService.IsSponsoredSubscription(subscription).Returns(false);
// Act
await _sut.HandleAsync(parsedEvent);
// Assert
await _stripeFacade.Received(1).UpdateSubscription(
Arg.Is(subscriptionId),
Arg.Is<SubscriptionUpdateOptions>(o =>
o.Items.Count == 3 &&
o.Items[0].Id == passwordManagerItemId &&
o.Items[0].Price == familiesPlan.PasswordManager.StripePlanId &&
o.Items[1].Id == premiumAccessItemId &&
o.Items[1].Deleted == true &&
o.Items[2].Id == seatAddOnItemId &&
o.Items[2].Deleted == true &&
o.Discounts.Count == 1 &&
o.Discounts[0].Coupon == CouponIDs.Milestone3SubscriptionDiscount &&
o.ProrationBehavior == ProrationBehavior.None));
await _organizationRepository.Received(1).ReplaceAsync(
Arg.Is<Organization>(org =>
org.Id == _organizationId &&
org.PlanType == PlanType.FamiliesAnnually &&
org.Plan == familiesPlan.Name &&
org.UsersGetPremium == familiesPlan.UsersGetPremium &&
org.Seats == familiesPlan.PasswordManager.BaseSeats));
await _mailer.Received(1).SendEmail(
Arg.Is<UpdatedInvoiceUpcomingMail>(email =>
email.ToEmails.Contains("org@example.com") &&
email.Subject == "Your Subscription Will Renew Soon"));
}
[Fact]
public async Task HandleAsync_WhenMilestone3Enabled_AndFamilies2025Plan_UpdatesSubscriptionOnlyNoAddons()
{
// Arrange
var parsedEvent = new Event { Id = "evt_123", Type = "invoice.upcoming" };
var customerId = "cus_123";
var subscriptionId = "sub_123";
var passwordManagerItemId = "si_pm_123";
var invoice = new Invoice
{
CustomerId = customerId,
AmountDue = 40000,
NextPaymentAttempt = DateTime.UtcNow.AddDays(7),
Lines = new StripeList<InvoiceLineItem>
{
Data = new List<InvoiceLineItem> { new() { Description = "Test Item" } }
}
};
var families2025Plan = new Families2025Plan();
var familiesPlan = new FamiliesPlan();
var subscription = new Subscription
{
Id = subscriptionId,
CustomerId = customerId,
Items = new StripeList<SubscriptionItem>
{
Data = new List<SubscriptionItem>
{
new()
{
Id = passwordManagerItemId,
Price = new Price { Id = families2025Plan.PasswordManager.StripePlanId }
}
}
},
AutomaticTax = new SubscriptionAutomaticTax { Enabled = true },
Metadata = new Dictionary<string, string>()
};
var customer = new Customer
{
Id = customerId,
Subscriptions = new StripeList<Subscription> { Data = new List<Subscription> { subscription } },
Address = new Address { Country = "US" }
};
var organization = new Organization
{
Id = _organizationId,
BillingEmail = "org@example.com",
PlanType = PlanType.FamiliesAnnually2025
};
_stripeEventService.GetInvoice(parsedEvent).Returns(invoice);
_stripeFacade.GetCustomer(customerId, Arg.Any<CustomerGetOptions>()).Returns(customer);
_stripeEventUtilityService
.GetIdsFromMetadata(subscription.Metadata)
.Returns(new Tuple<Guid?, Guid?, Guid?>(_organizationId, null, null));
_organizationRepository.GetByIdAsync(_organizationId).Returns(organization);
_pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually2025).Returns(families2025Plan);
_pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually).Returns(familiesPlan);
_featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3).Returns(true);
_stripeEventUtilityService.IsSponsoredSubscription(subscription).Returns(false);
// Act
await _sut.HandleAsync(parsedEvent);
// Assert
await _stripeFacade.Received(1).UpdateSubscription(
Arg.Is(subscriptionId),
Arg.Is<SubscriptionUpdateOptions>(o =>
o.Items.Count == 1 &&
o.Items[0].Id == passwordManagerItemId &&
o.Items[0].Price == familiesPlan.PasswordManager.StripePlanId &&
o.Discounts == null &&
o.ProrationBehavior == ProrationBehavior.None));
await _organizationRepository.Received(1).ReplaceAsync(
Arg.Is<Organization>(org =>
org.Id == _organizationId &&
org.PlanType == PlanType.FamiliesAnnually &&
org.Plan == familiesPlan.Name &&
org.UsersGetPremium == familiesPlan.UsersGetPremium &&
org.Seats == familiesPlan.PasswordManager.BaseSeats));
}
[Fact]
public async Task HandleAsync_WhenMilestone3Disabled_AndFamilies2025Plan_DoesNotUpdateSubscription()
{
// Arrange
var parsedEvent = new Event { Id = "evt_123", Type = "invoice.upcoming" };
var customerId = "cus_123";
var subscriptionId = "sub_123";
var passwordManagerItemId = "si_pm_123";
var invoice = new Invoice
{
CustomerId = customerId,
AmountDue = 40000,
NextPaymentAttempt = DateTime.UtcNow.AddDays(7),
Lines = new StripeList<InvoiceLineItem>
{
Data = new List<InvoiceLineItem> { new() { Description = "Test Item" } }
}
};
var families2025Plan = new Families2025Plan();
var subscription = new Subscription
{
Id = subscriptionId,
CustomerId = customerId,
Items = new StripeList<SubscriptionItem>
{
Data = new List<SubscriptionItem>
{
new()
{
Id = passwordManagerItemId,
Price = new Price { Id = families2025Plan.PasswordManager.StripePlanId }
}
}
},
AutomaticTax = new SubscriptionAutomaticTax { Enabled = true },
Metadata = new Dictionary<string, string>()
};
var customer = new Customer
{
Id = customerId,
Subscriptions = new StripeList<Subscription> { Data = new List<Subscription> { subscription } },
Address = new Address { Country = "US" }
};
var organization = new Organization
{
Id = _organizationId,
BillingEmail = "org@example.com",
PlanType = PlanType.FamiliesAnnually2025
};
_stripeEventService.GetInvoice(parsedEvent).Returns(invoice);
_stripeFacade.GetCustomer(customerId, Arg.Any<CustomerGetOptions>()).Returns(customer);
_stripeEventUtilityService
.GetIdsFromMetadata(subscription.Metadata)
.Returns(new Tuple<Guid?, Guid?, Guid?>(_organizationId, null, null));
_organizationRepository.GetByIdAsync(_organizationId).Returns(organization);
_pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually2025).Returns(families2025Plan);
_featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3).Returns(false);
_stripeEventUtilityService.IsSponsoredSubscription(subscription).Returns(false);
// Act
await _sut.HandleAsync(parsedEvent);
// Assert - should not update subscription or organization when feature flag is disabled
await _stripeFacade.DidNotReceive().UpdateSubscription(
Arg.Any<string>(),
Arg.Any<SubscriptionUpdateOptions>());
await _organizationRepository.DidNotReceive().ReplaceAsync(
Arg.Is<Organization>(org => org.PlanType == PlanType.FamiliesAnnually));
}
} }

View File

@@ -1,6 +1,8 @@
using System.Text.Json; using System.Reflection;
using System.Text.Json;
using AutoFixture; using AutoFixture;
using AutoFixture.Kernel; using AutoFixture.Kernel;
using AutoFixture.Xunit2;
using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities;
using Bit.Core.Auth.Enums; using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Models; using Bit.Core.Auth.Models;
@@ -9,7 +11,7 @@ using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Models.Business; using Bit.Core.Models.Business;
using Bit.Core.Models.Data; using Bit.Core.Models.Data;
using Bit.Core.Utilities; using Bit.Core.Test.Billing.Mocks;
using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.AutoFixture.Attributes;
using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.DataProtection;
@@ -20,12 +22,24 @@ public class OrganizationCustomization : ICustomization
{ {
public bool UseGroups { get; set; } public bool UseGroups { get; set; }
public PlanType PlanType { get; set; } public PlanType PlanType { get; set; }
public bool UseAutomaticUserConfirmation { get; set; }
public OrganizationCustomization()
{
}
public OrganizationCustomization(bool useAutomaticUserConfirmation, PlanType planType)
{
UseAutomaticUserConfirmation = useAutomaticUserConfirmation;
PlanType = planType;
}
public void Customize(IFixture fixture) public void Customize(IFixture fixture)
{ {
var organizationId = Guid.NewGuid(); var organizationId = Guid.NewGuid();
var maxCollections = (short)new Random().Next(10, short.MaxValue); var maxCollections = (short)new Random().Next(10, short.MaxValue);
var plan = StaticStore.Plans.FirstOrDefault(p => p.Type == PlanType); var plan = MockPlans.Plans.FirstOrDefault(p => p.Type == PlanType);
var seats = (short)new Random().Next(plan.PasswordManager.BaseSeats, plan.PasswordManager.MaxSeats ?? short.MaxValue); var seats = (short)new Random().Next(plan.PasswordManager.BaseSeats, plan.PasswordManager.MaxSeats ?? short.MaxValue);
var smSeats = plan.SupportsSecretsManager var smSeats = plan.SupportsSecretsManager
? (short?)new Random().Next(plan.SecretsManager.BaseSeats, plan.SecretsManager.MaxSeats ?? short.MaxValue) ? (short?)new Random().Next(plan.SecretsManager.BaseSeats, plan.SecretsManager.MaxSeats ?? short.MaxValue)
@@ -37,7 +51,8 @@ public class OrganizationCustomization : ICustomization
.With(o => o.UseGroups, UseGroups) .With(o => o.UseGroups, UseGroups)
.With(o => o.PlanType, PlanType) .With(o => o.PlanType, PlanType)
.With(o => o.Seats, seats) .With(o => o.Seats, seats)
.With(o => o.SmSeats, smSeats)); .With(o => o.SmSeats, smSeats)
.With(o => o.UseAutomaticUserConfirmation, UseAutomaticUserConfirmation));
fixture.Customize<Collection>(composer => fixture.Customize<Collection>(composer =>
composer composer
@@ -77,7 +92,7 @@ internal class PaidOrganization : ICustomization
public PlanType CheckedPlanType { get; set; } public PlanType CheckedPlanType { get; set; }
public void Customize(IFixture fixture) public void Customize(IFixture fixture)
{ {
var validUpgradePlans = StaticStore.Plans.Where(p => p.Type != PlanType.Free && p.LegacyYear == null).OrderBy(p => p.UpgradeSortOrder).Select(p => p.Type).ToList(); var validUpgradePlans = MockPlans.Plans.Where(p => p.Type != PlanType.Free && p.LegacyYear == null).OrderBy(p => p.UpgradeSortOrder).Select(p => p.Type).ToList();
var lowestActivePaidPlan = validUpgradePlans.First(); var lowestActivePaidPlan = validUpgradePlans.First();
CheckedPlanType = CheckedPlanType.Equals(PlanType.Free) ? lowestActivePaidPlan : CheckedPlanType; CheckedPlanType = CheckedPlanType.Equals(PlanType.Free) ? lowestActivePaidPlan : CheckedPlanType;
validUpgradePlans.Remove(lowestActivePaidPlan); validUpgradePlans.Remove(lowestActivePaidPlan);
@@ -105,7 +120,7 @@ internal class FreeOrganizationUpgrade : ICustomization
.With(o => o.PlanType, PlanType.Free)); .With(o => o.PlanType, PlanType.Free));
var plansToIgnore = new List<PlanType> { PlanType.Free, PlanType.Custom }; var plansToIgnore = new List<PlanType> { PlanType.Free, PlanType.Custom };
var selectedPlan = StaticStore.Plans.Last(p => !plansToIgnore.Contains(p.Type) && !p.Disabled); var selectedPlan = MockPlans.Plans.Last(p => !plansToIgnore.Contains(p.Type) && !p.Disabled);
fixture.Customize<OrganizationUpgrade>(composer => composer fixture.Customize<OrganizationUpgrade>(composer => composer
.With(ou => ou.Plan, selectedPlan.Type) .With(ou => ou.Plan, selectedPlan.Type)
@@ -153,7 +168,7 @@ public class SecretsManagerOrganizationCustomization : ICustomization
.With(o => o.Id, organizationId) .With(o => o.Id, organizationId)
.With(o => o.UseSecretsManager, true) .With(o => o.UseSecretsManager, true)
.With(o => o.PlanType, planType) .With(o => o.PlanType, planType)
.With(o => o.Plan, StaticStore.GetPlan(planType).Name) .With(o => o.Plan, MockPlans.Get(planType).Name)
.With(o => o.MaxAutoscaleSmSeats, (int?)null) .With(o => o.MaxAutoscaleSmSeats, (int?)null)
.With(o => o.MaxAutoscaleSmServiceAccounts, (int?)null)); .With(o => o.MaxAutoscaleSmServiceAccounts, (int?)null));
} }
@@ -277,3 +292,9 @@ internal class EphemeralDataProtectionAutoDataAttribute : CustomAutoDataAttribut
public EphemeralDataProtectionAutoDataAttribute() : base(new SutProviderCustomization(), new EphemeralDataProtectionCustomization()) public EphemeralDataProtectionAutoDataAttribute() : base(new SutProviderCustomization(), new EphemeralDataProtectionCustomization())
{ } { }
} }
internal class OrganizationAttribute(bool useAutomaticUserConfirmation = false, PlanType planType = PlanType.Free) : CustomizeAttribute
{
public override ICustomization GetCustomization(ParameterInfo parameter) =>
new OrganizationCustomization(useAutomaticUserConfirmation, planType);
}

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