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:
@@ -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:
|
||||||
15
.github/workflows/build.yml
vendored
15
.github/workflows/build.yml
vendored
@@ -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
|
||||||
|
|||||||
4
.github/workflows/test-database.yml
vendored
4
.github/workflows/test-database.yml
vendored
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -30,6 +30,7 @@
|
|||||||
},
|
},
|
||||||
"storage": {
|
"storage": {
|
||||||
"connectionString": "UseDevelopmentStorage=true"
|
"connectionString": "UseDevelopmentStorage=true"
|
||||||
}
|
},
|
||||||
|
"pricingUri": "https://billingpricing.qa.bitwarden.pw"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
"storage": {
|
"storage": {
|
||||||
"connectionString": "UseDevelopmentStorage=true"
|
"connectionString": "UseDevelopmentStorage=true"
|
||||||
},
|
},
|
||||||
"developmentDirectory": "../../../dev"
|
"developmentDirectory": "../../../dev",
|
||||||
|
"pricingUri": "https://billingpricing.qa.bitwarden.pw"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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:
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
},
|
},
|
||||||
"storage": {
|
"storage": {
|
||||||
"connectionString": "UseDevelopmentStorage=true"
|
"connectionString": "UseDevelopmentStorage=true"
|
||||||
}
|
},
|
||||||
|
"pricingUri": "https://billingpricing.qa.bitwarden.pw"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|
||||||
|
|||||||
@@ -195,8 +195,12 @@ 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))
|
||||||
{
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var passwordManagerItem =
|
var passwordManagerItem =
|
||||||
subscription.Items.FirstOrDefault(item => item.Price.Id == plan.PasswordManager.StripePlanId);
|
subscription.Items.FirstOrDefault(item => item.Price.Id == plan.PasswordManager.StripePlanId);
|
||||||
|
|
||||||
@@ -220,16 +224,20 @@ public class UpcomingInvoiceHandler(
|
|||||||
[
|
[
|
||||||
new SubscriptionItemOptions
|
new SubscriptionItemOptions
|
||||||
{
|
{
|
||||||
Id = passwordManagerItem.Id, Price = families.PasswordManager.StripePlanId
|
Id = passwordManagerItem.Id,
|
||||||
|
Price = families.PasswordManager.StripePlanId
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
Discounts =
|
|
||||||
[
|
|
||||||
new SubscriptionDiscountOptions { Coupon = CouponIDs.Milestone3SubscriptionDiscount }
|
|
||||||
],
|
|
||||||
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,6 +250,18 @@ public class UpcomingInvoiceHandler(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var seatAddOnItem = subscription.Items.FirstOrDefault(item => item.Price.Id == "personal-org-seat-annually");
|
||||||
|
|
||||||
|
if (seatAddOnItem != null)
|
||||||
|
{
|
||||||
|
options.Items.Add(new SubscriptionItemOptions
|
||||||
|
{
|
||||||
|
Id = seatAddOnItem.Id,
|
||||||
|
Deleted = true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await organizationRepository.ReplaceAsync(organization);
|
await organizationRepository.ReplaceAsync(organization);
|
||||||
@@ -257,7 +277,6 @@ public class UpcomingInvoiceHandler(
|
|||||||
@event.Id);
|
@event.Id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
|||||||
@@ -36,5 +36,6 @@
|
|||||||
"onyx": {
|
"onyx": {
|
||||||
"personaId": 68
|
"personaId": 68
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"pricingUri": "https://billingpricing.qa.bitwarden.pw"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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; }
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.");
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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.");
|
||||||
|
|||||||
@@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
15
src/Core/AdminConsole/Utilities/v2/Errors.cs
Normal file
15
src/Core/AdminConsole/Utilities/v2/Errors.cs
Normal 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);
|
||||||
@@ -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);
|
||||||
|
|
||||||
@@ -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.
|
||||||
25
src/Core/Billing/Models/SponsoredPlans.cs
Normal file
25
src/Core/Billing/Models/SponsoredPlans.cs
Normal 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)!;
|
||||||
|
}
|
||||||
@@ -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; }
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 }
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
|||||||
@@ -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>();
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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.");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 }
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
|||||||
@@ -150,10 +150,28 @@ 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))
|
||||||
{
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new collections if not already present
|
||||||
collection.SetNewId();
|
collection.SetNewId();
|
||||||
newCollections.Add(collection);
|
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
|
newCollectionUsers.Add(new CollectionUser
|
||||||
{
|
{
|
||||||
CollectionId = collection.Id,
|
CollectionId = collection.Id,
|
||||||
@@ -161,7 +179,6 @@ public class ImportCiphersCommand : IImportCiphersCommand
|
|||||||
Manage = true
|
Manage = true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Create associations based on the newly assigned ids
|
// Create associations based on the newly assigned ids
|
||||||
var collectionCiphers = new List<CollectionCipher>();
|
var collectionCiphers = new List<CollectionCipher>();
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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.");
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -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],
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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],
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>()
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 =>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
Reference in New Issue
Block a user