diff --git a/.github/ISSUE_TEMPLATE/bw-unified.yml b/.github/ISSUE_TEMPLATE/bw-lite.yml similarity index 96% rename from .github/ISSUE_TEMPLATE/bw-unified.yml rename to .github/ISSUE_TEMPLATE/bw-lite.yml index 240b1faa72..f46f4b3e37 100644 --- a/.github/ISSUE_TEMPLATE/bw-unified.yml +++ b/.github/ISSUE_TEMPLATE/bw-lite.yml @@ -1,6 +1,6 @@ -name: Bitwarden Unified Deployment Bug Report +name: Bitwarden Lite Deployment Bug Report description: File a bug report -labels: [bug, bw-unified-deploy] +labels: [bug, bw-lite-deploy] body: - type: markdown attributes: @@ -74,7 +74,7 @@ body: id: epic-label attributes: label: Issue-Link - description: Link to our pinned issue, tracking all Bitwarden Unified + description: Link to our pinned issue, tracking all Bitwarden Lite value: | https://github.com/bitwarden/server/issues/2480 validations: diff --git a/.github/renovate.json5 b/.github/renovate.json5 index bc377ed46c..e892e59b22 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -42,7 +42,7 @@ dependencyDashboardApproval: false, }, { - matchSourceUrls: ["https://github.com/bitwarden/sdk-internal"], + matchPackageNames: ["https://github.com/bitwarden/sdk-internal.git"], groupName: "sdk-internal", }, { diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2d92c68b93..877281ccb0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -22,7 +22,7 @@ env: jobs: lint: name: Lint - runs-on: ubuntu-24.04 + runs-on: ubuntu-22.04 steps: - name: Check out repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 @@ -38,7 +38,7 @@ jobs: build-artifacts: name: Build Docker images - runs-on: ubuntu-24.04 + runs-on: ubuntu-22.04 needs: - lint outputs: @@ -49,7 +49,6 @@ jobs: timeout-minutes: 45 strategy: fail-fast: false - max-parallel: 5 matrix: include: - project_name: Admin @@ -281,7 +280,7 @@ jobs: output-format: sarif - name: Upload Grype results to GitHub - uses: github/codeql-action/upload-sarif@dd746615b3b9d728a6a37ca2045b68ca76d4841a # v3.28.8 + uses: github/codeql-action/upload-sarif@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2 with: sarif_file: ${{ steps.container-scan.outputs.sarif }} sha: ${{ contains(github.event_name, 'pull_request') && github.event.pull_request.head.sha || github.sha }} @@ -292,7 +291,7 @@ jobs: upload: name: Upload - runs-on: ubuntu-24.04 + runs-on: ubuntu-22.04 needs: build-artifacts permissions: id-token: write @@ -410,7 +409,7 @@ jobs: build-mssqlmigratorutility: name: Build MSSQL migrator utility - runs-on: ubuntu-24.04 + runs-on: ubuntu-22.04 needs: - lint defaults: @@ -467,7 +466,7 @@ jobs: if: | github.event_name != 'pull_request' && (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: - build-artifacts permissions: @@ -490,7 +489,7 @@ jobs: - name: Log out from Azure 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 with: github-token: ${{ steps.retrieve-secret-pat.outputs.github-pat-bitwarden-devops-bot-repo-scope }} @@ -498,7 +497,7 @@ jobs: await github.rest.actions.createWorkflowDispatch({ owner: 'bitwarden', repo: 'self-host', - workflow_id: 'build-unified.yml', + workflow_id: 'build-bitwarden-lite.yml', ref: 'main', inputs: { server_branch: process.env.GITHUB_REF diff --git a/.github/workflows/test-database.yml b/.github/workflows/test-database.yml index fb1c18b158..20bc67bc6b 100644 --- a/.github/workflows/test-database.yml +++ b/.github/workflows/test-database.yml @@ -62,7 +62,7 @@ jobs: docker compose --profile mssql --profile postgres --profile mysql up -d shell: pwsh - - name: Add MariaDB for unified + - name: Add MariaDB for Bitwarden Lite # Use a different port than MySQL run: | docker run --detach --name mariadb --env MARIADB_ROOT_PASSWORD=mariadb-password -p 4306:3306 mariadb:10 @@ -133,7 +133,7 @@ jobs: # Default Sqlite BW_TEST_DATABASES__3__TYPE: "Sqlite" 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__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" diff --git a/bitwarden_license/src/Scim/appsettings.Development.json b/bitwarden_license/src/Scim/appsettings.Development.json index 32253a93c1..496d0c075f 100644 --- a/bitwarden_license/src/Scim/appsettings.Development.json +++ b/bitwarden_license/src/Scim/appsettings.Development.json @@ -30,6 +30,7 @@ }, "storage": { "connectionString": "UseDevelopmentStorage=true" - } + }, + "pricingUri": "https://billingpricing.qa.bitwarden.pw" } } diff --git a/bitwarden_license/src/Sso/appsettings.Development.json b/bitwarden_license/src/Sso/appsettings.Development.json index 8aae281068..6d9ec77815 100644 --- a/bitwarden_license/src/Sso/appsettings.Development.json +++ b/bitwarden_license/src/Sso/appsettings.Development.json @@ -24,6 +24,7 @@ "storage": { "connectionString": "UseDevelopmentStorage=true" }, - "developmentDirectory": "../../../dev" + "developmentDirectory": "../../../dev", + "pricingUri": "https://billingpricing.qa.bitwarden.pw" } } diff --git a/bitwarden_license/test/Commercial.Core.Test/AdminConsole/ProviderFeatures/RemoveOrganizationFromProviderCommandTests.cs b/bitwarden_license/test/Commercial.Core.Test/AdminConsole/ProviderFeatures/RemoveOrganizationFromProviderCommandTests.cs index 2bb02c3cee..b367b17c73 100644 --- a/bitwarden_license/test/Commercial.Core.Test/AdminConsole/ProviderFeatures/RemoveOrganizationFromProviderCommandTests.cs +++ b/bitwarden_license/test/Commercial.Core.Test/AdminConsole/ProviderFeatures/RemoveOrganizationFromProviderCommandTests.cs @@ -13,7 +13,7 @@ using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Repositories; using Bit.Core.Services; -using Bit.Core.Utilities; +using Bit.Core.Test.Billing.Mocks; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; @@ -207,7 +207,7 @@ public class RemoveOrganizationFromProviderCommandTests organization.PlanType = PlanType.TeamsMonthly; - var teamsMonthlyPlan = StaticStore.GetPlan(PlanType.TeamsMonthly); + var teamsMonthlyPlan = MockPlans.Get(PlanType.TeamsMonthly); sutProvider.GetDependency().GetPlanOrThrow(PlanType.TeamsMonthly).Returns(teamsMonthlyPlan); @@ -296,7 +296,7 @@ public class RemoveOrganizationFromProviderCommandTests organization.PlanType = PlanType.TeamsMonthly; - var teamsMonthlyPlan = StaticStore.GetPlan(PlanType.TeamsMonthly); + var teamsMonthlyPlan = MockPlans.Get(PlanType.TeamsMonthly); sutProvider.GetDependency().GetPlanOrThrow(PlanType.TeamsMonthly).Returns(teamsMonthlyPlan); @@ -416,7 +416,7 @@ public class RemoveOrganizationFromProviderCommandTests organization.PlanType = PlanType.TeamsMonthly; organization.Enabled = false; // Start with a disabled organization - var teamsMonthlyPlan = StaticStore.GetPlan(PlanType.TeamsMonthly); + var teamsMonthlyPlan = MockPlans.Get(PlanType.TeamsMonthly); sutProvider.GetDependency().GetPlanOrThrow(PlanType.TeamsMonthly).Returns(teamsMonthlyPlan); diff --git a/bitwarden_license/test/Commercial.Core.Test/AdminConsole/Services/ProviderServiceTests.cs b/bitwarden_license/test/Commercial.Core.Test/AdminConsole/Services/ProviderServiceTests.cs index e61cf5f97e..78376f6d98 100644 --- a/bitwarden_license/test/Commercial.Core.Test/AdminConsole/Services/ProviderServiceTests.cs +++ b/bitwarden_license/test/Commercial.Core.Test/AdminConsole/Services/ProviderServiceTests.cs @@ -20,6 +20,7 @@ using Bit.Core.Models.Business; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Test.AutoFixture.OrganizationFixtures; +using Bit.Core.Test.Billing.Mocks; using Bit.Core.Tokens; using Bit.Core.Utilities; using Bit.Test.Common.AutoFixture; @@ -811,12 +812,12 @@ public class ProviderServiceTests organization.Plan = "Enterprise (Monthly)"; sutProvider.GetDependency().GetPlanOrThrow(organization.PlanType) - .Returns(StaticStore.GetPlan(organization.PlanType)); + .Returns(MockPlans.Get(organization.PlanType)); var expectedPlanType = PlanType.EnterpriseMonthly2020; sutProvider.GetDependency().GetPlanOrThrow(expectedPlanType) - .Returns(StaticStore.GetPlan(expectedPlanType)); + .Returns(MockPlans.Get(expectedPlanType)); var expectedPlanId = "2020-enterprise-org-seat-monthly"; diff --git a/bitwarden_license/test/Commercial.Core.Test/Billing/Providers/Services/BusinessUnitConverterTests.cs b/bitwarden_license/test/Commercial.Core.Test/Billing/Providers/Services/BusinessUnitConverterTests.cs index ec52650097..c893886083 100644 --- a/bitwarden_license/test/Commercial.Core.Test/Billing/Providers/Services/BusinessUnitConverterTests.cs +++ b/bitwarden_license/test/Commercial.Core.Test/Billing/Providers/Services/BusinessUnitConverterTests.cs @@ -18,6 +18,7 @@ using Bit.Core.Enums; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; +using Bit.Core.Test.Billing.Mocks; using Bit.Core.Utilities; using Bit.Test.Common.AutoFixture.Attributes; using Microsoft.AspNetCore.DataProtection; @@ -72,7 +73,7 @@ public class BusinessUnitConverterTests { organization.PlanType = PlanType.EnterpriseAnnually2020; - var enterpriseAnnually2020 = StaticStore.GetPlan(PlanType.EnterpriseAnnually2020); + var enterpriseAnnually2020 = MockPlans.Get(PlanType.EnterpriseAnnually2020); var subscription = new Subscription { @@ -134,7 +135,7 @@ public class BusinessUnitConverterTests _pricingClient.GetPlanOrThrow(PlanType.EnterpriseAnnually2020) .Returns(enterpriseAnnually2020); - var enterpriseAnnually = StaticStore.GetPlan(PlanType.EnterpriseAnnually); + var enterpriseAnnually = MockPlans.Get(PlanType.EnterpriseAnnually); _pricingClient.GetPlanOrThrow(PlanType.EnterpriseAnnually) .Returns(enterpriseAnnually); @@ -242,7 +243,7 @@ public class BusinessUnitConverterTests argument.Status == ProviderStatusType.Pending && argument.Type == ProviderType.BusinessUnit)).Returns(provider); - var plan = StaticStore.GetPlan(organization.PlanType); + var plan = MockPlans.Get(organization.PlanType); _pricingClient.GetPlanOrThrow(organization.PlanType).Returns(plan); diff --git a/bitwarden_license/test/Commercial.Core.Test/Billing/Providers/Services/ProviderBillingServiceTests.cs b/bitwarden_license/test/Commercial.Core.Test/Billing/Providers/Services/ProviderBillingServiceTests.cs index 18c71364e6..daf35e7ae9 100644 --- a/bitwarden_license/test/Commercial.Core.Test/Billing/Providers/Services/ProviderBillingServiceTests.cs +++ b/bitwarden_license/test/Commercial.Core.Test/Billing/Providers/Services/ProviderBillingServiceTests.cs @@ -22,7 +22,7 @@ using Bit.Core.Exceptions; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; -using Bit.Core.Utilities; +using Bit.Core.Test.Billing.Mocks; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using Braintree; @@ -140,7 +140,7 @@ public class ProviderBillingServiceTests .Returns(existingPlan); sutProvider.GetDependency().GetPlanOrThrow(existingPlan.PlanType) - .Returns(StaticStore.GetPlan(existingPlan.PlanType)); + .Returns(MockPlans.Get(existingPlan.PlanType)); sutProvider.GetDependency().GetSubscriptionOrThrow(provider) .Returns(new Subscription @@ -155,7 +155,7 @@ public class ProviderBillingServiceTests Id = "si_ent_annual", Price = new Price { - Id = StaticStore.GetPlan(PlanType.EnterpriseAnnually).PasswordManager + Id = MockPlans.Get(PlanType.EnterpriseAnnually).PasswordManager .StripeProviderPortalSeatPlanId }, Quantity = 10 @@ -168,7 +168,7 @@ public class ProviderBillingServiceTests new ChangeProviderPlanCommand(provider, providerPlanId, PlanType.EnterpriseMonthly); sutProvider.GetDependency().GetPlanOrThrow(command.NewPlan) - .Returns(StaticStore.GetPlan(command.NewPlan)); + .Returns(MockPlans.Get(command.NewPlan)); // Act await sutProvider.Sut.ChangePlan(command); @@ -185,7 +185,7 @@ public class ProviderBillingServiceTests Arg.Is(p => 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) .SubscriptionUpdateAsync( Arg.Is(provider.GatewaySubscriptionId), @@ -491,7 +491,7 @@ public class ProviderBillingServiceTests foreach (var plan in providerPlans) { sutProvider.GetDependency().GetPlanOrThrow(plan.PlanType) - .Returns(StaticStore.GetPlan(plan.PlanType)); + .Returns(MockPlans.Get(plan.PlanType)); } sutProvider.GetDependency().GetByProviderId(provider.Id).Returns(providerPlans); @@ -514,7 +514,7 @@ public class ProviderBillingServiceTests sutProvider.GetDependency().GetSubscriptionOrThrow(provider).Returns(subscription); // 50 seats currently assigned with a seat minimum of 100 - var teamsMonthlyPlan = StaticStore.GetPlan(PlanType.TeamsMonthly); + var teamsMonthlyPlan = MockPlans.Get(PlanType.TeamsMonthly); sutProvider.GetDependency().GetManyDetailsByProviderAsync(provider.Id).Returns( [ @@ -573,7 +573,7 @@ public class ProviderBillingServiceTests foreach (var plan in providerPlans) { sutProvider.GetDependency().GetPlanOrThrow(plan.PlanType) - .Returns(StaticStore.GetPlan(plan.PlanType)); + .Returns(MockPlans.Get(plan.PlanType)); } var providerPlan = providerPlans.First(); @@ -598,7 +598,7 @@ public class ProviderBillingServiceTests sutProvider.GetDependency().GetSubscriptionOrThrow(provider).Returns(subscription); // 95 seats currently assigned with a seat minimum of 100 - var teamsMonthlyPlan = StaticStore.GetPlan(PlanType.TeamsMonthly); + var teamsMonthlyPlan = MockPlans.Get(PlanType.TeamsMonthly); sutProvider.GetDependency().GetManyDetailsByProviderAsync(provider.Id).Returns( [ @@ -661,7 +661,7 @@ public class ProviderBillingServiceTests foreach (var plan in providerPlans) { sutProvider.GetDependency().GetPlanOrThrow(plan.PlanType) - .Returns(StaticStore.GetPlan(plan.PlanType)); + .Returns(MockPlans.Get(plan.PlanType)); } var providerPlan = providerPlans.First(); @@ -686,7 +686,7 @@ public class ProviderBillingServiceTests sutProvider.GetDependency().GetSubscriptionOrThrow(provider).Returns(subscription); // 110 seats currently assigned with a seat minimum of 100 - var teamsMonthlyPlan = StaticStore.GetPlan(PlanType.TeamsMonthly); + var teamsMonthlyPlan = MockPlans.Get(PlanType.TeamsMonthly); sutProvider.GetDependency().GetManyDetailsByProviderAsync(provider.Id).Returns( [ @@ -749,7 +749,7 @@ public class ProviderBillingServiceTests foreach (var plan in providerPlans) { sutProvider.GetDependency().GetPlanOrThrow(plan.PlanType) - .Returns(StaticStore.GetPlan(plan.PlanType)); + .Returns(MockPlans.Get(plan.PlanType)); } var providerPlan = providerPlans.First(); @@ -774,7 +774,7 @@ public class ProviderBillingServiceTests sutProvider.GetDependency().GetSubscriptionOrThrow(provider).Returns(subscription); // 110 seats currently assigned with a seat minimum of 100 - var teamsMonthlyPlan = StaticStore.GetPlan(PlanType.TeamsMonthly); + var teamsMonthlyPlan = MockPlans.Get(PlanType.TeamsMonthly); sutProvider.GetDependency().GetManyDetailsByProviderAsync(provider.Id).Returns( [ @@ -827,13 +827,13 @@ public class ProviderBillingServiceTests } ]); - sutProvider.GetDependency().GetPlanOrThrow(planType).Returns(StaticStore.GetPlan(planType)); + sutProvider.GetDependency().GetPlanOrThrow(planType).Returns(MockPlans.Get(planType)); sutProvider.GetDependency().GetManyDetailsByProviderAsync(provider.Id).Returns( [ new ProviderOrganizationOrganizationDetails { - Plan = StaticStore.GetPlan(planType).Name, + Plan = MockPlans.Get(planType).Name, Status = OrganizationStatusType.Managed, Seats = 5 } @@ -865,13 +865,13 @@ public class ProviderBillingServiceTests } ]); - sutProvider.GetDependency().GetPlanOrThrow(planType).Returns(StaticStore.GetPlan(planType)); + sutProvider.GetDependency().GetPlanOrThrow(planType).Returns(MockPlans.Get(planType)); sutProvider.GetDependency().GetManyDetailsByProviderAsync(provider.Id).Returns( [ new ProviderOrganizationOrganizationDetails { - Plan = StaticStore.GetPlan(planType).Name, + Plan = MockPlans.Get(planType).Name, Status = OrganizationStatusType.Managed, Seats = 15 } @@ -1238,7 +1238,7 @@ public class ProviderBillingServiceTests .Returns(providerPlans); sutProvider.GetDependency().GetPlanOrThrow(PlanType.EnterpriseMonthly) - .Returns(StaticStore.GetPlan(PlanType.EnterpriseMonthly)); + .Returns(MockPlans.Get(PlanType.EnterpriseMonthly)); await ThrowsBillingExceptionAsync(() => sutProvider.Sut.SetupSubscription(provider)); @@ -1266,7 +1266,7 @@ public class ProviderBillingServiceTests .Returns(providerPlans); sutProvider.GetDependency().GetPlanOrThrow(PlanType.TeamsMonthly) - .Returns(StaticStore.GetPlan(PlanType.TeamsMonthly)); + .Returns(MockPlans.Get(PlanType.TeamsMonthly)); await ThrowsBillingExceptionAsync(() => sutProvider.Sut.SetupSubscription(provider)); @@ -1317,7 +1317,7 @@ public class ProviderBillingServiceTests foreach (var plan in providerPlans) { sutProvider.GetDependency().GetPlanOrThrow(plan.PlanType) - .Returns(StaticStore.GetPlan(plan.PlanType)); + .Returns(MockPlans.Get(plan.PlanType)); } sutProvider.GetDependency().GetByProviderId(provider.Id) @@ -1373,7 +1373,7 @@ public class ProviderBillingServiceTests foreach (var plan in providerPlans) { sutProvider.GetDependency().GetPlanOrThrow(plan.PlanType) - .Returns(StaticStore.GetPlan(plan.PlanType)); + .Returns(MockPlans.Get(plan.PlanType)); } sutProvider.GetDependency().GetByProviderId(provider.Id) @@ -1449,7 +1449,7 @@ public class ProviderBillingServiceTests foreach (var plan in providerPlans) { sutProvider.GetDependency().GetPlanOrThrow(plan.PlanType) - .Returns(StaticStore.GetPlan(plan.PlanType)); + .Returns(MockPlans.Get(plan.PlanType)); } sutProvider.GetDependency().GetByProviderId(provider.Id) @@ -1525,7 +1525,7 @@ public class ProviderBillingServiceTests foreach (var plan in providerPlans) { sutProvider.GetDependency().GetPlanOrThrow(plan.PlanType) - .Returns(StaticStore.GetPlan(plan.PlanType)); + .Returns(MockPlans.Get(plan.PlanType)); } sutProvider.GetDependency().GetByProviderId(provider.Id) @@ -1626,7 +1626,7 @@ public class ProviderBillingServiceTests foreach (var plan in providerPlans) { sutProvider.GetDependency().GetPlanOrThrow(plan.PlanType) - .Returns(StaticStore.GetPlan(plan.PlanType)); + .Returns(MockPlans.Get(plan.PlanType)); } sutProvider.GetDependency().GetByProviderId(provider.Id) @@ -1704,7 +1704,7 @@ public class ProviderBillingServiceTests foreach (var plan in providerPlans) { sutProvider.GetDependency().GetPlanOrThrow(plan.PlanType) - .Returns(StaticStore.GetPlan(plan.PlanType)); + .Returns(MockPlans.Get(plan.PlanType)); } sutProvider.GetDependency().GetByProviderId(provider.Id) @@ -1772,8 +1772,8 @@ public class ProviderBillingServiceTests const string enterpriseLineItemId = "enterprise_line_item_id"; const string teamsLineItemId = "teams_line_item_id"; - var enterprisePriceId = StaticStore.GetPlan(PlanType.EnterpriseMonthly).PasswordManager.StripeProviderPortalSeatPlanId; - var teamsPriceId = StaticStore.GetPlan(PlanType.TeamsMonthly).PasswordManager.StripeProviderPortalSeatPlanId; + var enterprisePriceId = MockPlans.Get(PlanType.EnterpriseMonthly).PasswordManager.StripeProviderPortalSeatPlanId; + var teamsPriceId = MockPlans.Get(PlanType.TeamsMonthly).PasswordManager.StripeProviderPortalSeatPlanId; var subscription = new Subscription { @@ -1806,7 +1806,7 @@ public class ProviderBillingServiceTests foreach (var plan in providerPlans) { sutProvider.GetDependency().GetPlanOrThrow(plan.PlanType) - .Returns(StaticStore.GetPlan(plan.PlanType)); + .Returns(MockPlans.Get(plan.PlanType)); } providerPlanRepository.GetByProviderId(provider.Id).Returns(providerPlans); @@ -1852,8 +1852,8 @@ public class ProviderBillingServiceTests const string enterpriseLineItemId = "enterprise_line_item_id"; const string teamsLineItemId = "teams_line_item_id"; - var enterprisePriceId = StaticStore.GetPlan(PlanType.EnterpriseMonthly).PasswordManager.StripeProviderPortalSeatPlanId; - var teamsPriceId = StaticStore.GetPlan(PlanType.TeamsMonthly).PasswordManager.StripeProviderPortalSeatPlanId; + var enterprisePriceId = MockPlans.Get(PlanType.EnterpriseMonthly).PasswordManager.StripeProviderPortalSeatPlanId; + var teamsPriceId = MockPlans.Get(PlanType.TeamsMonthly).PasswordManager.StripeProviderPortalSeatPlanId; var subscription = new Subscription { @@ -1886,7 +1886,7 @@ public class ProviderBillingServiceTests foreach (var plan in providerPlans) { sutProvider.GetDependency().GetPlanOrThrow(plan.PlanType) - .Returns(StaticStore.GetPlan(plan.PlanType)); + .Returns(MockPlans.Get(plan.PlanType)); } providerPlanRepository.GetByProviderId(provider.Id).Returns(providerPlans); @@ -1932,8 +1932,8 @@ public class ProviderBillingServiceTests const string enterpriseLineItemId = "enterprise_line_item_id"; const string teamsLineItemId = "teams_line_item_id"; - var enterprisePriceId = StaticStore.GetPlan(PlanType.EnterpriseMonthly).PasswordManager.StripeProviderPortalSeatPlanId; - var teamsPriceId = StaticStore.GetPlan(PlanType.TeamsMonthly).PasswordManager.StripeProviderPortalSeatPlanId; + var enterprisePriceId = MockPlans.Get(PlanType.EnterpriseMonthly).PasswordManager.StripeProviderPortalSeatPlanId; + var teamsPriceId = MockPlans.Get(PlanType.TeamsMonthly).PasswordManager.StripeProviderPortalSeatPlanId; var subscription = new Subscription { @@ -1966,7 +1966,7 @@ public class ProviderBillingServiceTests foreach (var plan in providerPlans) { sutProvider.GetDependency().GetPlanOrThrow(plan.PlanType) - .Returns(StaticStore.GetPlan(plan.PlanType)); + .Returns(MockPlans.Get(plan.PlanType)); } providerPlanRepository.GetByProviderId(provider.Id).Returns(providerPlans); @@ -2006,8 +2006,8 @@ public class ProviderBillingServiceTests const string enterpriseLineItemId = "enterprise_line_item_id"; const string teamsLineItemId = "teams_line_item_id"; - var enterprisePriceId = StaticStore.GetPlan(PlanType.EnterpriseMonthly).PasswordManager.StripeProviderPortalSeatPlanId; - var teamsPriceId = StaticStore.GetPlan(PlanType.TeamsMonthly).PasswordManager.StripeProviderPortalSeatPlanId; + var enterprisePriceId = MockPlans.Get(PlanType.EnterpriseMonthly).PasswordManager.StripeProviderPortalSeatPlanId; + var teamsPriceId = MockPlans.Get(PlanType.TeamsMonthly).PasswordManager.StripeProviderPortalSeatPlanId; var subscription = new Subscription { @@ -2040,7 +2040,7 @@ public class ProviderBillingServiceTests foreach (var plan in providerPlans) { sutProvider.GetDependency().GetPlanOrThrow(plan.PlanType) - .Returns(StaticStore.GetPlan(plan.PlanType)); + .Returns(MockPlans.Get(plan.PlanType)); } providerPlanRepository.GetByProviderId(provider.Id).Returns(providerPlans); @@ -2086,8 +2086,8 @@ public class ProviderBillingServiceTests const string enterpriseLineItemId = "enterprise_line_item_id"; const string teamsLineItemId = "teams_line_item_id"; - var enterprisePriceId = StaticStore.GetPlan(PlanType.EnterpriseMonthly).PasswordManager.StripeProviderPortalSeatPlanId; - var teamsPriceId = StaticStore.GetPlan(PlanType.TeamsMonthly).PasswordManager.StripeProviderPortalSeatPlanId; + var enterprisePriceId = MockPlans.Get(PlanType.EnterpriseMonthly).PasswordManager.StripeProviderPortalSeatPlanId; + var teamsPriceId = MockPlans.Get(PlanType.TeamsMonthly).PasswordManager.StripeProviderPortalSeatPlanId; var subscription = new Subscription { @@ -2120,7 +2120,7 @@ public class ProviderBillingServiceTests foreach (var plan in providerPlans) { sutProvider.GetDependency().GetPlanOrThrow(plan.PlanType) - .Returns(StaticStore.GetPlan(plan.PlanType)); + .Returns(MockPlans.Get(plan.PlanType)); } providerPlanRepository.GetByProviderId(provider.Id).Returns(providerPlans); diff --git a/bitwarden_license/test/Commercial.Core.Test/SecretsManager/Queries/Projects/MaxProjectsQueryTests.cs b/bitwarden_license/test/Commercial.Core.Test/SecretsManager/Queries/Projects/MaxProjectsQueryTests.cs index 16ae8f7f2c..776403fdd5 100644 --- a/bitwarden_license/test/Commercial.Core.Test/SecretsManager/Queries/Projects/MaxProjectsQueryTests.cs +++ b/bitwarden_license/test/Commercial.Core.Test/SecretsManager/Queries/Projects/MaxProjectsQueryTests.cs @@ -6,7 +6,7 @@ using Bit.Core.Exceptions; using Bit.Core.Repositories; using Bit.Core.SecretsManager.Repositories; using Bit.Core.Settings; -using Bit.Core.Utilities; +using Bit.Core.Test.Billing.Mocks; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; @@ -69,7 +69,7 @@ public class MaxProjectsQueryTests sutProvider.GetDependency().GetByIdAsync(organization.Id).Returns(organization); sutProvider.GetDependency().GetPlan(organization.PlanType) - .Returns(StaticStore.GetPlan(organization.PlanType)); + .Returns(MockPlans.Get(organization.PlanType)); var (limit, overLimit) = await sutProvider.Sut.GetByOrgIdAsync(organization.Id, 1); @@ -114,7 +114,7 @@ public class MaxProjectsQueryTests .Returns(projects); sutProvider.GetDependency().GetPlan(organization.PlanType) - .Returns(StaticStore.GetPlan(organization.PlanType)); + .Returns(MockPlans.Get(organization.PlanType)); var (max, overMax) = await sutProvider.Sut.GetByOrgIdAsync(organization.Id, projectsToAdd); diff --git a/bitwarden_license/test/Scim.IntegrationTest/appsettings.Development.json b/bitwarden_license/test/Scim.IntegrationTest/appsettings.Development.json new file mode 100644 index 0000000000..496d0c075f --- /dev/null +++ b/bitwarden_license/test/Scim.IntegrationTest/appsettings.Development.json @@ -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" + } +} diff --git a/dev/docker-compose.yml b/dev/docker-compose.yml index c5e42cf9e3..3554306ddb 100644 --- a/dev/docker-compose.yml +++ b/dev/docker-compose.yml @@ -57,7 +57,6 @@ services: mysql: image: mysql:8.0 - container_name: bw-mysql ports: - "3306:3306" command: @@ -88,7 +87,6 @@ services: idp: image: kenchan0130/simplesamlphp:1.19.8 - container_name: idp ports: - "8090:8080" environment: @@ -102,7 +100,6 @@ services: rabbitmq: image: rabbitmq:4.1.3-management - container_name: rabbitmq ports: - "5672:5672" - "15672:15672" @@ -116,7 +113,6 @@ services: reverse-proxy: image: nginx:alpine - container_name: reverse-proxy volumes: - "./reverse-proxy.conf:/etc/nginx/conf.d/default.conf" ports: @@ -126,7 +122,6 @@ services: - proxy service-bus: - container_name: service-bus image: mcr.microsoft.com/azure-messaging/servicebus-emulator:latest pull_policy: always volumes: @@ -142,7 +137,6 @@ services: redis: image: redis:alpine - container_name: bw-redis ports: - "6379:6379" volumes: diff --git a/src/Admin/appsettings.Development.json b/src/Admin/appsettings.Development.json index 861f9be98d..15d61f493f 100644 --- a/src/Admin/appsettings.Development.json +++ b/src/Admin/appsettings.Development.json @@ -27,6 +27,7 @@ }, "storage": { "connectionString": "UseDevelopmentStorage=true" - } + }, + "pricingUri": "https://billingpricing.qa.bitwarden.pw" } } diff --git a/src/Api/AdminConsole/Controllers/BaseAdminConsoleController.cs b/src/Api/AdminConsole/Controllers/BaseAdminConsoleController.cs new file mode 100644 index 0000000000..9b147c3c54 --- /dev/null +++ b/src/Api/AdminConsole/Controllers/BaseAdminConsoleController.cs @@ -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( + 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() + ); +} diff --git a/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs b/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs index 4b9f7e5d71..155b60ce5b 100644 --- a/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs +++ b/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs @@ -11,8 +11,10 @@ using Bit.Api.Models.Response; using Bit.Api.Vault.AuthorizationHandlers.Collections; using Bit.Core; using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Models.Data; using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; 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.Interfaces; 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.PolicyRequirements; using Bit.Core.AdminConsole.Repositories; +using Bit.Core.AdminConsole.Utilities.v2; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Repositories; using Bit.Core.Billing.Pricing; @@ -43,7 +46,7 @@ namespace Bit.Api.AdminConsole.Controllers; [Route("organizations/{orgId}/users")] [Authorize("Application")] -public class OrganizationUsersController : Controller +public class OrganizationUsersController : BaseAdminConsoleController { private readonly IOrganizationRepository _organizationRepository; private readonly IOrganizationUserRepository _organizationUserRepository; @@ -68,6 +71,7 @@ public class OrganizationUsersController : Controller private readonly IFeatureService _featureService; private readonly IPricingClient _pricingClient; private readonly IResendOrganizationInviteCommand _resendOrganizationInviteCommand; + private readonly IAutomaticallyConfirmOrganizationUserCommand _automaticallyConfirmOrganizationUserCommand; private readonly IConfirmOrganizationUserCommand _confirmOrganizationUserCommand; private readonly IRestoreOrganizationUserCommand _restoreOrganizationUserCommand; private readonly IInitPendingOrganizationCommand _initPendingOrganizationCommand; @@ -101,7 +105,8 @@ public class OrganizationUsersController : Controller IInitPendingOrganizationCommand initPendingOrganizationCommand, IRevokeOrganizationUserCommand revokeOrganizationUserCommand, IResendOrganizationInviteCommand resendOrganizationInviteCommand, - IAdminRecoverAccountCommand adminRecoverAccountCommand) + IAdminRecoverAccountCommand adminRecoverAccountCommand, + IAutomaticallyConfirmOrganizationUserCommand automaticallyConfirmOrganizationUserCommand) { _organizationRepository = organizationRepository; _organizationUserRepository = organizationUserRepository; @@ -126,6 +131,7 @@ public class OrganizationUsersController : Controller _featureService = featureService; _pricingClient = pricingClient; _resendOrganizationInviteCommand = resendOrganizationInviteCommand; + _automaticallyConfirmOrganizationUserCommand = automaticallyConfirmOrganizationUserCommand; _confirmOrganizationUserCommand = confirmOrganizationUserCommand; _restoreOrganizationUserCommand = restoreOrganizationUserCommand; _initPendingOrganizationCommand = initPendingOrganizationCommand; @@ -738,6 +744,31 @@ public class OrganizationUsersController : Controller await BulkEnableSecretsManagerAsync(orgId, model); } + [HttpPost("{id}/auto-confirm")] + [Authorize] + [RequireFeature(FeatureFlagKeys.AutomaticConfirmUsers)] + public async Task 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( Guid orgId, Guid id, diff --git a/src/Api/AdminConsole/Models/Response/Organizations/PolicyResponseModel.cs b/src/Api/AdminConsole/Models/Response/Organizations/PolicyResponseModel.cs index 81ca801308..0507de7a55 100644 --- a/src/Api/AdminConsole/Models/Response/Organizations/PolicyResponseModel.cs +++ b/src/Api/AdminConsole/Models/Response/Organizations/PolicyResponseModel.cs @@ -30,6 +30,7 @@ public class PolicyResponseModel : ResponseModel { Data = JsonSerializer.Deserialize>(policy.Data); } + RevisionDate = policy.RevisionDate; } public Guid Id { get; set; } @@ -37,4 +38,5 @@ public class PolicyResponseModel : ResponseModel public PolicyType Type { get; set; } public Dictionary Data { get; set; } public bool Enabled { get; set; } + public DateTime RevisionDate { get; set; } } diff --git a/src/Api/AdminConsole/Models/Response/ProfileOrganizationResponseModel.cs b/src/Api/AdminConsole/Models/Response/ProfileOrganizationResponseModel.cs index 97a58d038a..8c52092dae 100644 --- a/src/Api/AdminConsole/Models/Response/ProfileOrganizationResponseModel.cs +++ b/src/Api/AdminConsole/Models/Response/ProfileOrganizationResponseModel.cs @@ -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.Organizations.OrganizationUsers; using Bit.Core.Utilities; @@ -27,7 +28,7 @@ public class ProfileOrganizationResponseModel : BaseProfileOrganizationResponseM FamilySponsorshipToDelete = organizationDetails.FamilySponsorshipToDelete; FamilySponsorshipValidUntil = organizationDetails.FamilySponsorshipValidUntil; FamilySponsorshipAvailable = (organizationDetails.FamilySponsorshipFriendlyName == null || IsAdminInitiated) && - StaticStore.GetSponsoredPlan(PlanSponsorshipType.FamiliesForEnterprise) + SponsoredPlans.Get(PlanSponsorshipType.FamiliesForEnterprise) .UsersCanSponsor(organizationDetails); AccessSecretsManager = organizationDetails.AccessSecretsManager; } diff --git a/src/Api/Billing/Controllers/VNext/SelfHostedBillingController.cs b/src/Api/Billing/Controllers/VNext/SelfHostedBillingController.cs new file mode 100644 index 0000000000..bd40c777dc --- /dev/null +++ b/src/Api/Billing/Controllers/VNext/SelfHostedBillingController.cs @@ -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] + [HttpGet("metadata")] + [RequireFeature(FeatureFlagKeys.PM25379_UseNewOrganizationMetadataStructure)] + [InjectOrganization] + public async Task GetMetadataAsync([BindNever] Organization organization) + { + var metadata = await getOrganizationMetadataQuery.Run(organization); + + if (metadata == null) + { + return TypedResults.NotFound(); + } + + return TypedResults.Ok(metadata); + } +} diff --git a/src/Api/Vault/Controllers/CiphersController.cs b/src/Api/Vault/Controllers/CiphersController.cs index 0983225f84..c200810156 100644 --- a/src/Api/Vault/Controllers/CiphersController.cs +++ b/src/Api/Vault/Controllers/CiphersController.cs @@ -1422,11 +1422,9 @@ public class CiphersController : Controller throw new NotFoundException(); } - // Extract lastKnownRevisionDate from form data if present - DateTime? lastKnownRevisionDate = GetLastKnownRevisionDateFromForm(); 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(); } - // Extract lastKnownRevisionDate from form data if present - DateTime? lastKnownRevisionDate = GetLastKnownRevisionDateFromForm(); - await Request.GetFileAsync(async (stream, fileName, key) => { await _cipherService.CreateAttachmentShareAsync(cipher, stream, fileName, key, - Request.ContentLength.GetValueOrDefault(0), attachmentId, organizationId, lastKnownRevisionDate); + Request.ContentLength.GetValueOrDefault(0), attachmentId, organizationId); }); } diff --git a/src/Api/appsettings.Development.json b/src/Api/appsettings.Development.json index 82fb951261..87e92c4516 100644 --- a/src/Api/appsettings.Development.json +++ b/src/Api/appsettings.Development.json @@ -41,6 +41,7 @@ "phishingDomain": { "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" - } + }, + "pricingUri": "https://billingpricing.qa.bitwarden.pw" } } diff --git a/src/Billing/Services/Implementations/StripeEventUtilityService.cs b/src/Billing/Services/Implementations/StripeEventUtilityService.cs index 49e562de56..06a5d8a890 100644 --- a/src/Billing/Services/Implementations/StripeEventUtilityService.cs +++ b/src/Billing/Services/Implementations/StripeEventUtilityService.cs @@ -3,12 +3,12 @@ using Bit.Billing.Constants; using Bit.Core.Billing.Constants; +using Bit.Core.Billing.Models; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; -using Bit.Core.Utilities; using Braintree; using Stripe; using Customer = Stripe.Customer; @@ -112,7 +112,7 @@ public class StripeEventUtilityService : IStripeEventUtilityService } public bool IsSponsoredSubscription(Subscription subscription) => - StaticStore.SponsoredPlans + SponsoredPlans.All .Any(p => subscription.Items .Any(i => i.Plan.Id == p.StripePlanId)); diff --git a/src/Billing/Services/Implementations/UpcomingInvoiceHandler.cs b/src/Billing/Services/Implementations/UpcomingInvoiceHandler.cs index 936da609e9..6db0cb6373 100644 --- a/src/Billing/Services/Implementations/UpcomingInvoiceHandler.cs +++ b/src/Billing/Services/Implementations/UpcomingInvoiceHandler.cs @@ -195,41 +195,48 @@ public class UpcomingInvoiceHandler( Plan plan, bool milestone3) { - if (milestone3 && plan.Type == PlanType.FamiliesAnnually2019) + // currently these are the only plans that need aligned and both require the same flag and share most of the logic + if (!milestone3 || plan.Type is not (PlanType.FamiliesAnnually2019 or PlanType.FamiliesAnnually2025)) { - var passwordManagerItem = - subscription.Items.FirstOrDefault(item => item.Price.Id == plan.PasswordManager.StripePlanId); + return; + } - if (passwordManagerItem == null) - { - logger.LogWarning("Could not find Organization's ({OrganizationId}) password manager item while processing '{EventType}' event ({EventID})", - organization.Id, @event.Type, @event.Id); - return; - } + var passwordManagerItem = + subscription.Items.FirstOrDefault(item => item.Price.Id == plan.PasswordManager.StripePlanId); - var families = await pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually); + if (passwordManagerItem == null) + { + logger.LogWarning("Could not find Organization's ({OrganizationId}) password manager item while processing '{EventType}' event ({EventID})", + organization.Id, @event.Type, @event.Id); + return; + } - organization.PlanType = families.Type; - organization.Plan = families.Name; - organization.UsersGetPremium = families.UsersGetPremium; - organization.Seats = families.PasswordManager.BaseSeats; + var families = await pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually); - var options = new SubscriptionUpdateOptions - { - Items = - [ - new SubscriptionItemOptions - { - Id = passwordManagerItem.Id, + organization.PlanType = families.Type; + organization.Plan = families.Name; + organization.UsersGetPremium = families.UsersGetPremium; + organization.Seats = families.PasswordManager.BaseSeats; + + var options = new SubscriptionUpdateOptions + { + Items = + [ + new SubscriptionItemOptions + { + 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 => item.Price.Id == plan.PasswordManager.StripePremiumAccessPlanId); @@ -253,21 +260,21 @@ public class UpcomingInvoiceHandler( Deleted = true }); } + } - try - { - await organizationRepository.ReplaceAsync(organization); - await stripeFacade.UpdateSubscription(subscription.Id, options); - } - catch (Exception exception) - { - logger.LogError( - exception, - "Failed to align subscription concerns for Organization ({OrganizationID}) while processing '{EventType}' event ({EventID})", - organization.Id, - @event.Type, - @event.Id); - } + try + { + await organizationRepository.ReplaceAsync(organization); + await stripeFacade.UpdateSubscription(subscription.Id, options); + } + catch (Exception exception) + { + logger.LogError( + exception, + "Failed to align subscription concerns for Organization ({OrganizationID}) while processing '{EventType}' event ({EventID})", + organization.Id, + @event.Type, + @event.Id); } } diff --git a/src/Billing/appsettings.Development.json b/src/Billing/appsettings.Development.json index 7c4889c22f..fe8e47b2f6 100644 --- a/src/Billing/appsettings.Development.json +++ b/src/Billing/appsettings.Development.json @@ -35,6 +35,7 @@ "billingSettings": { "onyx": { "personaId": 68 - } - } + } + }, + "pricingUri": "https://billingpricing.qa.bitwarden.pw" } diff --git a/src/Core/AdminConsole/Enums/EventType.cs b/src/Core/AdminConsole/Enums/EventType.cs index 8073938fc5..09cda7ca0e 100644 --- a/src/Core/AdminConsole/Enums/EventType.cs +++ b/src/Core/AdminConsole/Enums/EventType.cs @@ -60,6 +60,7 @@ public enum EventType : int OrganizationUser_RejectedAuthRequest = 1514, OrganizationUser_Deleted = 1515, // Both user and organization user data were deleted OrganizationUser_Left = 1516, // User voluntarily left the organization + OrganizationUser_AutomaticallyConfirmed = 1517, Organization_Updated = 1600, Organization_PurgedVault = 1601, diff --git a/src/Core/AdminConsole/Enums/PolicyType.cs b/src/Core/AdminConsole/Enums/PolicyType.cs index 09fa4ec955..bd6daf7cdf 100644 --- a/src/Core/AdminConsole/Enums/PolicyType.cs +++ b/src/Core/AdminConsole/Enums/PolicyType.cs @@ -21,6 +21,7 @@ public enum PolicyType : byte UriMatchDefaults = 16, AutotypeDefaultSetting = 17, AutomaticUserConfirmation = 18, + BlockClaimedDomainAccountCreation = 19, } public static class PolicyTypeExtensions @@ -52,6 +53,7 @@ public static class PolicyTypeExtensions PolicyType.UriMatchDefaults => "URI match defaults", PolicyType.AutotypeDefaultSetting => "Autotype default setting", PolicyType.AutomaticUserConfirmation => "Automatically confirm invited users", + PolicyType.BlockClaimedDomainAccountCreation => "Block account creation for claimed domains", }; } } diff --git a/src/Core/AdminConsole/Models/Data/OrganizationUsers/AcceptedOrganizationUserToConfirm.cs b/src/Core/AdminConsole/Models/Data/OrganizationUsers/AcceptedOrganizationUserToConfirm.cs new file mode 100644 index 0000000000..0dc6d1c352 --- /dev/null +++ b/src/Core/AdminConsole/Models/Data/OrganizationUsers/AcceptedOrganizationUserToConfirm.cs @@ -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; } +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/AutoConfirmUser/AutomaticallyConfirmOrganizationUserCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/AutoConfirmUser/AutomaticallyConfirmOrganizationUserCommand.cs new file mode 100644 index 0000000000..67b5f0da80 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/AutoConfirmUser/AutomaticallyConfirmOrganizationUserCommand.cs @@ -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 logger) : IAutomaticallyConfirmOrganizationUserCommand +{ + public async Task AutomaticallyConfirmOrganizationUserAsync(AutomaticallyConfirmOrganizationUserRequest request) + { + var validatorRequest = await RetrieveDataAsync(request); + + var validatedData = await validator.ValidateAsync(validatorRequest); + + return await validatedData.Match>( + 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."); + } + } + + /// + /// Determines whether a default collection should be created for an organization user during the confirmation process. + /// + /// + /// The validation request containing information about the user, organization, and collection settings. + /// + /// The result is a boolean value indicating whether a default collection should be created. + private async Task ShouldCreateDefaultCollectionAsync(AutomaticallyConfirmOrganizationUserValidationRequest request) => + !string.IsNullOrWhiteSpace(request.DefaultUserCollectionName) + && (await policyRequirementQuery.GetAsync(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 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) + }; + } +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/AutoConfirmUser/AutomaticallyConfirmOrganizationUserRequest.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/AutoConfirmUser/AutomaticallyConfirmOrganizationUserRequest.cs new file mode 100644 index 0000000000..fcc8dacf66 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/AutoConfirmUser/AutomaticallyConfirmOrganizationUserRequest.cs @@ -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; + +/// +/// Automatically Confirm User Command Request +/// +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; } +} + +/// +/// Automatically Confirm User Validation Request +/// +/// +/// This is used to hold retrieved data and pass it to the validator +/// +public record AutomaticallyConfirmOrganizationUserValidationRequest : AutomaticallyConfirmOrganizationUserRequest +{ + public OrganizationUser? OrganizationUser { get; set; } + public Organization? Organization { get; set; } +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/AutoConfirmUser/AutomaticallyConfirmOrganizationUsersValidator.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/AutoConfirmUser/AutomaticallyConfirmOrganizationUsersValidator.cs new file mode 100644 index 0000000000..11b89de680 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/AutoConfirmUser/AutomaticallyConfirmOrganizationUsersValidator.cs @@ -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> 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 OrganizationHasAutomaticallyConfirmUsersPolicyEnabledAsync( + AutomaticallyConfirmOrganizationUserValidationRequest request) => + await policyRepository.GetByOrganizationIdTypeAsync(request.OrganizationId, + PolicyType.AutomaticUserConfirmation) is { Enabled: true } + && request.Organization is { UseAutomaticUserConfirmation: true }; + + private async Task 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(request.OrganizationUser.UserId!.Value)) + .IsTwoFactorRequiredForOrganization(request.Organization!.Id); + } + + private async Task OrganizationUserConformsToSingleOrgPolicyAsync( + AutomaticallyConfirmOrganizationUserValidationRequest request) + { + var allOrganizationUsersForUser = await organizationUserRepository + .GetManyByUserAsync(request.OrganizationUser!.UserId!.Value); + + if (allOrganizationUsersForUser.Count == 1) + { + return null; + } + + var policyRequirement = await policyRequirementQuery + .GetAsync(request.OrganizationUser!.UserId!.Value); + + if (policyRequirement.IsSingleOrgEnabledForThisOrganization(request.Organization!.Id)) + { + return new OrganizationEnforcesSingleOrgPolicy(); + } + + if (policyRequirement.IsSingleOrgEnabledForOrganizationsOtherThan(request.Organization.Id)) + { + return new OtherOrganizationEnforcesSingleOrgPolicy(); + } + + return null; + } +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/AutoConfirmUser/Errors.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/AutoConfirmUser/Errors.cs new file mode 100644 index 0000000000..1564daca6c --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/AutoConfirmUser/Errors.cs @@ -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."); diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/AutoConfirmUser/IAutomaticallyConfirmOrganizationUsersValidator.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/AutoConfirmUser/IAutomaticallyConfirmOrganizationUsersValidator.cs new file mode 100644 index 0000000000..544b65b53f --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/AutoConfirmUser/IAutomaticallyConfirmOrganizationUsersValidator.cs @@ -0,0 +1,9 @@ +using Bit.Core.AdminConsole.Utilities.v2.Validation; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUser; + +public interface IAutomaticallyConfirmOrganizationUsersValidator +{ + Task> ValidateAsync( + AutomaticallyConfirmOrganizationUserValidationRequest request); +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccount/DeleteClaimedOrganizationUserAccountCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccount/DeleteClaimedOrganizationUserAccountCommand.cs index 87c24c3ab4..c5c423f2bb 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccount/DeleteClaimedOrganizationUserAccountCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccount/DeleteClaimedOrganizationUserAccountCommand.cs @@ -1,4 +1,6 @@ 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.Enums; using Bit.Core.Exceptions; diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccount/DeleteClaimedOrganizationUserAccountValidator.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccount/DeleteClaimedOrganizationUserAccountValidator.cs index 315d45ea69..71eff3ae69 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccount/DeleteClaimedOrganizationUserAccountValidator.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccount/DeleteClaimedOrganizationUserAccountValidator.cs @@ -1,8 +1,9 @@ using Bit.Core.AdminConsole.Repositories; +using Bit.Core.AdminConsole.Utilities.v2.Validation; using Bit.Core.Context; using Bit.Core.Enums; 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; diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccount/Errors.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccount/Errors.cs index 6c8f7ee00c..a76104cc88 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccount/Errors.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccount/Errors.cs @@ -1,15 +1,6 @@ -namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccount; +using Bit.Core.AdminConsole.Utilities.v2; -/// -/// A strongly typed error containing a reason that an action failed. -/// This is used for business logic validation and other expected errors, not exceptions. -/// -public abstract record Error(string Message); -/// -/// An type that maps to a NotFoundResult at the api layer. -/// -/// -public abstract record NotFoundError(string Message) : Error(Message); +namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccount; public record UserNotFoundError() : NotFoundError("Invalid user."); public record UserNotClaimedError() : Error("Member is not claimed by the organization."); diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccount/IDeleteClaimedOrganizationUserAccountCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccount/IDeleteClaimedOrganizationUserAccountCommand.cs index 983a3a4f21..408d3e8bcd 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccount/IDeleteClaimedOrganizationUserAccountCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccount/IDeleteClaimedOrganizationUserAccountCommand.cs @@ -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 { diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccount/IDeleteClaimedOrganizationUserAccountValidator.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccount/IDeleteClaimedOrganizationUserAccountValidator.cs index f1a2c71b1b..05e97e896a 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccount/IDeleteClaimedOrganizationUserAccountValidator.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccount/IDeleteClaimedOrganizationUserAccountValidator.cs @@ -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 { diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IAutomaticallyConfirmOrganizationUserCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IAutomaticallyConfirmOrganizationUserCommand.cs new file mode 100644 index 0000000000..a1776416ae --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IAutomaticallyConfirmOrganizationUserCommand.cs @@ -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; + +/// +/// Command to automatically confirm an organization user. +/// +/// +/// 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. +/// +public interface IAutomaticallyConfirmOrganizationUserCommand +{ + /// + /// Automatically confirms the organization user based on the provided request data. + /// + /// The request containing necessary information to confirm the organization user. + /// + /// This action has side effects. The side effects are + ///
    + ///
  • Creating an event log entry.
  • + ///
  • Syncing organization keys with the user.
  • + ///
  • Deleting any registered user devices for the organization.
  • + ///
  • Sending an email to the confirmed user.
  • + ///
  • Creating the default collection if applicable.
  • + ///
+ /// + /// 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. + ///
+ /// + /// The result of the command. If there was an error, the result will contain a typed error describing the problem + /// that occurred. + /// + Task AutomaticallyConfirmOrganizationUserAsync(AutomaticallyConfirmOrganizationUserRequest request); +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SingleOrganizationPolicyRequirement.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SingleOrganizationPolicyRequirement.cs new file mode 100644 index 0000000000..d1e1efafd9 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SingleOrganizationPolicyRequirement.cs @@ -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) : 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 +{ + public override PolicyType PolicyType => PolicyType.SingleOrg; + + public override SingleOrganizationPolicyRequirement Create(IEnumerable policyDetails) => + new(policyDetails); +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs index 7c1987865a..e89592f020 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs @@ -53,6 +53,7 @@ public static class PolicyServiceCollectionExtensions services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); } @@ -65,5 +66,6 @@ public static class PolicyServiceCollectionExtensions services.AddScoped, RequireSsoPolicyRequirementFactory>(); services.AddScoped, RequireTwoFactorPolicyRequirementFactory>(); services.AddScoped, MasterPasswordPolicyRequirementFactory>(); + services.AddScoped, SingleOrganizationPolicyRequirementFactory>(); } } diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/BlockClaimedDomainAccountCreationPolicyValidator.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/BlockClaimedDomainAccountCreationPolicyValidator.cs new file mode 100644 index 0000000000..92ba11f5a6 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/BlockClaimedDomainAccountCreationPolicyValidator.cs @@ -0,0 +1,59 @@ +#nullable enable + +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces; +using Bit.Core.Services; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators; + +public class BlockClaimedDomainAccountCreationPolicyValidator : IPolicyValidator, IPolicyValidationEvent +{ + private readonly IOrganizationHasVerifiedDomainsQuery _organizationHasVerifiedDomainsQuery; + private readonly IFeatureService _featureService; + + public BlockClaimedDomainAccountCreationPolicyValidator( + IOrganizationHasVerifiedDomainsQuery organizationHasVerifiedDomainsQuery, + IFeatureService featureService) + { + _organizationHasVerifiedDomainsQuery = organizationHasVerifiedDomainsQuery; + _featureService = featureService; + } + + public PolicyType Type => PolicyType.BlockClaimedDomainAccountCreation; + + // No prerequisites - this policy stands alone + public IEnumerable RequiredPolicies => []; + + public async Task ValidateAsync(SavePolicyModel policyRequest, Policy? currentPolicy) + { + return await ValidateAsync(policyRequest.PolicyUpdate, currentPolicy); + } + + public async Task ValidateAsync(PolicyUpdate policyUpdate, Policy? currentPolicy) + { + // Check if feature is enabled + if (!_featureService.IsEnabled(FeatureFlagKeys.BlockClaimedDomainAccountCreation)) + { + return "This feature is not enabled"; + } + + // Only validate when trying to ENABLE the policy + if (policyUpdate is { Enabled: true }) + { + // Check if organization has at least one verified domain + if (!await _organizationHasVerifiedDomainsQuery.HasVerifiedDomainsAsync(policyUpdate.OrganizationId)) + { + return "You must claim at least one domain to turn on this policy"; + } + } + + // Disabling the policy is always allowed + return string.Empty; + } + + public Task OnSaveSideEffectsAsync(PolicyUpdate policyUpdate, Policy? currentPolicy) + => Task.CompletedTask; +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SingleOrgPolicyValidator.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SingleOrgPolicyValidator.cs index c0378bf5f9..d24c61e258 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SingleOrgPolicyValidator.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SingleOrgPolicyValidator.cs @@ -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.Models.Data; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces; @@ -29,8 +27,6 @@ public class SingleOrgPolicyValidator : IPolicyValidator, IPolicyValidationEvent private readonly IOrganizationRepository _organizationRepository; private readonly ISsoConfigRepository _ssoConfigRepository; private readonly ICurrentContext _currentContext; - private readonly IFeatureService _featureService; - private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand; private readonly IOrganizationHasVerifiedDomainsQuery _organizationHasVerifiedDomainsQuery; private readonly IRevokeNonCompliantOrganizationUserCommand _revokeNonCompliantOrganizationUserCommand; @@ -40,8 +36,6 @@ public class SingleOrgPolicyValidator : IPolicyValidator, IPolicyValidationEvent IOrganizationRepository organizationRepository, ISsoConfigRepository ssoConfigRepository, ICurrentContext currentContext, - IFeatureService featureService, - IRemoveOrganizationUserCommand removeOrganizationUserCommand, IOrganizationHasVerifiedDomainsQuery organizationHasVerifiedDomainsQuery, IRevokeNonCompliantOrganizationUserCommand revokeNonCompliantOrganizationUserCommand) { @@ -50,8 +44,6 @@ public class SingleOrgPolicyValidator : IPolicyValidator, IPolicyValidationEvent _organizationRepository = organizationRepository; _ssoConfigRepository = ssoConfigRepository; _currentContext = currentContext; - _featureService = featureService; - _removeOrganizationUserCommand = removeOrganizationUserCommand; _organizationHasVerifiedDomainsQuery = organizationHasVerifiedDomainsQuery; _revokeNonCompliantOrganizationUserCommand = revokeNonCompliantOrganizationUserCommand; } diff --git a/src/Core/AdminConsole/Repositories/IOrganizationUserRepository.cs b/src/Core/AdminConsole/Repositories/IOrganizationUserRepository.cs index b17de3c51d..bedb9d49ee 100644 --- a/src/Core/AdminConsole/Repositories/IOrganizationUserRepository.cs +++ b/src/Core/AdminConsole/Repositories/IOrganizationUserRepository.cs @@ -1,4 +1,5 @@ using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Models.Data.OrganizationUsers; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models; using Bit.Core.Entities; using Bit.Core.Enums; @@ -93,7 +94,7 @@ public interface IOrganizationUserRepository : IRepository - /// Accepted OrganizationUser to confirm + /// Accepted OrganizationUser to confirm /// True, if the user was updated. False, if not performed. - Task ConfirmOrganizationUserAsync(OrganizationUser organizationUser); + Task ConfirmOrganizationUserAsync(AcceptedOrganizationUserToConfirm organizationUserToConfirm); } diff --git a/src/Core/AdminConsole/Utilities/v2/Errors.cs b/src/Core/AdminConsole/Utilities/v2/Errors.cs new file mode 100644 index 0000000000..c1c66b2630 --- /dev/null +++ b/src/Core/AdminConsole/Utilities/v2/Errors.cs @@ -0,0 +1,15 @@ +namespace Bit.Core.AdminConsole.Utilities.v2; + +/// +/// A strongly typed error containing a reason that an action failed. +/// This is used for business logic validation and other expected errors, not exceptions. +/// +public abstract record Error(string Message); +/// +/// An type that maps to a NotFoundResult at the api layer. +/// +/// +public abstract record NotFoundError(string Message) : Error(Message); + +public abstract record BadRequestError(string Message) : Error(Message); +public abstract record InternalError(string Message) : Error(Message); diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccount/CommandResult.cs b/src/Core/AdminConsole/Utilities/v2/Results/CommandResult.cs similarity index 94% rename from src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccount/CommandResult.cs rename to src/Core/AdminConsole/Utilities/v2/Results/CommandResult.cs index fbb00a908a..fb1bd16b2d 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccount/CommandResult.cs +++ b/src/Core/AdminConsole/Utilities/v2/Results/CommandResult.cs @@ -1,7 +1,7 @@ using OneOf; using OneOf.Types; -namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccount; +namespace Bit.Core.AdminConsole.Utilities.v2.Results; /// /// Represents the result of a command. @@ -39,4 +39,3 @@ public record BulkCommandResult(Guid Id, CommandResult Result); /// A wrapper for with an ID, to identify the result in bulk operations. /// public record BulkCommandResult(Guid Id, CommandResult Result); - diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccount/ValidationResult.cs b/src/Core/AdminConsole/Utilities/v2/Validation/ValidationResult.cs similarity index 94% rename from src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccount/ValidationResult.cs rename to src/Core/AdminConsole/Utilities/v2/Validation/ValidationResult.cs index c84a0aeda1..e28eac9a1c 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccount/ValidationResult.cs +++ b/src/Core/AdminConsole/Utilities/v2/Validation/ValidationResult.cs @@ -1,7 +1,7 @@ using OneOf; using OneOf.Types; -namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccount; +namespace Bit.Core.AdminConsole.Utilities.v2.Validation; /// /// Represents the result of validating a request. diff --git a/src/Core/Auth/UserFeatures/Registration/Implementations/RegisterUserCommand.cs b/src/Core/Auth/UserFeatures/Registration/Implementations/RegisterUserCommand.cs index 4aaa9360a0..baeb24368e 100644 --- a/src/Core/Auth/UserFeatures/Registration/Implementations/RegisterUserCommand.cs +++ b/src/Core/Auth/UserFeatures/Registration/Implementations/RegisterUserCommand.cs @@ -15,16 +15,20 @@ using Bit.Core.Tokens; using Bit.Core.Utilities; using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Logging; using Newtonsoft.Json; namespace Bit.Core.Auth.UserFeatures.Registration.Implementations; public class RegisterUserCommand : IRegisterUserCommand { + private readonly ILogger _logger; private readonly IGlobalSettings _globalSettings; private readonly IOrganizationUserRepository _organizationUserRepository; private readonly IOrganizationRepository _organizationRepository; private readonly IPolicyRepository _policyRepository; + private readonly IOrganizationDomainRepository _organizationDomainRepository; + private readonly IFeatureService _featureService; private readonly IDataProtectorTokenFactory _orgUserInviteTokenDataFactory; private readonly IDataProtectorTokenFactory _registrationEmailVerificationTokenDataFactory; @@ -37,28 +41,32 @@ public class RegisterUserCommand : IRegisterUserCommand private readonly IValidateRedemptionTokenCommand _validateRedemptionTokenCommand; private readonly IDataProtectorTokenFactory _emergencyAccessInviteTokenDataFactory; - private readonly IFeatureService _featureService; private readonly string _disabledUserRegistrationExceptionMsg = "Open registration has been disabled by the system administrator."; public RegisterUserCommand( + ILogger logger, IGlobalSettings globalSettings, IOrganizationUserRepository organizationUserRepository, IOrganizationRepository organizationRepository, IPolicyRepository policyRepository, + IOrganizationDomainRepository organizationDomainRepository, + IFeatureService featureService, IDataProtectionProvider dataProtectionProvider, IDataProtectorTokenFactory orgUserInviteTokenDataFactory, IDataProtectorTokenFactory registrationEmailVerificationTokenDataFactory, IUserService userService, IMailService mailService, IValidateRedemptionTokenCommand validateRedemptionTokenCommand, - IDataProtectorTokenFactory emergencyAccessInviteTokenDataFactory, - IFeatureService featureService) + IDataProtectorTokenFactory emergencyAccessInviteTokenDataFactory) { + _logger = logger; _globalSettings = globalSettings; _organizationUserRepository = organizationUserRepository; _organizationRepository = organizationRepository; _policyRepository = policyRepository; + _organizationDomainRepository = organizationDomainRepository; + _featureService = featureService; _organizationServiceDataProtector = dataProtectionProvider.CreateProtector( "OrganizationServiceDataProtector"); @@ -77,6 +85,8 @@ public class RegisterUserCommand : IRegisterUserCommand public async Task RegisterUser(User user) { + await ValidateEmailDomainNotBlockedAsync(user.Email); + var result = await _userService.CreateUserAsync(user); if (result == IdentityResult.Success) { @@ -102,6 +112,11 @@ public class RegisterUserCommand : IRegisterUserCommand { TryValidateOrgInviteToken(orgInviteToken, orgUserId, user); var orgUser = await SetUserEmail2FaIfOrgPolicyEnabledAsync(orgUserId, user); + if (orgUser == null && orgUserId.HasValue) + { + throw new BadRequestException("Invalid organization user invitation."); + } + await ValidateEmailDomainNotBlockedAsync(user.Email, orgUser?.OrganizationId); user.ApiKey = CoreHelpers.SecureRandomString(30); @@ -265,6 +280,8 @@ public class RegisterUserCommand : IRegisterUserCommand string emailVerificationToken) { ValidateOpenRegistrationAllowed(); + await ValidateEmailDomainNotBlockedAsync(user.Email); + var tokenable = ValidateRegistrationEmailVerificationTokenable(emailVerificationToken, user.Email); user.EmailVerified = true; @@ -284,6 +301,7 @@ public class RegisterUserCommand : IRegisterUserCommand string orgSponsoredFreeFamilyPlanInviteToken) { ValidateOpenRegistrationAllowed(); + await ValidateEmailDomainNotBlockedAsync(user.Email); await ValidateOrgSponsoredFreeFamilyPlanInviteToken(orgSponsoredFreeFamilyPlanInviteToken, user.Email); user.EmailVerified = true; @@ -304,6 +322,7 @@ public class RegisterUserCommand : IRegisterUserCommand string acceptEmergencyAccessInviteToken, Guid acceptEmergencyAccessId) { ValidateOpenRegistrationAllowed(); + await ValidateEmailDomainNotBlockedAsync(user.Email); ValidateAcceptEmergencyAccessInviteToken(acceptEmergencyAccessInviteToken, acceptEmergencyAccessId, user.Email); user.EmailVerified = true; @@ -322,6 +341,7 @@ public class RegisterUserCommand : IRegisterUserCommand string providerInviteToken, Guid providerUserId) { ValidateOpenRegistrationAllowed(); + await ValidateEmailDomainNotBlockedAsync(user.Email); ValidateProviderInviteToken(providerInviteToken, providerUserId, user.Email); user.EmailVerified = true; @@ -387,6 +407,28 @@ public class RegisterUserCommand : IRegisterUserCommand return tokenable; } + private async Task ValidateEmailDomainNotBlockedAsync(string email, Guid? excludeOrganizationId = null) + { + // Only check if feature flag is enabled + if (!_featureService.IsEnabled(FeatureFlagKeys.BlockClaimedDomainAccountCreation)) + { + return; + } + + var emailDomain = EmailValidation.GetDomain(email); + + var isDomainBlocked = await _organizationDomainRepository.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync( + emailDomain, excludeOrganizationId); + if (isDomainBlocked) + { + _logger.LogInformation( + "User registration blocked by domain claim policy. Domain: {Domain}, ExcludedOrgId: {ExcludedOrgId}", + emailDomain, + excludeOrganizationId); + throw new BadRequestException("This email address is claimed by an organization using Bitwarden."); + } + } + /// /// We send different welcome emails depending on whether the user is joining a free/family or an enterprise organization. If information to populate the /// email isn't present we send the standard individual welcome email. diff --git a/src/Core/Auth/UserFeatures/Registration/Implementations/SendVerificationEmailForRegistrationCommand.cs b/src/Core/Auth/UserFeatures/Registration/Implementations/SendVerificationEmailForRegistrationCommand.cs index 3f89e9ad0e..5841cd2e62 100644 --- a/src/Core/Auth/UserFeatures/Registration/Implementations/SendVerificationEmailForRegistrationCommand.cs +++ b/src/Core/Auth/UserFeatures/Registration/Implementations/SendVerificationEmailForRegistrationCommand.cs @@ -5,6 +5,8 @@ using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; using Bit.Core.Tokens; +using Bit.Core.Utilities; +using Microsoft.Extensions.Logging; namespace Bit.Core.Auth.UserFeatures.Registration.Implementations; @@ -15,25 +17,30 @@ namespace Bit.Core.Auth.UserFeatures.Registration.Implementations; /// public class SendVerificationEmailForRegistrationCommand : ISendVerificationEmailForRegistrationCommand { - + private readonly ILogger _logger; private readonly IUserRepository _userRepository; private readonly GlobalSettings _globalSettings; private readonly IMailService _mailService; private readonly IDataProtectorTokenFactory _tokenDataFactory; private readonly IFeatureService _featureService; + private readonly IOrganizationDomainRepository _organizationDomainRepository; public SendVerificationEmailForRegistrationCommand( + ILogger logger, IUserRepository userRepository, GlobalSettings globalSettings, IMailService mailService, IDataProtectorTokenFactory tokenDataFactory, - IFeatureService featureService) + IFeatureService featureService, + IOrganizationDomainRepository organizationDomainRepository) { + _logger = logger; _userRepository = userRepository; _globalSettings = globalSettings; _mailService = mailService; _tokenDataFactory = tokenDataFactory; _featureService = featureService; + _organizationDomainRepository = organizationDomainRepository; } @@ -49,6 +56,20 @@ public class SendVerificationEmailForRegistrationCommand : ISendVerificationEmai throw new ArgumentNullException(nameof(email)); } + // Check if the email domain is blocked by an organization policy + if (_featureService.IsEnabled(FeatureFlagKeys.BlockClaimedDomainAccountCreation)) + { + var emailDomain = EmailValidation.GetDomain(email); + + if (await _organizationDomainRepository.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(emailDomain)) + { + _logger.LogInformation( + "User registration email verification blocked by domain claim policy. Domain: {Domain}", + emailDomain); + throw new BadRequestException("This email address is claimed by an organization using Bitwarden."); + } + } + // Check to see if the user already exists var user = await _userRepository.GetByEmailAsync(email); var userExists = user != null; diff --git a/src/Core/Billing/Models/SponsoredPlans.cs b/src/Core/Billing/Models/SponsoredPlans.cs new file mode 100644 index 0000000000..851c8557d4 --- /dev/null +++ b/src/Core/Billing/Models/SponsoredPlans.cs @@ -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 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)!; +} diff --git a/src/Core/Billing/Organizations/Commands/PreviewOrganizationTaxCommand.cs b/src/Core/Billing/Organizations/Commands/PreviewOrganizationTaxCommand.cs index 89d301c22a..143da0d67f 100644 --- a/src/Core/Billing/Organizations/Commands/PreviewOrganizationTaxCommand.cs +++ b/src/Core/Billing/Organizations/Commands/PreviewOrganizationTaxCommand.cs @@ -3,12 +3,12 @@ using Bit.Core.Billing.Commands; using Bit.Core.Billing.Constants; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Extensions; +using Bit.Core.Billing.Models; using Bit.Core.Billing.Organizations.Models; using Bit.Core.Billing.Payment.Models; using Bit.Core.Billing.Pricing; using Bit.Core.Enums; using Bit.Core.Services; -using Bit.Core.Utilities; using Microsoft.Extensions.Logging; using OneOf; using Stripe; @@ -54,7 +54,7 @@ public class PreviewOrganizationTaxCommand( switch (purchase) { case { PasswordManager.Sponsored: true }: - var sponsoredPlan = StaticStore.GetSponsoredPlan(PlanSponsorshipType.FamiliesForEnterprise); + var sponsoredPlan = SponsoredPlans.Get(PlanSponsorshipType.FamiliesForEnterprise); items.Add(new InvoiceSubscriptionDetailsItemOptions { Price = sponsoredPlan.StripePlanId, diff --git a/src/Core/Billing/Organizations/Models/SponsorOrganizationSubscriptionUpdate.cs b/src/Core/Billing/Organizations/Models/SponsorOrganizationSubscriptionUpdate.cs index ee603c67e0..6c1362d1c5 100644 --- a/src/Core/Billing/Organizations/Models/SponsorOrganizationSubscriptionUpdate.cs +++ b/src/Core/Billing/Organizations/Models/SponsorOrganizationSubscriptionUpdate.cs @@ -1,6 +1,7 @@ // FIXME: Update this file to be null safe and then delete the line below #nullable disable +using Bit.Core.Billing.Models; using Bit.Core.Models.Business; using Stripe; @@ -17,7 +18,7 @@ public class SponsorOrganizationSubscriptionUpdate : SubscriptionUpdate { _existingPlanStripeId = existingPlan.PasswordManager.StripePlanId; _sponsoredPlanStripeId = sponsoredPlan?.StripePlanId - ?? Core.Utilities.StaticStore.SponsoredPlans.FirstOrDefault()?.StripePlanId; + ?? SponsoredPlans.All.FirstOrDefault()?.StripePlanId; _applySponsorship = applySponsorship; } diff --git a/src/Core/Billing/Pricing/PricingClient.cs b/src/Core/Billing/Pricing/PricingClient.cs index 6fdef73885..ecb85ed7e8 100644 --- a/src/Core/Billing/Pricing/PricingClient.cs +++ b/src/Core/Billing/Pricing/PricingClient.cs @@ -6,7 +6,6 @@ using Bit.Core.Billing.Pricing.Organizations; using Bit.Core.Exceptions; using Bit.Core.Services; using Bit.Core.Settings; -using Bit.Core.Utilities; using Microsoft.Extensions.Logging; namespace Bit.Core.Billing.Pricing; @@ -28,13 +27,6 @@ public class PricingClient( return null; } - var usePricingService = featureService.IsEnabled(FeatureFlagKeys.UsePricingService); - - if (!usePricingService) - { - return StaticStore.GetPlan(planType); - } - var lookupKey = GetLookupKey(planType); if (lookupKey == null) @@ -77,13 +69,6 @@ public class PricingClient( return []; } - var usePricingService = featureService.IsEnabled(FeatureFlagKeys.UsePricingService); - - if (!usePricingService) - { - return StaticStore.Plans.ToList(); - } - var response = await httpClient.GetAsync("plans/organization"); if (response.IsSuccessStatusCode) @@ -114,11 +99,10 @@ public class PricingClient( return []; } - var usePricingService = featureService.IsEnabled(FeatureFlagKeys.UsePricingService); var fetchPremiumPriceFromPricingService = featureService.IsEnabled(FeatureFlagKeys.PM26793_FetchPremiumPriceFromPricingService); - if (!usePricingService || !fetchPremiumPriceFromPricingService) + if (!fetchPremiumPriceFromPricingService) { return [CurrentPremiumPlan]; } diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 3d0e7d71c9..c75c9ab1fe 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -141,6 +141,7 @@ public static class FeatureFlagKeys public const string AutomaticConfirmUsers = "pm-19934-auto-confirm-organization-users"; public const string PM23845_VNextApplicationCache = "pm-24957-refactor-memory-application-cache"; public const string AccountRecoveryCommand = "pm-25581-prevent-provider-account-recovery"; + public const string BlockClaimedDomainAccountCreation = "pm-28297-block-uninvited-claimed-domain-registration"; public const string PolicyValidatorsRefactor = "pm-26423-refactor-policy-side-effects"; /* Architecture */ @@ -185,7 +186,6 @@ public static class FeatureFlagKeys /* Billing Team */ 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 PM21821_ProviderPortalTakeover = "pm-21821-provider-portal-takeover"; public const string PM22415_TaxIDWarnings = "pm-22415-tax-id-warnings"; @@ -243,7 +243,6 @@ public static class FeatureFlagKeys /* Vault Team */ public const string PM8851_BrowserOnboardingNudge = "pm-8851-browser-onboarding-nudge"; - public const string PM9111ExtensionPersistAddEditForm = "pm-9111-extension-persist-add-edit-form"; public const string CipherKeyEncryption = "cipher-key-encryption"; public const string PM19941MigrateCipherDomainToSdk = "pm-19941-migrate-cipher-domain-to-sdk"; public const string EndUserNotifications = "pm-10609-end-user-notifications"; diff --git a/src/Core/Models/Business/SubscriptionInfo.cs b/src/Core/Models/Business/SubscriptionInfo.cs index be514cb39f..68a060b4a8 100644 --- a/src/Core/Models/Business/SubscriptionInfo.cs +++ b/src/Core/Models/Business/SubscriptionInfo.cs @@ -1,4 +1,5 @@ using Bit.Core.Billing.Extensions; +using Bit.Core.Billing.Models; using Stripe; #nullable enable @@ -150,7 +151,7 @@ public class SubscriptionInfo } 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; } diff --git a/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs b/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs index 8cfd0a8df1..91504b0b9b 100644 --- a/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs +++ b/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs @@ -14,6 +14,7 @@ using Bit.Core.AdminConsole.OrganizationFeatures.Organizations; using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers; 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.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers; @@ -135,6 +136,8 @@ public static class OrganizationServiceCollectionExtensions services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/CloudSyncSponsorshipsCommand.cs b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/CloudSyncSponsorshipsCommand.cs index 2756f8930b..566c723692 100644 --- a/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/CloudSyncSponsorshipsCommand.cs +++ b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/CloudSyncSponsorshipsCommand.cs @@ -1,5 +1,6 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.Billing.Extensions; +using Bit.Core.Billing.Models; using Bit.Core.Entities; using Bit.Core.Enums; 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.Repositories; using Bit.Core.Services; -using Bit.Core.Utilities; namespace Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Cloud; @@ -54,10 +54,9 @@ public class CloudSyncSponsorshipsCommand : ICloudSyncSponsorshipsCommand foreach (var selfHostedSponsorship in sponsorshipsData) { - var requiredSponsoringProductType = StaticStore.GetSponsoredPlan(selfHostedSponsorship.PlanSponsorshipType)?.SponsoringProductTierType; + var requiredSponsoringProductType = SponsoredPlans.Get(selfHostedSponsorship.PlanSponsorshipType).SponsoringProductTierType; var sponsoringOrgProductTier = sponsoringOrg.PlanType.GetProductTier(); - if (requiredSponsoringProductType == null - || sponsoringOrgProductTier != requiredSponsoringProductType.Value) + if (sponsoringOrgProductTier != requiredSponsoringProductType) { continue; // prevent unsupported sponsorships } diff --git a/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/SetUpSponsorshipCommand.cs b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/SetUpSponsorshipCommand.cs index a54106481c..0aebc3fc3b 100644 --- a/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/SetUpSponsorshipCommand.cs +++ b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/SetUpSponsorshipCommand.cs @@ -1,11 +1,11 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.Billing.Extensions; +using Bit.Core.Billing.Models; using Bit.Core.Entities; using Bit.Core.Exceptions; using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces; using Bit.Core.Repositories; using Bit.Core.Services; -using Bit.Core.Utilities; namespace Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Cloud; @@ -50,11 +50,10 @@ public class SetUpSponsorshipCommand : ISetUpSponsorshipCommand } // 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(); - if (requiredSponsoredProductType == null || - sponsoredOrganizationProductTier != requiredSponsoredProductType.Value) + if (sponsoredOrganizationProductTier != requiredSponsoredProductType) { throw new BadRequestException("Can only redeem sponsorship offer on families organizations."); } diff --git a/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/ValidateSponsorshipCommand.cs b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/ValidateSponsorshipCommand.cs index dcda77acea..a26d553570 100644 --- a/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/ValidateSponsorshipCommand.cs +++ b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/ValidateSponsorshipCommand.cs @@ -3,6 +3,7 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.Billing.Extensions; +using Bit.Core.Billing.Models; using Bit.Core.Entities; using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces; using Bit.Core.Repositories; @@ -95,7 +96,7 @@ public class ValidateSponsorshipCommand : CancelSponsorshipCommand, IValidateSpo return false; } - var sponsoredPlan = Utilities.StaticStore.GetSponsoredPlan(existingSponsorship.PlanSponsorshipType.Value); + var sponsoredPlan = SponsoredPlans.Get(existingSponsorship.PlanSponsorshipType.Value); var sponsoringOrganization = await _organizationRepository .GetByIdAsync(existingSponsorship.SponsoringOrganizationId.Value); diff --git a/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/CreateSponsorshipCommand.cs b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/CreateSponsorshipCommand.cs index a729937fad..ab4b17d215 100644 --- a/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/CreateSponsorshipCommand.cs +++ b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/CreateSponsorshipCommand.cs @@ -1,5 +1,6 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.Billing.Extensions; +using Bit.Core.Billing.Models; using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Enums; @@ -7,7 +8,6 @@ using Bit.Core.Exceptions; using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces; using Bit.Core.Repositories; using Bit.Core.Services; -using Bit.Core.Utilities; 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."); } - var requiredSponsoringProductType = StaticStore.GetSponsoredPlan(sponsorshipType)?.SponsoringProductTierType; + var requiredSponsoringProductType = SponsoredPlans.Get(sponsorshipType).SponsoringProductTierType; var sponsoringOrgProductTier = sponsoringOrganization.PlanType.GetProductTier(); - if (requiredSponsoringProductType == null || - sponsoringOrgProductTier != requiredSponsoringProductType.Value) + if (sponsoringOrgProductTier != requiredSponsoringProductType) { throw new BadRequestException("Specified Organization cannot sponsor other organizations."); } diff --git a/src/Core/Repositories/IOrganizationDomainRepository.cs b/src/Core/Repositories/IOrganizationDomainRepository.cs index d802fe65df..b993cd42fa 100644 --- a/src/Core/Repositories/IOrganizationDomainRepository.cs +++ b/src/Core/Repositories/IOrganizationDomainRepository.cs @@ -17,4 +17,5 @@ public interface IOrganizationDomainRepository : IRepository GetDomainByOrgIdAndDomainNameAsync(Guid orgId, string domainName); Task> GetExpiredOrganizationDomainsAsync(); Task DeleteExpiredAsync(int expirationPeriod); + Task HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(string domainName, Guid? excludeOrganizationId = null); } diff --git a/src/Core/Services/Implementations/StripePaymentService.cs b/src/Core/Services/Implementations/StripePaymentService.cs index 5dd1ff50e7..4c64abc73e 100644 --- a/src/Core/Services/Implementations/StripePaymentService.cs +++ b/src/Core/Services/Implementations/StripePaymentService.cs @@ -67,7 +67,7 @@ public class StripePaymentService : IPaymentService { var existingPlan = await _pricingClient.GetPlanOrThrow(org.PlanType); var sponsoredPlan = sponsorship?.PlanSponsorshipType != null - ? Utilities.StaticStore.GetSponsoredPlan(sponsorship.PlanSponsorshipType.Value) + ? SponsoredPlans.Get(sponsorship.PlanSponsorshipType.Value) : null; var subscriptionUpdate = new SponsorOrganizationSubscriptionUpdate(existingPlan, sponsoredPlan, applySponsorship); @@ -1072,7 +1072,7 @@ public class StripePaymentService : IPaymentService if (isSponsored) { - var sponsoredPlan = Utilities.StaticStore.GetSponsoredPlan(parameters.PasswordManager.SponsoredPlan.Value); + var sponsoredPlan = SponsoredPlans.Get(parameters.PasswordManager.SponsoredPlan.Value); options.SubscriptionDetails.Items.Add( new InvoiceSubscriptionDetailsItemOptions { Quantity = 1, Plan = sponsoredPlan.StripePlanId } ); diff --git a/src/Core/Settings/GlobalSettings.cs b/src/Core/Settings/GlobalSettings.cs index e2c2168656..147b88623a 100644 --- a/src/Core/Settings/GlobalSettings.cs +++ b/src/Core/Settings/GlobalSettings.cs @@ -21,7 +21,7 @@ public class GlobalSettings : IGlobalSettings } public bool SelfHosted { get; set; } - public bool UnifiedDeployment { get; set; } + public bool LiteDeployment { get; set; } public virtual string KnownProxies { get; set; } public virtual string SiteName { get; set; } public virtual string ProjectName { get; set; } @@ -783,7 +783,18 @@ public class GlobalSettings : IGlobalSettings { public virtual IConnectionStringSettings Redis { get; set; } = new ConnectionStringSettings(); public virtual IConnectionStringSettings Cosmos { get; set; } = new ConnectionStringSettings(); + public ExtendedCacheSettings DefaultExtendedCache { get; set; } = new ExtendedCacheSettings(); + } + /// + /// A collection of Settings for customizing the FusionCache used in extended caching. Defaults are + /// provided for every attribute so that only specific values need to be overridden if needed. + /// + public class ExtendedCacheSettings + { + public bool EnableDistributedCache { get; set; } = true; + public bool UseSharedRedisCache { get; set; } = true; + public IConnectionStringSettings Redis { get; set; } = new ConnectionStringSettings(); public TimeSpan Duration { get; set; } = TimeSpan.FromMinutes(30); public bool IsFailSafeEnabled { get; set; } = true; public TimeSpan FailSafeMaxDuration { get; set; } = TimeSpan.FromHours(2); diff --git a/src/Core/Settings/IGlobalSettings.cs b/src/Core/Settings/IGlobalSettings.cs index d77842373e..0fc99d63e3 100644 --- a/src/Core/Settings/IGlobalSettings.cs +++ b/src/Core/Settings/IGlobalSettings.cs @@ -6,7 +6,7 @@ public interface IGlobalSettings { // This interface exists for testing. Add settings here as needed for testing bool SelfHosted { get; set; } - bool UnifiedDeployment { get; set; } + bool LiteDeployment { get; set; } string KnownProxies { get; set; } string ProjectName { get; set; } bool EnableCloudCommunication { get; set; } diff --git a/src/Core/Utilities/EmailValidation.cs b/src/Core/Utilities/EmailValidation.cs index f6832945af..10892f85c4 100644 --- a/src/Core/Utilities/EmailValidation.cs +++ b/src/Core/Utilities/EmailValidation.cs @@ -1,4 +1,6 @@ -using System.Text.RegularExpressions; +using System.Net.Mail; +using System.Text.RegularExpressions; +using Bit.Core.Exceptions; using MimeKit; namespace Bit.Core.Utilities; @@ -41,4 +43,22 @@ public static class EmailValidation return true; } + + /// + /// Extracts the domain portion from an email address and normalizes it to lowercase. + /// + /// The email address to extract the domain from. + /// The domain portion of the email address in lowercase (e.g., "example.com"). + /// Thrown when the email address format is invalid. + public static string GetDomain(string email) + { + try + { + return new MailAddress(email).Host.ToLower(); + } + catch (Exception ex) when (ex is FormatException || ex is ArgumentException) + { + throw new BadRequestException("Invalid email address format."); + } + } } diff --git a/src/Core/Utilities/ExtendedCacheServiceCollectionExtensions.cs b/src/Core/Utilities/ExtendedCacheServiceCollectionExtensions.cs index 3f926fd468..a928240fd7 100644 --- a/src/Core/Utilities/ExtendedCacheServiceCollectionExtensions.cs +++ b/src/Core/Utilities/ExtendedCacheServiceCollectionExtensions.cs @@ -3,6 +3,7 @@ using Bit.Core.Utilities; using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Caching.StackExchangeRedis; using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Logging; using StackExchange.Redis; using ZiggyCreatures.Caching.Fusion; using ZiggyCreatures.Caching.Fusion.Backplane; @@ -14,77 +15,149 @@ namespace Microsoft.Extensions.DependencyInjection; public static class ExtendedCacheServiceCollectionExtensions { /// - /// Add Fusion Cache to the service - /// collection.
+ /// Adds a new, named Fusion Cache to the service + /// collection. If an existing cache of the same name is found, it will do nothing.
///
- /// If Redis is configured, it uses Redis for an L2 cache and backplane. If not, it simply uses in-memory caching. + /// Note: When re-using the existing Redis cache, it is expected to call this method after calling + /// services.AddDistributedCache(globalSettings)
This ensures that DI correctly finds, + /// configures, and re-uses all the shared Redis architecture. ///
- public static IServiceCollection TryAddExtendedCacheServices(this IServiceCollection services, GlobalSettings globalSettings) + public static IServiceCollection AddExtendedCache( + this IServiceCollection services, + string cacheName, + GlobalSettings globalSettings, + GlobalSettings.ExtendedCacheSettings? settings = null) { - if (services.Any(s => s.ServiceType == typeof(IFusionCache))) + settings ??= globalSettings.DistributedCache.DefaultExtendedCache; + if (settings is null || string.IsNullOrEmpty(cacheName)) { return services; } - var fusionCacheBuilder = services.AddFusionCache() - .WithOptions(options => + // If a cache already exists with this key, do nothing + if (services.Any(s => s.ServiceType == typeof(IFusionCache) && + s.ServiceKey?.Equals(cacheName) == true)) + { + return services; + } + + if (services.All(s => s.ServiceType != typeof(FusionCacheSystemTextJsonSerializer))) + { + services.AddFusionCacheSystemTextJsonSerializer(); + } + var fusionCacheBuilder = services + .AddFusionCache(cacheName) + .WithCacheKeyPrefix($"{cacheName}:") + .AsKeyedServiceByCacheName() + .WithOptions(opt => { - options.DistributedCacheCircuitBreakerDuration = globalSettings.DistributedCache.DistributedCacheCircuitBreakerDuration; + opt.DistributedCacheCircuitBreakerDuration = settings.DistributedCacheCircuitBreakerDuration; }) .WithDefaultEntryOptions(new FusionCacheEntryOptions { - Duration = globalSettings.DistributedCache.Duration, - IsFailSafeEnabled = globalSettings.DistributedCache.IsFailSafeEnabled, - FailSafeMaxDuration = globalSettings.DistributedCache.FailSafeMaxDuration, - FailSafeThrottleDuration = globalSettings.DistributedCache.FailSafeThrottleDuration, - EagerRefreshThreshold = globalSettings.DistributedCache.EagerRefreshThreshold, - FactorySoftTimeout = globalSettings.DistributedCache.FactorySoftTimeout, - FactoryHardTimeout = globalSettings.DistributedCache.FactoryHardTimeout, - DistributedCacheSoftTimeout = globalSettings.DistributedCache.DistributedCacheSoftTimeout, - DistributedCacheHardTimeout = globalSettings.DistributedCache.DistributedCacheHardTimeout, - AllowBackgroundDistributedCacheOperations = globalSettings.DistributedCache.AllowBackgroundDistributedCacheOperations, - JitterMaxDuration = globalSettings.DistributedCache.JitterMaxDuration + Duration = settings.Duration, + IsFailSafeEnabled = settings.IsFailSafeEnabled, + FailSafeMaxDuration = settings.FailSafeMaxDuration, + FailSafeThrottleDuration = settings.FailSafeThrottleDuration, + EagerRefreshThreshold = settings.EagerRefreshThreshold, + FactorySoftTimeout = settings.FactorySoftTimeout, + FactoryHardTimeout = settings.FactoryHardTimeout, + DistributedCacheSoftTimeout = settings.DistributedCacheSoftTimeout, + DistributedCacheHardTimeout = settings.DistributedCacheHardTimeout, + AllowBackgroundDistributedCacheOperations = settings.AllowBackgroundDistributedCacheOperations, + JitterMaxDuration = settings.JitterMaxDuration }) - .WithSerializer( - new FusionCacheSystemTextJsonSerializer() - ); + .WithRegisteredSerializer(); - if (!CoreHelpers.SettingHasValue(globalSettings.DistributedCache.Redis.ConnectionString)) - { + if (!settings.EnableDistributedCache) return services; - } - services.TryAddSingleton(sp => - ConnectionMultiplexer.Connect(globalSettings.DistributedCache.Redis.ConnectionString)); + if (settings.UseSharedRedisCache) + { + // Using Shared Redis, TryAdd and reuse all pieces (multiplexer, distributed cache and backplane) - fusionCacheBuilder - .WithDistributedCache(sp => + if (!CoreHelpers.SettingHasValue(globalSettings.DistributedCache.Redis.ConnectionString)) + return services; + + services.TryAddSingleton(sp => + CreateConnectionMultiplexer(sp, cacheName, globalSettings.DistributedCache.Redis.ConnectionString)); + + services.TryAddSingleton(sp => { - var cache = sp.GetService(); - if (cache is not null) - { - return cache; - } var mux = sp.GetRequiredService(); return new RedisCache(new RedisCacheOptions { ConnectionMultiplexerFactory = () => Task.FromResult(mux) }); - }) - .WithBackplane(sp => - { - var backplane = sp.GetService(); - if (backplane is not null) + }); + + services.TryAddSingleton(sp => { - return backplane; - } - var mux = sp.GetRequiredService(); + var mux = sp.GetRequiredService(); + return new RedisBackplane(new RedisBackplaneOptions + { + ConnectionMultiplexerFactory = () => Task.FromResult(mux) + }); + }); + + fusionCacheBuilder + .WithRegisteredDistributedCache() + .WithRegisteredBackplane(); + + return services; + } + + // Using keyed Redis / Distributed Cache. Create all pieces as keyed services. + + if (!CoreHelpers.SettingHasValue(settings.Redis.ConnectionString)) + return services; + + services.TryAddKeyedSingleton( + cacheName, + (sp, _) => CreateConnectionMultiplexer(sp, cacheName, settings.Redis.ConnectionString) + ); + services.TryAddKeyedSingleton( + cacheName, + (sp, _) => + { + var mux = sp.GetRequiredKeyedService(cacheName); + return new RedisCache(new RedisCacheOptions + { + ConnectionMultiplexerFactory = () => Task.FromResult(mux) + }); + } + ); + services.TryAddKeyedSingleton( + cacheName, + (sp, _) => + { + var mux = sp.GetRequiredKeyedService(cacheName); return new RedisBackplane(new RedisBackplaneOptions { ConnectionMultiplexerFactory = () => Task.FromResult(mux) }); - }); + } + ); + + fusionCacheBuilder + .WithRegisteredKeyedDistributedCacheByCacheName() + .WithRegisteredKeyedBackplaneByCacheName(); return services; } + + private static ConnectionMultiplexer CreateConnectionMultiplexer(IServiceProvider sp, string cacheName, + string connectionString) + { + try + { + return ConnectionMultiplexer.Connect(connectionString); + } + catch (Exception ex) + { + var logger = sp.GetService(); + logger?.LogError(ex, "Failed to connect to Redis for cache {CacheName}", cacheName); + throw; + } + } } diff --git a/src/Core/Utilities/README.md b/src/Core/Utilities/README.md new file mode 100644 index 0000000000..d2de7bf84f --- /dev/null +++ b/src/Core/Utilities/README.md @@ -0,0 +1,157 @@ +## Extended Cache + +`ExtendedCache` is a wrapper around [FusionCache](https://github.com/ZiggyCreatures/FusionCache) +that provides a simple way to register **named, isolated caches** with sensible defaults. +The goal is to make it trivial for each subsystem or feature to have its own cache - +with optional distributed caching and backplane support - without repeatedly wiring up +FusionCache, Redis, and related infrastructure. + +Each named cache automatically receives: + +- Its own `FusionCache` instance +- Its own configuration (default or overridden) +- Its own key prefix +- Optional distributed store +- Optional backplane + +`ExtendedCache` supports several deployment modes: + +- **Memory-only caching** (with stampede protection) +- **Memory + distributed cache + backplane** using the **shared** application Redis +- **Memory + distributed cache + backplane** using a **fully isolated** Redis instance + +**Note**: When using the shared Redis cache option (which is on by default, if the +Redis connection string is configured), it is expected to call +`services.AddDistributedCache(globalSettings)` **before** calling +`AddExtendedCache`. The idea is to set up the distributed cache in our normal pattern +and then "extend" it to include more functionality. + +### Configuration + +`ExtendedCache` exposes a set of default properties that define how each named cache behaves. +These map directly to FusionCache configuration options such as timeouts, duration, +jitter, fail-safe mode, etc. Any cache can override these defaults independently. + +#### Default configuration + +The simplest approach registers a new named cache with default settings and reusing +the existing distributed cache: + +``` csharp + services.AddDistributedCache(globalSettings); + services.AddExtendedCache(cacheName, globalSettings); +``` + +By default: + - If `GlobalSettings.DistributedCache.Redis.ConnectionString` is configured: + - The cache is memory + distributed (Redis) + - The Redis cache created by `AddDistributedCache` is re-used + - A Redis backplane is configured, re-using the same multiplexer + - If Redis is **not** configured the cache automatically falls back to memory-only + +#### Overriding default properties + +A number of default properties are provided (see +`GlobalSettings.DistributedCache.DefaultExtendedCache` for specific values). A named +cache can override any (or all) of these properties simply by providing its own +instance of `ExtendedCacheSettings`: + +``` csharp + services.AddExtendedCache(cacheName, globalSettings, new GlobalSettings.ExtendedCacheSettings + { + Duration = TimeSpan.FromHours(1), + }); +``` + +This example keeps all other defaults—including shared Redis—but changes the +default cached item duration from 30 minutes to 1 hour. + +#### Isolated Redis configuration + +ExtendedCache can also run in a fully isolated mode where the cache uses its own: + - Redis multiplexer + - Distributed cache + - Backplane + +To enable this, specify a Redis connection string and set `UseSharedRedisCache` +to `false`: + +``` csharp + services.AddExtendedCache(cacheName, globalSettings, new GlobalSettings.ExtendedCacheSettings + { + UseSharedRedisCache = false, + Redis = new GlobalSettings.ConnectionStringSettings { ConnectionString = "localhost:6379" } + }); +``` + +When configured this way: + - A dedicated `IConnectionMultiplexer` is created + - A dedicated `IDistributedCache` is created + - A dedicated FusionCache backplane is created + - All three are exposed to DI as keyed services (using the cache name as service key) + +### Accessing a named cache + +A named cache can be retrieved either: + - Directly via DI using keyed services + - Through `IFusionCacheProvider` (similar to + [IHttpClientFactory](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/http-requests?view=aspnetcore-7.0#named-clients)) + +#### Keyed service + +In the consuming class, declare an IFusionCache field: + +```csharp + private IFusionCache _cache; +``` + +Then ask DI to inject the keyed cache: + +```csharp + public MyService([FromKeyedServices("MyCache")] IFusionCache cache) + { + _cache = cache; + } +``` + +Or request it manually: + +```csharp + cache: provider.GetRequiredKeyedService(serviceKey: cacheName) +``` + +#### Injecting a provider + +Alternatively, an `IFusionCacheProvider` can be injected and used to request a named +cache - similar to how `IHttpClientFactory` can be used to create named `HttpClient` +instances + +In the class using the cache, use an injected provider to request the named cache: + +```csharp + private readonly IFusionCache _cache; + + public MyController(IFusionCacheProvider cacheProvider) + { + _cache = cacheProvider.GetCache("CacheName"); + } +``` + +### Using a cache + +Using the cache in code is as simple as replacing the direct repository calls with +`FusionCache`'s `GetOrSet` call. If the class previously fetched an `Item` from +an `ItemRepository`, all that we need to do is provide a key and the original +repository call as the fallback: + +```csharp + var item = _cache.GetOrSet( + $"item:{id}", + _ => _itemRepository.GetById(id) + ); +``` + +`ExtendedCache` doesn’t change how `FusionCache` is used in code, which means all +the functionality and full `FusionCache` API is available. See the +[FusionCache docs](https://github.com/ZiggyCreatures/FusionCache/blob/main/docs/CoreMethods.md) +for more details. diff --git a/src/Core/Utilities/StaticStore.cs b/src/Core/Utilities/StaticStore.cs index 36c4a54ae4..f0fbd80c38 100644 --- a/src/Core/Utilities/StaticStore.cs +++ b/src/Core/Utilities/StaticStore.cs @@ -1,13 +1,7 @@ // FIXME: Update this file to be null safe and then delete the line below #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.Models.Data.Organizations.OrganizationUsers; -using Bit.Core.Models.StaticStore; namespace Bit.Core.Utilities; @@ -110,56 +104,7 @@ public static class StaticStore GlobalDomains.Add(GlobalEquivalentDomainsType.Atlassian, new List { "atlassian.com", "bitbucket.org", "trello.com", "statuspage.io", "atlassian.net", "jira.com" }); GlobalDomains.Add(GlobalEquivalentDomainsType.Pinterest, new List { "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 - - Plans = new List - { - 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> GlobalDomains { get; set; } - [Obsolete("Use PricingClient.ListPlans to retrieve all plans.")] - public static IEnumerable Plans { get; } - public static IEnumerable 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); } diff --git a/src/Core/Vault/Services/ICipherService.cs b/src/Core/Vault/Services/ICipherService.cs index 110d4b6ea4..765dae30c1 100644 --- a/src/Core/Vault/Services/ICipherService.cs +++ b/src/Core/Vault/Services/ICipherService.cs @@ -17,7 +17,7 @@ public interface ICipherService Task CreateAttachmentAsync(Cipher cipher, Stream stream, string fileName, string key, long requestLength, Guid savingUserId, bool orgAdmin = false, DateTime? lastKnownRevisionDate = null); 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 DeleteManyAsync(IEnumerable cipherIds, Guid deletingUserId, Guid? organizationId = null, bool orgAdmin = false); Task DeleteAttachmentAsync(Cipher cipher, string attachmentId, Guid deletingUserId, bool orgAdmin = false); @@ -34,7 +34,7 @@ public interface ICipherService Task SoftDeleteManyAsync(IEnumerable cipherIds, Guid deletingUserId, Guid? organizationId = null, bool orgAdmin = false); Task RestoreAsync(CipherDetails cipherDetails, Guid restoringUserId, bool orgAdmin = false); Task> RestoreManyAsync(IEnumerable 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 GetAttachmentDownloadDataAsync(Cipher cipher, string attachmentId); Task ValidateCipherAttachmentFile(Cipher cipher, CipherAttachment.MetaData attachmentData); Task ValidateBulkCollectionAssignmentAsync(IEnumerable collectionIds, IEnumerable cipherIds, Guid userId); diff --git a/src/Core/Vault/Services/Implementations/CipherService.cs b/src/Core/Vault/Services/Implementations/CipherService.cs index 4e980f66b6..cbf4ec81e3 100644 --- a/src/Core/Vault/Services/Implementations/CipherService.cs +++ b/src/Core/Vault/Services/Implementations/CipherService.cs @@ -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) { 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, - long requestLength, string attachmentId, Guid organizationId, DateTime? lastKnownRevisionDate = null) + long requestLength, string attachmentId, Guid organizationId) { try { - ValidateCipherLastKnownRevisionDate(cipher, lastKnownRevisionDate); if (requestLength < 1) { throw new BadRequestException("No data to attach."); diff --git a/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationUserRepository.cs b/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationUserRepository.cs index ed5708844d..af52021ca7 100644 --- a/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationUserRepository.cs +++ b/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationUserRepository.cs @@ -2,6 +2,7 @@ using System.Text.Json; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Models.Data.OrganizationUsers; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models; using Bit.Core.AdminConsole.Utilities.DebuggingInstruments; using Bit.Core.Entities; @@ -671,7 +672,7 @@ public class OrganizationUserRepository : Repository, IO commandType: CommandType.StoredProcedure); } - public async Task ConfirmOrganizationUserAsync(OrganizationUser organizationUser) + public async Task ConfirmOrganizationUserAsync(AcceptedOrganizationUserToConfirm organizationUserToConfirm) { await using var connection = new SqlConnection(_marsConnectionString); @@ -679,10 +680,10 @@ public class OrganizationUserRepository : Repository, IO $"[{Schema}].[OrganizationUser_ConfirmById]", new { - organizationUser.Id, - organizationUser.UserId, + Id = organizationUserToConfirm.OrganizationUserId, + UserId = organizationUserToConfirm.UserId, RevisionDate = DateTime.UtcNow.Date, - Key = organizationUser.Key + Key = organizationUserToConfirm.Key }); return rowCount > 0; diff --git a/src/Infrastructure.Dapper/Repositories/OrganizationDomainRepository.cs b/src/Infrastructure.Dapper/Repositories/OrganizationDomainRepository.cs index 91cbc40ff6..a8171c286b 100644 --- a/src/Infrastructure.Dapper/Repositories/OrganizationDomainRepository.cs +++ b/src/Infrastructure.Dapper/Repositories/OrganizationDomainRepository.cs @@ -148,4 +148,16 @@ public class OrganizationDomainRepository : Repository commandType: CommandType.StoredProcedure) > 0; } } + + public async Task HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(string domainName, Guid? excludeOrganizationId = null) + { + await using var connection = new SqlConnection(ConnectionString); + + var result = await connection.QueryFirstOrDefaultAsync( + $"[{Schema}].[OrganizationDomain_HasVerifiedDomainWithBlockPolicy]", + new { DomainName = domainName, ExcludeOrganizationId = excludeOrganizationId }, + commandType: CommandType.StoredProcedure); + + return result; + } } diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs index e5016a20d4..fd31b1f0dc 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs +++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs @@ -3,6 +3,7 @@ using AutoMapper; using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Models.Data.OrganizationUsers; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models; using Bit.Core.Enums; using Bit.Core.Exceptions; @@ -943,23 +944,24 @@ public class OrganizationUserRepository : Repository ConfirmOrganizationUserAsync(Core.Entities.OrganizationUser organizationUser) + public async Task ConfirmOrganizationUserAsync(AcceptedOrganizationUserToConfirm organizationUserToConfirm) { using var scope = ServiceScopeFactory.CreateScope(); await using var dbContext = GetDatabaseContext(scope); 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 .SetProperty(y => y.Status, OrganizationUserStatusType.Confirmed) - .SetProperty(y => y.Key, organizationUser.Key)); + .SetProperty(y => y.Key, organizationUserToConfirm.Key)); if (result <= 0) { return false; } - await dbContext.UserBumpAccountRevisionDateByOrganizationUserIdAsync(organizationUser.Id); + await dbContext.UserBumpAccountRevisionDateByOrganizationUserIdAsync(organizationUserToConfirm.OrganizationUserId); return true; } diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/PolicyRepository.cs b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/PolicyRepository.cs index 1cca7a9bbb..894fb255be 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/PolicyRepository.cs +++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/PolicyRepository.cs @@ -217,7 +217,7 @@ public class PolicyRepository : Repository new OrganizationPolicyDetails { diff --git a/src/Infrastructure.EntityFramework/Repositories/OrganizationDomainRepository.cs b/src/Infrastructure.EntityFramework/Repositories/OrganizationDomainRepository.cs index 0ddf80130e..d337a5e856 100644 --- a/src/Infrastructure.EntityFramework/Repositories/OrganizationDomainRepository.cs +++ b/src/Infrastructure.EntityFramework/Repositories/OrganizationDomainRepository.cs @@ -177,5 +177,25 @@ public class OrganizationDomainRepository : Repository>(verifiedDomains); } + public async Task HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(string domainName, Guid? excludeOrganizationId = null) + { + using var scope = ServiceScopeFactory.CreateScope(); + var dbContext = GetDatabaseContext(scope); + + var query = from od in dbContext.OrganizationDomains + join o in dbContext.Organizations on od.OrganizationId equals o.Id + join p in dbContext.Policies on o.Id equals p.OrganizationId + where od.DomainName == domainName + && od.VerifiedDate != null + && o.Enabled + && o.UsePolicies + && o.UseOrganizationDomains + && (!excludeOrganizationId.HasValue || o.Id != excludeOrganizationId.Value) + && p.Type == Core.AdminConsole.Enums.PolicyType.BlockClaimedDomainAccountCreation + && p.Enabled + select od; + + return await query.AnyAsync(); + } } diff --git a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs index 7a79c5eb75..d52bae53f3 100644 --- a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs +++ b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs @@ -645,7 +645,7 @@ public static class ServiceCollectionExtensions ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto }; - if (!globalSettings.UnifiedDeployment) + if (!globalSettings.LiteDeployment) { // Trust the X-Forwarded-Host header of the nginx docker container try diff --git a/src/Sql/dbo/Stored Procedures/OrganizationDomain_HasVerifiedDomainWithBlockPolicy.sql b/src/Sql/dbo/Stored Procedures/OrganizationDomain_HasVerifiedDomainWithBlockPolicy.sql new file mode 100644 index 0000000000..bfa9d932c5 --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/OrganizationDomain_HasVerifiedDomainWithBlockPolicy.sql @@ -0,0 +1,34 @@ +CREATE PROCEDURE [dbo].[OrganizationDomain_HasVerifiedDomainWithBlockPolicy] + @DomainName NVARCHAR(255), + @ExcludeOrganizationId UNIQUEIDENTIFIER = NULL +AS +BEGIN + SET NOCOUNT ON + + -- Check if any organization has a verified domain matching the domain name + -- with the BlockClaimedDomainAccountCreation policy enabled (Type = 19) + -- If @ExcludeOrganizationId is provided, exclude that organization from the check + IF EXISTS ( + SELECT 1 + FROM [dbo].[OrganizationDomain] OD + INNER JOIN [dbo].[Organization] O + ON OD.OrganizationId = O.Id + INNER JOIN [dbo].[Policy] P + ON O.Id = P.OrganizationId + WHERE OD.DomainName = @DomainName + AND OD.VerifiedDate IS NOT NULL + AND O.Enabled = 1 + AND O.UsePolicies = 1 + AND O.UseOrganizationDomains = 1 + AND (@ExcludeOrganizationId IS NULL OR O.Id != @ExcludeOrganizationId) + AND P.Type = 19 -- BlockClaimedDomainAccountCreation + AND P.Enabled = 1 + ) + BEGIN + SELECT CAST(1 AS BIT) AS HasBlockPolicy + END + ELSE + BEGIN + SELECT CAST(0 AS BIT) AS HasBlockPolicy + END +END diff --git a/src/Sql/dbo/Tables/Organization.sql b/src/Sql/dbo/Tables/Organization.sql index e1ad6863af..c3c6597cfd 100644 --- a/src/Sql/dbo/Tables/Organization.sql +++ b/src/Sql/dbo/Tables/Organization.sql @@ -60,6 +60,7 @@ CREATE TABLE [dbo].[Organization] ( [UseAdminSponsoredFamilies] BIT NOT NULL CONSTRAINT [DF_Organization_UseAdminSponsoredFamilies] DEFAULT (0), [SyncSeats] BIT NOT NULL CONSTRAINT [DF_Organization_SyncSeats] DEFAULT (0), [UseAutomaticUserConfirmation] BIT NOT NULL CONSTRAINT [DF_Organization_UseAutomaticUserConfirmation] DEFAULT (0), + [MaxStorageGbIncreased] SMALLINT NULL, CONSTRAINT [PK_Organization] PRIMARY KEY CLUSTERED ([Id] ASC) ); diff --git a/src/Sql/dbo/Tables/User.sql b/src/Sql/dbo/Tables/User.sql index dc772ff1a7..854fe34f4a 100644 --- a/src/Sql/dbo/Tables/User.sql +++ b/src/Sql/dbo/Tables/User.sql @@ -45,6 +45,7 @@ [SecurityState] VARCHAR (MAX) NULL, [SecurityVersion] INT NULL, [SignedPublicKey] VARCHAR (MAX) NULL, + [MaxStorageGbIncreased] SMALLINT NULL, CONSTRAINT [PK_User] PRIMARY KEY CLUSTERED ([Id] ASC) ); diff --git a/src/Sql/dbo/Views/OrganizationUserOrganizationDetailsView.sql b/src/Sql/dbo/Views/OrganizationUserOrganizationDetailsView.sql index a7e1db6e81..564bb71ca9 100644 --- a/src/Sql/dbo/Views/OrganizationUserOrganizationDetailsView.sql +++ b/src/Sql/dbo/Views/OrganizationUserOrganizationDetailsView.sql @@ -24,7 +24,7 @@ SELECT O.[UseSecretsManager], O.[Seats], O.[MaxCollections], - O.[MaxStorageGb], + COALESCE(O.[MaxStorageGbIncreased], O.[MaxStorageGb]) AS [MaxStorageGb], O.[Identifier], OU.[Key], OU.[ResetPasswordKey], diff --git a/src/Sql/dbo/Views/OrganizationView.sql b/src/Sql/dbo/Views/OrganizationView.sql index 58989273fd..9cb4eb72a1 100644 --- a/src/Sql/dbo/Views/OrganizationView.sql +++ b/src/Sql/dbo/Views/OrganizationView.sql @@ -1,6 +1,66 @@ CREATE VIEW [dbo].[OrganizationView] AS 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 [dbo].[Organization] diff --git a/src/Sql/dbo/Views/ProviderUserProviderOrganizationDetailsView.sql b/src/Sql/dbo/Views/ProviderUserProviderOrganizationDetailsView.sql index 42e877ab15..27c28f8e71 100644 --- a/src/Sql/dbo/Views/ProviderUserProviderOrganizationDetailsView.sql +++ b/src/Sql/dbo/Views/ProviderUserProviderOrganizationDetailsView.sql @@ -23,7 +23,7 @@ SELECT O.[UseCustomPermissions], O.[Seats], O.[MaxCollections], - O.[MaxStorageGb], + COALESCE(O.[MaxStorageGbIncreased], O.[MaxStorageGb]) AS [MaxStorageGb], O.[Identifier], PO.[Key], O.[PublicKey], diff --git a/src/Sql/dbo/Views/UserView.sql b/src/Sql/dbo/Views/UserView.sql index 82fa8a2c63..fa8dbf334b 100644 --- a/src/Sql/dbo/Views/UserView.sql +++ b/src/Sql/dbo/Views/UserView.sql @@ -1,6 +1,51 @@ CREATE VIEW [dbo].[UserView] AS 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 [dbo].[User] diff --git a/test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUserControllerAutoConfirmTests.cs b/test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUserControllerAutoConfirmTests.cs new file mode 100644 index 0000000000..8df1fcaf2b --- /dev/null +++ b/test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUserControllerAutoConfirmTests.cs @@ -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, 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(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() + .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().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() + .UpsertAsync(organization); + + var testKey = $"test-key-{Guid.NewGuid()}"; + + await _factory.GetService().CreateAsync(new Policy + { + OrganizationId = organization.Id, + Type = PolicyType.AutomaticUserConfirmation, + Enabled = true + }); + + await _factory.GetService().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(); + 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(); + var collections = await collectionRepository.GetManyByUserIdAsync(organizationUser.UserId!.Value); + + Assert.NotEmpty(collections); + Assert.Single(collections.Where(c => c.Type == CollectionType.DefaultUserCollection)); + + await _factory.GetService().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() + .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().CreateAsync(new Policy + { + OrganizationId = organization.Id, + Type = PolicyType.AutomaticUserConfirmation, + Enabled = true + }); + + await _factory.GetService().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(); + 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() + .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().DeleteAsync(organization); + } + + public Task DisposeAsync() + { + _client.Dispose(); + return Task.CompletedTask; + } +} diff --git a/test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUserControllerTests.cs b/test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUserControllerTests.cs index 7c61a88bd8..0fef4a0cd0 100644 --- a/test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUserControllerTests.cs +++ b/test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUserControllerTests.cs @@ -218,7 +218,7 @@ public class OrganizationUserControllerTests : IClassFixture, IAsy await _factory.LoginWithNewAccount(_ownerEmail); // 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); // Authorize with the organization api key diff --git a/test/Api.IntegrationTest/AdminConsole/Public/Controllers/PoliciesControllerTests.cs b/test/Api.IntegrationTest/AdminConsole/Public/Controllers/PoliciesControllerTests.cs index 0b5ab660b9..6144d7eebb 100644 --- a/test/Api.IntegrationTest/AdminConsole/Public/Controllers/PoliciesControllerTests.cs +++ b/test/Api.IntegrationTest/AdminConsole/Public/Controllers/PoliciesControllerTests.cs @@ -39,7 +39,7 @@ public class PoliciesControllerTests : IClassFixture, IAs await _factory.LoginWithNewAccount(_ownerEmail); // 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); // Authorize with the organization api key diff --git a/test/Api.Test/AdminConsole/Controllers/OrganizationUsersControllerTests.cs b/test/Api.Test/AdminConsole/Controllers/OrganizationUsersControllerTests.cs index 5875cda05a..ae14001223 100644 --- a/test/Api.Test/AdminConsole/Controllers/OrganizationUsersControllerTests.cs +++ b/test/Api.Test/AdminConsole/Controllers/OrganizationUsersControllerTests.cs @@ -9,10 +9,12 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; 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.Policies; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; using Bit.Core.AdminConsole.Repositories; +using Bit.Core.AdminConsole.Utilities.v2.Results; using Bit.Core.Auth.Entities; using Bit.Core.Auth.Repositories; 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.Requests; using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc.ModelBinding; using NSubstitute; +using OneOf.Types; using Xunit; namespace Bit.Api.Test.AdminConsole.Controllers; @@ -476,7 +480,7 @@ public class OrganizationUsersControllerTests var result = await sutProvider.Sut.PutResetPassword(orgId, orgUserId, model); - Assert.IsType(result); + Assert.IsType(result); } [Theory] @@ -506,7 +510,7 @@ public class OrganizationUsersControllerTests var result = await sutProvider.Sut.PutResetPassword(orgId, orgUserId, model); - Assert.IsType(result); + Assert.IsType(result); } [Theory] @@ -521,7 +525,7 @@ public class OrganizationUsersControllerTests var result = await sutProvider.Sut.PutResetPassword(orgId, orgUserId, model); - Assert.IsType(result); + Assert.IsType(result); } [Theory] @@ -594,4 +598,190 @@ public class OrganizationUsersControllerTests Assert.IsType>(result); } + + [Theory] + [BitAutoData] + public async Task AutomaticallyConfirmOrganizationUserAsync_UserIdNull_ReturnsUnauthorized( + Guid orgId, + Guid orgUserId, + OrganizationUserConfirmRequestModel model, + SutProvider sutProvider) + { + // Arrange + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers) + .Returns(true); + + sutProvider.GetDependency() + .GetProperUserId(Arg.Any()) + .Returns((Guid?)null); + + // Act + var result = await sutProvider.Sut.AutomaticallyConfirmOrganizationUserAsync(orgId, orgUserId, model); + + // Assert + Assert.IsType(result); + } + + [Theory] + [BitAutoData] + public async Task AutomaticallyConfirmOrganizationUserAsync_UserIdEmpty_ReturnsUnauthorized( + Guid orgId, + Guid orgUserId, + OrganizationUserConfirmRequestModel model, + SutProvider sutProvider) + { + // Arrange + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers) + .Returns(true); + + sutProvider.GetDependency() + .GetProperUserId(Arg.Any()) + .Returns(Guid.Empty); + + // Act + var result = await sutProvider.Sut.AutomaticallyConfirmOrganizationUserAsync(orgId, orgUserId, model); + + // Assert + Assert.IsType(result); + } + + [Theory] + [BitAutoData] + public async Task AutomaticallyConfirmOrganizationUserAsync_Success_ReturnsOk( + Guid orgId, + Guid orgUserId, + Guid userId, + OrganizationUserConfirmRequestModel model, + SutProvider sutProvider) + { + // Arrange + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers) + .Returns(true); + + sutProvider.GetDependency() + .GetProperUserId(Arg.Any()) + .Returns(userId); + + sutProvider.GetDependency() + .OrganizationOwner(orgId) + .Returns(true); + + sutProvider.GetDependency() + .AutomaticallyConfirmOrganizationUserAsync(Arg.Any()) + .Returns(new CommandResult(new None())); + + // Act + var result = await sutProvider.Sut.AutomaticallyConfirmOrganizationUserAsync(orgId, orgUserId, model); + + // Assert + Assert.IsType(result); + } + + [Theory] + [BitAutoData] + public async Task AutomaticallyConfirmOrganizationUserAsync_NotFoundError_ReturnsNotFound( + Guid orgId, + Guid orgUserId, + Guid userId, + OrganizationUserConfirmRequestModel model, + SutProvider sutProvider) + { + // Arrange + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers) + .Returns(true); + + sutProvider.GetDependency() + .GetProperUserId(Arg.Any()) + .Returns(userId); + + sutProvider.GetDependency() + .OrganizationOwner(orgId) + .Returns(false); + + var notFoundError = new OrganizationNotFound(); + sutProvider.GetDependency() + .AutomaticallyConfirmOrganizationUserAsync(Arg.Any()) + .Returns(new CommandResult(notFoundError)); + + // Act + var result = await sutProvider.Sut.AutomaticallyConfirmOrganizationUserAsync(orgId, orgUserId, model); + + // Assert + var notFoundResult = Assert.IsType>(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 sutProvider) + { + // Arrange + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers) + .Returns(true); + + sutProvider.GetDependency() + .GetProperUserId(Arg.Any()) + .Returns(userId); + + sutProvider.GetDependency() + .OrganizationOwner(orgId) + .Returns(true); + + var badRequestError = new UserIsNotAccepted(); + sutProvider.GetDependency() + .AutomaticallyConfirmOrganizationUserAsync(Arg.Any()) + .Returns(new CommandResult(badRequestError)); + + // Act + var result = await sutProvider.Sut.AutomaticallyConfirmOrganizationUserAsync(orgId, orgUserId, model); + + // Assert + var badRequestResult = Assert.IsType>(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 sutProvider) + { + // Arrange + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers) + .Returns(true); + + sutProvider.GetDependency() + .GetProperUserId(Arg.Any()) + .Returns(userId); + + sutProvider.GetDependency() + .OrganizationOwner(orgId) + .Returns(true); + + var internalError = new FailedToWriteToEventLog(); + sutProvider.GetDependency() + .AutomaticallyConfirmOrganizationUserAsync(Arg.Any()) + .Returns(new CommandResult(internalError)); + + // Act + var result = await sutProvider.Sut.AutomaticallyConfirmOrganizationUserAsync(orgId, orgUserId, model); + + // Assert + var problemResult = Assert.IsType>(result); + Assert.Equal(StatusCodes.Status500InternalServerError, problemResult.StatusCode); + } } diff --git a/test/Api.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs b/test/Api.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs index 00fd3c3b4e..f999dd520e 100644 --- a/test/Api.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs +++ b/test/Api.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs @@ -30,8 +30,8 @@ using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Repositories; using Bit.Core.Services; +using Bit.Core.Test.Billing.Mocks; using Bit.Core.Tokens; -using Bit.Core.Utilities; using Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider; using NSubstitute; using Xunit; @@ -305,7 +305,7 @@ public class OrganizationsControllerTests : IDisposable // Arrange _currentContext.OrganizationOwner(organization.Id).Returns(true); - var plan = StaticStore.GetPlan(PlanType.EnterpriseAnnually); + var plan = MockPlans.Get(PlanType.EnterpriseAnnually); _pricingClient.GetPlan(Arg.Any()).Returns(plan); _organizationService diff --git a/test/Api.Test/Billing/Controllers/OrganizationSponsorshipsControllerTests.cs b/test/Api.Test/Billing/Controllers/OrganizationSponsorshipsControllerTests.cs index 2ad7686c30..87334dc085 100644 --- a/test/Api.Test/Billing/Controllers/OrganizationSponsorshipsControllerTests.cs +++ b/test/Api.Test/Billing/Controllers/OrganizationSponsorshipsControllerTests.cs @@ -10,7 +10,7 @@ using Bit.Core.Models.Data; using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces; using Bit.Core.Repositories; using Bit.Core.Services; -using Bit.Core.Utilities; +using Bit.Core.Test.Billing.Mocks; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; @@ -24,11 +24,11 @@ namespace Bit.Api.Test.Billing.Controllers; public class OrganizationSponsorshipsControllerTests { public static IEnumerable EnterprisePlanTypes => - Enum.GetValues().Where(p => StaticStore.GetPlan(p).ProductTier == ProductTierType.Enterprise).Select(p => new object[] { p }); + Enum.GetValues().Where(p => MockPlans.Get(p).ProductTier == ProductTierType.Enterprise).Select(p => new object[] { p }); public static IEnumerable NonEnterprisePlanTypes => - Enum.GetValues().Where(p => StaticStore.GetPlan(p).ProductTier != ProductTierType.Enterprise).Select(p => new object[] { p }); + Enum.GetValues().Where(p => MockPlans.Get(p).ProductTier != ProductTierType.Enterprise).Select(p => new object[] { p }); public static IEnumerable NonFamiliesPlanTypes => - Enum.GetValues().Where(p => StaticStore.GetPlan(p).ProductTier != ProductTierType.Families).Select(p => new object[] { p }); + Enum.GetValues().Where(p => MockPlans.Get(p).ProductTier != ProductTierType.Families).Select(p => new object[] { p }); public static IEnumerable NonConfirmedOrganizationUsersStatuses => Enum.GetValues() diff --git a/test/Api.Test/Billing/Controllers/ProviderBillingControllerTests.cs b/test/Api.Test/Billing/Controllers/ProviderBillingControllerTests.cs index 75bd13eae8..f59fce4011 100644 --- a/test/Api.Test/Billing/Controllers/ProviderBillingControllerTests.cs +++ b/test/Api.Test/Billing/Controllers/ProviderBillingControllerTests.cs @@ -17,7 +17,7 @@ using Bit.Core.Context; using Bit.Core.Models.Api; using Bit.Core.Models.BitStripe; using Bit.Core.Services; -using Bit.Core.Utilities; +using Bit.Core.Test.Billing.Mocks; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using Microsoft.AspNetCore.Http; @@ -351,7 +351,7 @@ public class ProviderBillingControllerTests foreach (var providerPlan in providerPlans) { - var plan = StaticStore.GetPlan(providerPlan.PlanType); + var plan = MockPlans.Get(providerPlan.PlanType); sutProvider.GetDependency().GetPlanOrThrow(providerPlan.PlanType).Returns(plan); var priceId = ProviderPriceAdapter.GetPriceId(provider, subscription, providerPlan.PlanType); sutProvider.GetDependency().PriceGetAsync(priceId) @@ -372,7 +372,7 @@ public class ProviderBillingControllerTests Assert.Equal(subscription.Customer!.Discount!.Coupon!.PercentOff, response.DiscountPercentage); 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); Assert.NotNull(providerTeamsPlan); Assert.Equal(50, providerTeamsPlan.SeatMinimum); @@ -381,7 +381,7 @@ public class ProviderBillingControllerTests Assert.Equal(60 * teamsPlan.PasswordManager.ProviderPortalSeatPrice, providerTeamsPlan.Cost); 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); Assert.NotNull(providerEnterprisePlan); Assert.Equal(100, providerEnterprisePlan.SeatMinimum); @@ -498,7 +498,7 @@ public class ProviderBillingControllerTests foreach (var providerPlan in providerPlans) { - var plan = StaticStore.GetPlan(providerPlan.PlanType); + var plan = MockPlans.Get(providerPlan.PlanType); sutProvider.GetDependency().GetPlanOrThrow(providerPlan.PlanType).Returns(plan); var priceId = ProviderPriceAdapter.GetPriceId(provider, subscription, providerPlan.PlanType); sutProvider.GetDependency().PriceGetAsync(priceId) diff --git a/test/Api.Test/SecretsManager/Controllers/ServiceAccountsControllerTests.cs b/test/Api.Test/SecretsManager/Controllers/ServiceAccountsControllerTests.cs index 78224a8bd8..5d3b7f2fa5 100644 --- a/test/Api.Test/SecretsManager/Controllers/ServiceAccountsControllerTests.cs +++ b/test/Api.Test/SecretsManager/Controllers/ServiceAccountsControllerTests.cs @@ -16,7 +16,7 @@ using Bit.Core.SecretsManager.Models.Data; using Bit.Core.SecretsManager.Queries.ServiceAccounts.Interfaces; using Bit.Core.SecretsManager.Repositories; using Bit.Core.Services; -using Bit.Core.Utilities; +using Bit.Core.Test.Billing.Mocks; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.Helpers; @@ -121,7 +121,7 @@ public class ServiceAccountsControllerTests { ArrangeCreateServiceAccountAutoScalingTest(newSlotsRequired, sutProvider, data, organization); - sutProvider.GetDependency().GetPlanOrThrow(organization.PlanType).Returns(StaticStore.GetPlan(organization.PlanType)); + sutProvider.GetDependency().GetPlanOrThrow(organization.PlanType).Returns(MockPlans.Get(organization.PlanType)); await sutProvider.Sut.CreateAsync(organization.Id, data); diff --git a/test/Api.Test/Vault/Controllers/SyncControllerTests.cs b/test/Api.Test/Vault/Controllers/SyncControllerTests.cs index a46eba283d..e6d34592c7 100644 --- a/test/Api.Test/Vault/Controllers/SyncControllerTests.cs +++ b/test/Api.Test/Vault/Controllers/SyncControllerTests.cs @@ -18,9 +18,9 @@ using Bit.Core.Models.Data; using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Repositories; using Bit.Core.Services; +using Bit.Core.Test.Billing.Mocks; using Bit.Core.Tools.Entities; using Bit.Core.Tools.Repositories; -using Bit.Core.Utilities; using Bit.Core.Vault.Entities; using Bit.Core.Vault.Models.Data; using Bit.Core.Vault.Repositories; @@ -335,7 +335,7 @@ public class SyncControllerTests if (matchedProviderUserOrgDetails != null) { - var providerOrgProductType = StaticStore.GetPlan(matchedProviderUserOrgDetails.PlanType).ProductTier; + var providerOrgProductType = MockPlans.Get(matchedProviderUserOrgDetails.PlanType).ProductTier; Assert.Equal(providerOrgProductType, profProviderOrg.ProductTierType); } } diff --git a/test/Billing.Test/Billing.Test.csproj b/test/Billing.Test/Billing.Test.csproj index 4d7f887c90..84443753ce 100644 --- a/test/Billing.Test/Billing.Test.csproj +++ b/test/Billing.Test/Billing.Test.csproj @@ -24,6 +24,7 @@ + diff --git a/test/Billing.Test/Services/ProviderEventServiceTests.cs b/test/Billing.Test/Services/ProviderEventServiceTests.cs index d5f273fa65..34c69b95c2 100644 --- a/test/Billing.Test/Services/ProviderEventServiceTests.cs +++ b/test/Billing.Test/Services/ProviderEventServiceTests.cs @@ -9,7 +9,7 @@ using Bit.Core.Billing.Providers.Entities; using Bit.Core.Billing.Providers.Repositories; using Bit.Core.Enums; using Bit.Core.Repositories; -using Bit.Core.Utilities; +using Bit.Core.Test.Billing.Mocks; using NSubstitute; using Stripe; using Xunit; @@ -237,7 +237,7 @@ public class ProviderEventServiceTests 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); @@ -246,8 +246,8 @@ public class ProviderEventServiceTests await _providerEventService.TryRecordInvoiceLineItems(stripeEvent); // Assert - var teamsPlan = StaticStore.GetPlan(PlanType.TeamsMonthly); - var enterprisePlan = StaticStore.GetPlan(PlanType.EnterpriseMonthly); + var teamsPlan = MockPlans.Get(PlanType.TeamsMonthly); + var enterprisePlan = MockPlans.Get(PlanType.EnterpriseMonthly); await _providerInvoiceItemRepository.Received(1).CreateAsync(Arg.Is( options => diff --git a/test/Billing.Test/Services/SubscriptionUpdatedHandlerTests.cs b/test/Billing.Test/Services/SubscriptionUpdatedHandlerTests.cs index 16287bc5c9..83ebd4aaa7 100644 --- a/test/Billing.Test/Services/SubscriptionUpdatedHandlerTests.cs +++ b/test/Billing.Test/Services/SubscriptionUpdatedHandlerTests.cs @@ -8,11 +8,11 @@ using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces; using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Services; using Bit.Core.Billing.Enums; -using Bit.Core.Billing.Models.StaticStore.Plans; using Bit.Core.Billing.Pricing; using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces; using Bit.Core.Repositories; using Bit.Core.Services; +using Bit.Core.Test.Billing.Mocks.Plans; using Microsoft.Extensions.Logging; using Newtonsoft.Json.Linq; using NSubstitute; diff --git a/test/Billing.Test/Services/UpcomingInvoiceHandlerTests.cs b/test/Billing.Test/Services/UpcomingInvoiceHandlerTests.cs index e463521bcd..89c926ee31 100644 --- a/test/Billing.Test/Services/UpcomingInvoiceHandlerTests.cs +++ b/test/Billing.Test/Services/UpcomingInvoiceHandlerTests.cs @@ -5,7 +5,6 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Billing.Enums; -using Bit.Core.Billing.Models.StaticStore.Plans; using Bit.Core.Billing.Payment.Models; using Bit.Core.Billing.Payment.Queries; using Bit.Core.Billing.Pricing; @@ -16,6 +15,7 @@ using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterpri using Bit.Core.Platform.Mail.Mailer; using Bit.Core.Repositories; using Bit.Core.Services; +using Bit.Core.Test.Billing.Mocks.Plans; using Microsoft.Extensions.Logging; using NSubstitute; using NSubstitute.ExceptionExtensions; @@ -1141,7 +1141,7 @@ public class UpcomingInvoiceHandlerTests } [Fact] - public async Task HandleAsync_WhenMilestone3Disabled_DoesNotUpdateSubscription() + public async Task HandleAsync_WhenMilestone3Disabled_AndFamilies2019Plan_DoesNotUpdateSubscription() { // Arrange var parsedEvent = new Event { Id = "evt_123", Type = "invoice.upcoming" }; @@ -1789,4 +1789,170 @@ public class UpcomingInvoiceHandlerTests 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 + { + Data = new List { new() { Description = "Test Item" } } + } + }; + + var families2025Plan = new Families2025Plan(); + var familiesPlan = new FamiliesPlan(); + + var subscription = new Subscription + { + Id = subscriptionId, + CustomerId = customerId, + Items = new StripeList + { + Data = new List + { + new() + { + Id = passwordManagerItemId, + Price = new Price { Id = families2025Plan.PasswordManager.StripePlanId } + } + } + }, + AutomaticTax = new SubscriptionAutomaticTax { Enabled = true }, + Metadata = new Dictionary() + }; + + var customer = new Customer + { + Id = customerId, + Subscriptions = new StripeList { Data = new List { 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()).Returns(customer); + _stripeEventUtilityService + .GetIdsFromMetadata(subscription.Metadata) + .Returns(new Tuple(_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(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(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 + { + Data = new List { new() { Description = "Test Item" } } + } + }; + + var families2025Plan = new Families2025Plan(); + + var subscription = new Subscription + { + Id = subscriptionId, + CustomerId = customerId, + Items = new StripeList + { + Data = new List + { + new() + { + Id = passwordManagerItemId, + Price = new Price { Id = families2025Plan.PasswordManager.StripePlanId } + } + } + }, + AutomaticTax = new SubscriptionAutomaticTax { Enabled = true }, + Metadata = new Dictionary() + }; + + var customer = new Customer + { + Id = customerId, + Subscriptions = new StripeList { Data = new List { 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()).Returns(customer); + _stripeEventUtilityService + .GetIdsFromMetadata(subscription.Metadata) + .Returns(new Tuple(_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(), + Arg.Any()); + + await _organizationRepository.DidNotReceive().ReplaceAsync( + Arg.Is(org => org.PlanType == PlanType.FamiliesAnnually)); + } } diff --git a/test/Core.Test/AdminConsole/AutoFixture/OrganizationFixtures.cs b/test/Core.Test/AdminConsole/AutoFixture/OrganizationFixtures.cs index e906862e3f..c874fe58d8 100644 --- a/test/Core.Test/AdminConsole/AutoFixture/OrganizationFixtures.cs +++ b/test/Core.Test/AdminConsole/AutoFixture/OrganizationFixtures.cs @@ -1,6 +1,8 @@ -using System.Text.Json; +using System.Reflection; +using System.Text.Json; using AutoFixture; using AutoFixture.Kernel; +using AutoFixture.Xunit2; using Bit.Core.AdminConsole.Entities; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Models; @@ -9,7 +11,7 @@ using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Models.Business; 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.Attributes; using Microsoft.AspNetCore.DataProtection; @@ -20,12 +22,24 @@ public class OrganizationCustomization : ICustomization { public bool UseGroups { 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) { var organizationId = Guid.NewGuid(); 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 smSeats = plan.SupportsSecretsManager ? (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.PlanType, PlanType) .With(o => o.Seats, seats) - .With(o => o.SmSeats, smSeats)); + .With(o => o.SmSeats, smSeats) + .With(o => o.UseAutomaticUserConfirmation, UseAutomaticUserConfirmation)); fixture.Customize(composer => composer @@ -77,7 +92,7 @@ internal class PaidOrganization : ICustomization public PlanType CheckedPlanType { get; set; } 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(); CheckedPlanType = CheckedPlanType.Equals(PlanType.Free) ? lowestActivePaidPlan : CheckedPlanType; validUpgradePlans.Remove(lowestActivePaidPlan); @@ -105,7 +120,7 @@ internal class FreeOrganizationUpgrade : ICustomization .With(o => o.PlanType, PlanType.Free)); var plansToIgnore = new List { 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(composer => composer .With(ou => ou.Plan, selectedPlan.Type) @@ -153,7 +168,7 @@ public class SecretsManagerOrganizationCustomization : ICustomization .With(o => o.Id, organizationId) .With(o => o.UseSecretsManager, true) .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.MaxAutoscaleSmServiceAccounts, (int?)null)); } @@ -277,3 +292,9 @@ internal class EphemeralDataProtectionAutoDataAttribute : CustomAutoDataAttribut 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); +} diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/AutoConfirmUsers/AutomaticallyConfirmOrganizationUsersValidatorTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/AutoConfirmUsers/AutomaticallyConfirmOrganizationUsersValidatorTests.cs new file mode 100644 index 0000000000..eb377a8d08 --- /dev/null +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/AutoConfirmUsers/AutomaticallyConfirmOrganizationUsersValidatorTests.cs @@ -0,0 +1,696 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Models.Data; +using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUser; +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.Auth.UserFeatures.TwoFactorAuth.Interfaces; +using Bit.Core.Billing.Enums; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Repositories; +using Bit.Core.Test.AdminConsole.AutoFixture; +using Bit.Core.Test.AutoFixture.OrganizationFixtures; +using Bit.Core.Test.AutoFixture.OrganizationUserFixtures; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUsers; + +[SutProviderCustomize] +public class AutomaticallyConfirmOrganizationUsersValidatorTests +{ + [Theory] + [BitAutoData] + public async Task ValidateAsync_WithNullOrganizationUser_ReturnsUserNotFoundError( + SutProvider sutProvider, + Organization organization) + { + // Arrange + var request = new AutomaticallyConfirmOrganizationUserValidationRequest + { + PerformedBy = Substitute.For(), + DefaultUserCollectionName = "test-collection", + OrganizationUser = null, + OrganizationUserId = Guid.NewGuid(), + Organization = organization, + OrganizationId = organization.Id, + Key = "test-key" + }; + + // Act + var result = await sutProvider.Sut.ValidateAsync(request); + + // Assert + Assert.True(result.IsError); + Assert.IsType(result.AsError); + } + + [Theory] + [BitAutoData] + public async Task ValidateAsync_WithNullUserId_ReturnsUserNotFoundError( + SutProvider sutProvider, + Organization organization, + [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser) + { + // Arrange + organizationUser.UserId = null; + organizationUser.OrganizationId = organization.Id; + + var request = new AutomaticallyConfirmOrganizationUserValidationRequest + { + PerformedBy = Substitute.For(), + DefaultUserCollectionName = "test-collection", + OrganizationUser = organizationUser, + OrganizationUserId = organizationUser.Id, + Organization = organization, + OrganizationId = organization.Id, + Key = "test-key" + }; + + // Act + var result = await sutProvider.Sut.ValidateAsync(request); + + // Assert + Assert.True(result.IsError); + Assert.IsType(result.AsError); + } + + [Theory] + [BitAutoData] + public async Task ValidateAsync_WithNullOrganization_ReturnsOrganizationNotFoundError( + SutProvider sutProvider, + [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser, + Guid userId) + { + // Arrange + organizationUser.UserId = userId; + + var request = new AutomaticallyConfirmOrganizationUserValidationRequest + { + PerformedBy = Substitute.For(), + DefaultUserCollectionName = "test-collection", + OrganizationUser = organizationUser, + OrganizationUserId = organizationUser.Id, + Organization = null, + OrganizationId = organizationUser.OrganizationId, + Key = "test-key" + }; + + // Act + var result = await sutProvider.Sut.ValidateAsync(request); + + // Assert + Assert.True(result.IsError); + Assert.IsType(result.AsError); + } + + [Theory] + [BitAutoData] + public async Task ValidateAsync_WithValidAcceptedUser_ReturnsValidResult( + SutProvider sutProvider, + [Organization(useAutomaticUserConfirmation: true, planType: PlanType.EnterpriseAnnually)] Organization organization, + [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser, + Guid userId, + [Policy(PolicyType.AutomaticUserConfirmation)] Policy autoConfirmPolicy) + { + // Arrange + organizationUser.UserId = userId; + organizationUser.OrganizationId = organization.Id; + + var request = new AutomaticallyConfirmOrganizationUserValidationRequest + { + PerformedBy = Substitute.For(), + DefaultUserCollectionName = "test-collection", + OrganizationUser = organizationUser, + OrganizationUserId = organizationUser.Id, + Organization = organization, + OrganizationId = organization.Id, + Key = "test-key" + }; + + sutProvider.GetDependency() + .GetByOrganizationIdTypeAsync(organization.Id, PolicyType.AutomaticUserConfirmation) + .Returns(autoConfirmPolicy); + + sutProvider.GetDependency() + .TwoFactorIsEnabledAsync(Arg.Any>()) + .Returns([(userId, true)]); + + sutProvider.GetDependency() + .GetManyByUserAsync(userId) + .Returns([organizationUser]); + + // Act + var result = await sutProvider.Sut.ValidateAsync(request); + + // Assert + Assert.True(result.IsValid); + Assert.Equal(request, result.Request); + } + + [Theory] + [BitAutoData] + public async Task ValidateAsync_WithMismatchedOrganizationId_ReturnsOrganizationUserIdIsInvalidError( + SutProvider sutProvider, + Organization organization, + [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser, + Guid userId) + { + // Arrange + organizationUser.UserId = userId; + organizationUser.OrganizationId = Guid.NewGuid(); // Different from organization.Id + + var request = new AutomaticallyConfirmOrganizationUserValidationRequest + { + PerformedBy = Substitute.For(), + DefaultUserCollectionName = "test-collection", + OrganizationUser = organizationUser, + OrganizationUserId = organizationUser.Id, + Organization = organization, + OrganizationId = organization.Id, + Key = "test-key" + }; + + sutProvider.GetDependency() + .GetManyByUserAsync(userId) + .Returns([organizationUser]); + + // Act + var result = await sutProvider.Sut.ValidateAsync(request); + + // Assert + Assert.True(result.IsError); + Assert.IsType(result.AsError); + } + + [Theory] + [BitAutoData(OrganizationUserStatusType.Invited)] + [BitAutoData(OrganizationUserStatusType.Revoked)] + [BitAutoData(OrganizationUserStatusType.Confirmed)] + public async Task ValidateAsync_WithNotAcceptedStatus_ReturnsUserIsNotAcceptedError( + OrganizationUserStatusType statusType, + SutProvider sutProvider, + Organization organization, + [OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser, + Guid userId) + { + // Arrange + organizationUser.UserId = userId; + organizationUser.OrganizationId = organization.Id; + organizationUser.Status = statusType; + + var request = new AutomaticallyConfirmOrganizationUserValidationRequest + { + PerformedBy = Substitute.For(), + DefaultUserCollectionName = "test-collection", + OrganizationUser = organizationUser, + OrganizationUserId = organizationUser.Id, + Organization = organization, + OrganizationId = organization.Id, + Key = "test-key" + }; + + // Act + var result = await sutProvider.Sut.ValidateAsync(request); + + // Assert + Assert.True(result.IsError); + Assert.IsType(result.AsError); + } + + [Theory] + [BitAutoData(OrganizationUserType.Owner)] + [BitAutoData(OrganizationUserType.Custom)] + [BitAutoData(OrganizationUserType.Admin)] + public async Task ValidateAsync_WithNonUserType_ReturnsUserIsNotUserTypeError( + OrganizationUserType userType, + SutProvider sutProvider, + Organization organization, + [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser, + Guid userId) + { + // Arrange + organizationUser.UserId = userId; + organizationUser.OrganizationId = organization.Id; + organizationUser.Type = userType; + + var request = new AutomaticallyConfirmOrganizationUserValidationRequest + { + PerformedBy = Substitute.For(), + DefaultUserCollectionName = "test-collection", + OrganizationUser = organizationUser, + OrganizationUserId = organizationUser.Id, + Organization = organization, + OrganizationId = organization.Id, + Key = "test-key" + }; + + // Act + var result = await sutProvider.Sut.ValidateAsync(request); + + // Assert + Assert.True(result.IsError); + Assert.IsType(result.AsError); + } + + [Theory] + [BitAutoData] + public async Task ValidateAsync_UserWithout2FA_And2FARequired_ReturnsError( + SutProvider sutProvider, + [Organization(useAutomaticUserConfirmation: true)] Organization organization, + [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser, + Guid userId, + [Policy(PolicyType.AutomaticUserConfirmation)] Policy autoConfirmPolicy) + { + // Arrange + organizationUser.UserId = userId; + organizationUser.OrganizationId = organization.Id; + + var request = new AutomaticallyConfirmOrganizationUserValidationRequest + { + PerformedBy = Substitute.For(), + DefaultUserCollectionName = "test-collection", + OrganizationUser = organizationUser, + OrganizationUserId = organizationUser.Id, + Organization = organization, + OrganizationId = organization.Id, + Key = "test-key" + }; + + var twoFactorPolicyDetails = new PolicyDetails + { + OrganizationId = organization.Id, + PolicyType = PolicyType.TwoFactorAuthentication + }; + + sutProvider.GetDependency() + .GetByOrganizationIdTypeAsync(organization.Id, PolicyType.AutomaticUserConfirmation) + .Returns(autoConfirmPolicy); + + sutProvider.GetDependency() + .TwoFactorIsEnabledAsync(Arg.Any>()) + .Returns([(userId, false)]); + + sutProvider.GetDependency() + .GetAsync(userId) + .Returns(new RequireTwoFactorPolicyRequirement([twoFactorPolicyDetails])); + + sutProvider.GetDependency() + .GetManyByUserAsync(userId) + .Returns([organizationUser]); + + // Act + var result = await sutProvider.Sut.ValidateAsync(request); + + // Assert + Assert.True(result.IsError); + Assert.IsType(result.AsError); + } + + [Theory] + [BitAutoData] + public async Task ValidateAsync_UserWith2FA_ReturnsValidResult( + SutProvider sutProvider, + [Organization(useAutomaticUserConfirmation: true)] Organization organization, + [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser, + Guid userId, + [Policy(PolicyType.AutomaticUserConfirmation)] Policy autoConfirmPolicy) + { + // Arrange + organizationUser.UserId = userId; + organizationUser.OrganizationId = organization.Id; + + var request = new AutomaticallyConfirmOrganizationUserValidationRequest + { + PerformedBy = Substitute.For(), + DefaultUserCollectionName = "test-collection", + OrganizationUser = organizationUser, + OrganizationUserId = organizationUser.Id, + Organization = organization, + OrganizationId = organization.Id, + Key = "test-key" + }; + + sutProvider.GetDependency() + .GetByOrganizationIdTypeAsync(organization.Id, PolicyType.AutomaticUserConfirmation) + .Returns(autoConfirmPolicy); + + sutProvider.GetDependency() + .TwoFactorIsEnabledAsync(Arg.Any>()) + .Returns([(userId, true)]); + + sutProvider.GetDependency() + .GetManyByUserAsync(userId) + .Returns([organizationUser]); + + // Act + var result = await sutProvider.Sut.ValidateAsync(request); + + // Assert + Assert.True(result.IsValid); + } + + [Theory] + [BitAutoData] + public async Task ValidateAsync_UserWithout2FA_And2FANotRequired_ReturnsValidResult( + SutProvider sutProvider, + [Organization(useAutomaticUserConfirmation: true)] Organization organization, + [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser, + Guid userId, + [Policy(PolicyType.AutomaticUserConfirmation)] Policy autoConfirmPolicy) + { + // Arrange + organizationUser.UserId = userId; + organizationUser.OrganizationId = organization.Id; + + var request = new AutomaticallyConfirmOrganizationUserValidationRequest + { + PerformedBy = Substitute.For(), + DefaultUserCollectionName = "test-collection", + OrganizationUser = organizationUser, + OrganizationUserId = organizationUser.Id, + Organization = organization, + OrganizationId = organization.Id, + Key = "test-key" + }; + + sutProvider.GetDependency() + .GetByOrganizationIdTypeAsync(organization.Id, PolicyType.AutomaticUserConfirmation) + .Returns(autoConfirmPolicy); + + sutProvider.GetDependency() + .TwoFactorIsEnabledAsync(Arg.Any>()) + .Returns([(userId, false)]); + + sutProvider.GetDependency() + .GetAsync(userId) + .Returns(new RequireTwoFactorPolicyRequirement([])); // No 2FA policy + + sutProvider.GetDependency() + .GetManyByUserAsync(userId) + .Returns([organizationUser]); + + // Act + var result = await sutProvider.Sut.ValidateAsync(request); + + // Assert + Assert.True(result.IsValid); + } + + [Theory] + [BitAutoData] + public async Task ValidateAsync_UserInMultipleOrgs_WithSingleOrgPolicyOnThisOrg_ReturnsError( + SutProvider sutProvider, + [Organization(useAutomaticUserConfirmation: true)] Organization organization, + [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser, + OrganizationUser otherOrgUser, + Guid userId, + [Policy(PolicyType.AutomaticUserConfirmation)] Policy autoConfirmPolicy) + { + // Arrange + organizationUser.UserId = userId; + organizationUser.OrganizationId = organization.Id; + + var request = new AutomaticallyConfirmOrganizationUserValidationRequest + { + PerformedBy = Substitute.For(), + DefaultUserCollectionName = "test-collection", + OrganizationUser = organizationUser, + OrganizationUserId = organizationUser.Id, + Organization = organization, + OrganizationId = organization.Id, + Key = "test-key" + }; + + var singleOrgPolicyDetails = new PolicyDetails + { + OrganizationId = organization.Id, + PolicyType = PolicyType.SingleOrg + }; + + sutProvider.GetDependency() + .GetByOrganizationIdTypeAsync(organization.Id, PolicyType.AutomaticUserConfirmation) + .Returns(autoConfirmPolicy); + + sutProvider.GetDependency() + .TwoFactorIsEnabledAsync(Arg.Any>()) + .Returns([(userId, true)]); + + sutProvider.GetDependency() + .GetManyByUserAsync(userId) + .Returns([organizationUser, otherOrgUser]); + + sutProvider.GetDependency() + .GetAsync(userId) + .Returns(new SingleOrganizationPolicyRequirement([singleOrgPolicyDetails])); + + // Act + var result = await sutProvider.Sut.ValidateAsync(request); + + // Assert + Assert.True(result.IsError); + Assert.IsType(result.AsError); + } + + [Theory] + [BitAutoData] + public async Task ValidateAsync_UserInMultipleOrgs_WithSingleOrgPolicyOnOtherOrg_ReturnsError( + SutProvider sutProvider, + [Organization(useAutomaticUserConfirmation: true)] Organization organization, + [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser, + OrganizationUser otherOrgUser, + Guid userId, + [Policy(PolicyType.AutomaticUserConfirmation)] Policy autoConfirmPolicy) + { + // Arrange + organizationUser.UserId = userId; + organizationUser.OrganizationId = organization.Id; + + var request = new AutomaticallyConfirmOrganizationUserValidationRequest + { + PerformedBy = Substitute.For(), + DefaultUserCollectionName = "test-collection", + OrganizationUser = organizationUser, + OrganizationUserId = organizationUser.Id, + Organization = organization, + OrganizationId = organization.Id, + Key = "test-key" + }; + + var otherOrgId = Guid.NewGuid(); // Different org + var singleOrgPolicyDetails = new PolicyDetails + { + OrganizationId = otherOrgId, + PolicyType = PolicyType.SingleOrg, + }; + + sutProvider.GetDependency() + .GetByOrganizationIdTypeAsync(organization.Id, PolicyType.AutomaticUserConfirmation) + .Returns(autoConfirmPolicy); + + sutProvider.GetDependency() + .TwoFactorIsEnabledAsync(Arg.Any>()) + .Returns([(userId, true)]); + + sutProvider.GetDependency() + .GetManyByUserAsync(userId) + .Returns([organizationUser, otherOrgUser]); + + sutProvider.GetDependency() + .GetAsync(userId) + .Returns(new SingleOrganizationPolicyRequirement([singleOrgPolicyDetails])); + + // Act + var result = await sutProvider.Sut.ValidateAsync(request); + + // Assert + Assert.True(result.IsError); + Assert.IsType(result.AsError); + } + + [Theory] + [BitAutoData] + public async Task ValidateAsync_UserInSingleOrg_ReturnsValidResult( + SutProvider sutProvider, + [Organization(useAutomaticUserConfirmation: true)] Organization organization, + [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser, + Guid userId, + [Policy(PolicyType.AutomaticUserConfirmation)] Policy autoConfirmPolicy) + { + // Arrange + organizationUser.UserId = userId; + organizationUser.OrganizationId = organization.Id; + + var request = new AutomaticallyConfirmOrganizationUserValidationRequest + { + PerformedBy = Substitute.For(), + DefaultUserCollectionName = "test-collection", + OrganizationUser = organizationUser, + OrganizationUserId = organizationUser.Id, + Organization = organization, + OrganizationId = organization.Id, + Key = "test-key" + }; + + sutProvider.GetDependency() + .GetByOrganizationIdTypeAsync(organization.Id, PolicyType.AutomaticUserConfirmation) + .Returns(autoConfirmPolicy); + + sutProvider.GetDependency() + .TwoFactorIsEnabledAsync(Arg.Any>()) + .Returns([(userId, true)]); + + sutProvider.GetDependency() + .GetManyByUserAsync(userId) + .Returns([organizationUser]); // Single org + + // Act + var result = await sutProvider.Sut.ValidateAsync(request); + + // Assert + Assert.True(result.IsValid); + } + + [Theory] + [BitAutoData] + public async Task ValidateAsync_UserInMultipleOrgs_WithNoSingleOrgPolicy_ReturnsValidResult( + SutProvider sutProvider, + [Organization(useAutomaticUserConfirmation: true)] Organization organization, + [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser, + OrganizationUser otherOrgUser, + Guid userId, + Policy autoConfirmPolicy) + { + // Arrange + organizationUser.UserId = userId; + organizationUser.OrganizationId = organization.Id; + autoConfirmPolicy.Type = PolicyType.AutomaticUserConfirmation; + autoConfirmPolicy.Enabled = true; + + var request = new AutomaticallyConfirmOrganizationUserValidationRequest + { + PerformedBy = Substitute.For(), + DefaultUserCollectionName = "test-collection", + OrganizationUser = organizationUser, + OrganizationUserId = organizationUser.Id, + Organization = organization, + OrganizationId = organization.Id, + Key = "test-key" + }; + + sutProvider.GetDependency() + .GetByOrganizationIdTypeAsync(organization.Id, PolicyType.AutomaticUserConfirmation) + .Returns(autoConfirmPolicy); + + sutProvider.GetDependency() + .TwoFactorIsEnabledAsync(Arg.Any>()) + .Returns([(userId, true)]); + + sutProvider.GetDependency() + .GetManyByUserAsync(userId) + .Returns([organizationUser, otherOrgUser]); + + sutProvider.GetDependency() + .GetAsync(userId) + .Returns(new SingleOrganizationPolicyRequirement([])); + + // Act + var result = await sutProvider.Sut.ValidateAsync(request); + + // Assert + Assert.True(result.IsValid); + } + + [Theory] + [BitAutoData] + public async Task ValidateAsync_WithAutoConfirmPolicyDisabled_ReturnsAutoConfirmPolicyNotEnabledError( + SutProvider sutProvider, + Organization organization, + [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser, + Guid userId) + { + // Arrange + organizationUser.UserId = userId; + organizationUser.OrganizationId = organization.Id; + + var request = new AutomaticallyConfirmOrganizationUserValidationRequest + { + PerformedBy = Substitute.For(), + DefaultUserCollectionName = "test-collection", + OrganizationUser = organizationUser, + OrganizationUserId = organizationUser.Id, + Organization = organization, + OrganizationId = organization.Id, + Key = "test-key" + }; + + sutProvider.GetDependency() + .GetByOrganizationIdTypeAsync(organization.Id, PolicyType.AutomaticUserConfirmation) + .Returns((Policy)null); + + sutProvider.GetDependency() + .TwoFactorIsEnabledAsync(Arg.Any>()) + .Returns([(userId, true)]); + + sutProvider.GetDependency() + .GetManyByUserAsync(userId) + .Returns([organizationUser]); + + // Act + var result = await sutProvider.Sut.ValidateAsync(request); + + // Assert + Assert.True(result.IsError); + Assert.IsType(result.AsError); + } + + [Theory] + [BitAutoData] + public async Task ValidateAsync_WithOrganizationUseAutomaticUserConfirmationDisabled_ReturnsAutoConfirmPolicyNotEnabledError( + SutProvider sutProvider, + [Organization(useAutomaticUserConfirmation: false)] Organization organization, + [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser, + Guid userId, + [Policy(PolicyType.AutomaticUserConfirmation)] Policy autoConfirmPolicy) + { + // Arrange + organizationUser.UserId = userId; + organizationUser.OrganizationId = organization.Id; + + var request = new AutomaticallyConfirmOrganizationUserValidationRequest + { + PerformedBy = Substitute.For(), + DefaultUserCollectionName = "test-collection", + OrganizationUser = organizationUser, + OrganizationUserId = organizationUser.Id, + Organization = organization, + OrganizationId = organization.Id, + Key = "test-key" + }; + + sutProvider.GetDependency() + .GetByOrganizationIdTypeAsync(organization.Id, PolicyType.AutomaticUserConfirmation) + .Returns(autoConfirmPolicy); + + sutProvider.GetDependency() + .TwoFactorIsEnabledAsync(Arg.Any>()) + .Returns([(userId, true)]); + + sutProvider.GetDependency() + .GetManyByUserAsync(userId) + .Returns([organizationUser]); + + // Act + var result = await sutProvider.Sut.ValidateAsync(request); + + // Assert + Assert.True(result.IsError); + Assert.IsType(result.AsError); + } +} diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/AutoConfirmUsers/AutomaticallyConfirmUsersCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/AutoConfirmUsers/AutomaticallyConfirmUsersCommandTests.cs new file mode 100644 index 0000000000..1035d5c578 --- /dev/null +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/AutoConfirmUsers/AutomaticallyConfirmUsersCommandTests.cs @@ -0,0 +1,730 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Models.Data; +using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; +using Bit.Core.AdminConsole.Models.Data.OrganizationUsers; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUser; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; +using Bit.Core.AdminConsole.Utilities.v2; +using Bit.Core.AdminConsole.Utilities.v2.Validation; +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 Bit.Core.Test.AutoFixture.OrganizationUserFixtures; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Microsoft.Extensions.Logging; +using NSubstitute; +using NSubstitute.ExceptionExtensions; +using Xunit; + +namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUsers; + +[SutProviderCustomize] +public class AutomaticallyConfirmUsersCommandTests +{ + [Theory] + [BitAutoData] + public async Task AutomaticallyConfirmOrganizationUserAsync_WithValidRequest_ConfirmsUserSuccessfully( + SutProvider sutProvider, + Organization organization, + [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser, + User user, + Guid performingUserId, + string key, + string defaultCollectionName) + { + // Arrange + organizationUser.UserId = user.Id; + organizationUser.OrganizationId = organization.Id; + + var request = new AutomaticallyConfirmOrganizationUserRequest + { + OrganizationUserId = organizationUser.Id, + OrganizationId = organization.Id, + Key = key, + DefaultUserCollectionName = defaultCollectionName, + PerformedBy = new StandardUser(performingUserId, true) + }; + + SetupRepositoryMocks(sutProvider, organizationUser, organization, user); + SetupValidatorMock(sutProvider, request, organizationUser, organization, true); + + sutProvider.GetDependency() + .ConfirmOrganizationUserAsync(Arg.Is(o => + o.OrganizationUserId == organizationUser.Id && o.Key == request.Key)) + .Returns(true); + + // Act + var result = await sutProvider.Sut.AutomaticallyConfirmOrganizationUserAsync(request); + + // Assert + Assert.True(result.IsSuccess); + + await sutProvider.GetDependency() + .Received(1) + .ConfirmOrganizationUserAsync(Arg.Is(o => + o.OrganizationUserId == organizationUser.Id && o.Key == request.Key)); + + await AssertSuccessfulOperationsAsync(sutProvider, organizationUser, organization, user, key); + } + + [Theory] + [BitAutoData] + public async Task AutomaticallyConfirmOrganizationUserAsync_WithInvalidUserOrgId_ReturnsOrganizationUserIdIsInvalidError( + SutProvider sutProvider, + Organization organization, + [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser, + User user, + Guid performingUserId, + string key, + string defaultCollectionName) + { + // Arrange + organizationUser.UserId = user.Id; + organizationUser.OrganizationId = Guid.NewGuid(); // User belongs to another organization + var request = new AutomaticallyConfirmOrganizationUserRequest + { + OrganizationUserId = organizationUser.Id, + OrganizationId = organization.Id, + Key = key, + DefaultUserCollectionName = defaultCollectionName, + PerformedBy = new StandardUser(performingUserId, true) + }; + + SetupRepositoryMocks(sutProvider, organizationUser, organization, user); + SetupValidatorMock(sutProvider, request, organizationUser, organization, false, new OrganizationUserIdIsInvalid()); + + // Act + var result = await sutProvider.Sut.AutomaticallyConfirmOrganizationUserAsync(request); + + // Assert + Assert.True(result.IsError); + Assert.IsType(result.AsError); + + await sutProvider.GetDependency() + .DidNotReceive() + .ConfirmOrganizationUserAsync(Arg.Any()); + } + + [Theory] + [BitAutoData] + public async Task AutomaticallyConfirmOrganizationUserAsync_WhenAlreadyConfirmed_ReturnsNoneSuccess( + SutProvider sutProvider, + Organization organization, + [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser, + User user, + Guid performingUserId, + string key, + string defaultCollectionName) + { + // Arrange + organizationUser.UserId = user.Id; + organizationUser.OrganizationId = organization.Id; + var request = new AutomaticallyConfirmOrganizationUserRequest + { + OrganizationUserId = organizationUser.Id, + OrganizationId = organization.Id, + Key = key, + DefaultUserCollectionName = defaultCollectionName, + PerformedBy = new StandardUser(performingUserId, true) + }; + + SetupRepositoryMocks(sutProvider, organizationUser, organization, user); + SetupValidatorMock(sutProvider, request, organizationUser, organization, true); + + // Return false to indicate the user is already confirmed + sutProvider.GetDependency() + .ConfirmOrganizationUserAsync(Arg.Is(x => + x.OrganizationUserId == organizationUser.Id && x.Key == request.Key)) + .Returns(false); + + // Act + var result = await sutProvider.Sut.AutomaticallyConfirmOrganizationUserAsync(request); + + // Assert + Assert.True(result.IsSuccess); + + await sutProvider.GetDependency() + .Received(1) + .ConfirmOrganizationUserAsync(Arg.Is(x => + x.OrganizationUserId == organizationUser.Id && x.Key == request.Key)); + + // Verify no side effects occurred + await sutProvider.GetDependency() + .DidNotReceive() + .LogOrganizationUserEventAsync(Arg.Any(), Arg.Any(), Arg.Any()); + + await sutProvider.GetDependency() + .DidNotReceive() + .PushSyncOrgKeysAsync(Arg.Any()); + } + + [Theory] + [BitAutoData] + public async Task AutomaticallyConfirmOrganizationUserAsync_WithDefaultCollectionEnabled_CreatesDefaultCollection( + SutProvider sutProvider, + Organization organization, + [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser, + User user, + Guid performingUserId, + string key, + string defaultCollectionName) + { + // Arrange + organizationUser.UserId = user.Id; + organizationUser.OrganizationId = organization.Id; + var request = new AutomaticallyConfirmOrganizationUserRequest + { + OrganizationUserId = organizationUser.Id, + OrganizationId = organization.Id, + Key = key, + DefaultUserCollectionName = defaultCollectionName, // Non-empty to trigger creation + PerformedBy = new StandardUser(performingUserId, true) + }; + + SetupRepositoryMocks(sutProvider, organizationUser, organization, user); + SetupValidatorMock(sutProvider, request, organizationUser, organization, true); + SetupPolicyRequirementMock(sutProvider, user.Id, organization.Id, true); // Policy requires collection + + sutProvider.GetDependency().ConfirmOrganizationUserAsync( + Arg.Is(o => + o.OrganizationUserId == organizationUser.Id && o.Key == request.Key)) + .Returns(true); + + // Act + var result = await sutProvider.Sut.AutomaticallyConfirmOrganizationUserAsync(request); + + // Assert + Assert.True(result.IsSuccess); + + await sutProvider.GetDependency() + .Received(1) + .CreateAsync( + Arg.Is(c => + c.OrganizationId == organization.Id && + c.Name == defaultCollectionName && + c.Type == CollectionType.DefaultUserCollection), + Arg.Is>(groups => groups == null), + Arg.Is>(access => + access.FirstOrDefault(x => x.Id == organizationUser.Id && x.Manage) != null)); + } + + [Theory] + [BitAutoData] + public async Task AutomaticallyConfirmOrganizationUserAsync_WithDefaultCollectionDisabled_DoesNotCreateCollection( + SutProvider sutProvider, + Organization organization, + [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser, + User user, + Guid performingUserId, + string key) + { + // Arrange + organizationUser.UserId = user.Id; + organizationUser.OrganizationId = organization.Id; + var request = new AutomaticallyConfirmOrganizationUserRequest + { + OrganizationUserId = organizationUser.Id, + OrganizationId = organization.Id, + Key = key, + DefaultUserCollectionName = string.Empty, // Empty, so the collection won't be created + PerformedBy = new StandardUser(performingUserId, true) + }; + + SetupRepositoryMocks(sutProvider, organizationUser, organization, user); + SetupValidatorMock(sutProvider, request, organizationUser, organization, true); + SetupPolicyRequirementMock(sutProvider, user.Id, organization.Id, false); // Policy doesn't require + + sutProvider.GetDependency() + .ConfirmOrganizationUserAsync(Arg.Is(o => + o.OrganizationUserId == organizationUser.Id && o.Key == request.Key)) + .Returns(true); + + // Act + var result = await sutProvider.Sut.AutomaticallyConfirmOrganizationUserAsync(request); + + // Assert + Assert.True(result.IsSuccess); + + await sutProvider.GetDependency() + .DidNotReceive() + .CreateAsync(Arg.Any(), + Arg.Any>(), + Arg.Any>()); + } + + [Theory] + [BitAutoData] + public async Task AutomaticallyConfirmOrganizationUserAsync_WhenCreateDefaultCollectionFails_LogsErrorButReturnsSuccess( + SutProvider sutProvider, + Organization organization, + [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser, + User user, + Guid performingUserId, + string key, + string defaultCollectionName) + { + // Arrange + organizationUser.UserId = user.Id; + organizationUser.OrganizationId = organization.Id; + var request = new AutomaticallyConfirmOrganizationUserRequest + { + OrganizationUserId = organizationUser.Id, + OrganizationId = organization.Id, + Key = key, + DefaultUserCollectionName = defaultCollectionName, // Non-empty to trigger creation + PerformedBy = new StandardUser(performingUserId, true) + }; + + SetupRepositoryMocks(sutProvider, organizationUser, organization, user); + SetupValidatorMock(sutProvider, request, organizationUser, organization, true); + SetupPolicyRequirementMock(sutProvider, user.Id, organization.Id, true); + + sutProvider.GetDependency() + .ConfirmOrganizationUserAsync(Arg.Is(o => + o.OrganizationUserId == organizationUser.Id && o.Key == request.Key)).Returns(true); + + var collectionException = new Exception("Collection creation failed"); + sutProvider.GetDependency() + .CreateAsync(Arg.Any(), + Arg.Any>(), + Arg.Any>()) + .ThrowsAsync(collectionException); + + // Act + var result = await sutProvider.Sut.AutomaticallyConfirmOrganizationUserAsync(request); + + // Assert - side effects are fire-and-forget, so command returns success even if collection creation fails + Assert.True(result.IsSuccess); + + sutProvider.GetDependency>() + .Received(1) + .Log( + LogLevel.Error, + Arg.Any(), + Arg.Is(o => o.ToString()!.Contains("Failed to create default collection for user")), + collectionException, + Arg.Any>()); + } + + [Theory] + [BitAutoData] + public async Task AutomaticallyConfirmOrganizationUserAsync_WhenEventLogFails_LogsErrorButReturnsSuccess( + SutProvider sutProvider, + Organization organization, + [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser, + User user, + Guid performingUserId, + string key, + string defaultCollectionName) + { + // Arrange + organizationUser.UserId = user.Id; + organizationUser.OrganizationId = organization.Id; + var request = new AutomaticallyConfirmOrganizationUserRequest + { + OrganizationUserId = organizationUser.Id, + OrganizationId = organization.Id, + Key = key, + DefaultUserCollectionName = defaultCollectionName, + PerformedBy = new StandardUser(performingUserId, true) + }; + + SetupRepositoryMocks(sutProvider, organizationUser, organization, user); + SetupValidatorMock(sutProvider, request, organizationUser, organization, true); + + sutProvider.GetDependency() + .ConfirmOrganizationUserAsync(Arg.Is(o => + o.OrganizationUserId == organizationUser.Id && o.Key == request.Key)) + .Returns(true); + + var eventException = new Exception("Event logging failed"); + sutProvider.GetDependency() + .LogOrganizationUserEventAsync(Arg.Any(), + EventType.OrganizationUser_AutomaticallyConfirmed, + Arg.Any()) + .ThrowsAsync(eventException); + + // Act + var result = await sutProvider.Sut.AutomaticallyConfirmOrganizationUserAsync(request); + + // Assert - side effects are fire-and-forget, so command returns success even if event log fails + Assert.True(result.IsSuccess); + + sutProvider.GetDependency>() + .Received(1) + .Log( + LogLevel.Error, + Arg.Any(), + Arg.Is(o => o.ToString()!.Contains("Failed to log OrganizationUser_AutomaticallyConfirmed event")), + eventException, + Arg.Any>()); + } + + [Theory] + [BitAutoData] + public async Task AutomaticallyConfirmOrganizationUserAsync_WhenSendEmailFails_LogsErrorButReturnsSuccess( + SutProvider sutProvider, + Organization organization, + [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser, + User user, + Guid performingUserId, + string key, + string defaultCollectionName) + { + // Arrange + organizationUser.UserId = user.Id; + organizationUser.OrganizationId = organization.Id; + var request = new AutomaticallyConfirmOrganizationUserRequest + { + OrganizationUserId = organizationUser.Id, + OrganizationId = organization.Id, + Key = key, + DefaultUserCollectionName = defaultCollectionName, + PerformedBy = new StandardUser(performingUserId, true) + }; + + SetupRepositoryMocks(sutProvider, organizationUser, organization, user); + SetupValidatorMock(sutProvider, request, organizationUser, organization, true); + + sutProvider.GetDependency() + .ConfirmOrganizationUserAsync(Arg.Is(o => + o.OrganizationUserId == organizationUser.Id && o.Key == request.Key)) + .Returns(true); + + var emailException = new Exception("Email sending failed"); + sutProvider.GetDependency() + .SendOrganizationConfirmedEmailAsync(organization.Name, user.Email, organizationUser.AccessSecretsManager) + .ThrowsAsync(emailException); + + // Act + var result = await sutProvider.Sut.AutomaticallyConfirmOrganizationUserAsync(request); + + // Assert - side effects are fire-and-forget, so command returns success even if email fails + Assert.True(result.IsSuccess); + + sutProvider.GetDependency>() + .Received(1) + .Log( + LogLevel.Error, + Arg.Any(), + Arg.Is(o => o.ToString()!.Contains("Failed to send OrganizationUserConfirmed")), + emailException, + Arg.Any>()); + } + + [Theory] + [BitAutoData] + public async Task AutomaticallyConfirmOrganizationUserAsync_WhenUserNotFoundForEmail_LogsErrorButReturnsSuccess( + SutProvider sutProvider, + Organization organization, + [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser, + User user, + Guid performingUserId, + string key, + string defaultCollectionName) + { + // Arrange + organizationUser.UserId = user.Id; + organizationUser.OrganizationId = organization.Id; + var request = new AutomaticallyConfirmOrganizationUserRequest + { + OrganizationUserId = organizationUser.Id, + OrganizationId = organization.Id, + Key = key, + DefaultUserCollectionName = defaultCollectionName, + PerformedBy = new StandardUser(performingUserId, true) + }; + + SetupRepositoryMocks(sutProvider, organizationUser, organization, user); + SetupValidatorMock(sutProvider, request, organizationUser, organization, true); + + sutProvider.GetDependency() + .ConfirmOrganizationUserAsync(Arg.Is(o => + o.OrganizationUserId == organizationUser.Id && o.Key == request.Key)) + .Returns(true); + + // Return null when retrieving user for email + sutProvider.GetDependency() + .GetByIdAsync(user.Id) + .Returns((User)null!); + + // Act + var result = await sutProvider.Sut.AutomaticallyConfirmOrganizationUserAsync(request); + + // Assert - side effects are fire-and-forget, so command returns success even if user not found for email + Assert.True(result.IsSuccess); + } + + [Theory] + [BitAutoData] + public async Task AutomaticallyConfirmOrganizationUserAsync_WhenDeleteDeviceRegistrationFails_LogsErrorButReturnsSuccess( + SutProvider sutProvider, + Organization organization, + [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser, + User user, + Guid performingUserId, + string key, + string defaultCollectionName, + Device device) + { + // Arrange + organizationUser.UserId = user.Id; + organizationUser.OrganizationId = organization.Id; + device.UserId = user.Id; + device.PushToken = "test-push-token"; + var request = new AutomaticallyConfirmOrganizationUserRequest + { + OrganizationUserId = organizationUser.Id, + OrganizationId = organization.Id, + Key = key, + DefaultUserCollectionName = defaultCollectionName, + PerformedBy = new StandardUser(performingUserId, true) + }; + + SetupRepositoryMocks(sutProvider, organizationUser, organization, user); + SetupValidatorMock(sutProvider, request, organizationUser, organization, true); + + sutProvider.GetDependency() + .ConfirmOrganizationUserAsync(Arg.Is(o => + o.OrganizationUserId == organizationUser.Id && o.Key == request.Key)) + .Returns(true); + + sutProvider.GetDependency() + .GetManyByUserIdAsync(user.Id) + .Returns(new List { device }); + + var deviceException = new Exception("Device registration deletion failed"); + sutProvider.GetDependency() + .DeleteUserRegistrationOrganizationAsync(Arg.Any>(), organization.Id.ToString()) + .ThrowsAsync(deviceException); + + // Act + var result = await sutProvider.Sut.AutomaticallyConfirmOrganizationUserAsync(request); + + // Assert - side effects are fire-and-forget, so command returns success even if device registration deletion fails + Assert.True(result.IsSuccess); + + sutProvider.GetDependency>() + .Received(1) + .Log( + LogLevel.Error, + Arg.Any(), + Arg.Is(o => o.ToString()!.Contains("Failed to delete device registration")), + deviceException, + Arg.Any>()); + } + + [Theory] + [BitAutoData] + public async Task AutomaticallyConfirmOrganizationUserAsync_WhenPushSyncOrgKeysFails_LogsErrorButReturnsSuccess( + SutProvider sutProvider, + Organization organization, + [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser, + User user, + Guid performingUserId, + string key, + string defaultCollectionName) + { + // Arrange + organizationUser.UserId = user.Id; + organizationUser.OrganizationId = organization.Id; + var request = new AutomaticallyConfirmOrganizationUserRequest + { + OrganizationUserId = organizationUser.Id, + OrganizationId = organization.Id, + Key = key, + DefaultUserCollectionName = defaultCollectionName, + PerformedBy = new StandardUser(performingUserId, true) + }; + + SetupRepositoryMocks(sutProvider, organizationUser, organization, user); + SetupValidatorMock(sutProvider, request, organizationUser, organization, true); + + sutProvider.GetDependency() + .ConfirmOrganizationUserAsync(Arg.Is(o => + o.OrganizationUserId == organizationUser.Id && o.Key == request.Key)) + .Returns(true); + + var pushException = new Exception("Push sync failed"); + sutProvider.GetDependency() + .PushSyncOrgKeysAsync(user.Id) + .ThrowsAsync(pushException); + + // Act + var result = await sutProvider.Sut.AutomaticallyConfirmOrganizationUserAsync(request); + + // Assert - side effects are fire-and-forget, so command returns success even if push sync fails + Assert.True(result.IsSuccess); + + sutProvider.GetDependency>() + .Received(1) + .Log( + LogLevel.Error, + Arg.Any(), + Arg.Is(o => o.ToString()!.Contains("Failed to push organization keys")), + pushException, + Arg.Any>()); + } + + [Theory] + [BitAutoData] + public async Task AutomaticallyConfirmOrganizationUserAsync_WithDevicesWithoutPushToken_FiltersCorrectly( + SutProvider sutProvider, + Organization organization, + [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser, + User user, + Guid performingUserId, + string key, + string defaultCollectionName, + Device deviceWithToken, + Device deviceWithoutToken) + { + // Arrange + organizationUser.UserId = user.Id; + organizationUser.OrganizationId = organization.Id; + deviceWithToken.UserId = user.Id; + deviceWithToken.PushToken = "test-token"; + deviceWithoutToken.UserId = user.Id; + deviceWithoutToken.PushToken = null; + var request = new AutomaticallyConfirmOrganizationUserRequest + { + OrganizationUserId = organizationUser.Id, + OrganizationId = organization.Id, + Key = key, + DefaultUserCollectionName = defaultCollectionName, + PerformedBy = new StandardUser(performingUserId, true) + }; + + SetupRepositoryMocks(sutProvider, organizationUser, organization, user); + SetupValidatorMock(sutProvider, request, organizationUser, organization, true); + + sutProvider.GetDependency() + .ConfirmOrganizationUserAsync(Arg.Is(o => + o.OrganizationUserId == organizationUser.Id && o.Key == request.Key)) + .Returns(true); + + sutProvider.GetDependency() + .GetManyByUserIdAsync(user.Id) + .Returns(new List { deviceWithToken, deviceWithoutToken }); + + // Act + var result = await sutProvider.Sut.AutomaticallyConfirmOrganizationUserAsync(request); + + // Assert + Assert.True(result.IsSuccess); + + await sutProvider.GetDependency() + .Received(1) + .DeleteUserRegistrationOrganizationAsync( + Arg.Is>(devices => + devices.Count(d => deviceWithToken.Id.ToString() == d) == 1), + organization.Id.ToString()); + } + + private static void SetupRepositoryMocks( + SutProvider sutProvider, + OrganizationUser organizationUser, + Organization organization, + User user) + { + sutProvider.GetDependency() + .GetByIdAsync(organizationUser.Id) + .Returns(organizationUser); + + sutProvider.GetDependency() + .GetByIdAsync(organization.Id) + .Returns(organization); + + sutProvider.GetDependency() + .GetByIdAsync(user.Id) + .Returns(user); + + sutProvider.GetDependency() + .GetManyByUserIdAsync(user.Id) + .Returns(new List()); + } + + private static void SetupValidatorMock( + SutProvider sutProvider, + AutomaticallyConfirmOrganizationUserRequest originalRequest, + OrganizationUser organizationUser, + Organization organization, + bool isValid, + Error? error = null) + { + var validationRequest = new AutomaticallyConfirmOrganizationUserValidationRequest + { + PerformedBy = originalRequest.PerformedBy, + DefaultUserCollectionName = originalRequest.DefaultUserCollectionName, + OrganizationUserId = originalRequest.OrganizationUserId, + OrganizationUser = organizationUser, + OrganizationId = originalRequest.OrganizationId, + Organization = organization, + Key = originalRequest.Key + }; + + var validationResult = isValid + ? ValidationResultHelpers.Valid(validationRequest) + : ValidationResultHelpers.Invalid(validationRequest, error ?? new UserIsNotAccepted()); + + sutProvider.GetDependency() + .ValidateAsync(Arg.Any()) + .Returns(validationResult); + } + + private static void SetupPolicyRequirementMock( + SutProvider sutProvider, + Guid userId, + Guid organizationId, + bool requiresDefaultCollection) + { + var policyDetails = requiresDefaultCollection + ? new List { new() { OrganizationId = organizationId } } + : new List(); + + var policyRequirement = new OrganizationDataOwnershipPolicyRequirement( + requiresDefaultCollection ? OrganizationDataOwnershipState.Enabled : OrganizationDataOwnershipState.Disabled, + policyDetails); + + sutProvider.GetDependency() + .GetAsync(userId) + .Returns(policyRequirement); + } + + private static async Task AssertSuccessfulOperationsAsync( + SutProvider sutProvider, + OrganizationUser organizationUser, + Organization organization, + User user, + string key) + { + await sutProvider.GetDependency() + .Received(1) + .LogOrganizationUserEventAsync( + Arg.Is(x => x.Id == organizationUser.Id), + EventType.OrganizationUser_AutomaticallyConfirmed, + Arg.Any()); + + await sutProvider.GetDependency() + .Received(1) + .SendOrganizationConfirmedEmailAsync( + organization.Name, + user.Email, + organizationUser.AccessSecretsManager); + + await sutProvider.GetDependency() + .Received(1) + .PushSyncOrgKeysAsync(user.Id); + + await sutProvider.GetDependency() + .Received(1) + .DeleteUserRegistrationOrganizationAsync( + Arg.Any>(), + organization.Id.ToString()); + } +} diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccountvNext/DeleteClaimedOrganizationUserAccountCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccountvNext/DeleteClaimedOrganizationUserAccountCommandTests.cs index c223520a04..dfb1b35be0 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccountvNext/DeleteClaimedOrganizationUserAccountCommandTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccountvNext/DeleteClaimedOrganizationUserAccountCommandTests.cs @@ -1,5 +1,7 @@ using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccount; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; +using Bit.Core.AdminConsole.Utilities.v2; +using Bit.Core.AdminConsole.Utilities.v2.Validation; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUserCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUserCommandTests.cs index 10dcff9e2a..5d82f0717d 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUserCommandTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUserCommandTests.cs @@ -13,7 +13,6 @@ using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Utilities.Commands; using Bit.Core.AdminConsole.Utilities.Errors; using Bit.Core.AdminConsole.Utilities.Validation; -using Bit.Core.Billing.Models.StaticStore.Plans; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Models.Business; @@ -22,6 +21,7 @@ using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface; using Bit.Core.Repositories; using Bit.Core.Services; +using Bit.Core.Test.Billing.Mocks.Plans; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using Microsoft.Extensions.Time.Testing; @@ -29,6 +29,7 @@ using NSubstitute; using NSubstitute.ExceptionExtensions; using Xunit; using static Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Helpers.InviteUserOrganizationValidationRequestHelpers; +using Enterprise2019Plan = Bit.Core.Test.Billing.Mocks.Plans.Enterprise2019Plan; namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers; diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteOrganizationUsersValidatorTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteOrganizationUsersValidatorTests.cs index a5b220b94a..04ef3961ca 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteOrganizationUsersValidatorTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteOrganizationUsersValidatorTests.cs @@ -3,12 +3,12 @@ using Bit.Core.AdminConsole.Models.Business; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation; using Bit.Core.AdminConsole.Utilities.Validation; -using Bit.Core.Billing.Models.StaticStore.Plans; using Bit.Core.Exceptions; using Bit.Core.Models.Business; using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface; using Bit.Core.Repositories; using Bit.Core.Services; +using Bit.Core.Test.Billing.Mocks.Plans; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteUserOrganizationValidationTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteUserOrganizationValidationTests.cs index be5586f8a6..482b369780 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteUserOrganizationValidationTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteUserOrganizationValidationTests.cs @@ -2,7 +2,7 @@ using Bit.Core.AdminConsole.Models.Business; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Organization; using Bit.Core.AdminConsole.Utilities.Validation; -using Bit.Core.Billing.Models.StaticStore.Plans; +using Bit.Core.Test.Billing.Mocks.Plans; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using Xunit; diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteUserPaymentValidationTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteUserPaymentValidationTests.cs index 738ae71298..72a146205b 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteUserPaymentValidationTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteUserPaymentValidationTests.cs @@ -5,7 +5,7 @@ using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.V using Bit.Core.AdminConsole.Utilities.Validation; using Bit.Core.Billing.Constants; using Bit.Core.Billing.Enums; -using Bit.Core.Billing.Models.StaticStore.Plans; +using Bit.Core.Test.Billing.Mocks.Plans; using Bit.Test.Common.AutoFixture.Attributes; using Xunit; diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/PasswordManagerInviteUserValidatorTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/PasswordManagerInviteUserValidatorTests.cs index 571832d675..46ca37522f 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/PasswordManagerInviteUserValidatorTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/PasswordManagerInviteUserValidatorTests.cs @@ -3,7 +3,7 @@ using Bit.Core.AdminConsole.Models.Business; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.PasswordManager; using Bit.Core.AdminConsole.Utilities.Validation; using Bit.Core.Billing.Enums; -using Bit.Core.Billing.Models.StaticStore.Plans; +using Bit.Core.Test.Billing.Mocks.Plans; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using Xunit; diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/GetOrganizationSubscriptionsToUpdateQueryTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/GetOrganizationSubscriptionsToUpdateQueryTests.cs index af6b5a17f7..f1c4797de8 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/GetOrganizationSubscriptionsToUpdateQueryTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/GetOrganizationSubscriptionsToUpdateQueryTests.cs @@ -1,9 +1,9 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.OrganizationFeatures.Organizations; using Bit.Core.Billing.Enums; -using Bit.Core.Billing.Models.StaticStore.Plans; using Bit.Core.Billing.Pricing; using Bit.Core.Repositories; +using Bit.Core.Test.Billing.Mocks.Plans; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationSignUp/CloudOrganizationSignUpCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationSignUp/CloudOrganizationSignUpCommandTests.cs index feb5ef2a40..c1fea1455e 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationSignUp/CloudOrganizationSignUpCommandTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationSignUp/CloudOrganizationSignUpCommandTests.cs @@ -10,7 +10,7 @@ using Bit.Core.Exceptions; using Bit.Core.Models.Business; using Bit.Core.Models.Data; using Bit.Core.Repositories; -using Bit.Core.Utilities; +using Bit.Core.Test.Billing.Mocks; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; @@ -28,7 +28,7 @@ public class CloudICloudOrganizationSignUpCommandTests { signup.Plan = planType; - var plan = StaticStore.GetPlan(signup.Plan); + var plan = MockPlans.Get(signup.Plan); signup.AdditionalSeats = 0; signup.PaymentMethodType = PaymentMethodType.Card; @@ -37,7 +37,7 @@ public class CloudICloudOrganizationSignUpCommandTests signup.IsFromSecretsManagerTrial = false; signup.IsFromProvider = false; - sutProvider.GetDependency().GetPlanOrThrow(signup.Plan).Returns(StaticStore.GetPlan(signup.Plan)); + sutProvider.GetDependency().GetPlanOrThrow(signup.Plan).Returns(MockPlans.Get(signup.Plan)); var result = await sutProvider.Sut.SignUpOrganizationAsync(signup); @@ -77,7 +77,7 @@ public class CloudICloudOrganizationSignUpCommandTests signup.UseSecretsManager = false; signup.IsFromProvider = false; - sutProvider.GetDependency().GetPlanOrThrow(signup.Plan).Returns(StaticStore.GetPlan(signup.Plan)); + sutProvider.GetDependency().GetPlanOrThrow(signup.Plan).Returns(MockPlans.Get(signup.Plan)); // Extract orgUserId when created Guid? orgUserId = null; @@ -112,7 +112,7 @@ public class CloudICloudOrganizationSignUpCommandTests { signup.Plan = planType; - var plan = StaticStore.GetPlan(signup.Plan); + var plan = MockPlans.Get(signup.Plan); signup.UseSecretsManager = true; signup.AdditionalSeats = 15; @@ -123,7 +123,7 @@ public class CloudICloudOrganizationSignUpCommandTests signup.IsFromSecretsManagerTrial = false; signup.IsFromProvider = false; - sutProvider.GetDependency().GetPlanOrThrow(signup.Plan).Returns(StaticStore.GetPlan(signup.Plan)); + sutProvider.GetDependency().GetPlanOrThrow(signup.Plan).Returns(MockPlans.Get(signup.Plan)); var result = await sutProvider.Sut.SignUpOrganizationAsync(signup); @@ -164,7 +164,7 @@ public class CloudICloudOrganizationSignUpCommandTests signup.PremiumAccessAddon = false; signup.IsFromProvider = true; - sutProvider.GetDependency().GetPlanOrThrow(signup.Plan).Returns(StaticStore.GetPlan(signup.Plan)); + sutProvider.GetDependency().GetPlanOrThrow(signup.Plan).Returns(MockPlans.Get(signup.Plan)); var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.SignUpOrganizationAsync(signup)); Assert.Contains("Organizations with a Managed Service Provider do not support Secrets Manager.", exception.Message); @@ -184,7 +184,7 @@ public class CloudICloudOrganizationSignUpCommandTests signup.AdditionalStorageGb = 0; signup.IsFromProvider = false; - sutProvider.GetDependency().GetPlanOrThrow(signup.Plan).Returns(StaticStore.GetPlan(signup.Plan)); + sutProvider.GetDependency().GetPlanOrThrow(signup.Plan).Returns(MockPlans.Get(signup.Plan)); var exception = await Assert.ThrowsAsync( () => sutProvider.Sut.SignUpOrganizationAsync(signup)); @@ -204,7 +204,7 @@ public class CloudICloudOrganizationSignUpCommandTests signup.AdditionalServiceAccounts = 10; signup.IsFromProvider = false; - sutProvider.GetDependency().GetPlanOrThrow(signup.Plan).Returns(StaticStore.GetPlan(signup.Plan)); + sutProvider.GetDependency().GetPlanOrThrow(signup.Plan).Returns(MockPlans.Get(signup.Plan)); var exception = await Assert.ThrowsAsync( () => sutProvider.Sut.SignUpOrganizationAsync(signup)); @@ -224,7 +224,7 @@ public class CloudICloudOrganizationSignUpCommandTests signup.AdditionalServiceAccounts = -10; signup.IsFromProvider = false; - sutProvider.GetDependency().GetPlanOrThrow(signup.Plan).Returns(StaticStore.GetPlan(signup.Plan)); + sutProvider.GetDependency().GetPlanOrThrow(signup.Plan).Returns(MockPlans.Get(signup.Plan)); var exception = await Assert.ThrowsAsync( () => sutProvider.Sut.SignUpOrganizationAsync(signup)); @@ -244,7 +244,7 @@ public class CloudICloudOrganizationSignUpCommandTests Owner = new User { Id = Guid.NewGuid() } }; - sutProvider.GetDependency().GetPlanOrThrow(signup.Plan).Returns(StaticStore.GetPlan(signup.Plan)); + sutProvider.GetDependency().GetPlanOrThrow(signup.Plan).Returns(MockPlans.Get(signup.Plan)); sutProvider.GetDependency() .GetCountByFreeOrganizationAdminUserAsync(signup.Owner.Id) diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationSignUp/ProviderClientOrganizationSignUpCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationSignUp/ProviderClientOrganizationSignUpCommandTests.cs index 881f134b4c..5385b4cdea 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationSignUp/ProviderClientOrganizationSignUpCommandTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationSignUp/ProviderClientOrganizationSignUpCommandTests.cs @@ -10,7 +10,7 @@ using Bit.Core.Models.Data; using Bit.Core.Models.StaticStore; using Bit.Core.Repositories; using Bit.Core.Services; -using Bit.Core.Utilities; +using Bit.Core.Test.Billing.Mocks; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; @@ -36,7 +36,7 @@ public class ProviderClientOrganizationSignUpCommandTests signup.AdditionalSeats = 15; signup.CollectionName = collectionName; - var plan = StaticStore.GetPlan(signup.Plan); + var plan = MockPlans.Get(signup.Plan); sutProvider.GetDependency() .GetPlanOrThrow(signup.Plan) .Returns(plan); @@ -112,7 +112,7 @@ public class ProviderClientOrganizationSignUpCommandTests signup.Plan = PlanType.TeamsMonthly; signup.AdditionalSeats = -5; - var plan = StaticStore.GetPlan(signup.Plan); + var plan = MockPlans.Get(signup.Plan); sutProvider.GetDependency() .GetPlanOrThrow(signup.Plan) .Returns(plan); @@ -132,7 +132,7 @@ public class ProviderClientOrganizationSignUpCommandTests { signup.Plan = planType; - var plan = StaticStore.GetPlan(signup.Plan); + var plan = MockPlans.Get(signup.Plan); sutProvider.GetDependency() .GetPlanOrThrow(signup.Plan) .Returns(plan); diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/UpdateOrganizationSubscriptionCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/UpdateOrganizationSubscriptionCommandTests.cs index 37a5627919..f9fc086873 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/UpdateOrganizationSubscriptionCommandTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/UpdateOrganizationSubscriptionCommandTests.cs @@ -2,10 +2,10 @@ using Bit.Core.AdminConsole.Models.Data.Organizations; using Bit.Core.AdminConsole.OrganizationFeatures.Organizations; using Bit.Core.Billing.Enums; -using Bit.Core.Billing.Models.StaticStore.Plans; using Bit.Core.Models.StaticStore; using Bit.Core.Repositories; using Bit.Core.Services; +using Bit.Core.Test.Billing.Mocks.Plans; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/BlockClaimedDomainAccountCreationPolicyValidatorTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/BlockClaimedDomainAccountCreationPolicyValidatorTests.cs new file mode 100644 index 0000000000..e317a5886e --- /dev/null +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/BlockClaimedDomainAccountCreationPolicyValidatorTests.cs @@ -0,0 +1,189 @@ +namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.PolicyValidators; + +using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators; +using Bit.Core.Services; +using Bit.Core.Test.AdminConsole.AutoFixture; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; + +[SutProviderCustomize] +public class BlockClaimedDomainAccountCreationPolicyValidatorTests +{ + [Theory, BitAutoData] + public async Task ValidateAsync_EnablingPolicy_NoVerifiedDomains_ValidationError( + [PolicyUpdate(PolicyType.BlockClaimedDomainAccountCreation, true)] PolicyUpdate policyUpdate, + SutProvider sutProvider) + { + // Arrange + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.BlockClaimedDomainAccountCreation) + .Returns(true); + + sutProvider.GetDependency() + .HasVerifiedDomainsAsync(policyUpdate.OrganizationId) + .Returns(false); + + // Act + var result = await sutProvider.Sut.ValidateAsync(policyUpdate, null); + + // Assert + Assert.Equal("You must claim at least one domain to turn on this policy", result); + } + + [Theory, BitAutoData] + public async Task ValidateAsync_EnablingPolicy_HasVerifiedDomains_Success( + [PolicyUpdate(PolicyType.BlockClaimedDomainAccountCreation, true)] PolicyUpdate policyUpdate, + SutProvider sutProvider) + { + // Arrange + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.BlockClaimedDomainAccountCreation) + .Returns(true); + + sutProvider.GetDependency() + .HasVerifiedDomainsAsync(policyUpdate.OrganizationId) + .Returns(true); + + // Act + var result = await sutProvider.Sut.ValidateAsync(policyUpdate, null); + + // Assert + Assert.True(string.IsNullOrEmpty(result)); + } + + [Theory, BitAutoData] + public async Task ValidateAsync_DisablingPolicy_NoValidation( + [PolicyUpdate(PolicyType.BlockClaimedDomainAccountCreation, false)] PolicyUpdate policyUpdate, + SutProvider sutProvider) + { + // Arrange + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.BlockClaimedDomainAccountCreation) + .Returns(true); + + // Act + var result = await sutProvider.Sut.ValidateAsync(policyUpdate, null); + + // Assert + Assert.True(string.IsNullOrEmpty(result)); + await sutProvider.GetDependency() + .DidNotReceive() + .HasVerifiedDomainsAsync(Arg.Any()); + } + + [Theory, BitAutoData] + public async Task ValidateAsync_WithSavePolicyModel_EnablingPolicy_NoVerifiedDomains_ValidationError( + [PolicyUpdate(PolicyType.BlockClaimedDomainAccountCreation, true)] PolicyUpdate policyUpdate, + SutProvider sutProvider) + { + // Arrange + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.BlockClaimedDomainAccountCreation) + .Returns(true); + + sutProvider.GetDependency() + .HasVerifiedDomainsAsync(policyUpdate.OrganizationId) + .Returns(false); + + var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel()); + + // Act + var result = await sutProvider.Sut.ValidateAsync(savePolicyModel, null); + + // Assert + Assert.Equal("You must claim at least one domain to turn on this policy", result); + } + + [Theory, BitAutoData] + public async Task ValidateAsync_WithSavePolicyModel_EnablingPolicy_HasVerifiedDomains_Success( + [PolicyUpdate(PolicyType.BlockClaimedDomainAccountCreation, true)] PolicyUpdate policyUpdate, + SutProvider sutProvider) + { + // Arrange + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.BlockClaimedDomainAccountCreation) + .Returns(true); + + sutProvider.GetDependency() + .HasVerifiedDomainsAsync(policyUpdate.OrganizationId) + .Returns(true); + + var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel()); + + // Act + var result = await sutProvider.Sut.ValidateAsync(savePolicyModel, null); + + // Assert + Assert.True(string.IsNullOrEmpty(result)); + } + + [Theory, BitAutoData] + public async Task ValidateAsync_WithSavePolicyModel_DisablingPolicy_NoValidation( + [PolicyUpdate(PolicyType.BlockClaimedDomainAccountCreation, false)] PolicyUpdate policyUpdate, + SutProvider sutProvider) + { + // Arrange + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.BlockClaimedDomainAccountCreation) + .Returns(true); + + var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel()); + + // Act + var result = await sutProvider.Sut.ValidateAsync(savePolicyModel, null); + + // Assert + Assert.True(string.IsNullOrEmpty(result)); + await sutProvider.GetDependency() + .DidNotReceive() + .HasVerifiedDomainsAsync(Arg.Any()); + } + + [Theory, BitAutoData] + public async Task ValidateAsync_FeatureFlagDisabled_ReturnsError( + [PolicyUpdate(PolicyType.BlockClaimedDomainAccountCreation, true)] PolicyUpdate policyUpdate, + SutProvider sutProvider) + { + // Arrange + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.BlockClaimedDomainAccountCreation) + .Returns(false); + + // Act + var result = await sutProvider.Sut.ValidateAsync(policyUpdate, null); + + // Assert + Assert.Equal("This feature is not enabled", result); + await sutProvider.GetDependency() + .DidNotReceive() + .HasVerifiedDomainsAsync(Arg.Any()); + } + + [Fact] + public void Type_ReturnsBlockClaimedDomainAccountCreation() + { + // Arrange + var validator = new BlockClaimedDomainAccountCreationPolicyValidator(null, null); + + // Act & Assert + Assert.Equal(PolicyType.BlockClaimedDomainAccountCreation, validator.Type); + } + + [Fact] + public void RequiredPolicies_ReturnsEmpty() + { + // Arrange + var validator = new BlockClaimedDomainAccountCreationPolicyValidator(null, null); + + // Act + var requiredPolicies = validator.RequiredPolicies.ToList(); + + // Assert + Assert.Empty(requiredPolicies); + } +} diff --git a/test/Core.Test/AdminConsole/Services/OrganizationServiceTests.cs b/test/Core.Test/AdminConsole/Services/OrganizationServiceTests.cs index 33f2e78799..821ce78074 100644 --- a/test/Core.Test/AdminConsole/Services/OrganizationServiceTests.cs +++ b/test/Core.Test/AdminConsole/Services/OrganizationServiceTests.cs @@ -21,8 +21,8 @@ using Bit.Core.Services; using Bit.Core.Settings; using Bit.Core.Test.AutoFixture.OrganizationFixtures; using Bit.Core.Test.AutoFixture.OrganizationUserFixtures; +using Bit.Core.Test.Billing.Mocks; using Bit.Core.Tokens; -using Bit.Core.Utilities; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.Fakes; @@ -618,7 +618,7 @@ public class OrganizationServiceTests SetupOrgUserRepositoryCreateManyAsyncMock(organizationUserRepository); SetupOrgUserRepositoryCreateAsyncMock(organizationUserRepository); - sutProvider.GetDependency().GetPlanOrThrow(organization.PlanType).Returns(StaticStore.GetPlan(organization.PlanType)); + sutProvider.GetDependency().GetPlanOrThrow(organization.PlanType).Returns(MockPlans.Get(organization.PlanType)); await sutProvider.Sut.InviteUsersAsync(organization.Id, savingUser.Id, systemUser: null, invites); @@ -666,7 +666,7 @@ public class OrganizationServiceTests .SendInvitesAsync(Arg.Any()).ThrowsAsync(); sutProvider.GetDependency().GetPlanOrThrow(organization.PlanType) - .Returns(StaticStore.GetPlan(organization.PlanType)); + .Returns(MockPlans.Get(organization.PlanType)); await Assert.ThrowsAsync(async () => await sutProvider.Sut.InviteUsersAsync(organization.Id, savingUser.Id, systemUser: null, invites)); @@ -732,7 +732,7 @@ public class OrganizationServiceTests sutProvider.GetDependency().GetByIdAsync(organization.Id).Returns(organization); sutProvider.GetDependency().GetPlanOrThrow(organization.PlanType) - .Returns(StaticStore.GetPlan(organization.PlanType)); + .Returns(MockPlans.Get(organization.PlanType)); var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.UpdateSubscription(organization.Id, seatAdjustment, maxAutoscaleSeats)); @@ -757,7 +757,7 @@ public class OrganizationServiceTests organization.SmSeats = 100; sutProvider.GetDependency().GetPlanOrThrow(organization.PlanType) - .Returns(StaticStore.GetPlan(organization.PlanType)); + .Returns(MockPlans.Get(organization.PlanType)); sutProvider.GetDependency() .GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts { @@ -837,7 +837,7 @@ public class OrganizationServiceTests [BitAutoData(PlanType.EnterpriseMonthly)] public void ValidateSecretsManagerPlan_ThrowsException_WhenNoSecretsManagerSeats(PlanType planType, SutProvider sutProvider) { - var plan = StaticStore.GetPlan(planType); + var plan = MockPlans.Get(planType); var signup = new OrganizationUpgrade { UseSecretsManager = true, @@ -854,7 +854,7 @@ public class OrganizationServiceTests [BitAutoData(PlanType.Free)] public void ValidateSecretsManagerPlan_ThrowsException_WhenSubtractingSeats(PlanType planType, SutProvider sutProvider) { - var plan = StaticStore.GetPlan(planType); + var plan = MockPlans.Get(planType); var signup = new OrganizationUpgrade { UseSecretsManager = true, @@ -871,7 +871,7 @@ public class OrganizationServiceTests PlanType planType, SutProvider sutProvider) { - var plan = StaticStore.GetPlan(planType); + var plan = MockPlans.Get(planType); var signup = new OrganizationUpgrade { UseSecretsManager = true, @@ -890,7 +890,7 @@ public class OrganizationServiceTests [BitAutoData(PlanType.EnterpriseMonthly)] public void ValidateSecretsManagerPlan_ThrowsException_WhenMoreSeatsThanPasswordManagerSeats(PlanType planType, SutProvider sutProvider) { - var plan = StaticStore.GetPlan(planType); + var plan = MockPlans.Get(planType); var signup = new OrganizationUpgrade { UseSecretsManager = true, @@ -912,7 +912,7 @@ public class OrganizationServiceTests PlanType planType, SutProvider sutProvider) { - var plan = StaticStore.GetPlan(planType); + var plan = MockPlans.Get(planType); var signup = new OrganizationUpgrade { UseSecretsManager = true, @@ -930,7 +930,7 @@ public class OrganizationServiceTests PlanType planType, SutProvider sutProvider) { - var plan = StaticStore.GetPlan(planType); + var plan = MockPlans.Get(planType); var signup = new OrganizationUpgrade { UseSecretsManager = true, @@ -952,7 +952,7 @@ public class OrganizationServiceTests PlanType planType, SutProvider sutProvider) { - var plan = StaticStore.GetPlan(planType); + var plan = MockPlans.Get(planType); var signup = new OrganizationUpgrade { UseSecretsManager = true, diff --git a/test/Core.Test/Auth/UserFeatures/Registration/RegisterUserCommandTests.cs b/test/Core.Test/Auth/UserFeatures/Registration/RegisterUserCommandTests.cs index 16a48b12e3..f40eea636c 100644 --- a/test/Core.Test/Auth/UserFeatures/Registration/RegisterUserCommandTests.cs +++ b/test/Core.Test/Auth/UserFeatures/Registration/RegisterUserCommandTests.cs @@ -38,6 +38,12 @@ public class RegisterUserCommandTests public async Task RegisterUser_Succeeds(SutProvider sutProvider, User user) { // Arrange + user.Email = $"test+{Guid.NewGuid()}@example.com"; + + sutProvider.GetDependency() + .HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(Arg.Any()) + .Returns(false); + sutProvider.GetDependency() .CreateUserAsync(user) .Returns(IdentityResult.Success); @@ -62,6 +68,12 @@ public class RegisterUserCommandTests public async Task RegisterUser_WhenCreateUserFails_ReturnsIdentityResultFailed(SutProvider sutProvider, User user) { // Arrange + user.Email = $"test+{Guid.NewGuid()}@example.com"; + + sutProvider.GetDependency() + .HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(Arg.Any()) + .Returns(false); + sutProvider.GetDependency() .CreateUserAsync(user) .Returns(IdentityResult.Failed()); @@ -416,6 +428,138 @@ public class RegisterUserCommandTests Assert.Equal(expectedErrorMessage, exception.Message); } + [Theory] + [BitAutoData] + public async Task RegisterUserViaOrganizationInviteToken_BlockedDomainFromDifferentOrg_ThrowsBadRequestException( + SutProvider sutProvider, User user, string masterPasswordHash, OrganizationUser orgUser, string orgInviteToken, Guid orgUserId) + { + // Arrange + user.Email = "user@blocked-domain.com"; + orgUser.Email = user.Email; + orgUser.Id = orgUserId; + var blockingOrganizationId = Guid.NewGuid(); // Different org that has the domain blocked + orgUser.OrganizationId = Guid.NewGuid(); // The org they're trying to join + + var orgInviteTokenable = new OrgUserInviteTokenable(orgUser); + + sutProvider.GetDependency>() + .TryUnprotect(orgInviteToken, out Arg.Any()) + .Returns(callInfo => + { + callInfo[1] = orgInviteTokenable; + return true; + }); + + sutProvider.GetDependency() + .GetByIdAsync(orgUserId) + .Returns(orgUser); + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.BlockClaimedDomainAccountCreation) + .Returns(true); + + // Mock the new overload that excludes the organization - it should return true (domain IS blocked by another org) + sutProvider.GetDependency() + .HasVerifiedDomainWithBlockClaimedDomainPolicyAsync("blocked-domain.com", orgUser.OrganizationId) + .Returns(true); + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.RegisterUserViaOrganizationInviteToken(user, masterPasswordHash, orgInviteToken, orgUserId)); + Assert.Equal("This email address is claimed by an organization using Bitwarden.", exception.Message); + } + + [Theory] + [BitAutoData] + public async Task RegisterUserViaOrganizationInviteToken_BlockedDomainFromSameOrg_Succeeds( + SutProvider sutProvider, User user, string masterPasswordHash, OrganizationUser orgUser, string orgInviteToken, Guid orgUserId) + { + // Arrange + user.Email = "user@company-domain.com"; + user.ReferenceData = null; + orgUser.Email = user.Email; + orgUser.Id = orgUserId; + // The organization owns the domain and is trying to invite the user + orgUser.OrganizationId = Guid.NewGuid(); + + var orgInviteTokenable = new OrgUserInviteTokenable(orgUser); + + sutProvider.GetDependency>() + .TryUnprotect(orgInviteToken, out Arg.Any()) + .Returns(callInfo => + { + callInfo[1] = orgInviteTokenable; + return true; + }); + + sutProvider.GetDependency() + .GetByIdAsync(orgUserId) + .Returns(orgUser); + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.BlockClaimedDomainAccountCreation) + .Returns(true); + + // Mock the new overload - it should return false (domain is NOT blocked by OTHER orgs) + sutProvider.GetDependency() + .HasVerifiedDomainWithBlockClaimedDomainPolicyAsync("company-domain.com", orgUser.OrganizationId) + .Returns(false); + + sutProvider.GetDependency() + .CreateUserAsync(user, masterPasswordHash) + .Returns(IdentityResult.Success); + + // Act + var result = await sutProvider.Sut.RegisterUserViaOrganizationInviteToken(user, masterPasswordHash, orgInviteToken, orgUserId); + + // Assert + Assert.True(result.Succeeded); + await sutProvider.GetDependency() + .Received(1) + .HasVerifiedDomainWithBlockClaimedDomainPolicyAsync("company-domain.com", orgUser.OrganizationId); + } + + [Theory] + [BitAutoData] + public async Task RegisterUserViaOrganizationInviteToken_WithValidTokenButNullOrgUser_ThrowsBadRequestException( + SutProvider sutProvider, User user, string masterPasswordHash, OrganizationUser orgUser, string orgInviteToken, Guid orgUserId) + { + // Arrange + user.Email = "user@example.com"; + orgUser.Email = user.Email; + orgUser.Id = orgUserId; + + var orgInviteTokenable = new OrgUserInviteTokenable(orgUser); + + sutProvider.GetDependency>() + .TryUnprotect(orgInviteToken, out Arg.Any()) + .Returns(callInfo => + { + callInfo[1] = orgInviteTokenable; + return true; + }); + + // Mock GetByIdAsync to return null - simulating a deleted or non-existent organization user + sutProvider.GetDependency() + .GetByIdAsync(orgUserId) + .Returns((OrganizationUser)null); + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.RegisterUserViaOrganizationInviteToken(user, masterPasswordHash, orgInviteToken, orgUserId)); + Assert.Equal("Invalid organization user invitation.", exception.Message); + + // Verify that GetByIdAsync was called + await sutProvider.GetDependency() + .Received(1) + .GetByIdAsync(orgUserId); + + // Verify that user creation was never attempted + await sutProvider.GetDependency() + .DidNotReceive() + .CreateUserAsync(Arg.Any(), Arg.Any()); + } + // ----------------------------------------------------------------------------------------------- // RegisterUserViaEmailVerificationToken tests // ----------------------------------------------------------------------------------------------- @@ -425,6 +569,12 @@ public class RegisterUserCommandTests public async Task RegisterUserViaEmailVerificationToken_Succeeds(SutProvider sutProvider, User user, string masterPasswordHash, string emailVerificationToken, bool receiveMarketingMaterials) { // Arrange + user.Email = $"test+{Guid.NewGuid()}@example.com"; + + sutProvider.GetDependency() + .HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(Arg.Any()) + .Returns(false); + sutProvider.GetDependency>() .TryUnprotect(emailVerificationToken, out Arg.Any()) .Returns(callInfo => @@ -457,6 +607,12 @@ public class RegisterUserCommandTests public async Task RegisterUserViaEmailVerificationToken_InvalidToken_ThrowsBadRequestException(SutProvider sutProvider, User user, string masterPasswordHash, string emailVerificationToken, bool receiveMarketingMaterials) { // Arrange + user.Email = $"test+{Guid.NewGuid()}@example.com"; + + sutProvider.GetDependency() + .HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(Arg.Any()) + .Returns(false); + sutProvider.GetDependency>() .TryUnprotect(emailVerificationToken, out Arg.Any()) .Returns(callInfo => @@ -495,6 +651,12 @@ public class RegisterUserCommandTests string orgSponsoredFreeFamilyPlanInviteToken) { // Arrange + user.Email = $"test+{Guid.NewGuid()}@example.com"; + + sutProvider.GetDependency() + .HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(Arg.Any()) + .Returns(false); + sutProvider.GetDependency() .ValidateRedemptionTokenAsync(orgSponsoredFreeFamilyPlanInviteToken, user.Email) .Returns((true, new OrganizationSponsorship())); @@ -524,6 +686,12 @@ public class RegisterUserCommandTests string masterPasswordHash, string orgSponsoredFreeFamilyPlanInviteToken) { // Arrange + user.Email = $"test+{Guid.NewGuid()}@example.com"; + + sutProvider.GetDependency() + .HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(Arg.Any()) + .Returns(false); + sutProvider.GetDependency() .ValidateRedemptionTokenAsync(orgSponsoredFreeFamilyPlanInviteToken, user.Email) .Returns((false, new OrganizationSponsorship())); @@ -561,9 +729,14 @@ public class RegisterUserCommandTests EmergencyAccess emergencyAccess, string acceptEmergencyAccessInviteToken, Guid acceptEmergencyAccessId) { // Arrange + user.Email = $"test+{Guid.NewGuid()}@example.com"; emergencyAccess.Email = user.Email; emergencyAccess.Id = acceptEmergencyAccessId; + sutProvider.GetDependency() + .HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(Arg.Any()) + .Returns(false); + sutProvider.GetDependency>() .TryUnprotect(acceptEmergencyAccessInviteToken, out Arg.Any()) .Returns(callInfo => @@ -597,9 +770,14 @@ public class RegisterUserCommandTests string masterPasswordHash, EmergencyAccess emergencyAccess, string acceptEmergencyAccessInviteToken, Guid acceptEmergencyAccessId) { // Arrange + user.Email = $"test+{Guid.NewGuid()}@example.com"; emergencyAccess.Email = "wrong@email.com"; emergencyAccess.Id = acceptEmergencyAccessId; + sutProvider.GetDependency() + .HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(Arg.Any()) + .Returns(false); + sutProvider.GetDependency>() .TryUnprotect(acceptEmergencyAccessInviteToken, out Arg.Any()) .Returns(callInfo => @@ -640,6 +818,8 @@ public class RegisterUserCommandTests User user, string masterPasswordHash, Guid providerUserId) { // Arrange + user.Email = $"test+{Guid.NewGuid()}@example.com"; + // Start with plaintext var nowMillis = CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow); var decryptedProviderInviteToken = $"ProviderUserInvite {providerUserId} {user.Email} {nowMillis}"; @@ -662,6 +842,10 @@ public class RegisterUserCommandTests sutProvider.GetDependency() .OrganizationInviteExpirationHours.Returns(120); // 5 days + sutProvider.GetDependency() + .HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(Arg.Any()) + .Returns(false); + sutProvider.GetDependency() .CreateUserAsync(user, masterPasswordHash) .Returns(IdentityResult.Success); @@ -691,6 +875,8 @@ public class RegisterUserCommandTests User user, string masterPasswordHash, Guid providerUserId) { // Arrange + user.Email = $"test+{Guid.NewGuid()}@example.com"; + // Start with plaintext var nowMillis = CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow); var decryptedProviderInviteToken = $"ProviderUserInvite {providerUserId} {user.Email} {nowMillis}"; @@ -713,6 +899,10 @@ public class RegisterUserCommandTests sutProvider.GetDependency() .OrganizationInviteExpirationHours.Returns(120); // 5 days + sutProvider.GetDependency() + .HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(Arg.Any()) + .Returns(false); + // Using sutProvider in the parameters of the function means that the constructor has already run for the // command so we have to recreate it in order for our mock overrides to be used. sutProvider.Create(); @@ -762,6 +952,66 @@ public class RegisterUserCommandTests } // ----------------------------------------------------------------------------------------------- + // Domain blocking tests (BlockClaimedDomainAccountCreation policy) + // ----------------------------------------------------------------------------------------------- + + [Theory] + [BitAutoData] + public async Task RegisterUser_BlockedDomain_ThrowsBadRequestException( + SutProvider sutProvider, User user) + { + // Arrange + user.Email = "user@blocked-domain.com"; + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.BlockClaimedDomainAccountCreation) + .Returns(true); + + sutProvider.GetDependency() + .HasVerifiedDomainWithBlockClaimedDomainPolicyAsync("blocked-domain.com") + .Returns(true); + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.RegisterUser(user)); + Assert.Equal("This email address is claimed by an organization using Bitwarden.", exception.Message); + + // Verify user creation was never attempted + await sutProvider.GetDependency() + .DidNotReceive() + .CreateUserAsync(Arg.Any()); + } + + [Theory] + [BitAutoData] + public async Task RegisterUser_AllowedDomain_Succeeds( + SutProvider sutProvider, User user) + { + // Arrange + user.Email = "user@allowed-domain.com"; + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.BlockClaimedDomainAccountCreation) + .Returns(true); + + sutProvider.GetDependency() + .HasVerifiedDomainWithBlockClaimedDomainPolicyAsync("allowed-domain.com") + .Returns(false); + + sutProvider.GetDependency() + .CreateUserAsync(user) + .Returns(IdentityResult.Success); + + // Act + var result = await sutProvider.Sut.RegisterUser(user); + + // Assert + Assert.True(result.Succeeded); + await sutProvider.GetDependency() + .Received(1) + .HasVerifiedDomainWithBlockClaimedDomainPolicyAsync("allowed-domain.com"); + } + // SendWelcomeEmail tests // ----------------------------------------------------------------------------------------------- [Theory] @@ -799,6 +1049,194 @@ public class RegisterUserCommandTests .SendFreeOrgOrFamilyOrgUserWelcomeEmailAsync(user, organization.Name); } + [Theory] + [BitAutoData] + public async Task RegisterUserViaEmailVerificationToken_BlockedDomain_ThrowsBadRequestException( + SutProvider sutProvider, User user, string masterPasswordHash, + string emailVerificationToken, bool receiveMarketingMaterials) + { + // Arrange + user.Email = "user@blocked-domain.com"; + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.BlockClaimedDomainAccountCreation) + .Returns(true); + + sutProvider.GetDependency() + .HasVerifiedDomainWithBlockClaimedDomainPolicyAsync("blocked-domain.com") + .Returns(true); + + sutProvider.GetDependency>() + .TryUnprotect(emailVerificationToken, out Arg.Any()) + .Returns(callInfo => + { + callInfo[1] = new RegistrationEmailVerificationTokenable(user.Email, user.Name, receiveMarketingMaterials); + return true; + }); + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.RegisterUserViaEmailVerificationToken(user, masterPasswordHash, emailVerificationToken)); + Assert.Equal("This email address is claimed by an organization using Bitwarden.", exception.Message); + } + + [Theory] + [BitAutoData] + public async Task RegisterUserViaOrganizationSponsoredFreeFamilyPlanInviteToken_BlockedDomain_ThrowsBadRequestException( + SutProvider sutProvider, User user, string masterPasswordHash, + string orgSponsoredFreeFamilyPlanInviteToken) + { + // Arrange + user.Email = "user@blocked-domain.com"; + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.BlockClaimedDomainAccountCreation) + .Returns(true); + + sutProvider.GetDependency() + .HasVerifiedDomainWithBlockClaimedDomainPolicyAsync("blocked-domain.com") + .Returns(true); + + sutProvider.GetDependency() + .ValidateRedemptionTokenAsync(orgSponsoredFreeFamilyPlanInviteToken, user.Email) + .Returns((true, new OrganizationSponsorship())); + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.RegisterUserViaOrganizationSponsoredFreeFamilyPlanInviteToken(user, masterPasswordHash, orgSponsoredFreeFamilyPlanInviteToken)); + Assert.Equal("This email address is claimed by an organization using Bitwarden.", exception.Message); + } + + [Theory] + [BitAutoData] + public async Task RegisterUserViaAcceptEmergencyAccessInviteToken_BlockedDomain_ThrowsBadRequestException( + SutProvider sutProvider, User user, string masterPasswordHash, + EmergencyAccess emergencyAccess, string acceptEmergencyAccessInviteToken, Guid acceptEmergencyAccessId) + { + // Arrange + user.Email = "user@blocked-domain.com"; + emergencyAccess.Email = user.Email; + emergencyAccess.Id = acceptEmergencyAccessId; + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.BlockClaimedDomainAccountCreation) + .Returns(true); + + sutProvider.GetDependency() + .HasVerifiedDomainWithBlockClaimedDomainPolicyAsync("blocked-domain.com") + .Returns(true); + + sutProvider.GetDependency>() + .TryUnprotect(acceptEmergencyAccessInviteToken, out Arg.Any()) + .Returns(callInfo => + { + callInfo[1] = new EmergencyAccessInviteTokenable(emergencyAccess, 10); + return true; + }); + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.RegisterUserViaAcceptEmergencyAccessInviteToken(user, masterPasswordHash, acceptEmergencyAccessInviteToken, acceptEmergencyAccessId)); + Assert.Equal("This email address is claimed by an organization using Bitwarden.", exception.Message); + } + + [Theory] + [BitAutoData] + public async Task RegisterUserViaProviderInviteToken_BlockedDomain_ThrowsBadRequestException( + SutProvider sutProvider, User user, string masterPasswordHash, Guid providerUserId) + { + // Arrange + user.Email = "user@blocked-domain.com"; + + // Start with plaintext + var nowMillis = CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow); + var decryptedProviderInviteToken = $"ProviderUserInvite {providerUserId} {user.Email} {nowMillis}"; + + // Get the byte array of the plaintext + var decryptedProviderInviteTokenByteArray = Encoding.UTF8.GetBytes(decryptedProviderInviteToken); + + // Base64 encode the byte array (this is passed to protector.protect(bytes)) + var base64EncodedProviderInvToken = WebEncoders.Base64UrlEncode(decryptedProviderInviteTokenByteArray); + + var mockDataProtector = Substitute.For(); + + // Given any byte array, just return the decryptedProviderInviteTokenByteArray (sidestepping any actual encryption) + mockDataProtector.Unprotect(Arg.Any()).Returns(decryptedProviderInviteTokenByteArray); + + sutProvider.GetDependency() + .CreateProtector("ProviderServiceDataProtector") + .Returns(mockDataProtector); + + sutProvider.GetDependency() + .OrganizationInviteExpirationHours.Returns(120); // 5 days + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.BlockClaimedDomainAccountCreation) + .Returns(true); + + sutProvider.GetDependency() + .HasVerifiedDomainWithBlockClaimedDomainPolicyAsync("blocked-domain.com") + .Returns(true); + + // Using sutProvider in the parameters of the function means that the constructor has already run for the + // command so we have to recreate it in order for our mock overrides to be used. + sutProvider.Create(); + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.RegisterUserViaProviderInviteToken(user, masterPasswordHash, base64EncodedProviderInvToken, providerUserId)); + Assert.Equal("This email address is claimed by an organization using Bitwarden.", exception.Message); + } + + // ----------------------------------------------------------------------------------------------- + // Invalid email format tests + // ----------------------------------------------------------------------------------------------- + + [Theory] + [BitAutoData] + public async Task RegisterUser_InvalidEmailFormat_ThrowsBadRequestException( + SutProvider sutProvider, User user) + { + // Arrange + user.Email = "invalid-email-format"; + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.BlockClaimedDomainAccountCreation) + .Returns(true); + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.RegisterUser(user)); + Assert.Equal("Invalid email address format.", exception.Message); + } + + [Theory] + [BitAutoData] + public async Task RegisterUserViaEmailVerificationToken_InvalidEmailFormat_ThrowsBadRequestException( + SutProvider sutProvider, User user, string masterPasswordHash, + string emailVerificationToken, bool receiveMarketingMaterials) + { + // Arrange + user.Email = "invalid-email-format"; + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.BlockClaimedDomainAccountCreation) + .Returns(true); + + sutProvider.GetDependency>() + .TryUnprotect(emailVerificationToken, out Arg.Any()) + .Returns(callInfo => + { + callInfo[1] = new RegistrationEmailVerificationTokenable(user.Email, user.Name, receiveMarketingMaterials); + return true; + }); + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.RegisterUserViaEmailVerificationToken(user, masterPasswordHash, emailVerificationToken)); + Assert.Equal("Invalid email address format.", exception.Message); + } + [Theory] [BitAutoData] public async Task SendWelcomeEmail_OrganizationNull_SendsIndividualWelcomeEmail( diff --git a/test/Core.Test/Auth/UserFeatures/Registration/SendVerificationEmailForRegistrationCommandTests.cs b/test/Core.Test/Auth/UserFeatures/Registration/SendVerificationEmailForRegistrationCommandTests.cs index f4f620f8a9..bb4bce08c1 100644 --- a/test/Core.Test/Auth/UserFeatures/Registration/SendVerificationEmailForRegistrationCommandTests.cs +++ b/test/Core.Test/Auth/UserFeatures/Registration/SendVerificationEmailForRegistrationCommandTests.cs @@ -21,9 +21,11 @@ public class SendVerificationEmailForRegistrationCommandTests [Theory] [BitAutoData] public async Task SendVerificationEmailForRegistrationCommand_WhenIsNewUserAndEnableEmailVerificationTrue_SendsEmailAndReturnsNull(SutProvider sutProvider, - string email, string name, bool receiveMarketingEmails) + string name, bool receiveMarketingEmails) { // Arrange + var email = $"test+{Guid.NewGuid()}@example.com"; + sutProvider.GetDependency() .GetByEmailAsync(email) .ReturnsNull(); @@ -34,6 +36,10 @@ public class SendVerificationEmailForRegistrationCommandTests sutProvider.GetDependency() .DisableUserRegistration = false; + sutProvider.GetDependency() + .HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(Arg.Any()) + .Returns(false); + sutProvider.GetDependency() .SendRegistrationVerificationEmailAsync(email, Arg.Any()) .Returns(Task.CompletedTask); @@ -56,9 +62,11 @@ public class SendVerificationEmailForRegistrationCommandTests [Theory] [BitAutoData] public async Task SendVerificationEmailForRegistrationCommand_WhenIsExistingUserAndEnableEmailVerificationTrue_ReturnsNull(SutProvider sutProvider, - string email, string name, bool receiveMarketingEmails) + string name, bool receiveMarketingEmails) { // Arrange + var email = $"test+{Guid.NewGuid()}@example.com"; + sutProvider.GetDependency() .GetByEmailAsync(email) .Returns(new User()); @@ -69,6 +77,10 @@ public class SendVerificationEmailForRegistrationCommandTests sutProvider.GetDependency() .DisableUserRegistration = false; + sutProvider.GetDependency() + .HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(Arg.Any()) + .Returns(false); + var mockedToken = "token"; sutProvider.GetDependency>() .Protect(Arg.Any()) @@ -87,9 +99,11 @@ public class SendVerificationEmailForRegistrationCommandTests [Theory] [BitAutoData] public async Task SendVerificationEmailForRegistrationCommand_WhenIsNewUserAndEnableEmailVerificationFalse_ReturnsToken(SutProvider sutProvider, - string email, string name, bool receiveMarketingEmails) + string name, bool receiveMarketingEmails) { // Arrange + var email = $"test+{Guid.NewGuid()}@example.com"; + sutProvider.GetDependency() .GetByEmailAsync(email) .ReturnsNull(); @@ -100,6 +114,10 @@ public class SendVerificationEmailForRegistrationCommandTests sutProvider.GetDependency() .DisableUserRegistration = false; + sutProvider.GetDependency() + .HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(Arg.Any()) + .Returns(false); + var mockedToken = "token"; sutProvider.GetDependency>() .Protect(Arg.Any()) @@ -128,9 +146,11 @@ public class SendVerificationEmailForRegistrationCommandTests [Theory] [BitAutoData] public async Task SendVerificationEmailForRegistrationCommand_WhenIsExistingUserAndEnableEmailVerificationFalse_ThrowsBadRequestException(SutProvider sutProvider, - string email, string name, bool receiveMarketingEmails) + string name, bool receiveMarketingEmails) { // Arrange + var email = $"test+{Guid.NewGuid()}@example.com"; + sutProvider.GetDependency() .GetByEmailAsync(email) .Returns(new User()); @@ -138,6 +158,13 @@ public class SendVerificationEmailForRegistrationCommandTests sutProvider.GetDependency() .EnableEmailVerification = false; + sutProvider.GetDependency() + .DisableUserRegistration = false; + + sutProvider.GetDependency() + .HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(Arg.Any()) + .Returns(false); + // Act & Assert await Assert.ThrowsAsync(() => sutProvider.Sut.Run(email, name, receiveMarketingEmails)); } @@ -162,4 +189,88 @@ public class SendVerificationEmailForRegistrationCommandTests .DisableUserRegistration = false; await Assert.ThrowsAsync(async () => await sutProvider.Sut.Run("", name, receiveMarketingEmails)); } + + [Theory] + [BitAutoData] + public async Task SendVerificationEmailForRegistrationCommand_WhenBlockedDomain_ThrowsBadRequestException(SutProvider sutProvider, + string name, bool receiveMarketingEmails) + { + // Arrange + var email = $"test+{Guid.NewGuid()}@blockedcompany.com"; + + sutProvider.GetDependency() + .DisableUserRegistration = false; + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.BlockClaimedDomainAccountCreation) + .Returns(true); + + sutProvider.GetDependency() + .HasVerifiedDomainWithBlockClaimedDomainPolicyAsync("blockedcompany.com") + .Returns(true); + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.Run(email, name, receiveMarketingEmails)); + Assert.Equal("This email address is claimed by an organization using Bitwarden.", exception.Message); + } + + [Theory] + [BitAutoData] + public async Task SendVerificationEmailForRegistrationCommand_WhenAllowedDomain_Succeeds(SutProvider sutProvider, + string name, bool receiveMarketingEmails) + { + // Arrange + var email = $"test+{Guid.NewGuid()}@allowedcompany.com"; + + sutProvider.GetDependency() + .GetByEmailAsync(email) + .ReturnsNull(); + + sutProvider.GetDependency() + .EnableEmailVerification = false; + + sutProvider.GetDependency() + .DisableUserRegistration = false; + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.BlockClaimedDomainAccountCreation) + .Returns(true); + + sutProvider.GetDependency() + .HasVerifiedDomainWithBlockClaimedDomainPolicyAsync("allowedcompany.com") + .Returns(false); + + var mockedToken = "token"; + sutProvider.GetDependency>() + .Protect(Arg.Any()) + .Returns(mockedToken); + + // Act + var result = await sutProvider.Sut.Run(email, name, receiveMarketingEmails); + + // Assert + Assert.Equal(mockedToken, result); + } + + [Theory] + [BitAutoData] + public async Task SendVerificationEmailForRegistrationCommand_InvalidEmailFormat_ThrowsBadRequestException( + SutProvider sutProvider, + string name, bool receiveMarketingEmails) + { + // Arrange + var email = "invalid-email-format"; + + sutProvider.GetDependency() + .DisableUserRegistration = false; + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.BlockClaimedDomainAccountCreation) + .Returns(true); + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.Run(email, name, receiveMarketingEmails)); + Assert.Equal("Invalid email address format.", exception.Message); + } } diff --git a/test/Core.Test/Billing/Mocks/MockPlans.cs b/test/Core.Test/Billing/Mocks/MockPlans.cs new file mode 100644 index 0000000000..b4737434fb --- /dev/null +++ b/test/Core.Test/Billing/Mocks/MockPlans.cs @@ -0,0 +1,37 @@ +using Bit.Core.Billing.Enums; +using Bit.Core.Models.StaticStore; +using Bit.Core.Test.Billing.Mocks.Plans; + +namespace Bit.Core.Test.Billing.Mocks; + +public class MockPlans +{ + public static List Plans => + [ + new CustomPlan(), + new Enterprise2019Plan(false), + new Enterprise2019Plan(true), + new Enterprise2020Plan(false), + new Enterprise2020Plan(true), + new Enterprise2023Plan(false), + new Enterprise2023Plan(true), + new EnterprisePlan(false), + new EnterprisePlan(true), + new Families2019Plan(), + new Families2025Plan(), + new FamiliesPlan(), + new FreePlan(), + new Teams2019Plan(false), + new Teams2019Plan(true), + new Teams2020Plan(false), + new Teams2020Plan(true), + new Teams2023Plan(false), + new Teams2023Plan(true), + new TeamsPlan(false), + new TeamsPlan(true), + new TeamsStarterPlan(), + new TeamsStarterPlan2023() + ]; + + public static Plan Get(PlanType planType) => Plans.SingleOrDefault(p => p.Type == planType)!; +} diff --git a/src/Core/Billing/Models/StaticStore/Plans/CustomPlan.cs b/test/Core.Test/Billing/Mocks/Plans/CustomPlan.cs similarity index 89% rename from src/Core/Billing/Models/StaticStore/Plans/CustomPlan.cs rename to test/Core.Test/Billing/Mocks/Plans/CustomPlan.cs index ce55cb422e..0105b7d07f 100644 --- a/src/Core/Billing/Models/StaticStore/Plans/CustomPlan.cs +++ b/test/Core.Test/Billing/Mocks/Plans/CustomPlan.cs @@ -1,7 +1,7 @@ using Bit.Core.Billing.Enums; using Bit.Core.Models.StaticStore; -namespace Bit.Core.Billing.Models.StaticStore.Plans; +namespace Bit.Core.Test.Billing.Mocks.Plans; public record CustomPlan : Plan { diff --git a/src/Core/Billing/Models/StaticStore/Plans/Enterprise2019Plan.cs b/test/Core.Test/Billing/Mocks/Plans/Enterprise2019Plan.cs similarity index 98% rename from src/Core/Billing/Models/StaticStore/Plans/Enterprise2019Plan.cs rename to test/Core.Test/Billing/Mocks/Plans/Enterprise2019Plan.cs index b584647a26..27f3710b96 100644 --- a/src/Core/Billing/Models/StaticStore/Plans/Enterprise2019Plan.cs +++ b/test/Core.Test/Billing/Mocks/Plans/Enterprise2019Plan.cs @@ -1,7 +1,7 @@ using Bit.Core.Billing.Enums; using Bit.Core.Models.StaticStore; -namespace Bit.Core.Billing.Models.StaticStore.Plans; +namespace Bit.Core.Test.Billing.Mocks.Plans; public record Enterprise2019Plan : Plan { diff --git a/src/Core/Billing/Models/StaticStore/Plans/Enterprise2020Plan.cs b/test/Core.Test/Billing/Mocks/Plans/Enterprise2020Plan.cs similarity index 98% rename from src/Core/Billing/Models/StaticStore/Plans/Enterprise2020Plan.cs rename to test/Core.Test/Billing/Mocks/Plans/Enterprise2020Plan.cs index a1a6113cbc..8f56125fc1 100644 --- a/src/Core/Billing/Models/StaticStore/Plans/Enterprise2020Plan.cs +++ b/test/Core.Test/Billing/Mocks/Plans/Enterprise2020Plan.cs @@ -1,7 +1,7 @@ using Bit.Core.Billing.Enums; using Bit.Core.Models.StaticStore; -namespace Bit.Core.Billing.Models.StaticStore.Plans; +namespace Bit.Core.Test.Billing.Mocks.Plans; public record Enterprise2020Plan : Plan { diff --git a/src/Core/Billing/Models/StaticStore/Plans/EnterprisePlan.cs b/test/Core.Test/Billing/Mocks/Plans/EnterprisePlan.cs similarity index 98% rename from src/Core/Billing/Models/StaticStore/Plans/EnterprisePlan.cs rename to test/Core.Test/Billing/Mocks/Plans/EnterprisePlan.cs index 8aeca521d1..563adc82a3 100644 --- a/src/Core/Billing/Models/StaticStore/Plans/EnterprisePlan.cs +++ b/test/Core.Test/Billing/Mocks/Plans/EnterprisePlan.cs @@ -1,7 +1,7 @@ using Bit.Core.Billing.Enums; using Bit.Core.Models.StaticStore; -namespace Bit.Core.Billing.Models.StaticStore.Plans; +namespace Bit.Core.Test.Billing.Mocks.Plans; public record EnterprisePlan : Plan { diff --git a/src/Core/Billing/Models/StaticStore/Plans/EnterprisePlan2023.cs b/test/Core.Test/Billing/Mocks/Plans/EnterprisePlan2023.cs similarity index 98% rename from src/Core/Billing/Models/StaticStore/Plans/EnterprisePlan2023.cs rename to test/Core.Test/Billing/Mocks/Plans/EnterprisePlan2023.cs index dce1719a49..f221821ed3 100644 --- a/src/Core/Billing/Models/StaticStore/Plans/EnterprisePlan2023.cs +++ b/test/Core.Test/Billing/Mocks/Plans/EnterprisePlan2023.cs @@ -1,7 +1,7 @@ using Bit.Core.Billing.Enums; using Bit.Core.Models.StaticStore; -namespace Bit.Core.Billing.Models.StaticStore.Plans; +namespace Bit.Core.Test.Billing.Mocks.Plans; public record Enterprise2023Plan : Plan { diff --git a/src/Core/Billing/Models/StaticStore/Plans/Families2019Plan.cs b/test/Core.Test/Billing/Mocks/Plans/Families2019Plan.cs similarity index 96% rename from src/Core/Billing/Models/StaticStore/Plans/Families2019Plan.cs rename to test/Core.Test/Billing/Mocks/Plans/Families2019Plan.cs index 93ab2c39a1..a0257d88e9 100644 --- a/src/Core/Billing/Models/StaticStore/Plans/Families2019Plan.cs +++ b/test/Core.Test/Billing/Mocks/Plans/Families2019Plan.cs @@ -1,7 +1,7 @@ using Bit.Core.Billing.Enums; using Bit.Core.Models.StaticStore; -namespace Bit.Core.Billing.Models.StaticStore.Plans; +namespace Bit.Core.Test.Billing.Mocks.Plans; public record Families2019Plan : Plan { diff --git a/src/Core/Billing/Models/StaticStore/Plans/Families2025Plan.cs b/test/Core.Test/Billing/Mocks/Plans/Families2025Plan.cs similarity index 95% rename from src/Core/Billing/Models/StaticStore/Plans/Families2025Plan.cs rename to test/Core.Test/Billing/Mocks/Plans/Families2025Plan.cs index 77e238e98e..5f5424bbcf 100644 --- a/src/Core/Billing/Models/StaticStore/Plans/Families2025Plan.cs +++ b/test/Core.Test/Billing/Mocks/Plans/Families2025Plan.cs @@ -1,7 +1,7 @@ using Bit.Core.Billing.Enums; using Bit.Core.Models.StaticStore; -namespace Bit.Core.Billing.Models.StaticStore.Plans; +namespace Bit.Core.Test.Billing.Mocks.Plans; public record Families2025Plan : Plan { diff --git a/src/Core/Billing/Models/StaticStore/Plans/FamiliesPlan.cs b/test/Core.Test/Billing/Mocks/Plans/FamiliesPlan.cs similarity index 95% rename from src/Core/Billing/Models/StaticStore/Plans/FamiliesPlan.cs rename to test/Core.Test/Billing/Mocks/Plans/FamiliesPlan.cs index b2edc1168b..70aa613ee0 100644 --- a/src/Core/Billing/Models/StaticStore/Plans/FamiliesPlan.cs +++ b/test/Core.Test/Billing/Mocks/Plans/FamiliesPlan.cs @@ -1,7 +1,7 @@ using Bit.Core.Billing.Enums; using Bit.Core.Models.StaticStore; -namespace Bit.Core.Billing.Models.StaticStore.Plans; +namespace Bit.Core.Test.Billing.Mocks.Plans; public record FamiliesPlan : Plan { diff --git a/src/Core/Billing/Models/StaticStore/Plans/FreePlan.cs b/test/Core.Test/Billing/Mocks/Plans/FreePlan.cs similarity index 95% rename from src/Core/Billing/Models/StaticStore/Plans/FreePlan.cs rename to test/Core.Test/Billing/Mocks/Plans/FreePlan.cs index 3b0a8b7480..307f58c803 100644 --- a/src/Core/Billing/Models/StaticStore/Plans/FreePlan.cs +++ b/test/Core.Test/Billing/Mocks/Plans/FreePlan.cs @@ -1,7 +1,7 @@ using Bit.Core.Billing.Enums; using Bit.Core.Models.StaticStore; -namespace Bit.Core.Billing.Models.StaticStore.Plans; +namespace Bit.Core.Test.Billing.Mocks.Plans; public record FreePlan : Plan { diff --git a/src/Core/Billing/Models/StaticStore/Plans/Teams2019Plan.cs b/test/Core.Test/Billing/Mocks/Plans/Teams2019Plan.cs similarity index 98% rename from src/Core/Billing/Models/StaticStore/Plans/Teams2019Plan.cs rename to test/Core.Test/Billing/Mocks/Plans/Teams2019Plan.cs index 27ed5e0bf4..f1aad7c16f 100644 --- a/src/Core/Billing/Models/StaticStore/Plans/Teams2019Plan.cs +++ b/test/Core.Test/Billing/Mocks/Plans/Teams2019Plan.cs @@ -1,7 +1,7 @@ using Bit.Core.Billing.Enums; using Bit.Core.Models.StaticStore; -namespace Bit.Core.Billing.Models.StaticStore.Plans; +namespace Bit.Core.Test.Billing.Mocks.Plans; public record Teams2019Plan : Plan { diff --git a/src/Core/Billing/Models/StaticStore/Plans/Teams2020Plan.cs b/test/Core.Test/Billing/Mocks/Plans/Teams2020Plan.cs similarity index 98% rename from src/Core/Billing/Models/StaticStore/Plans/Teams2020Plan.cs rename to test/Core.Test/Billing/Mocks/Plans/Teams2020Plan.cs index a760b9692e..546f1f84c5 100644 --- a/src/Core/Billing/Models/StaticStore/Plans/Teams2020Plan.cs +++ b/test/Core.Test/Billing/Mocks/Plans/Teams2020Plan.cs @@ -1,7 +1,7 @@ using Bit.Core.Billing.Enums; using Bit.Core.Models.StaticStore; -namespace Bit.Core.Billing.Models.StaticStore.Plans; +namespace Bit.Core.Test.Billing.Mocks.Plans; public record Teams2020Plan : Plan { diff --git a/src/Core/Billing/Models/StaticStore/Plans/TeamsPlan.cs b/test/Core.Test/Billing/Mocks/Plans/TeamsPlan.cs similarity index 98% rename from src/Core/Billing/Models/StaticStore/Plans/TeamsPlan.cs rename to test/Core.Test/Billing/Mocks/Plans/TeamsPlan.cs index 654792ee0b..e0ecd35346 100644 --- a/src/Core/Billing/Models/StaticStore/Plans/TeamsPlan.cs +++ b/test/Core.Test/Billing/Mocks/Plans/TeamsPlan.cs @@ -1,7 +1,7 @@ using Bit.Core.Billing.Enums; using Bit.Core.Models.StaticStore; -namespace Bit.Core.Billing.Models.StaticStore.Plans; +namespace Bit.Core.Test.Billing.Mocks.Plans; public record TeamsPlan : Plan { diff --git a/src/Core/Billing/Models/StaticStore/Plans/TeamsPlan2023.cs b/test/Core.Test/Billing/Mocks/Plans/TeamsPlan2023.cs similarity index 98% rename from src/Core/Billing/Models/StaticStore/Plans/TeamsPlan2023.cs rename to test/Core.Test/Billing/Mocks/Plans/TeamsPlan2023.cs index 8498af6b13..5ec2acd61c 100644 --- a/src/Core/Billing/Models/StaticStore/Plans/TeamsPlan2023.cs +++ b/test/Core.Test/Billing/Mocks/Plans/TeamsPlan2023.cs @@ -1,7 +1,7 @@ using Bit.Core.Billing.Enums; using Bit.Core.Models.StaticStore; -namespace Bit.Core.Billing.Models.StaticStore.Plans; +namespace Bit.Core.Test.Billing.Mocks.Plans; public record Teams2023Plan : Plan { diff --git a/src/Core/Billing/Models/StaticStore/Plans/TeamsStarterPlan.cs b/test/Core.Test/Billing/Mocks/Plans/TeamsStarterPlan.cs similarity index 97% rename from src/Core/Billing/Models/StaticStore/Plans/TeamsStarterPlan.cs rename to test/Core.Test/Billing/Mocks/Plans/TeamsStarterPlan.cs index d78844e429..119f431a56 100644 --- a/src/Core/Billing/Models/StaticStore/Plans/TeamsStarterPlan.cs +++ b/test/Core.Test/Billing/Mocks/Plans/TeamsStarterPlan.cs @@ -1,7 +1,7 @@ using Bit.Core.Billing.Enums; using Bit.Core.Models.StaticStore; -namespace Bit.Core.Billing.Models.StaticStore.Plans; +namespace Bit.Core.Test.Billing.Mocks.Plans; public record TeamsStarterPlan : Plan { diff --git a/src/Core/Billing/Models/StaticStore/Plans/TeamsStarterPlan2023.cs b/test/Core.Test/Billing/Mocks/Plans/TeamsStarterPlan2023.cs similarity index 97% rename from src/Core/Billing/Models/StaticStore/Plans/TeamsStarterPlan2023.cs rename to test/Core.Test/Billing/Mocks/Plans/TeamsStarterPlan2023.cs index ea15d9eb95..40952e75fb 100644 --- a/src/Core/Billing/Models/StaticStore/Plans/TeamsStarterPlan2023.cs +++ b/test/Core.Test/Billing/Mocks/Plans/TeamsStarterPlan2023.cs @@ -1,7 +1,7 @@ using Bit.Core.Billing.Enums; using Bit.Core.Models.StaticStore; -namespace Bit.Core.Billing.Models.StaticStore.Plans; +namespace Bit.Core.Test.Billing.Mocks.Plans; public record TeamsStarterPlan2023 : Plan { diff --git a/test/Core.Test/Billing/Organizations/Commands/PreviewOrganizationTaxCommandTests.cs b/test/Core.Test/Billing/Organizations/Commands/PreviewOrganizationTaxCommandTests.cs index 8b3a044118..ef2b1512c9 100644 --- a/test/Core.Test/Billing/Organizations/Commands/PreviewOrganizationTaxCommandTests.cs +++ b/test/Core.Test/Billing/Organizations/Commands/PreviewOrganizationTaxCommandTests.cs @@ -1,11 +1,11 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.Billing.Enums; -using Bit.Core.Billing.Models.StaticStore.Plans; using Bit.Core.Billing.Organizations.Commands; using Bit.Core.Billing.Organizations.Models; using Bit.Core.Billing.Payment.Models; using Bit.Core.Billing.Pricing; using Bit.Core.Services; +using Bit.Core.Test.Billing.Mocks.Plans; using Microsoft.Extensions.Logging; using NSubstitute; using Stripe; diff --git a/test/Core.Test/Billing/Organizations/Queries/GetOrganizationMetadataQueryTests.cs b/test/Core.Test/Billing/Organizations/Queries/GetOrganizationMetadataQueryTests.cs index 9f4b8474b5..e4cb0b0109 100644 --- a/test/Core.Test/Billing/Organizations/Queries/GetOrganizationMetadataQueryTests.cs +++ b/test/Core.Test/Billing/Organizations/Queries/GetOrganizationMetadataQueryTests.cs @@ -8,7 +8,7 @@ using Bit.Core.Billing.Services; using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Repositories; using Bit.Core.Settings; -using Bit.Core.Utilities; +using Bit.Core.Test.Billing.Mocks; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; @@ -163,7 +163,7 @@ public class GetOrganizationMetadataQueryTests sutProvider.GetDependency() .GetPlanOrThrow(organization.PlanType) - .Returns(StaticStore.GetPlan(organization.PlanType)); + .Returns(MockPlans.Get(organization.PlanType)); var result = await sutProvider.Sut.Run(organization); @@ -216,7 +216,7 @@ public class GetOrganizationMetadataQueryTests sutProvider.GetDependency() .GetPlanOrThrow(organization.PlanType) - .Returns(StaticStore.GetPlan(organization.PlanType)); + .Returns(MockPlans.Get(organization.PlanType)); var result = await sutProvider.Sut.Run(organization); @@ -282,7 +282,7 @@ public class GetOrganizationMetadataQueryTests sutProvider.GetDependency() .GetPlanOrThrow(organization.PlanType) - .Returns(StaticStore.GetPlan(organization.PlanType)); + .Returns(MockPlans.Get(organization.PlanType)); var result = await sutProvider.Sut.Run(organization); @@ -349,7 +349,7 @@ public class GetOrganizationMetadataQueryTests sutProvider.GetDependency() .GetPlanOrThrow(organization.PlanType) - .Returns(StaticStore.GetPlan(organization.PlanType)); + .Returns(MockPlans.Get(organization.PlanType)); var result = await sutProvider.Sut.Run(organization); diff --git a/test/Core.Test/Billing/Pricing/PricingClientTests.cs b/test/Core.Test/Billing/Pricing/PricingClientTests.cs index 189df15b9c..43329e9c2e 100644 --- a/test/Core.Test/Billing/Pricing/PricingClientTests.cs +++ b/test/Core.Test/Billing/Pricing/PricingClientTests.cs @@ -3,7 +3,6 @@ using Bit.Core.Billing; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Pricing; using Bit.Core.Services; -using Bit.Core.Utilities; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using Microsoft.Extensions.Logging; @@ -34,7 +33,6 @@ public class PricingClientTests var featureService = Substitute.For(); featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3).Returns(true); - featureService.IsEnabled(FeatureFlagKeys.UsePricingService).Returns(true); var globalSettings = new GlobalSettings { SelfHosted = false }; @@ -70,7 +68,6 @@ public class PricingClientTests var featureService = Substitute.For(); featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3).Returns(false); - featureService.IsEnabled(FeatureFlagKeys.UsePricingService).Returns(true); var globalSettings = new GlobalSettings { SelfHosted = false }; @@ -109,7 +106,6 @@ public class PricingClientTests var featureService = Substitute.For(); featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3).Returns(false); - featureService.IsEnabled(FeatureFlagKeys.UsePricingService).Returns(true); var globalSettings = new GlobalSettings { SelfHosted = false }; @@ -144,7 +140,6 @@ public class PricingClientTests var featureService = Substitute.For(); featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3).Returns(true); - featureService.IsEnabled(FeatureFlagKeys.UsePricingService).Returns(true); var globalSettings = new GlobalSettings { SelfHosted = false }; @@ -179,7 +174,6 @@ public class PricingClientTests var featureService = Substitute.For(); featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3).Returns(true); - featureService.IsEnabled(FeatureFlagKeys.UsePricingService).Returns(true); var globalSettings = new GlobalSettings { SelfHosted = false }; @@ -217,7 +211,6 @@ public class PricingClientTests var featureService = Substitute.For(); featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3).Returns(false); - featureService.IsEnabled(FeatureFlagKeys.UsePricingService).Returns(true); var globalSettings = new GlobalSettings { SelfHosted = false }; @@ -258,7 +251,6 @@ public class PricingClientTests var featureService = Substitute.For(); featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3).Returns(false); - featureService.IsEnabled(FeatureFlagKeys.UsePricingService).Returns(true); var globalSettings = new GlobalSettings { SelfHosted = false }; @@ -297,7 +289,6 @@ public class PricingClientTests var featureService = Substitute.For(); featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3).Returns(true); - featureService.IsEnabled(FeatureFlagKeys.UsePricingService).Returns(true); var globalSettings = new GlobalSettings { SelfHosted = false }; @@ -339,33 +330,12 @@ public class PricingClientTests Assert.Null(result); } - [Theory, BitAutoData] - public async Task GetPlan_WhenPricingServiceDisabled_ReturnsStaticStorePlan( - SutProvider sutProvider) - { - // Arrange - sutProvider.GetDependency().SelfHosted = false; - - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.UsePricingService) - .Returns(false); - - // Act - var result = await sutProvider.Sut.GetPlan(PlanType.FamiliesAnnually); - - // Assert - Assert.NotNull(result); - Assert.Equal(PlanType.FamiliesAnnually, result.Type); - } - [Theory, BitAutoData] public async Task GetPlan_WhenLookupKeyNotFound_ReturnsNull( SutProvider sutProvider) { // Arrange - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.UsePricingService) - .Returns(true); + sutProvider.GetDependency().SelfHosted = false; // Act - Using PlanType that doesn't have a lookup key mapping var result = await sutProvider.Sut.GetPlan(unchecked((PlanType)999)); @@ -384,7 +354,6 @@ public class PricingClientTests var featureService = Substitute.For(); featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3).Returns(true); - featureService.IsEnabled(FeatureFlagKeys.UsePricingService).Returns(true); var globalSettings = new GlobalSettings { SelfHosted = false }; @@ -413,7 +382,6 @@ public class PricingClientTests var featureService = Substitute.For(); featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3).Returns(true); - featureService.IsEnabled(FeatureFlagKeys.UsePricingService).Returns(true); var globalSettings = new GlobalSettings { SelfHosted = false }; @@ -450,26 +418,6 @@ public class PricingClientTests Assert.Empty(result); } - [Theory, BitAutoData] - public async Task ListPlans_WhenPricingServiceDisabled_ReturnsStaticStorePlans( - SutProvider sutProvider) - { - // Arrange - sutProvider.GetDependency().SelfHosted = false; - - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.UsePricingService) - .Returns(false); - - // Act - var result = await sutProvider.Sut.ListPlans(); - - // Assert - Assert.NotNull(result); - Assert.NotEmpty(result); - Assert.Equal(StaticStore.Plans.Count(), result.Count); - } - [Fact] public async Task ListPlans_WhenPricingServiceReturnsError_ThrowsBillingException() { @@ -479,7 +427,6 @@ public class PricingClientTests .Respond(HttpStatusCode.InternalServerError); var featureService = Substitute.For(); - featureService.IsEnabled(FeatureFlagKeys.UsePricingService).Returns(true); var globalSettings = new GlobalSettings { SelfHosted = false }; diff --git a/test/Core.Test/Billing/Services/OrganizationBillingServiceTests.cs b/test/Core.Test/Billing/Services/OrganizationBillingServiceTests.cs index 40fa4c412d..6a7e9d3190 100644 --- a/test/Core.Test/Billing/Services/OrganizationBillingServiceTests.cs +++ b/test/Core.Test/Billing/Services/OrganizationBillingServiceTests.cs @@ -10,7 +10,7 @@ using Bit.Core.Billing.Services; using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Repositories; using Bit.Core.Services; -using Bit.Core.Utilities; +using Bit.Core.Test.Billing.Mocks; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; @@ -31,10 +31,10 @@ public class OrganizationBillingServiceTests SutProvider sutProvider) { sutProvider.GetDependency().GetByIdAsync(organizationId).Returns(organization); - sutProvider.GetDependency().ListPlans().Returns(StaticStore.Plans.ToList()); + sutProvider.GetDependency().ListPlans().Returns(MockPlans.Plans.ToList()); sutProvider.GetDependency().GetPlanOrThrow(organization.PlanType) - .Returns(StaticStore.GetPlan(organization.PlanType)); + .Returns(MockPlans.Get(organization.PlanType)); var subscriberService = sutProvider.GetDependency(); var organizationSeatCount = new OrganizationSeatCounts { Users = 1, Sponsored = 0 }; @@ -97,10 +97,10 @@ public class OrganizationBillingServiceTests { sutProvider.GetDependency().GetByIdAsync(organizationId).Returns(organization); - sutProvider.GetDependency().ListPlans().Returns(StaticStore.Plans.ToList()); + sutProvider.GetDependency().ListPlans().Returns(MockPlans.Plans.ToList()); sutProvider.GetDependency().GetPlanOrThrow(organization.PlanType) - .Returns(StaticStore.GetPlan(organization.PlanType)); + .Returns(MockPlans.Get(organization.PlanType)); sutProvider.GetDependency() .GetOccupiedSeatCountByOrganizationIdAsync(organization.Id) @@ -134,7 +134,7 @@ public class OrganizationBillingServiceTests SutProvider sutProvider) { // Arrange - var plan = StaticStore.GetPlan(PlanType.TeamsAnnually); + var plan = MockPlans.Get(PlanType.TeamsAnnually); organization.PlanType = PlanType.TeamsAnnually; organization.GatewayCustomerId = "cus_test123"; organization.GatewaySubscriptionId = null; @@ -210,7 +210,7 @@ public class OrganizationBillingServiceTests SutProvider sutProvider) { // Arrange - var plan = StaticStore.GetPlan(PlanType.TeamsAnnually); + var plan = MockPlans.Get(PlanType.TeamsAnnually); organization.PlanType = PlanType.TeamsAnnually; organization.GatewayCustomerId = "cus_test123"; organization.GatewaySubscriptionId = null; @@ -284,7 +284,7 @@ public class OrganizationBillingServiceTests SutProvider sutProvider) { // Arrange - var plan = StaticStore.GetPlan(PlanType.TeamsAnnually); + var plan = MockPlans.Get(PlanType.TeamsAnnually); organization.PlanType = PlanType.TeamsAnnually; organization.GatewayCustomerId = "cus_test123"; organization.GatewaySubscriptionId = null; diff --git a/test/Core.Test/Models/Business/CompleteSubscriptionUpdateTests.cs b/test/Core.Test/Models/Business/CompleteSubscriptionUpdateTests.cs index dee805033a..39374755eb 100644 --- a/test/Core.Test/Models/Business/CompleteSubscriptionUpdateTests.cs +++ b/test/Core.Test/Models/Business/CompleteSubscriptionUpdateTests.cs @@ -2,7 +2,7 @@ using Bit.Core.Billing.Enums; using Bit.Core.Models.Business; using Bit.Core.Test.AutoFixture.OrganizationFixtures; -using Bit.Core.Utilities; +using Bit.Core.Test.Billing.Mocks; using Bit.Test.Common.AutoFixture.Attributes; using Stripe; using Xunit; @@ -17,7 +17,7 @@ public class CompleteSubscriptionUpdateTests public void UpgradeItemOptions_TeamsStarterToTeams_ReturnsCorrectOptions( Organization organization) { - var teamsStarterPlan = StaticStore.GetPlan(PlanType.TeamsStarter); + var teamsStarterPlan = MockPlans.Get(PlanType.TeamsStarter); var subscription = new Subscription { @@ -35,7 +35,7 @@ public class CompleteSubscriptionUpdateTests } }; - var teamsMonthlyPlan = StaticStore.GetPlan(PlanType.TeamsMonthly); + var teamsMonthlyPlan = MockPlans.Get(PlanType.TeamsMonthly); var updatedSubscriptionData = new SubscriptionData { @@ -66,7 +66,7 @@ public class CompleteSubscriptionUpdateTests // 5 purchased, 1 base organization.MaxStorageGb = 6; - var teamsMonthlyPlan = StaticStore.GetPlan(PlanType.TeamsMonthly); + var teamsMonthlyPlan = MockPlans.Get(PlanType.TeamsMonthly); var subscription = new Subscription { @@ -102,7 +102,7 @@ public class CompleteSubscriptionUpdateTests } }; - var enterpriseMonthlyPlan = StaticStore.GetPlan(PlanType.EnterpriseMonthly); + var enterpriseMonthlyPlan = MockPlans.Get(PlanType.EnterpriseMonthly); var updatedSubscriptionData = new SubscriptionData { @@ -173,7 +173,7 @@ public class CompleteSubscriptionUpdateTests // 5 purchased, 1 base organization.MaxStorageGb = 6; - var teamsMonthlyPlan = StaticStore.GetPlan(PlanType.TeamsMonthly); + var teamsMonthlyPlan = MockPlans.Get(PlanType.TeamsMonthly); var subscription = new Subscription { @@ -209,7 +209,7 @@ public class CompleteSubscriptionUpdateTests } }; - var enterpriseMonthlyPlan = StaticStore.GetPlan(PlanType.EnterpriseMonthly); + var enterpriseMonthlyPlan = MockPlans.Get(PlanType.EnterpriseMonthly); var updatedSubscriptionData = new SubscriptionData { @@ -277,8 +277,8 @@ public class CompleteSubscriptionUpdateTests public void RevertItemOptions_TeamsStarterToTeams_ReturnsCorrectOptions( Organization organization) { - var teamsStarterPlan = StaticStore.GetPlan(PlanType.TeamsStarter); - var teamsMonthlyPlan = StaticStore.GetPlan(PlanType.TeamsMonthly); + var teamsStarterPlan = MockPlans.Get(PlanType.TeamsStarter); + var teamsMonthlyPlan = MockPlans.Get(PlanType.TeamsMonthly); var subscription = new Subscription { @@ -325,8 +325,8 @@ public class CompleteSubscriptionUpdateTests // 5 purchased, 1 base organization.MaxStorageGb = 6; - var teamsMonthlyPlan = StaticStore.GetPlan(PlanType.TeamsMonthly); - var enterpriseMonthlyPlan = StaticStore.GetPlan(PlanType.EnterpriseMonthly); + var teamsMonthlyPlan = MockPlans.Get(PlanType.TeamsMonthly); + var enterpriseMonthlyPlan = MockPlans.Get(PlanType.EnterpriseMonthly); var subscription = new Subscription { @@ -431,8 +431,8 @@ public class CompleteSubscriptionUpdateTests // 5 purchased, 1 base organization.MaxStorageGb = 6; - var teamsMonthlyPlan = StaticStore.GetPlan(PlanType.TeamsMonthly); - var enterpriseMonthlyPlan = StaticStore.GetPlan(PlanType.EnterpriseMonthly); + var teamsMonthlyPlan = MockPlans.Get(PlanType.TeamsMonthly); + var enterpriseMonthlyPlan = MockPlans.Get(PlanType.EnterpriseMonthly); var subscription = new Subscription { diff --git a/test/Core.Test/Models/Business/SeatSubscriptionUpdateTests.cs b/test/Core.Test/Models/Business/SeatSubscriptionUpdateTests.cs index b6e9f63640..d96f9fea95 100644 --- a/test/Core.Test/Models/Business/SeatSubscriptionUpdateTests.cs +++ b/test/Core.Test/Models/Business/SeatSubscriptionUpdateTests.cs @@ -1,7 +1,7 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.Billing.Enums; using Bit.Core.Models.Business; -using Bit.Core.Utilities; +using Bit.Core.Test.Billing.Mocks; using Bit.Test.Common.AutoFixture.Attributes; using Stripe; using Xunit; @@ -27,7 +27,7 @@ public class SeatSubscriptionUpdateTests public void UpgradeItemsOptions_ReturnsCorrectOptions(PlanType planType, Organization organization) { - var plan = StaticStore.GetPlan(planType); + var plan = MockPlans.Get(planType); organization.PlanType = planType; var subscription = new Subscription { @@ -69,7 +69,7 @@ public class SeatSubscriptionUpdateTests [BitAutoData(PlanType.TeamsAnnually)] public void RevertItemsOptions_ReturnsCorrectOptions(PlanType planType, Organization organization) { - var plan = StaticStore.GetPlan(planType); + var plan = MockPlans.Get(planType); organization.PlanType = planType; var subscription = new Subscription { diff --git a/test/Core.Test/Models/Business/SecretsManagerSubscriptionUpdateTests.cs b/test/Core.Test/Models/Business/SecretsManagerSubscriptionUpdateTests.cs index 20405b07b0..1f75b6a23a 100644 --- a/test/Core.Test/Models/Business/SecretsManagerSubscriptionUpdateTests.cs +++ b/test/Core.Test/Models/Business/SecretsManagerSubscriptionUpdateTests.cs @@ -4,7 +4,7 @@ using Bit.Core.Exceptions; using Bit.Core.Models.Business; using Bit.Core.Models.StaticStore; using Bit.Core.Test.AutoFixture.OrganizationFixtures; -using Bit.Core.Utilities; +using Bit.Core.Test.Billing.Mocks; using Bit.Test.Common.AutoFixture.Attributes; using Xunit; @@ -16,7 +16,7 @@ public class SecretsManagerSubscriptionUpdateTests private static TheoryData ToPlanTheory(List types) { var theoryData = new TheoryData(); - var plans = types.Select(StaticStore.GetPlan).ToArray(); + var plans = types.Select(MockPlans.Get).ToArray(); theoryData.AddRange(plans); return theoryData; } diff --git a/test/Core.Test/Models/Business/ServiceAccountSubscriptionUpdateTests.cs b/test/Core.Test/Models/Business/ServiceAccountSubscriptionUpdateTests.cs index 3663277933..a1e9669c87 100644 --- a/test/Core.Test/Models/Business/ServiceAccountSubscriptionUpdateTests.cs +++ b/test/Core.Test/Models/Business/ServiceAccountSubscriptionUpdateTests.cs @@ -1,7 +1,7 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.Billing.Enums; using Bit.Core.Models.Business; -using Bit.Core.Utilities; +using Bit.Core.Test.Billing.Mocks; using Bit.Test.Common.AutoFixture.Attributes; using Stripe; using Xunit; @@ -27,7 +27,7 @@ public class ServiceAccountSubscriptionUpdateTests public void UpgradeItemsOptions_ReturnsCorrectOptions(PlanType planType, Organization organization) { - var plan = StaticStore.GetPlan(planType); + var plan = MockPlans.Get(planType); organization.PlanType = planType; var subscription = new Subscription { @@ -69,7 +69,7 @@ public class ServiceAccountSubscriptionUpdateTests [BitAutoData(PlanType.TeamsAnnually)] public void RevertItemsOptions_ReturnsCorrectOptions(PlanType planType, Organization organization) { - var plan = StaticStore.GetPlan(planType); + var plan = MockPlans.Get(planType); organization.PlanType = planType; var quantity = 5; var subscription = new Subscription diff --git a/test/Core.Test/Models/Business/SmSeatSubscriptionUpdateTests.cs b/test/Core.Test/Models/Business/SmSeatSubscriptionUpdateTests.cs index ee9dc615b6..d9fcaf991e 100644 --- a/test/Core.Test/Models/Business/SmSeatSubscriptionUpdateTests.cs +++ b/test/Core.Test/Models/Business/SmSeatSubscriptionUpdateTests.cs @@ -1,7 +1,7 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.Billing.Enums; using Bit.Core.Models.Business; -using Bit.Core.Utilities; +using Bit.Core.Test.Billing.Mocks; using Bit.Test.Common.AutoFixture.Attributes; using Stripe; using Xunit; @@ -27,7 +27,7 @@ public class SmSeatSubscriptionUpdateTests public void UpgradeItemsOptions_ReturnsCorrectOptions(PlanType planType, Organization organization) { - var plan = StaticStore.GetPlan(planType); + var plan = MockPlans.Get(planType); organization.PlanType = planType; var quantity = 3; var subscription = new Subscription @@ -70,7 +70,7 @@ public class SmSeatSubscriptionUpdateTests [BitAutoData(PlanType.TeamsAnnually)] public void RevertItemsOptions_ReturnsCorrectOptions(PlanType planType, Organization organization) { - var plan = StaticStore.GetPlan(planType); + var plan = MockPlans.Get(planType); organization.PlanType = planType; var quantity = 5; var subscription = new Subscription diff --git a/test/Core.Test/Models/Business/StorageSubscriptionUpdateTests.cs b/test/Core.Test/Models/Business/StorageSubscriptionUpdateTests.cs index 79b29fcd0c..21326c5324 100644 --- a/test/Core.Test/Models/Business/StorageSubscriptionUpdateTests.cs +++ b/test/Core.Test/Models/Business/StorageSubscriptionUpdateTests.cs @@ -1,6 +1,6 @@ using Bit.Core.Billing.Enums; using Bit.Core.Models.Business; -using Bit.Core.Utilities; +using Bit.Core.Test.Billing.Mocks; using Bit.Test.Common.AutoFixture.Attributes; using Stripe; using Xunit; @@ -26,7 +26,7 @@ public class StorageSubscriptionUpdateTests public void UpgradeItemsOptions_ReturnsCorrectOptions(PlanType planType) { - var plan = StaticStore.GetPlan(planType); + var plan = MockPlans.Get(planType); var subscription = new Subscription { Items = new StripeList @@ -77,7 +77,7 @@ public class StorageSubscriptionUpdateTests [BitAutoData(PlanType.TeamsStarter)] public void RevertItemsOptions_ReturnsCorrectOptions(PlanType planType) { - var plan = StaticStore.GetPlan(planType); + var plan = MockPlans.Get(planType); var subscription = new Subscription { Items = new StripeList diff --git a/test/Core.Test/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/FamiliesForEnterpriseTestsBase.cs b/test/Core.Test/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/FamiliesForEnterpriseTestsBase.cs index 5feee0f13a..515b4d7ba1 100644 --- a/test/Core.Test/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/FamiliesForEnterpriseTestsBase.cs +++ b/test/Core.Test/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/FamiliesForEnterpriseTestsBase.cs @@ -1,22 +1,22 @@ using Bit.Core.Billing.Enums; using Bit.Core.Enums; -using Bit.Core.Utilities; +using Bit.Core.Test.Billing.Mocks; namespace Bit.Core.Test.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise; public abstract class FamiliesForEnterpriseTestsBase { public static IEnumerable EnterprisePlanTypes => - Enum.GetValues().Where(p => StaticStore.GetPlan(p).ProductTier == ProductTierType.Enterprise).Select(p => new object[] { p }); + Enum.GetValues().Where(p => MockPlans.Get(p).ProductTier == ProductTierType.Enterprise).Select(p => new object[] { p }); public static IEnumerable NonEnterprisePlanTypes => - Enum.GetValues().Where(p => StaticStore.GetPlan(p).ProductTier != ProductTierType.Enterprise).Select(p => new object[] { p }); + Enum.GetValues().Where(p => MockPlans.Get(p).ProductTier != ProductTierType.Enterprise).Select(p => new object[] { p }); public static IEnumerable FamiliesPlanTypes => - Enum.GetValues().Where(p => StaticStore.GetPlan(p).ProductTier == ProductTierType.Families).Select(p => new object[] { p }); + Enum.GetValues().Where(p => MockPlans.Get(p).ProductTier == ProductTierType.Families).Select(p => new object[] { p }); public static IEnumerable NonFamiliesPlanTypes => - Enum.GetValues().Where(p => StaticStore.GetPlan(p).ProductTier != ProductTierType.Families).Select(p => new object[] { p }); + Enum.GetValues().Where(p => MockPlans.Get(p).ProductTier != ProductTierType.Families).Select(p => new object[] { p }); public static IEnumerable NonConfirmedOrganizationUsersStatuses => Enum.GetValues() diff --git a/test/Core.Test/OrganizationFeatures/OrganizationSubscriptionUpdate/AddSecretsManagerSubscriptionCommandTests.cs b/test/Core.Test/OrganizationFeatures/OrganizationSubscriptionUpdate/AddSecretsManagerSubscriptionCommandTests.cs index 02ae40798b..fb64c11312 100644 --- a/test/Core.Test/OrganizationFeatures/OrganizationSubscriptionUpdate/AddSecretsManagerSubscriptionCommandTests.cs +++ b/test/Core.Test/OrganizationFeatures/OrganizationSubscriptionUpdate/AddSecretsManagerSubscriptionCommandTests.cs @@ -9,7 +9,7 @@ using Bit.Core.Models.Business; using Bit.Core.Models.StaticStore; using Bit.Core.OrganizationFeatures.OrganizationSubscriptions; using Bit.Core.Services; -using Bit.Core.Utilities; +using Bit.Core.Test.Billing.Mocks; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; @@ -42,7 +42,7 @@ public class AddSecretsManagerSubscriptionCommandTests { organization.PlanType = planType; - var plan = StaticStore.GetPlan(organization.PlanType); + var plan = MockPlans.Get(organization.PlanType); sutProvider.GetDependency().GetPlanOrThrow(organization.PlanType).Returns(plan); await sutProvider.Sut.SignUpAsync(organization, additionalSmSeats, additionalServiceAccounts); @@ -88,7 +88,7 @@ public class AddSecretsManagerSubscriptionCommandTests organization.GatewayCustomerId = null; organization.PlanType = PlanType.EnterpriseAnnually; sutProvider.GetDependency().GetPlanOrThrow(organization.PlanType) - .Returns(StaticStore.GetPlan(organization.PlanType)); + .Returns(MockPlans.Get(organization.PlanType)); var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.SignUpAsync(organization, additionalSmSeats, additionalServiceAccounts)); Assert.Contains("No payment method found.", exception.Message); @@ -106,7 +106,7 @@ public class AddSecretsManagerSubscriptionCommandTests organization.GatewaySubscriptionId = null; organization.PlanType = PlanType.EnterpriseAnnually; sutProvider.GetDependency().GetPlanOrThrow(organization.PlanType) - .Returns(StaticStore.GetPlan(organization.PlanType)); + .Returns(MockPlans.Get(organization.PlanType)); var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.SignUpAsync(organization, additionalSmSeats, additionalServiceAccounts)); Assert.Contains("No subscription found.", exception.Message); @@ -139,7 +139,7 @@ public class AddSecretsManagerSubscriptionCommandTests provider.Type = ProviderType.Msp; sutProvider.GetDependency().GetByOrganizationIdAsync(organization.Id).Returns(provider); sutProvider.GetDependency().GetPlanOrThrow(organization.PlanType) - .Returns(StaticStore.GetPlan(organization.PlanType)); + .Returns(MockPlans.Get(organization.PlanType)); var exception = await Assert.ThrowsAsync( () => sutProvider.Sut.SignUpAsync(organization, 10, 10)); diff --git a/test/Core.Test/OrganizationFeatures/OrganizationSubscriptionUpdate/UpdateSecretsManagerSubscriptionCommandTests.cs b/test/Core.Test/OrganizationFeatures/OrganizationSubscriptionUpdate/UpdateSecretsManagerSubscriptionCommandTests.cs index 1e764de6d7..baa9e04c22 100644 --- a/test/Core.Test/OrganizationFeatures/OrganizationSubscriptionUpdate/UpdateSecretsManagerSubscriptionCommandTests.cs +++ b/test/Core.Test/OrganizationFeatures/OrganizationSubscriptionUpdate/UpdateSecretsManagerSubscriptionCommandTests.cs @@ -11,7 +11,7 @@ using Bit.Core.SecretsManager.Repositories; using Bit.Core.Services; using Bit.Core.Settings; using Bit.Core.Test.AutoFixture.OrganizationFixtures; -using Bit.Core.Utilities; +using Bit.Core.Test.Billing.Mocks; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; @@ -26,7 +26,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests private static TheoryData ToPlanTheory(List types) { var theoryData = new TheoryData(); - var plans = types.Select(StaticStore.GetPlan).ToArray(); + var plans = types.Select(MockPlans.Get).ToArray(); theoryData.AddRange(plans); return theoryData; } @@ -164,7 +164,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests Organization organization, SutProvider sutProvider) { - var plan = StaticStore.GetPlan(organization.PlanType); + var plan = MockPlans.Get(organization.PlanType); var update = new SecretsManagerSubscriptionUpdate(organization, plan, autoscaling).AdjustSeats(2); sutProvider.GetDependency().SelfHosted.Returns(true); @@ -180,7 +180,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests SutProvider sutProvider, Organization organization) { - var plan = StaticStore.GetPlan(organization.PlanType); + var plan = MockPlans.Get(organization.PlanType); organization.UseSecretsManager = false; var update = new SecretsManagerSubscriptionUpdate(organization, plan, false); @@ -289,7 +289,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests organization.MaxAutoscaleSmSeats = maxSeatCount; organization.PlanType = PlanType.EnterpriseAnnually; - var plan = StaticStore.GetPlan(organization.PlanType); + var plan = MockPlans.Get(organization.PlanType); var update = new SecretsManagerSubscriptionUpdate(organization, plan, false) { @@ -334,7 +334,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests var ownerDetailsList = new List { new() { Email = "owner@example.com" } }; organization.PlanType = PlanType.EnterpriseAnnually; - var plan = StaticStore.GetPlan(organization.PlanType); + var plan = MockPlans.Get(organization.PlanType); var update = new SecretsManagerSubscriptionUpdate(organization, plan, false) { @@ -372,7 +372,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests SutProvider sutProvider) { organization.SmSeats = null; - var plan = StaticStore.GetPlan(organization.PlanType); + var plan = MockPlans.Get(organization.PlanType); var update = new SecretsManagerSubscriptionUpdate(organization, plan, false).AdjustSeats(1); var exception = await Assert.ThrowsAsync( @@ -388,7 +388,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests Organization organization, SutProvider sutProvider) { - var plan = StaticStore.GetPlan(organization.PlanType); + var plan = MockPlans.Get(organization.PlanType); var update = new SecretsManagerSubscriptionUpdate(organization, plan, true).AdjustSeats(-2); var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.UpdateSubscriptionAsync(update)); @@ -404,7 +404,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests SutProvider sutProvider) { organization.PlanType = planType; - var plan = StaticStore.GetPlan(organization.PlanType); + var plan = MockPlans.Get(organization.PlanType); var update = new SecretsManagerSubscriptionUpdate(organization, plan, false).AdjustSeats(1); var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.UpdateSubscriptionAsync(update)); @@ -422,7 +422,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests organization.SmSeats = 9; organization.MaxAutoscaleSmSeats = 10; - var plan = StaticStore.GetPlan(organization.PlanType); + var plan = MockPlans.Get(organization.PlanType); var update = new SecretsManagerSubscriptionUpdate(organization, plan, true).AdjustSeats(2); var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.UpdateSubscriptionAsync(update)); @@ -436,7 +436,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests Organization organization, SutProvider sutProvider) { - var plan = StaticStore.GetPlan(organization.PlanType); + var plan = MockPlans.Get(organization.PlanType); var update = new SecretsManagerSubscriptionUpdate(organization, plan, false) { SmSeats = organization.SmSeats + 10, @@ -455,7 +455,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests Organization organization, SutProvider sutProvider) { - var plan = StaticStore.GetPlan(organization.PlanType); + var plan = MockPlans.Get(organization.PlanType); var update = new SecretsManagerSubscriptionUpdate(organization, plan, false) { SmSeats = 0, @@ -475,7 +475,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests SutProvider sutProvider) { organization.SmSeats = 8; - var plan = StaticStore.GetPlan(organization.PlanType); + var plan = MockPlans.Get(organization.PlanType); var update = new SecretsManagerSubscriptionUpdate(organization, plan, false) { SmSeats = 7, @@ -498,7 +498,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests var smServiceAccounts = 300; var existingServiceAccountCount = 299; - var plan = StaticStore.GetPlan(organization.PlanType); + var plan = MockPlans.Get(organization.PlanType); var update = new SecretsManagerSubscriptionUpdate(organization, plan, false) { SmServiceAccounts = smServiceAccounts, @@ -531,7 +531,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests SutProvider sutProvider) { var smServiceAccounts = 300; - var plan = StaticStore.GetPlan(organization.PlanType); + var plan = MockPlans.Get(organization.PlanType); var update = new SecretsManagerSubscriptionUpdate(organization, plan, false) { SmServiceAccounts = smServiceAccounts, @@ -571,7 +571,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests SutProvider sutProvider) { organization.SmServiceAccounts = null; - var plan = StaticStore.GetPlan(organization.PlanType); + var plan = MockPlans.Get(organization.PlanType); var update = new SecretsManagerSubscriptionUpdate(organization, plan, false).AdjustServiceAccounts(1); var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.UpdateSubscriptionAsync(update)); @@ -585,7 +585,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests Organization organization, SutProvider sutProvider) { - var plan = StaticStore.GetPlan(organization.PlanType); + var plan = MockPlans.Get(organization.PlanType); var update = new SecretsManagerSubscriptionUpdate(organization, plan, true).AdjustServiceAccounts(-2); var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.UpdateSubscriptionAsync(update)); @@ -601,7 +601,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests SutProvider sutProvider) { organization.PlanType = planType; - var plan = StaticStore.GetPlan(organization.PlanType); + var plan = MockPlans.Get(organization.PlanType); var update = new SecretsManagerSubscriptionUpdate(organization, plan, false).AdjustServiceAccounts(1); var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.UpdateSubscriptionAsync(update)); @@ -619,7 +619,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests organization.SmServiceAccounts = 9; organization.MaxAutoscaleSmServiceAccounts = 10; - var plan = StaticStore.GetPlan(organization.PlanType); + var plan = MockPlans.Get(organization.PlanType); var update = new SecretsManagerSubscriptionUpdate(organization, plan, true).AdjustServiceAccounts(2); var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.UpdateSubscriptionAsync(update)); @@ -639,7 +639,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests organization.SmServiceAccounts = smServiceAccount - 5; organization.MaxAutoscaleSmServiceAccounts = 2 * smServiceAccount; - var plan = StaticStore.GetPlan(organization.PlanType); + var plan = MockPlans.Get(organization.PlanType); var update = new SecretsManagerSubscriptionUpdate(organization, plan, false) { SmServiceAccounts = smServiceAccount, @@ -662,7 +662,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests organization.SmServiceAccounts = newSmServiceAccounts - 10; - var plan = StaticStore.GetPlan(organization.PlanType); + var plan = MockPlans.Get(organization.PlanType); var update = new SecretsManagerSubscriptionUpdate(organization, plan, false) { SmServiceAccounts = newSmServiceAccounts, @@ -707,7 +707,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests organization.SmSeats = smSeats - 1; organization.MaxAutoscaleSmSeats = smSeats * 2; - var plan = StaticStore.GetPlan(organization.PlanType); + var plan = MockPlans.Get(organization.PlanType); var update = new SecretsManagerSubscriptionUpdate(organization, plan, false) { SmSeats = smSeats, @@ -728,7 +728,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests { organization.PlanType = planType; organization.SmSeats = 2; - var plan = StaticStore.GetPlan(organization.PlanType); + var plan = MockPlans.Get(organization.PlanType); var update = new SecretsManagerSubscriptionUpdate(organization, plan, false) { MaxAutoscaleSmSeats = 3 @@ -748,7 +748,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests { organization.PlanType = planType; organization.SmSeats = 2; - var plan = StaticStore.GetPlan(organization.PlanType); + var plan = MockPlans.Get(organization.PlanType); var update = new SecretsManagerSubscriptionUpdate(organization, plan, false) { MaxAutoscaleSmSeats = 2 @@ -769,7 +769,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests organization.PlanType = planType; organization.SmServiceAccounts = 3; - var plan = StaticStore.GetPlan(organization.PlanType); + var plan = MockPlans.Get(organization.PlanType); var update = new SecretsManagerSubscriptionUpdate(organization, plan, false) { MaxAutoscaleSmServiceAccounts = 3 }; var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.UpdateSubscriptionAsync(update)); diff --git a/test/Core.Test/OrganizationFeatures/OrganizationSubscriptionUpdate/UpgradeOrganizationPlanCommandTests.cs b/test/Core.Test/OrganizationFeatures/OrganizationSubscriptionUpdate/UpgradeOrganizationPlanCommandTests.cs index 704f89ba3f..3841f7a619 100644 --- a/test/Core.Test/OrganizationFeatures/OrganizationSubscriptionUpdate/UpgradeOrganizationPlanCommandTests.cs +++ b/test/Core.Test/OrganizationFeatures/OrganizationSubscriptionUpdate/UpgradeOrganizationPlanCommandTests.cs @@ -8,7 +8,7 @@ using Bit.Core.Repositories; using Bit.Core.SecretsManager.Repositories; using Bit.Core.Services; using Bit.Core.Test.AutoFixture.OrganizationFixtures; -using Bit.Core.Utilities; +using Bit.Core.Test.Billing.Mocks; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; @@ -45,7 +45,7 @@ public class UpgradeOrganizationPlanCommandTests SutProvider sutProvider) { upgrade.Plan = organization.PlanType; - sutProvider.GetDependency().GetPlanOrThrow(organization.PlanType).Returns(StaticStore.GetPlan(organization.PlanType)); + sutProvider.GetDependency().GetPlanOrThrow(organization.PlanType).Returns(MockPlans.Get(organization.PlanType)); sutProvider.GetDependency().GetByIdAsync(organization.Id).Returns(organization); var exception = await Assert.ThrowsAsync( () => sutProvider.Sut.UpgradePlanAsync(organization.Id, upgrade)); @@ -61,7 +61,7 @@ public class UpgradeOrganizationPlanCommandTests upgrade.AdditionalSmSeats = 10; upgrade.AdditionalServiceAccounts = 10; sutProvider.GetDependency().GetByIdAsync(organization.Id).Returns(organization); - sutProvider.GetDependency().GetPlanOrThrow(organization.PlanType).Returns(StaticStore.GetPlan(organization.PlanType)); + sutProvider.GetDependency().GetPlanOrThrow(organization.PlanType).Returns(MockPlans.Get(organization.PlanType)); var exception = await Assert.ThrowsAsync( () => sutProvider.Sut.UpgradePlanAsync(organization.Id, upgrade)); Assert.Contains("already on this plan", exception.Message); @@ -73,11 +73,11 @@ public class UpgradeOrganizationPlanCommandTests SutProvider sutProvider) { sutProvider.GetDependency().GetByIdAsync(organization.Id).Returns(organization); - sutProvider.GetDependency().GetPlanOrThrow(organization.PlanType).Returns(StaticStore.GetPlan(organization.PlanType)); + sutProvider.GetDependency().GetPlanOrThrow(organization.PlanType).Returns(MockPlans.Get(organization.PlanType)); upgrade.AdditionalSmSeats = 10; upgrade.AdditionalSeats = 10; upgrade.Plan = PlanType.TeamsAnnually; - sutProvider.GetDependency().GetPlanOrThrow(upgrade.Plan).Returns(StaticStore.GetPlan(upgrade.Plan)); + sutProvider.GetDependency().GetPlanOrThrow(upgrade.Plan).Returns(MockPlans.Get(upgrade.Plan)); sutProvider.GetDependency() .GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts { @@ -104,7 +104,7 @@ public class UpgradeOrganizationPlanCommandTests organization.PlanType = PlanType.FamiliesAnnually; - sutProvider.GetDependency().GetPlanOrThrow(organization.PlanType).Returns(StaticStore.GetPlan(organization.PlanType)); + sutProvider.GetDependency().GetPlanOrThrow(organization.PlanType).Returns(MockPlans.Get(organization.PlanType)); organizationUpgrade.AdditionalSeats = 30; organizationUpgrade.UseSecretsManager = true; @@ -113,7 +113,7 @@ public class UpgradeOrganizationPlanCommandTests organizationUpgrade.AdditionalStorageGb = 3; organizationUpgrade.Plan = planType; - sutProvider.GetDependency().GetPlanOrThrow(organizationUpgrade.Plan).Returns(StaticStore.GetPlan(organizationUpgrade.Plan)); + sutProvider.GetDependency().GetPlanOrThrow(organizationUpgrade.Plan).Returns(MockPlans.Get(organizationUpgrade.Plan)); sutProvider.GetDependency() .GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts { @@ -123,7 +123,7 @@ public class UpgradeOrganizationPlanCommandTests await sutProvider.Sut.UpgradePlanAsync(organization.Id, organizationUpgrade); await sutProvider.GetDependency().Received(1).AdjustSubscription( organization, - StaticStore.GetPlan(planType), + MockPlans.Get(planType), organizationUpgrade.AdditionalSeats, organizationUpgrade.UseSecretsManager, organizationUpgrade.AdditionalSmSeats, @@ -141,12 +141,12 @@ public class UpgradeOrganizationPlanCommandTests public async Task UpgradePlan_SM_Passes(PlanType planType, Organization organization, OrganizationUpgrade upgrade, SutProvider sutProvider) { - sutProvider.GetDependency().GetPlanOrThrow(organization.PlanType).Returns(StaticStore.GetPlan(organization.PlanType)); + sutProvider.GetDependency().GetPlanOrThrow(organization.PlanType).Returns(MockPlans.Get(organization.PlanType)); upgrade.Plan = planType; - sutProvider.GetDependency().GetPlanOrThrow(upgrade.Plan).Returns(StaticStore.GetPlan(upgrade.Plan)); + sutProvider.GetDependency().GetPlanOrThrow(upgrade.Plan).Returns(MockPlans.Get(upgrade.Plan)); - var plan = StaticStore.GetPlan(upgrade.Plan); + var plan = MockPlans.Get(upgrade.Plan); sutProvider.GetDependency().GetByIdAsync(organization.Id).Returns(organization); @@ -184,10 +184,10 @@ public class UpgradeOrganizationPlanCommandTests upgrade.AdditionalSeats = 15; upgrade.AdditionalSmSeats = 1; upgrade.AdditionalServiceAccounts = 0; - sutProvider.GetDependency().GetPlanOrThrow(upgrade.Plan).Returns(StaticStore.GetPlan(upgrade.Plan)); + sutProvider.GetDependency().GetPlanOrThrow(upgrade.Plan).Returns(MockPlans.Get(upgrade.Plan)); organization.SmSeats = 2; - sutProvider.GetDependency().GetPlanOrThrow(organization.PlanType).Returns(StaticStore.GetPlan(organization.PlanType)); + sutProvider.GetDependency().GetPlanOrThrow(organization.PlanType).Returns(MockPlans.Get(organization.PlanType)); sutProvider.GetDependency().GetByIdAsync(organization.Id).Returns(organization); sutProvider.GetDependency() @@ -218,11 +218,11 @@ public class UpgradeOrganizationPlanCommandTests upgrade.AdditionalSeats = 15; upgrade.AdditionalSmSeats = 1; upgrade.AdditionalServiceAccounts = 0; - sutProvider.GetDependency().GetPlanOrThrow(upgrade.Plan).Returns(StaticStore.GetPlan(upgrade.Plan)); + sutProvider.GetDependency().GetPlanOrThrow(upgrade.Plan).Returns(MockPlans.Get(upgrade.Plan)); organization.SmSeats = 1; organization.SmServiceAccounts = currentServiceAccounts; - sutProvider.GetDependency().GetPlanOrThrow(organization.PlanType).Returns(StaticStore.GetPlan(organization.PlanType)); + sutProvider.GetDependency().GetPlanOrThrow(organization.PlanType).Returns(MockPlans.Get(organization.PlanType)); sutProvider.GetDependency().GetByIdAsync(organization.Id).Returns(organization); sutProvider.GetDependency() diff --git a/test/Core.Test/Services/StripePaymentServiceTests.cs b/test/Core.Test/Services/StripePaymentServiceTests.cs index 863fe716d4..dc62af0872 100644 --- a/test/Core.Test/Services/StripePaymentServiceTests.cs +++ b/test/Core.Test/Services/StripePaymentServiceTests.cs @@ -1,11 +1,11 @@ using Bit.Core.Billing.Constants; using Bit.Core.Billing.Enums; -using Bit.Core.Billing.Models.StaticStore.Plans; using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Tax.Requests; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Services; +using Bit.Core.Test.Billing.Mocks.Plans; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; diff --git a/test/Core.Test/Utilities/EmailValidationTests.cs b/test/Core.Test/Utilities/EmailValidationTests.cs new file mode 100644 index 0000000000..ac59f5f44a --- /dev/null +++ b/test/Core.Test/Utilities/EmailValidationTests.cs @@ -0,0 +1,51 @@ +using Bit.Core.Exceptions; +using Bit.Core.Utilities; +using Xunit; + +namespace Bit.Core.Test.Utilities; + +public class EmailValidationTests +{ + [Theory] + [InlineData("user@Example.COM", "example.com")] + [InlineData("user@EXAMPLE.COM", "example.com")] + [InlineData("user@example.com", "example.com")] + [InlineData("user@Example.Com", "example.com")] + [InlineData("User@DOMAIN.CO.UK", "domain.co.uk")] + public void GetDomain_WithMixedCaseEmail_ReturnsLowercaseDomain(string email, string expectedDomain) + { + // Act + var result = EmailValidation.GetDomain(email); + + // Assert + Assert.Equal(expectedDomain, result); + } + + [Theory] + [InlineData("hello@world.com", "world.com")] // regular email address + [InlineData("hello@world.planet.com", "world.planet.com")] // subdomain + [InlineData("hello+1@world.com", "world.com")] // alias + [InlineData("hello.there@world.com", "world.com")] // period in local-part + [InlineData("hello@wörldé.com", "wörldé.com")] // unicode domain + [InlineData("hello@world.cömé", "world.cömé")] // unicode top-level domain + public void GetDomain_WithValidEmail_ReturnsLowercaseDomain(string email, string expectedDomain) + { + // Act + var result = EmailValidation.GetDomain(email); + + // Assert + Assert.Equal(expectedDomain, result); + } + + [Theory] + [InlineData("invalid-email")] + [InlineData("@example.com")] + [InlineData("user@")] + [InlineData("")] + public void GetDomain_WithInvalidEmail_ThrowsBadRequestException(string email) + { + // Act & Assert + var exception = Assert.Throws(() => EmailValidation.GetDomain(email)); + Assert.Equal("Invalid email address format.", exception.Message); + } +} diff --git a/test/Core.Test/Utilities/ExtendedCacheServiceCollectionExtensionsTests.cs b/test/Core.Test/Utilities/ExtendedCacheServiceCollectionExtensionsTests.cs index f2156a6d26..6f7fa4df06 100644 --- a/test/Core.Test/Utilities/ExtendedCacheServiceCollectionExtensionsTests.cs +++ b/test/Core.Test/Utilities/ExtendedCacheServiceCollectionExtensionsTests.cs @@ -14,6 +14,7 @@ public class ExtendedCacheServiceCollectionExtensionsTests { private readonly IServiceCollection _services; private readonly GlobalSettings _globalSettings; + private const string _cacheName = "TestCache"; public ExtendedCacheServiceCollectionExtensionsTests() { @@ -33,129 +34,276 @@ public class ExtendedCacheServiceCollectionExtensionsTests } [Fact] - public void TryAddFusionCoreServices_CustomSettings_OverridesDefaults() + public void AddExtendedCache_CustomSettings_OverridesDefaults() { - var settings = CreateGlobalSettings(new Dictionary + var settings = new GlobalSettings.ExtendedCacheSettings { - { "GlobalSettings:DistributedCache:Duration", "00:12:00" }, - { "GlobalSettings:DistributedCache:FailSafeMaxDuration", "01:30:00" }, - { "GlobalSettings:DistributedCache:FailSafeThrottleDuration", "00:01:00" }, - { "GlobalSettings:DistributedCache:EagerRefreshThreshold", "0.75" }, - { "GlobalSettings:DistributedCache:FactorySoftTimeout", "00:00:00.020" }, - { "GlobalSettings:DistributedCache:FactoryHardTimeout", "00:00:03" }, - { "GlobalSettings:DistributedCache:DistributedCacheSoftTimeout", "00:00:00.500" }, - { "GlobalSettings:DistributedCache:DistributedCacheHardTimeout", "00:00:01.500" }, - { "GlobalSettings:DistributedCache:JitterMaxDuration", "00:00:05" }, - { "GlobalSettings:DistributedCache:IsFailSafeEnabled", "false" }, - { "GlobalSettings:DistributedCache:AllowBackgroundDistributedCacheOperations", "false" }, + Duration = TimeSpan.FromMinutes(12), + FailSafeMaxDuration = TimeSpan.FromHours(1.5), + FailSafeThrottleDuration = TimeSpan.FromMinutes(1), + EagerRefreshThreshold = 0.75f, + FactorySoftTimeout = TimeSpan.FromMilliseconds(20), + FactoryHardTimeout = TimeSpan.FromSeconds(3), + DistributedCacheSoftTimeout = TimeSpan.FromSeconds(0.5), + DistributedCacheHardTimeout = TimeSpan.FromSeconds(1.5), + JitterMaxDuration = TimeSpan.FromSeconds(5), + IsFailSafeEnabled = false, + AllowBackgroundDistributedCacheOperations = false, + }; + + _services.AddExtendedCache(_cacheName, _globalSettings, settings); + + using var provider = _services.BuildServiceProvider(); + var cache = provider.GetRequiredKeyedService(_cacheName); + var opt = cache.DefaultEntryOptions; + + Assert.Equal(TimeSpan.FromMinutes(12), opt.Duration); + Assert.False(opt.IsFailSafeEnabled); + Assert.Equal(TimeSpan.FromHours(1.5), opt.FailSafeMaxDuration); + Assert.Equal(TimeSpan.FromMinutes(1), opt.FailSafeThrottleDuration); + Assert.Equal(0.75f, opt.EagerRefreshThreshold); + Assert.Equal(TimeSpan.FromMilliseconds(20), opt.FactorySoftTimeout); + Assert.Equal(TimeSpan.FromMilliseconds(3000), opt.FactoryHardTimeout); + Assert.Equal(TimeSpan.FromSeconds(0.5), opt.DistributedCacheSoftTimeout); + Assert.Equal(TimeSpan.FromSeconds(1.5), opt.DistributedCacheHardTimeout); + Assert.False(opt.AllowBackgroundDistributedCacheOperations); + Assert.Equal(TimeSpan.FromSeconds(5), opt.JitterMaxDuration); + } + + [Fact] + public void AddExtendedCache_DefaultSettings_ConfiguresExpectedValues() + { + _services.AddExtendedCache(_cacheName, _globalSettings); + + using var provider = _services.BuildServiceProvider(); + var cache = provider.GetRequiredKeyedService(_cacheName); + var opt = cache.DefaultEntryOptions; + + Assert.Equal(TimeSpan.FromMinutes(30), opt.Duration); + Assert.True(opt.IsFailSafeEnabled); + Assert.Equal(TimeSpan.FromHours(2), opt.FailSafeMaxDuration); + Assert.Equal(TimeSpan.FromSeconds(30), opt.FailSafeThrottleDuration); + Assert.Equal(0.9f, opt.EagerRefreshThreshold); + Assert.Equal(TimeSpan.FromMilliseconds(100), opt.FactorySoftTimeout); + Assert.Equal(TimeSpan.FromMilliseconds(1500), opt.FactoryHardTimeout); + Assert.Equal(TimeSpan.FromSeconds(1), opt.DistributedCacheSoftTimeout); + Assert.Equal(TimeSpan.FromSeconds(2), opt.DistributedCacheHardTimeout); + Assert.True(opt.AllowBackgroundDistributedCacheOperations); + Assert.Equal(TimeSpan.FromSeconds(2), opt.JitterMaxDuration); + } + + [Fact] + public void AddExtendedCache_DisabledDistributedCache_DoesNotRegisterBackplaneOrRedis() + { + var settings = new GlobalSettings.ExtendedCacheSettings + { + EnableDistributedCache = false, + }; + + _services.AddExtendedCache(_cacheName, _globalSettings, settings); + + using var provider = _services.BuildServiceProvider(); + var cache = provider.GetRequiredKeyedService(_cacheName); + + Assert.False(cache.HasDistributedCache); + Assert.False(cache.HasBackplane); + } + + [Fact] + public void AddExtendedCache_EmptyCacheName_DoesNothing() + { + _services.AddExtendedCache(string.Empty, _globalSettings); + + var regs = _services.Where(s => s.ServiceType == typeof(IFusionCache)).ToList(); + Assert.Empty(regs); + + using var provider = _services.BuildServiceProvider(); + var cache = provider.GetKeyedService(_cacheName); + Assert.Null(cache); + } + + [Fact] + public void AddExtendedCache_MultipleCalls_OnlyAddsOneCacheService() + { + var settings = CreateGlobalSettings(new() + { + { "GlobalSettings:DistributedCache:Redis:ConnectionString", "localhost:6379" } }); - _services.TryAddExtendedCacheServices(settings); - using var provider = _services.BuildServiceProvider(); - var fusionCache = provider.GetRequiredService(); - var options = fusionCache.DefaultEntryOptions; + // Provide a multiplexer (shared) + _services.AddSingleton(Substitute.For()); - Assert.Equal(TimeSpan.FromMinutes(12), options.Duration); - Assert.False(options.IsFailSafeEnabled); - Assert.Equal(TimeSpan.FromHours(1.5), options.FailSafeMaxDuration); - Assert.Equal(TimeSpan.FromMinutes(1), options.FailSafeThrottleDuration); - Assert.Equal(0.75f, options.EagerRefreshThreshold); - Assert.Equal(TimeSpan.FromMilliseconds(20), options.FactorySoftTimeout); - Assert.Equal(TimeSpan.FromMilliseconds(3000), options.FactoryHardTimeout); - Assert.Equal(TimeSpan.FromSeconds(0.5), options.DistributedCacheSoftTimeout); - Assert.Equal(TimeSpan.FromSeconds(1.5), options.DistributedCacheHardTimeout); - Assert.False(options.AllowBackgroundDistributedCacheOperations); - Assert.Equal(TimeSpan.FromSeconds(5), options.JitterMaxDuration); + _services.AddExtendedCache(_cacheName, settings); + _services.AddExtendedCache(_cacheName, settings); + _services.AddExtendedCache(_cacheName, settings); + + var regs = _services.Where(s => s.ServiceType == typeof(IFusionCache)).ToList(); + Assert.Single(regs); + + using var provider = _services.BuildServiceProvider(); + var cache = provider.GetRequiredKeyedService(_cacheName); + Assert.NotNull(cache); } [Fact] - public void TryAddFusionCoreServices_DefaultSettings_ConfiguresExpectedValues() + public void AddExtendedCache_MultipleDifferentCaches_AddsAll() { - _services.TryAddExtendedCacheServices(_globalSettings); + _services.AddExtendedCache("Cache1", _globalSettings); + _services.AddExtendedCache("Cache2", _globalSettings); + using var provider = _services.BuildServiceProvider(); - var fusionCache = provider.GetRequiredService(); - var options = fusionCache.DefaultEntryOptions; + var cache1 = provider.GetRequiredKeyedService("Cache1"); + var cache2 = provider.GetRequiredKeyedService("Cache2"); - Assert.Equal(TimeSpan.FromMinutes(30), options.Duration); - Assert.True(options.IsFailSafeEnabled); - Assert.Equal(TimeSpan.FromHours(2), options.FailSafeMaxDuration); - Assert.Equal(TimeSpan.FromSeconds(30), options.FailSafeThrottleDuration); - Assert.Equal(0.9f, options.EagerRefreshThreshold); - Assert.Equal(TimeSpan.FromMilliseconds(100), options.FactorySoftTimeout); - Assert.Equal(TimeSpan.FromMilliseconds(1500), options.FactoryHardTimeout); - Assert.Equal(TimeSpan.FromSeconds(1), options.DistributedCacheSoftTimeout); - Assert.Equal(TimeSpan.FromSeconds(2), options.DistributedCacheHardTimeout); - Assert.True(options.AllowBackgroundDistributedCacheOperations); - Assert.Equal(TimeSpan.FromSeconds(2), options.JitterMaxDuration); + Assert.NotNull(cache1); + Assert.NotNull(cache2); + Assert.NotSame(cache1, cache2); } [Fact] - public void TryAddFusionCoreServices_MultipleCalls_OnlyConfiguresOnce() + public void AddExtendedCache_WithRedis_EnablesDistributedCacheAndBackplane() { - var settings = CreateGlobalSettings(new Dictionary + var settings = CreateGlobalSettings(new() { { "GlobalSettings:DistributedCache:Redis:ConnectionString", "localhost:6379" }, + { "GlobalSettings:DistributedCache:DefaultExtendedCache:UseSharedRedisCache", "true" } }); - _services.AddSingleton(Substitute.For()); - _services.TryAddExtendedCacheServices(settings); - _services.TryAddExtendedCacheServices(settings); - _services.TryAddExtendedCacheServices(settings); - var registrations = _services.Where(s => s.ServiceType == typeof(IFusionCache)).ToList(); - Assert.Single(registrations); + // Provide a multiplexer (shared) + _services.AddSingleton(Substitute.For()); + + _services.AddExtendedCache(_cacheName, settings); using var provider = _services.BuildServiceProvider(); - var fusionCache = provider.GetRequiredService(); - Assert.NotNull(fusionCache); + var cache = provider.GetRequiredKeyedService(_cacheName); + + Assert.True(cache.HasDistributedCache); + Assert.True(cache.HasBackplane); } [Fact] - public void TryAddFusionCoreServices_WithRedis_EnablesDistributedCacheAndBackplane() + public void AddExtendedCache_InvalidRedisConnection_LogsAndThrows() { - var settings = CreateGlobalSettings(new Dictionary + var settings = new GlobalSettings.ExtendedCacheSettings { - { "GlobalSettings:DistributedCache:Redis:ConnectionString", "localhost:6379" }, - }); + UseSharedRedisCache = false, + Redis = new GlobalSettings.ConnectionStringSettings { ConnectionString = "invalid:9999" } + }; + + _services.AddExtendedCache(_cacheName, _globalSettings, settings); - _services.AddSingleton(Substitute.For()); - _services.TryAddExtendedCacheServices(settings); using var provider = _services.BuildServiceProvider(); - - var fusionCache = provider.GetRequiredService(); - Assert.True(fusionCache.HasDistributedCache); - Assert.True(fusionCache.HasBackplane); + Assert.Throws(() => + { + var cache = provider.GetRequiredKeyedService(_cacheName); + // Trigger lazy initialization + cache.GetOrDefault("test"); + }); } [Fact] - public void TryAddFusionCoreServices_WithExistingRedis_EnablesDistributedCacheAndBackplane() + public void AddExtendedCache_WithExistingRedis_UsesExistingDistributedCacheAndBackplane() { - var settings = CreateGlobalSettings(new Dictionary + var settings = CreateGlobalSettings(new() { { "GlobalSettings:DistributedCache:Redis:ConnectionString", "localhost:6379" }, }); _services.AddSingleton(Substitute.For()); _services.AddSingleton(Substitute.For()); - _services.TryAddExtendedCacheServices(settings); - using var provider = _services.BuildServiceProvider(); - var fusionCache = provider.GetRequiredService(); - Assert.True(fusionCache.HasDistributedCache); - Assert.True(fusionCache.HasBackplane); - var distributedCache = provider.GetRequiredService(); - Assert.NotNull(distributedCache); + _services.AddExtendedCache(_cacheName, settings); + + using var provider = _services.BuildServiceProvider(); + var cache = provider.GetRequiredKeyedService(_cacheName); + + Assert.True(cache.HasDistributedCache); + Assert.True(cache.HasBackplane); + + var existingCache = provider.GetRequiredService(); + Assert.NotNull(existingCache); } [Fact] - public void TryAddFusionCoreServices_WithoutRedis_DisablesDistributedCacheAndBackplane() + public void AddExtendedCache_NoRedis_DisablesDistributedCacheAndBackplane() { - _services.TryAddExtendedCacheServices(_globalSettings); - using var provider = _services.BuildServiceProvider(); + _services.AddExtendedCache(_cacheName, _globalSettings); - var fusionCache = provider.GetRequiredService(); - Assert.False(fusionCache.HasDistributedCache); - Assert.False(fusionCache.HasBackplane); + using var provider = _services.BuildServiceProvider(); + var cache = provider.GetRequiredKeyedService(_cacheName); + + Assert.False(cache.HasDistributedCache); + Assert.False(cache.HasBackplane); + } + + [Fact] + public void AddExtendedCache_NoSharedRedisButNoConnectionString_DisablesDistributedCacheAndBackplane() + { + var settings = new GlobalSettings.ExtendedCacheSettings + { + UseSharedRedisCache = false, + // No Redis connection string + }; + + _services.AddExtendedCache(_cacheName, _globalSettings, settings); + + using var provider = _services.BuildServiceProvider(); + var cache = provider.GetRequiredKeyedService(_cacheName); + + Assert.False(cache.HasDistributedCache); + Assert.False(cache.HasBackplane); + } + + [Fact] + public void AddExtendedCache_KeyedRedis_UsesSeparateMultiplexers() + { + var settingsA = new GlobalSettings.ExtendedCacheSettings + { + EnableDistributedCache = true, + UseSharedRedisCache = false, + Redis = new GlobalSettings.ConnectionStringSettings { ConnectionString = "localhost:6379" } + }; + var settingsB = new GlobalSettings.ExtendedCacheSettings + { + EnableDistributedCache = true, + UseSharedRedisCache = false, + Redis = new GlobalSettings.ConnectionStringSettings { ConnectionString = "localhost:6380" } + }; + + _services.AddKeyedSingleton("CacheA", Substitute.For()); + _services.AddKeyedSingleton("CacheB", Substitute.For()); + + _services.AddExtendedCache("CacheA", _globalSettings, settingsA); + _services.AddExtendedCache("CacheB", _globalSettings, settingsB); + + using var provider = _services.BuildServiceProvider(); + var muxA = provider.GetRequiredKeyedService("CacheA"); + var muxB = provider.GetRequiredKeyedService("CacheB"); + + Assert.NotNull(muxA); + Assert.NotNull(muxB); + Assert.NotSame(muxA, muxB); + } + + [Fact] + public void AddExtendedCache_WithExistingKeyedDistributedCache_ReusesIt() + { + var existingCache = Substitute.For(); + _services.AddKeyedSingleton(_cacheName, existingCache); + + var settings = new GlobalSettings.ExtendedCacheSettings + { + UseSharedRedisCache = false, + Redis = new GlobalSettings.ConnectionStringSettings { ConnectionString = "localhost:6379" } + }; + + _services.AddExtendedCache(_cacheName, _globalSettings, settings); + + using var provider = _services.BuildServiceProvider(); + var resolved = provider.GetRequiredKeyedService(_cacheName); + + Assert.Same(existingCache, resolved); } private static GlobalSettings CreateGlobalSettings(Dictionary data) diff --git a/test/Core.Test/Utilities/StaticStoreTests.cs b/test/Core.Test/Utilities/StaticStoreTests.cs index 01e2ab8914..5d5b03dcd6 100644 --- a/test/Core.Test/Utilities/StaticStoreTests.cs +++ b/test/Core.Test/Utilities/StaticStoreTests.cs @@ -1,5 +1,4 @@ -using Bit.Core.Billing.Enums; -using Bit.Core.Utilities; +using Bit.Core.Utilities; using Xunit; namespace Bit.Core.Test.Utilities; @@ -7,28 +6,6 @@ namespace Bit.Core.Test.Utilities; public class StaticStoreTests { - [Fact] - public void StaticStore_Initialization_Success() - { - var plans = StaticStore.Plans.ToList(); - Assert.NotNull(plans); - Assert.NotEmpty(plans); - Assert.Equal(23, plans.Count); - } - - [Theory] - [InlineData(PlanType.EnterpriseAnnually)] - [InlineData(PlanType.EnterpriseMonthly)] - [InlineData(PlanType.TeamsMonthly)] - [InlineData(PlanType.TeamsAnnually)] - [InlineData(PlanType.TeamsStarter)] - public void StaticStore_GetPlan_Success(PlanType planType) - { - var plan = StaticStore.GetPlan(planType); - Assert.NotNull(plan); - Assert.Equal(planType, plan.Type); - } - [Fact] public void StaticStore_GlobalEquivalentDomains_OnlyAsciiAllowed() { diff --git a/test/Core.Test/Vault/Services/CipherServiceTests.cs b/test/Core.Test/Vault/Services/CipherServiceTests.cs index fb53c41bad..c5eecb8f34 100644 --- a/test/Core.Test/Vault/Services/CipherServiceTests.cs +++ b/test/Core.Test/Vault/Services/CipherServiceTests.cs @@ -225,130 +225,6 @@ public class CipherServiceTests Assert.NotNull(result.uploadUrl); } - [Theory, BitAutoData] - public async Task UploadFileForExistingAttachmentAsync_WrongRevisionDate_Throws(SutProvider sutProvider, - Cipher cipher) - { - var lastKnownRevisionDate = cipher.RevisionDate.AddDays(-1); - var stream = new MemoryStream(); - var attachment = new CipherAttachment.MetaData - { - AttachmentId = "test-attachment-id", - Size = 100, - FileName = "test.txt", - Key = "test-key" - }; - - var exception = await Assert.ThrowsAsync( - () => sutProvider.Sut.UploadFileForExistingAttachmentAsync(stream, cipher, attachment, lastKnownRevisionDate)); - Assert.Contains("out of date", exception.Message); - } - - [Theory] - [BitAutoData("")] - [BitAutoData("Correct Time")] - public async Task UploadFileForExistingAttachmentAsync_CorrectRevisionDate_DoesNotThrow(string revisionDateString, - SutProvider sutProvider, CipherDetails cipher) - { - var lastKnownRevisionDate = string.IsNullOrEmpty(revisionDateString) ? (DateTime?)null : cipher.RevisionDate; - var stream = new MemoryStream(new byte[100]); - var attachmentId = "test-attachment-id"; - var attachment = new CipherAttachment.MetaData - { - AttachmentId = attachmentId, - Size = 100, - FileName = "test.txt", - Key = "test-key" - }; - - // Set the attachment on the cipher so ValidateCipherAttachmentFile can find it - cipher.SetAttachments(new Dictionary - { - [attachmentId] = attachment - }); - - sutProvider.GetDependency() - .UploadNewAttachmentAsync(stream, cipher, attachment) - .Returns(Task.CompletedTask); - - sutProvider.GetDependency() - .ValidateFileAsync(cipher, attachment, Arg.Any()) - .Returns((true, 100L)); - - sutProvider.GetDependency() - .UpdateAttachmentAsync(Arg.Any()) - .Returns(Task.CompletedTask); - - await sutProvider.Sut.UploadFileForExistingAttachmentAsync(stream, cipher, attachment, lastKnownRevisionDate); - - await sutProvider.GetDependency().Received(1) - .UploadNewAttachmentAsync(stream, cipher, attachment); - } - - [Theory, BitAutoData] - public async Task CreateAttachmentShareAsync_WrongRevisionDate_Throws(SutProvider sutProvider, - Cipher cipher, Guid organizationId) - { - var lastKnownRevisionDate = cipher.RevisionDate.AddDays(-1); - var stream = new MemoryStream(); - var fileName = "test.txt"; - var key = "test-key"; - var attachmentId = "attachment-id"; - - var exception = await Assert.ThrowsAsync( - () => sutProvider.Sut.CreateAttachmentShareAsync(cipher, stream, fileName, key, 100, attachmentId, organizationId, lastKnownRevisionDate)); - Assert.Contains("out of date", exception.Message); - } - - [Theory] - [BitAutoData("")] - [BitAutoData("Correct Time")] - public async Task CreateAttachmentShareAsync_CorrectRevisionDate_DoesNotThrow(string revisionDateString, - SutProvider sutProvider, CipherDetails cipher, Guid organizationId) - { - var lastKnownRevisionDate = string.IsNullOrEmpty(revisionDateString) ? (DateTime?)null : cipher.RevisionDate; - var stream = new MemoryStream(new byte[100]); - var fileName = "test.txt"; - var key = "test-key"; - var attachmentId = "attachment-id"; - - // Setup cipher with existing attachment (no TempMetadata) - cipher.OrganizationId = null; - cipher.SetAttachments(new Dictionary - { - [attachmentId] = new CipherAttachment.MetaData - { - AttachmentId = attachmentId, - Size = 100, - FileName = "existing.txt", - Key = "existing-key" - } - }); - - // Mock organization - var organization = new Organization - { - Id = organizationId, - MaxStorageGb = 1 - }; - sutProvider.GetDependency() - .GetByIdAsync(organizationId) - .Returns(organization); - - sutProvider.GetDependency() - .UploadShareAttachmentAsync(stream, cipher.Id, organizationId, Arg.Any()) - .Returns(Task.CompletedTask); - - sutProvider.GetDependency() - .UpdateAttachmentAsync(Arg.Any()) - .Returns(Task.CompletedTask); - - await sutProvider.Sut.CreateAttachmentShareAsync(cipher, stream, fileName, key, 100, attachmentId, organizationId, lastKnownRevisionDate); - - await sutProvider.GetDependency().Received(1) - .UploadShareAttachmentAsync(stream, cipher.Id, organizationId, Arg.Any()); - } - [Theory] [BitAutoData] public async Task SaveDetailsAsync_PersonalVault_WithOrganizationDataOwnershipPolicyEnabled_Throws( diff --git a/test/Identity.IntegrationTest/Controllers/AccountsControllerTests.cs b/test/Identity.IntegrationTest/Controllers/AccountsControllerTests.cs index 88e8af3dc6..8325dcf1bb 100644 --- a/test/Identity.IntegrationTest/Controllers/AccountsControllerTests.cs +++ b/test/Identity.IntegrationTest/Controllers/AccountsControllerTests.cs @@ -242,7 +242,7 @@ public class AccountsControllerTests : IClassFixture var orgInviteToken = "BwOrgUserInviteToken_CfDJ8HOzu6wr6nVLouuDxgOHsMwPcj9Guuip5k_XLD1bBGpwQS1f66c9kB6X4rvKGxNdywhgimzgvG9SgLwwJU70O8P879XyP94W6kSoT4N25a73kgW3nU3vl3fAtGSS52xdBjNU8o4sxmomRvhOZIQ0jwtVjdMC2IdybTbxwCZhvN0hKIFs265k6wFRSym1eu4NjjZ8pmnMneG0PlKnNZL93tDe8FMcqStJXoddIEgbA99VJp8z1LQmOMfEdoMEM7Zs8W5bZ34N4YEGu8XCrVau59kGtWQk7N4rPV5okzQbTpeoY_4FeywgLFGm-tDtTPEdSEBJkRjexANri7CGdg3dpnMifQc_bTmjZd32gOjw8N8v"; var orgUserId = new Guid("5e45fbdc-a080-4a77-93ff-b19c0161e81e"); - var orgUser = new OrganizationUser { Id = orgUserId, Email = email }; + var orgUser = new OrganizationUser { Id = orgUserId, Email = email, OrganizationId = Guid.NewGuid() }; var orgInviteTokenable = new OrgUserInviteTokenable(orgUser) { @@ -259,6 +259,12 @@ public class AccountsControllerTests : IClassFixture }); }); + localFactory.SubstituteService(orgUserRepository => + { + orgUserRepository.GetByIdAsync(orgUserId) + .Returns(orgUser); + }); + var registerFinishReqModel = new RegisterFinishRequestModel { Email = email, diff --git a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationDomainRepositoryTests.cs b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationDomainRepositoryTests.cs index 6c1ac00073..74a4fb13ee 100644 --- a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationDomainRepositoryTests.cs +++ b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationDomainRepositoryTests.cs @@ -1,4 +1,6 @@ using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Repositories; using Bit.Core.Entities; using Bit.Core.Repositories; using Xunit; @@ -7,7 +9,7 @@ namespace Bit.Infrastructure.IntegrationTest.AdminConsole.Repositories; public class OrganizationDomainRepositoryTests { - [DatabaseTheory, DatabaseData] + [Theory, DatabaseData] public async Task GetExpiredOrganizationDomainsAsync_ShouldReturn3DaysOldUnverifiedDomains( IUserRepository userRepository, IOrganizationRepository organizationRepository, @@ -74,7 +76,7 @@ public class OrganizationDomainRepositoryTests Assert.NotNull(expectedDomain2); } - [DatabaseTheory, DatabaseData] + [Theory, DatabaseData] public async Task GetExpiredOrganizationDomainsAsync_ShouldNotReturnDomainsUnder3DaysOld( IUserRepository userRepository, IOrganizationRepository organizationRepository, @@ -120,7 +122,7 @@ public class OrganizationDomainRepositoryTests Assert.Null(expectedDomain2); } - [DatabaseTheory, DatabaseData] + [Theory, DatabaseData] public async Task GetExpiredOrganizationDomainsAsync_ShouldNotReturnVerifiedDomains( IUserRepository userRepository, IOrganizationRepository organizationRepository, @@ -189,7 +191,7 @@ public class OrganizationDomainRepositoryTests Assert.Null(expectedDomain2); } - [DatabaseTheory, DatabaseData] + [Theory, DatabaseData] public async Task GetManyByNextRunDateAsync_ShouldReturnUnverifiedDomains( IOrganizationRepository organizationRepository, IOrganizationDomainRepository organizationDomainRepository) @@ -228,7 +230,7 @@ public class OrganizationDomainRepositoryTests Assert.NotNull(expectedDomain); } - [DatabaseTheory, DatabaseData] + [Theory, DatabaseData] public async Task GetManyByNextRunDateAsync_ShouldNotReturnUnverifiedDomains_WhenNextRunDateIsOutside36hoursWindow( IOrganizationRepository organizationRepository, IOrganizationDomainRepository organizationDomainRepository) @@ -267,7 +269,7 @@ public class OrganizationDomainRepositoryTests Assert.Null(expectedDomain); } - [DatabaseTheory, DatabaseData] + [Theory, DatabaseData] public async Task GetManyByNextRunDateAsync_ShouldNotReturnVerifiedDomains( IOrganizationRepository organizationRepository, IOrganizationDomainRepository organizationDomainRepository) @@ -307,7 +309,7 @@ public class OrganizationDomainRepositoryTests Assert.Null(expectedDomain); } - [DatabaseTheory, DatabaseData] + [Theory, DatabaseData] public async Task GetVerifiedDomainsByOrganizationIdsAsync_ShouldVerifiedDomainsMatchesOrganizationIds( IOrganizationRepository organizationRepository, IOrganizationDomainRepository organizationDomainRepository) @@ -383,4 +385,437 @@ public class OrganizationDomainRepositoryTests Assert.Null(otherOrganizationDomain); Assert.Null(unverifiedDomain); } + + [Theory, DatabaseData] + public async Task HasVerifiedDomainWithBlockClaimedDomainPolicyAsync_WithVerifiedDomainAndBlockPolicy_ReturnsTrue( + IOrganizationRepository organizationRepository, + IOrganizationDomainRepository organizationDomainRepository, + IPolicyRepository policyRepository) + { + // Arrange + var id = Guid.NewGuid(); + var domainName = $"test-{id}.example.com"; + + var organization = await organizationRepository.CreateAsync(new Organization + { + Name = $"Test Org {id}", + BillingEmail = $"test+{id}@example.com", + Plan = "Test", + PrivateKey = "privatekey", + Enabled = true, + UsePolicies = true, + UseOrganizationDomains = true + }); + + var organizationDomain = new OrganizationDomain + { + OrganizationId = organization.Id, + DomainName = domainName, + Txt = "btw+12345" + }; + organizationDomain.SetNextRunDate(1); + organizationDomain.SetVerifiedDate(); + await organizationDomainRepository.CreateAsync(organizationDomain); + + var policy = new Policy + { + OrganizationId = organization.Id, + Type = PolicyType.BlockClaimedDomainAccountCreation, + Enabled = true + }; + await policyRepository.CreateAsync(policy); + + // Act + var result = await organizationDomainRepository.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(domainName); + + // Assert + Assert.True(result); + } + + [Theory, DatabaseData] + public async Task HasVerifiedDomainWithBlockClaimedDomainPolicyAsync_WithUnverifiedDomain_ReturnsFalse( + IOrganizationRepository organizationRepository, + IOrganizationDomainRepository organizationDomainRepository, + IPolicyRepository policyRepository) + { + // Arrange + var id = Guid.NewGuid(); + var domainName = $"test-{id}.example.com"; + + var organization = await organizationRepository.CreateAsync(new Organization + { + Name = $"Test Org {id}", + BillingEmail = $"test+{id}@example.com", + Plan = "Test", + PrivateKey = "privatekey", + Enabled = true, + UsePolicies = true, + UseOrganizationDomains = true + }); + + var organizationDomain = new OrganizationDomain + { + OrganizationId = organization.Id, + DomainName = domainName, + Txt = "btw+12345" + }; + organizationDomain.SetNextRunDate(1); + // Do not verify the domain + await organizationDomainRepository.CreateAsync(organizationDomain); + + var policy = new Policy + { + OrganizationId = organization.Id, + Type = PolicyType.BlockClaimedDomainAccountCreation, + Enabled = true + }; + await policyRepository.CreateAsync(policy); + + // Act + var result = await organizationDomainRepository.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(domainName); + + // Assert + Assert.False(result); + } + + [Theory, DatabaseData] + public async Task HasVerifiedDomainWithBlockClaimedDomainPolicyAsync_WithDisabledPolicy_ReturnsFalse( + IOrganizationRepository organizationRepository, + IOrganizationDomainRepository organizationDomainRepository, + IPolicyRepository policyRepository) + { + // Arrange + var id = Guid.NewGuid(); + var domainName = $"test-{id}.example.com"; + + var organization = await organizationRepository.CreateAsync(new Organization + { + Name = $"Test Org {id}", + BillingEmail = $"test+{id}@example.com", + Plan = "Test", + PrivateKey = "privatekey", + Enabled = true, + UsePolicies = true, + UseOrganizationDomains = true + }); + + var organizationDomain = new OrganizationDomain + { + OrganizationId = organization.Id, + DomainName = domainName, + Txt = "btw+12345" + }; + organizationDomain.SetNextRunDate(1); + organizationDomain.SetVerifiedDate(); + await organizationDomainRepository.CreateAsync(organizationDomain); + + var policy = new Policy + { + OrganizationId = organization.Id, + Type = PolicyType.BlockClaimedDomainAccountCreation, + Enabled = false + }; + await policyRepository.CreateAsync(policy); + + // Act + var result = await organizationDomainRepository.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(domainName); + + // Assert + Assert.False(result); + } + + [Theory, DatabaseData] + public async Task HasVerifiedDomainWithBlockClaimedDomainPolicyAsync_WithDisabledOrganization_ReturnsFalse( + IOrganizationRepository organizationRepository, + IOrganizationDomainRepository organizationDomainRepository, + IPolicyRepository policyRepository) + { + // Arrange + var id = Guid.NewGuid(); + var domainName = $"test-{id}.example.com"; + + var organization = await organizationRepository.CreateAsync(new Organization + { + Name = $"Test Org {id}", + BillingEmail = $"test+{id}@example.com", + Plan = "Test", + PrivateKey = "privatekey", + Enabled = false, + UsePolicies = true, + UseOrganizationDomains = true + }); + + var organizationDomain = new OrganizationDomain + { + OrganizationId = organization.Id, + DomainName = domainName, + Txt = "btw+12345" + }; + organizationDomain.SetNextRunDate(1); + organizationDomain.SetVerifiedDate(); + await organizationDomainRepository.CreateAsync(organizationDomain); + + var policy = new Policy + { + OrganizationId = organization.Id, + Type = PolicyType.BlockClaimedDomainAccountCreation, + Enabled = true + }; + await policyRepository.CreateAsync(policy); + + // Act + var result = await organizationDomainRepository.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(domainName); + + // Assert + Assert.False(result); + } + + [Theory, DatabaseData] + public async Task HasVerifiedDomainWithBlockClaimedDomainPolicyAsync_WithUsePoliciesFalse_ReturnsFalse( + IOrganizationRepository organizationRepository, + IOrganizationDomainRepository organizationDomainRepository, + IPolicyRepository policyRepository) + { + // Arrange + var id = Guid.NewGuid(); + var domainName = $"test-{id}.example.com"; + + var organization = await organizationRepository.CreateAsync(new Organization + { + Name = $"Test Org {id}", + BillingEmail = $"test+{id}@example.com", + Plan = "Test", + PrivateKey = "privatekey", + Enabled = true, + UsePolicies = false, // Organization doesn't have policies feature + UseOrganizationDomains = true + }); + + var organizationDomain = new OrganizationDomain + { + OrganizationId = organization.Id, + DomainName = domainName, + Txt = "btw+12345" + }; + organizationDomain.SetNextRunDate(1); + organizationDomain.SetVerifiedDate(); + await organizationDomainRepository.CreateAsync(organizationDomain); + + var policy = new Policy + { + OrganizationId = organization.Id, + Type = PolicyType.BlockClaimedDomainAccountCreation, + Enabled = true + }; + await policyRepository.CreateAsync(policy); + + // Act + var result = await organizationDomainRepository.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(domainName); + + // Assert + Assert.False(result); + } + + [Theory, DatabaseData] + public async Task HasVerifiedDomainWithBlockClaimedDomainPolicyAsync_WithUseOrganizationDomainsFalse_ReturnsFalse( + IOrganizationRepository organizationRepository, + IOrganizationDomainRepository organizationDomainRepository, + IPolicyRepository policyRepository) + { + // Arrange + var id = Guid.NewGuid(); + var domainName = $"test-{id}.example.com"; + + var organization = await organizationRepository.CreateAsync(new Organization + { + Name = $"Test Org {id}", + BillingEmail = $"test+{id}@example.com", + Plan = "Test", + PrivateKey = "privatekey", + Enabled = true, + UsePolicies = true, + UseOrganizationDomains = false // Organization doesn't have organization domains feature + }); + + var organizationDomain = new OrganizationDomain + { + OrganizationId = organization.Id, + DomainName = domainName, + Txt = "btw+12345" + }; + organizationDomain.SetNextRunDate(1); + organizationDomain.SetVerifiedDate(); + await organizationDomainRepository.CreateAsync(organizationDomain); + + var policy = new Policy + { + OrganizationId = organization.Id, + Type = PolicyType.BlockClaimedDomainAccountCreation, + Enabled = true + }; + await policyRepository.CreateAsync(policy); + + // Act + var result = await organizationDomainRepository.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(domainName); + + // Assert + Assert.False(result); + } + + [Theory, DatabaseData] + public async Task HasVerifiedDomainWithBlockClaimedDomainPolicyAsync_WithNoPolicyOfType_ReturnsFalse( + IOrganizationRepository organizationRepository, + IOrganizationDomainRepository organizationDomainRepository) + { + // Arrange + var id = Guid.NewGuid(); + var domainName = $"test-{id}.example.com"; + + var organization = await organizationRepository.CreateAsync(new Organization + { + Name = $"Test Org {id}", + BillingEmail = $"test+{id}@example.com", + Plan = "Test", + PrivateKey = "privatekey", + Enabled = true, + UsePolicies = true, + UseOrganizationDomains = true + }); + + var organizationDomain = new OrganizationDomain + { + OrganizationId = organization.Id, + DomainName = domainName, + Txt = "btw+12345" + }; + organizationDomain.SetNextRunDate(1); + organizationDomain.SetVerifiedDate(); + await organizationDomainRepository.CreateAsync(organizationDomain); + + // No policy created + + // Act + var result = await organizationDomainRepository.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(domainName); + + // Assert + Assert.False(result); + } + + [Theory, DatabaseData] + public async Task HasVerifiedDomainWithBlockClaimedDomainPolicyAsync_WithNonExistentDomain_ReturnsFalse( + IOrganizationDomainRepository organizationDomainRepository) + { + // Arrange + var domainName = $"nonexistent-{Guid.NewGuid()}.example.com"; + + // Act + var result = await organizationDomainRepository.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(domainName); + + // Assert + Assert.False(result); + } + + [Theory, DatabaseData] + public async Task HasVerifiedDomainWithBlockClaimedDomainPolicyAsync_ExcludeOrganization_WhenSameOrg_ReturnsFalse( + IOrganizationRepository organizationRepository, + IOrganizationDomainRepository organizationDomainRepository, + IPolicyRepository policyRepository) + { + // Arrange + var id = Guid.NewGuid(); + var domainName = $"test-{id}.example.com"; + + var organization = await organizationRepository.CreateAsync(new Organization + { + Name = $"Test Org {id}", + BillingEmail = $"test+{id}@example.com", + Plan = "Test", + PrivateKey = "privatekey", + Enabled = true, + UsePolicies = true, + UseOrganizationDomains = true + }); + + var organizationDomain = new OrganizationDomain + { + OrganizationId = organization.Id, + DomainName = domainName, + Txt = "btw+12345" + }; + organizationDomain.SetNextRunDate(1); + organizationDomain.SetVerifiedDate(); + await organizationDomainRepository.CreateAsync(organizationDomain); + + var policy = new Policy + { + OrganizationId = organization.Id, + Type = PolicyType.BlockClaimedDomainAccountCreation, + Enabled = true + }; + await policyRepository.CreateAsync(policy); + + // Act - Exclude the same organization that has the domain + var result = await organizationDomainRepository.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(domainName, organization.Id); + + // Assert - Should return false because we're excluding the only org with this domain + Assert.False(result); + } + + [Theory, DatabaseData] + public async Task HasVerifiedDomainWithBlockClaimedDomainPolicyAsync_ExcludeOrganization_WhenDifferentOrg_ReturnsTrue( + IOrganizationRepository organizationRepository, + IOrganizationDomainRepository organizationDomainRepository, + IPolicyRepository policyRepository) + { + // Arrange + var id = Guid.NewGuid(); + var domainName = $"test-{id}.example.com"; + + var organization1 = await organizationRepository.CreateAsync(new Organization + { + Name = $"Test Org 1 {id}", + BillingEmail = $"test1+{id}@example.com", + Plan = "Test", + PrivateKey = "privatekey", + Enabled = true, + UsePolicies = true, + UseOrganizationDomains = true + }); + + var organizationDomain1 = new OrganizationDomain + { + OrganizationId = organization1.Id, + DomainName = domainName, + Txt = "btw+12345" + }; + organizationDomain1.SetNextRunDate(1); + organizationDomain1.SetVerifiedDate(); + await organizationDomainRepository.CreateAsync(organizationDomain1); + + var policy1 = new Policy + { + OrganizationId = organization1.Id, + Type = PolicyType.BlockClaimedDomainAccountCreation, + Enabled = true + }; + await policyRepository.CreateAsync(policy1); + + // Create a second organization (the one we'll exclude) + var organization2 = await organizationRepository.CreateAsync(new Organization + { + Name = $"Test Org 2 {id}", + BillingEmail = $"test2+{id}@example.com", + Plan = "Test", + PrivateKey = "privatekey", + Enabled = true, + UsePolicies = true, + UseOrganizationDomains = true + }); + + // Act - Exclude organization2 (but organization1 still has the domain blocked) + var result = await organizationDomainRepository.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(domainName, organization2.Id); + + // Assert - Should return true because organization1 (not excluded) has the domain blocked + Assert.True(result); + } } diff --git a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepository/OrganizationUserRepositoryTests.cs b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepository/OrganizationUserRepositoryTests.cs index 157d6a2589..1e1e512b27 100644 --- a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepository/OrganizationUserRepositoryTests.cs +++ b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepository/OrganizationUserRepositoryTests.cs @@ -1,4 +1,5 @@ using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Models.Data.OrganizationUsers; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Auth.Entities; @@ -1487,8 +1488,15 @@ public class OrganizationUserRepositoryTests const string key = "test-key"; orgUser.Key = key; + var acceptedOrganizationUser = new AcceptedOrganizationUserToConfirm + { + OrganizationUserId = orgUser.Id, + UserId = user.Id, + Key = key + }; + // Act - var result = await organizationUserRepository.ConfirmOrganizationUserAsync(orgUser); + var result = await organizationUserRepository.ConfirmOrganizationUserAsync(acceptedOrganizationUser); // Assert Assert.True(result); @@ -1502,27 +1510,6 @@ public class OrganizationUserRepositoryTests await userRepository.DeleteAsync(user); } - [Theory, DatabaseData] - public async Task ConfirmOrganizationUserAsync_WhenUserIsInvited_ReturnsFalse(IOrganizationUserRepository organizationUserRepository, - IOrganizationRepository organizationRepository) - { - // Arrange - var organization = await organizationRepository.CreateTestOrganizationAsync(); - var orgUser = await organizationUserRepository.CreateTestOrganizationUserInviteAsync(organization); - - // Act - var result = await organizationUserRepository.ConfirmOrganizationUserAsync(orgUser); - - // Assert - Assert.False(result); - var unchangedUser = await organizationUserRepository.GetByIdAsync(orgUser.Id); - Assert.NotNull(unchangedUser); - Assert.Equal(OrganizationUserStatusType.Invited, unchangedUser.Status); - - // Annul - await organizationRepository.DeleteAsync(organization); - } - [Theory, DatabaseData] public async Task ConfirmOrganizationUserAsync_WhenUserIsAlreadyConfirmed_ReturnsFalse(IOrganizationUserRepository organizationUserRepository, IOrganizationRepository organizationRepository, @@ -1533,8 +1520,17 @@ public class OrganizationUserRepositoryTests var user = await userRepository.CreateTestUserAsync(); var orgUser = await organizationUserRepository.CreateConfirmedTestOrganizationUserAsync(organization, user); + orgUser.Status = OrganizationUserStatusType.Accepted; // To simulate a second call to ConfirmOrganizationUserAsync + + var acceptedOrganizationUser = new AcceptedOrganizationUserToConfirm + { + OrganizationUserId = orgUser.Id, + UserId = user.Id, + Key = "test-key" + }; + // Act - var result = await organizationUserRepository.ConfirmOrganizationUserAsync(orgUser); + var result = await organizationUserRepository.ConfirmOrganizationUserAsync(acceptedOrganizationUser); // Assert Assert.False(result); @@ -1547,30 +1543,6 @@ public class OrganizationUserRepositoryTests await userRepository.DeleteAsync(user); } - [Theory, DatabaseData] - public async Task ConfirmOrganizationUserAsync_WhenUserIsRevoked_ReturnsFalse(IOrganizationUserRepository organizationUserRepository, - IOrganizationRepository organizationRepository, - IUserRepository userRepository) - { - // Arrange - var organization = await organizationRepository.CreateTestOrganizationAsync(); - var user = await userRepository.CreateTestUserAsync(); - var orgUser = await organizationUserRepository.CreateRevokedTestOrganizationUserAsync(organization, user); - - // Act - var result = await organizationUserRepository.ConfirmOrganizationUserAsync(orgUser); - - // Assert - Assert.False(result); - var unchangedUser = await organizationUserRepository.GetByIdAsync(orgUser.Id); - Assert.NotNull(unchangedUser); - Assert.Equal(OrganizationUserStatusType.Revoked, unchangedUser.Status); - - // Annul - await organizationRepository.DeleteAsync(organization); - await userRepository.DeleteAsync(user); - } - [Theory, DatabaseData] public async Task ConfirmOrganizationUserAsync_IsIdempotent_WhenCalledMultipleTimes( IOrganizationUserRepository organizationUserRepository, @@ -1582,9 +1554,16 @@ public class OrganizationUserRepositoryTests var user = await userRepository.CreateTestUserAsync(); var orgUser = await organizationUserRepository.CreateAcceptedTestOrganizationUserAsync(organization, user); + var acceptedOrganizationUser = new AcceptedOrganizationUserToConfirm + { + OrganizationUserId = orgUser.Id, + UserId = user.Id, + Key = "test-key" + }; + // Act - First call should confirm - var firstResult = await organizationUserRepository.ConfirmOrganizationUserAsync(orgUser); - var secondResult = await organizationUserRepository.ConfirmOrganizationUserAsync(orgUser); + var firstResult = await organizationUserRepository.ConfirmOrganizationUserAsync(acceptedOrganizationUser); + var secondResult = await organizationUserRepository.ConfirmOrganizationUserAsync(acceptedOrganizationUser); // Assert Assert.True(firstResult); @@ -1603,14 +1582,11 @@ public class OrganizationUserRepositoryTests IOrganizationUserRepository organizationUserRepository) { // Arrange - var nonExistentUser = new OrganizationUser + var nonExistentUser = new AcceptedOrganizationUserToConfirm { - Id = Guid.NewGuid(), - OrganizationId = Guid.NewGuid(), + OrganizationUserId = Guid.NewGuid(), UserId = Guid.NewGuid(), - Email = "nonexistent@bitwarden.com", - Status = OrganizationUserStatusType.Accepted, - Type = OrganizationUserType.Owner + Key = "test-key" }; // Act diff --git a/util/Migrator/DbScripts/2025-11-04_00_BlockClaimedDomainAccountCreationPolicy.sql b/util/Migrator/DbScripts/2025-11-04_00_BlockClaimedDomainAccountCreationPolicy.sql new file mode 100644 index 0000000000..04f09b080b --- /dev/null +++ b/util/Migrator/DbScripts/2025-11-04_00_BlockClaimedDomainAccountCreationPolicy.sql @@ -0,0 +1,41 @@ +-- Add stored procedure for checking if a domain has the BlockClaimedDomainAccountCreation policy enabled +-- This supports the BlockClaimedDomainAccountCreation policy (Type = 19) which prevents users from +-- creating personal accounts using email addresses from domains claimed by organizations. +-- The optional @ExcludeOrganizationId parameter allows excluding a specific organization from the check, +-- enabling users to join the organization that owns their email domain. + +CREATE OR ALTER PROCEDURE [dbo].[OrganizationDomain_HasVerifiedDomainWithBlockPolicy] + @DomainName NVARCHAR(255), + @ExcludeOrganizationId UNIQUEIDENTIFIER = NULL +AS +BEGIN + SET NOCOUNT ON + + -- Check if any organization has a verified domain matching the domain name + -- with the BlockClaimedDomainAccountCreation policy enabled (Type = 19) + -- If @ExcludeOrganizationId is provided, exclude that organization from the check + IF EXISTS ( + SELECT 1 + FROM [dbo].[OrganizationDomain] OD + INNER JOIN [dbo].[Organization] O + ON OD.OrganizationId = O.Id + INNER JOIN [dbo].[Policy] P + ON O.Id = P.OrganizationId + WHERE OD.DomainName = @DomainName + AND OD.VerifiedDate IS NOT NULL + AND O.Enabled = 1 + AND O.UsePolicies = 1 + AND O.UseOrganizationDomains = 1 + AND (@ExcludeOrganizationId IS NULL OR O.Id != @ExcludeOrganizationId) + AND P.Type = 19 -- BlockClaimedDomainAccountCreation + AND P.Enabled = 1 + ) + BEGIN + SELECT CAST(1 AS BIT) AS HasBlockPolicy + END + ELSE + BEGIN + SELECT CAST(0 AS BIT) AS HasBlockPolicy + END +END +GO diff --git a/util/Migrator/DbScripts/2025-11-12_00_AddMaxStorageGbIncreased.sql b/util/Migrator/DbScripts/2025-11-12_00_AddMaxStorageGbIncreased.sql new file mode 100644 index 0000000000..4e4909b826 --- /dev/null +++ b/util/Migrator/DbScripts/2025-11-12_00_AddMaxStorageGbIncreased.sql @@ -0,0 +1,350 @@ +-- Add MaxStorageGbIncreased column to User table +IF COL_LENGTH('[dbo].[User]', 'MaxStorageGbIncreased') IS NULL +BEGIN + ALTER TABLE [dbo].[User] ADD [MaxStorageGbIncreased] SMALLINT NULL; +END +GO + +-- Add MaxStorageGbIncreased column to Organization table +IF COL_LENGTH('[dbo].[Organization]', 'MaxStorageGbIncreased') IS NULL +BEGIN + ALTER TABLE [dbo].[Organization] ADD [MaxStorageGbIncreased] SMALLINT NULL; +END +GO + +-- Update UserView to use COALESCE for MaxStorageGb +CREATE OR ALTER VIEW [dbo].[UserView] +AS +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 + [dbo].[User] +GO + +-- Update OrganizationView to use COALESCE for MaxStorageGb +CREATE OR ALTER VIEW [dbo].[OrganizationView] +AS +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 + [dbo].[Organization] +GO + + +-- Update OrganizationUserOrganizationDetailsView +CREATE OR ALTER VIEW [dbo].[OrganizationUserOrganizationDetailsView] +AS +SELECT + OU.[UserId], + OU.[OrganizationId], + OU.[Id] OrganizationUserId, + O.[Name], + O.[Enabled], + O.[PlanType], + O.[UsePolicies], + O.[UseSso], + O.[UseKeyConnector], + O.[UseScim], + O.[UseGroups], + O.[UseDirectory], + O.[UseEvents], + O.[UseTotp], + O.[Use2fa], + O.[UseApi], + O.[UseResetPassword], + O.[SelfHost], + O.[UsersGetPremium], + O.[UseCustomPermissions], + O.[UseSecretsManager], + O.[Seats], + O.[MaxCollections], + COALESCE(O.[MaxStorageGbIncreased], O.[MaxStorageGb]) AS [MaxStorageGb], + O.[Identifier], + OU.[Key], + OU.[ResetPasswordKey], + O.[PublicKey], + O.[PrivateKey], + OU.[Status], + OU.[Type], + SU.[ExternalId] SsoExternalId, + OU.[Permissions], + PO.[ProviderId], + P.[Name] ProviderName, + P.[Type] ProviderType, + SS.[Enabled] SsoEnabled, + SS.[Data] SsoConfig, + OS.[FriendlyName] FamilySponsorshipFriendlyName, + OS.[LastSyncDate] FamilySponsorshipLastSyncDate, + OS.[ToDelete] FamilySponsorshipToDelete, + OS.[ValidUntil] FamilySponsorshipValidUntil, + OU.[AccessSecretsManager], + O.[UsePasswordManager], + O.[SmSeats], + O.[SmServiceAccounts], + O.[LimitCollectionCreation], + O.[LimitCollectionDeletion], + O.[AllowAdminAccessToAllCollectionItems], + O.[UseRiskInsights], + O.[LimitItemDeletion], + O.[UseAdminSponsoredFamilies], + O.[UseOrganizationDomains], + OS.[IsAdminInitiated], + O.[UseAutomaticUserConfirmation] +FROM + [dbo].[OrganizationUser] OU +LEFT JOIN + [dbo].[Organization] O ON O.[Id] = OU.[OrganizationId] +LEFT JOIN + [dbo].[SsoUser] SU ON SU.[UserId] = OU.[UserId] AND SU.[OrganizationId] = OU.[OrganizationId] +LEFT JOIN + [dbo].[ProviderOrganization] PO ON PO.[OrganizationId] = O.[Id] +LEFT JOIN + [dbo].[Provider] P ON P.[Id] = PO.[ProviderId] +LEFT JOIN + [dbo].[SsoConfig] SS ON SS.[OrganizationId] = OU.[OrganizationId] +LEFT JOIN + [dbo].[OrganizationSponsorship] OS ON OS.[SponsoringOrganizationUserID] = OU.[Id] +GO + +-- Update ProviderUserProviderOrganizationDetailsView +CREATE OR ALTER VIEW [dbo].[ProviderUserProviderOrganizationDetailsView] +AS +SELECT + PU.[UserId], + PO.[OrganizationId], + O.[Name], + O.[Enabled], + O.[UsePolicies], + O.[UseSso], + O.[UseKeyConnector], + O.[UseScim], + O.[UseGroups], + O.[UseDirectory], + O.[UseEvents], + O.[UseTotp], + O.[Use2fa], + O.[UseApi], + O.[UseResetPassword], + O.[UseSecretsManager], + O.[UsePasswordManager], + O.[SelfHost], + O.[UsersGetPremium], + O.[UseCustomPermissions], + O.[Seats], + O.[MaxCollections], + COALESCE(O.[MaxStorageGbIncreased], O.[MaxStorageGb]) AS [MaxStorageGb], + O.[Identifier], + PO.[Key], + O.[PublicKey], + O.[PrivateKey], + PU.[Status], + PU.[Type], + PO.[ProviderId], + PU.[Id] ProviderUserId, + P.[Name] ProviderName, + O.[PlanType], + O.[LimitCollectionCreation], + O.[LimitCollectionDeletion], + O.[AllowAdminAccessToAllCollectionItems], + O.[UseRiskInsights], + O.[UseAdminSponsoredFamilies], + P.[Type] ProviderType, + O.[LimitItemDeletion], + O.[UseOrganizationDomains], + O.[UseAutomaticUserConfirmation], + SS.[Enabled] SsoEnabled, + SS.[Data] SsoConfig +FROM + [dbo].[ProviderUser] PU +INNER JOIN + [dbo].[ProviderOrganization] PO ON PO.[ProviderId] = PU.[ProviderId] +INNER JOIN + [dbo].[Organization] O ON O.[Id] = PO.[OrganizationId] +INNER JOIN + [dbo].[Provider] P ON P.[Id] = PU.[ProviderId] +LEFT JOIN + [dbo].[SsoConfig] SS ON SS.[OrganizationId] = O.[Id] +GO + +-- Refresh views that reference Organization table +EXEC sp_refreshview N'[dbo].[OrganizationCipherDetailsCollectionsView]'; +EXEC sp_refreshview N'[dbo].[OrganizationUserOrganizationDetailsView]'; +EXEC sp_refreshview N'[dbo].[ProviderOrganizationOrganizationDetailsView]'; +EXEC sp_refreshview N'[dbo].[ProviderUserProviderOrganizationDetailsView]'; +GO + +-- Refresh views that reference User table +EXEC sp_refreshview N'[dbo].[EmergencyAccessDetailsView]'; +EXEC sp_refreshview N'[dbo].[OrganizationUserUserDetailsView]'; +EXEC sp_refreshview N'[dbo].[ProviderUserUserDetailsView]'; +EXEC sp_refreshview N'[dbo].[UserEmailDomainView]'; +GO + +-- Refresh stored procedures that reference UserView +EXEC sp_refreshsqlmodule N'[dbo].[Notification_ReadByUserIdAndStatus]'; +EXEC sp_refreshsqlmodule N'[dbo].[OrganizationUser_ReadById]'; +EXEC sp_refreshsqlmodule N'[dbo].[OrganizationUser_ReadByIds]'; +EXEC sp_refreshsqlmodule N'[dbo].[OrganizationUser_ReadByOrganizationId]'; +EXEC sp_refreshsqlmodule N'[dbo].[OrganizationUser_ReadByOrganizationIdEmail]'; +EXEC sp_refreshsqlmodule N'[dbo].[OrganizationUser_ReadByOrganizationIdUserId]'; +EXEC sp_refreshsqlmodule N'[dbo].[OrganizationUser_ReadByOrganizationIdWithClaimedDomains]'; +EXEC sp_refreshsqlmodule N'[dbo].[OrganizationUser_ReadByOrganizationIdWithClaimedDomains_V2]'; +EXEC sp_refreshsqlmodule N'[dbo].[OrganizationUser_ReadByUserId]'; +EXEC sp_refreshsqlmodule N'[dbo].[OrganizationUser_ReadByUserIds]'; +EXEC sp_refreshsqlmodule N'[dbo].[OrganizationUser_ReadByUserIdWithPolicyDetails]'; +EXEC sp_refreshsqlmodule N'[dbo].[OrganizationUser_ReadOccupiedSeatCountByOrganizationId]'; +EXEC sp_refreshsqlmodule N'[dbo].[OrganizationUser_ReadOccupiedSmSeatCountByOrganizationId]'; +EXEC sp_refreshsqlmodule N'[dbo].[ProviderUser_ReadById]'; +EXEC sp_refreshsqlmodule N'[dbo].[ProviderUser_ReadByIds]'; +EXEC sp_refreshsqlmodule N'[dbo].[ProviderUser_ReadByOrganizationIdStatus]'; +EXEC sp_refreshsqlmodule N'[dbo].[ProviderUser_ReadByProviderId]'; +EXEC sp_refreshsqlmodule N'[dbo].[ProviderUser_ReadByProviderIdUserId]'; +EXEC sp_refreshsqlmodule N'[dbo].[ProviderUser_ReadByUserId]'; +EXEC sp_refreshsqlmodule N'[dbo].[User_ReadByEmail]'; +EXEC sp_refreshsqlmodule N'[dbo].[User_ReadByEmails]'; +EXEC sp_refreshsqlmodule N'[dbo].[User_ReadById]'; +EXEC sp_refreshsqlmodule N'[dbo].[User_ReadByIds]'; +EXEC sp_refreshsqlmodule N'[dbo].[User_ReadByIdsWithCalculatedPremium]'; +EXEC sp_refreshsqlmodule N'[dbo].[User_ReadByPremium]'; +EXEC sp_refreshsqlmodule N'[dbo].[User_ReadBySsoUserOrganizationIdExternalId]'; +EXEC sp_refreshsqlmodule N'[dbo].[User_Search]'; +GO + +-- Refresh stored procedures that reference OrganizationView +EXEC sp_refreshsqlmodule N'[dbo].[Organization_GetOrganizationsForSubscriptionSync]'; +EXEC sp_refreshsqlmodule N'[dbo].[Organization_ReadByClaimedUserEmailDomain]'; +EXEC sp_refreshsqlmodule N'[dbo].[Organization_ReadByEnabled]'; +EXEC sp_refreshsqlmodule N'[dbo].[Organization_ReadById]'; +EXEC sp_refreshsqlmodule N'[dbo].[Organization_ReadByIdentifier]'; +EXEC sp_refreshsqlmodule N'[dbo].[Organization_ReadByLicenseKey]'; +EXEC sp_refreshsqlmodule N'[dbo].[Organization_ReadByProviderId]'; +EXEC sp_refreshsqlmodule N'[dbo].[Organization_ReadByUserId]'; +EXEC sp_refreshsqlmodule N'[dbo].[Organization_ReadManyByIds]'; +EXEC sp_refreshsqlmodule N'[dbo].[Organization_ReadOccupiedSeatCountByOrganizationId]'; +EXEC sp_refreshsqlmodule N'[dbo].[Organization_Search]'; +EXEC sp_refreshsqlmodule N'[dbo].[Organization_UnassignedToProviderSearch]'; +EXEC sp_refreshsqlmodule N'[dbo].[OrganizationDomainSsoDetails_ReadByEmail]'; +EXEC sp_refreshsqlmodule N'[dbo].[PolicyDetails_ReadByOrganizationId]'; +EXEC sp_refreshsqlmodule N'[dbo].[PolicyDetails_ReadByUserId]'; +EXEC sp_refreshsqlmodule N'[dbo].[PolicyDetails_ReadByUserIdsPolicyType]'; +EXEC sp_refreshsqlmodule N'[dbo].[ProviderOrganization_ReadById]'; +EXEC sp_refreshsqlmodule N'[dbo].[ProviderOrganization_ReadByOrganizationId]'; +EXEC sp_refreshsqlmodule N'[dbo].[ProviderOrganization_ReadCountByOrganizationIds]'; +EXEC sp_refreshsqlmodule N'[dbo].[ProviderOrganizationProviderDetails_ReadByUserId]'; +EXEC sp_refreshsqlmodule N'[dbo].[VerifiedOrganizationDomainSsoDetails_ReadByEmail]'; +GO + +-- Refresh stored procedures that reference OrganizationUserOrganizationDetailsView +EXEC sp_refreshsqlmodule N'[dbo].[OrganizationUserOrganizationDetails_ReadByUserIdStatus]'; +EXEC sp_refreshsqlmodule N'[dbo].[OrganizationUserOrganizationDetails_ReadByUserIdStatusOrganizationId]'; +GO + +-- Refresh stored procedures that reference ProviderUserProviderOrganizationDetailsView +EXEC sp_refreshsqlmodule N'[dbo].[ProviderUserProviderOrganizationDetails_ReadByUserIdStatus]'; +GO + diff --git a/util/MySqlMigrations/Migrations/20251112155802_AddMaxStorageGbIncreasedColumn.Designer.cs b/util/MySqlMigrations/Migrations/20251112155802_AddMaxStorageGbIncreasedColumn.Designer.cs new file mode 100644 index 0000000000..0d50ccc349 --- /dev/null +++ b/util/MySqlMigrations/Migrations/20251112155802_AddMaxStorageGbIncreasedColumn.Designer.cs @@ -0,0 +1,3446 @@ +// +using System; +using Bit.Infrastructure.EntityFramework.Repositories; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Bit.MySqlMigrations.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20251112155802_AddMaxStorageGbIncreasedColumn")] + partial class AddMaxStorageGbIncreasedColumn + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("Bit.Core.Dirt.Reports.Models.Data.OrganizationMemberBaseDetail", b => + { + b.Property("CipherId") + .HasColumnType("char(36)"); + + b.Property("CollectionId") + .HasColumnType("char(36)"); + + b.Property("CollectionName") + .HasColumnType("longtext"); + + b.Property("Email") + .HasColumnType("longtext"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("GroupName") + .HasColumnType("longtext"); + + b.Property("HidePasswords") + .HasColumnType("tinyint(1)"); + + b.Property("Manage") + .HasColumnType("tinyint(1)"); + + b.Property("ReadOnly") + .HasColumnType("tinyint(1)"); + + b.Property("ResetPasswordKey") + .HasColumnType("longtext"); + + b.Property("TwoFactorProviders") + .HasColumnType("longtext"); + + b.Property("UserGuid") + .HasColumnType("char(36)"); + + b.Property("UserName") + .HasColumnType("longtext"); + + b.Property("UsesKeyConnector") + .HasColumnType("tinyint(1)"); + + b.ToTable("OrganizationMemberBaseDetails"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AllowAdminAccessToAllCollectionItems") + .HasColumnType("tinyint(1)") + .HasDefaultValue(true); + + b.Property("BillingEmail") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("BusinessAddress1") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("BusinessAddress2") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("BusinessAddress3") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("BusinessCountry") + .HasMaxLength(2) + .HasColumnType("varchar(2)"); + + b.Property("BusinessName") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("BusinessTaxNumber") + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("ExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("Gateway") + .HasColumnType("tinyint unsigned"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Identifier") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("LimitCollectionCreation") + .HasColumnType("tinyint(1)"); + + b.Property("LimitCollectionDeletion") + .HasColumnType("tinyint(1)"); + + b.Property("LimitItemDeletion") + .HasColumnType("tinyint(1)"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("int"); + + b.Property("MaxAutoscaleSmSeats") + .HasColumnType("int"); + + b.Property("MaxAutoscaleSmServiceAccounts") + .HasColumnType("int"); + + b.Property("MaxCollections") + .HasColumnType("smallint"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("MaxStorageGbIncreased") + .HasColumnType("smallint"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("OwnersNotifiedOfAutoscaling") + .HasColumnType("datetime(6)"); + + b.Property("Plan") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("PlanType") + .HasColumnType("tinyint unsigned"); + + b.Property("PrivateKey") + .HasColumnType("longtext"); + + b.Property("PublicKey") + .HasColumnType("longtext"); + + b.Property("ReferenceData") + .HasColumnType("longtext"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Seats") + .HasColumnType("int"); + + b.Property("SelfHost") + .HasColumnType("tinyint(1)"); + + b.Property("SmSeats") + .HasColumnType("int"); + + b.Property("SmServiceAccounts") + .HasColumnType("int"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("Storage") + .HasColumnType("bigint"); + + b.Property("SyncSeats") + .HasColumnType("tinyint(1)"); + + b.Property("TwoFactorProviders") + .HasColumnType("longtext"); + + b.Property("Use2fa") + .HasColumnType("tinyint(1)"); + + b.Property("UseAdminSponsoredFamilies") + .HasColumnType("tinyint(1)"); + + b.Property("UseApi") + .HasColumnType("tinyint(1)"); + + b.Property("UseAutomaticUserConfirmation") + .HasColumnType("tinyint(1)"); + + b.Property("UseCustomPermissions") + .HasColumnType("tinyint(1)"); + + b.Property("UseDirectory") + .HasColumnType("tinyint(1)"); + + b.Property("UseEvents") + .HasColumnType("tinyint(1)"); + + b.Property("UseGroups") + .HasColumnType("tinyint(1)"); + + b.Property("UseKeyConnector") + .HasColumnType("tinyint(1)"); + + b.Property("UseOrganizationDomains") + .HasColumnType("tinyint(1)"); + + b.Property("UsePasswordManager") + .HasColumnType("tinyint(1)"); + + b.Property("UsePolicies") + .HasColumnType("tinyint(1)"); + + b.Property("UseResetPassword") + .HasColumnType("tinyint(1)"); + + b.Property("UseRiskInsights") + .HasColumnType("tinyint(1)"); + + b.Property("UseScim") + .HasColumnType("tinyint(1)"); + + b.Property("UseSecretsManager") + .HasColumnType("tinyint(1)"); + + b.Property("UseSso") + .HasColumnType("tinyint(1)"); + + b.Property("UseTotp") + .HasColumnType("tinyint(1)"); + + b.Property("UsersGetPremium") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("Id", "Enabled") + .HasAnnotation("Npgsql:IndexInclude", new[] { "UseTotp" }); + + b.ToTable("Organization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Configuration") + .HasColumnType("longtext"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "Type") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationIntegration", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegrationConfiguration", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Configuration") + .HasColumnType("longtext"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("EventType") + .HasColumnType("int"); + + b.Property("Filters") + .HasColumnType("longtext"); + + b.Property("OrganizationIntegrationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Template") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationIntegrationId"); + + b.ToTable("OrganizationIntegrationConfiguration", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "Type") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Policy", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("BillingEmail") + .HasColumnType("longtext"); + + b.Property("BillingPhone") + .HasColumnType("longtext"); + + b.Property("BusinessAddress1") + .HasColumnType("longtext"); + + b.Property("BusinessAddress2") + .HasColumnType("longtext"); + + b.Property("BusinessAddress3") + .HasColumnType("longtext"); + + b.Property("BusinessCountry") + .HasColumnType("longtext"); + + b.Property("BusinessName") + .HasColumnType("longtext"); + + b.Property("BusinessTaxNumber") + .HasColumnType("longtext"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("DiscountId") + .HasColumnType("longtext"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("Gateway") + .HasColumnType("tinyint unsigned"); + + b.Property("GatewayCustomerId") + .HasColumnType("longtext"); + + b.Property("GatewaySubscriptionId") + .HasColumnType("longtext"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UseEvents") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("Provider", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Settings") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderOrganization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .HasColumnType("longtext"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("Permissions") + .HasColumnType("longtext"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId"); + + b.ToTable("ProviderUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AccessCode") + .HasMaxLength(25) + .HasColumnType("varchar(25)"); + + b.Property("Approved") + .HasColumnType("tinyint(1)"); + + b.Property("AuthenticationDate") + .HasColumnType("datetime(6)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("MasterPasswordHash") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("PublicKey") + .HasColumnType("longtext"); + + b.Property("RequestCountryName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("RequestDeviceIdentifier") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("RequestDeviceType") + .HasColumnType("tinyint unsigned"); + + b.Property("RequestIpAddress") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("ResponseDate") + .HasColumnType("datetime(6)"); + + b.Property("ResponseDeviceId") + .HasColumnType("char(36)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ResponseDeviceId"); + + b.HasIndex("UserId"); + + b.ToTable("AuthRequest", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.EmergencyAccess", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("GranteeId") + .HasColumnType("char(36)"); + + b.Property("GrantorId") + .HasColumnType("char(36)"); + + b.Property("KeyEncrypted") + .HasColumnType("longtext"); + + b.Property("LastNotificationDate") + .HasColumnType("datetime(6)"); + + b.Property("RecoveryInitiatedDate") + .HasColumnType("datetime(6)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("WaitTimeDays") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("GranteeId"); + + b.HasIndex("GrantorId"); + + b.ToTable("EmergencyAccess", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.Grant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ClientId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ConsumedDate") + .HasColumnType("datetime(6)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Description") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("SessionId") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("SubjectId") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.HasKey("Id") + .HasName("PK_Grant") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpirationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("Grant", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("SsoConfig", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId"); + + b.HasIndex("OrganizationId", "ExternalId") + .IsUnique() + .HasAnnotation("Npgsql:IndexInclude", new[] { "UserId" }) + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "UserId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SsoUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AaGuid") + .HasColumnType("char(36)"); + + b.Property("Counter") + .HasColumnType("int"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("CredentialId") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("EncryptedPrivateKey") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("EncryptedPublicKey") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("EncryptedUserKey") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("PublicKey") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("SupportsPrf") + .HasColumnType("tinyint(1)"); + + b.Property("Type") + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("WebAuthnCredential", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ClientOrganizationMigrationRecord", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("GatewayCustomerId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("GatewaySubscriptionId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("int"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("PlanType") + .HasColumnType("tinyint unsigned"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("Seats") + .HasColumnType("int"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId", "OrganizationId") + .IsUnique(); + + b.ToTable("ClientOrganizationMigrationRecord", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("InstallationId") + .HasColumnType("char(36)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("InstallationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationInstallation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AssignedSeats") + .HasColumnType("int"); + + b.Property("ClientId") + .HasColumnType("char(36)"); + + b.Property("ClientName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Created") + .HasColumnType("datetime(6)"); + + b.Property("InvoiceId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("InvoiceNumber") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("PlanName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("Total") + .HasColumnType("decimal(65,30)"); + + b.Property("UsedSeats") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderInvoiceItem", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AllocatedSeats") + .HasColumnType("int"); + + b.Property("PlanType") + .HasColumnType("tinyint unsigned"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("PurchasedSeats") + .HasColumnType("int"); + + b.Property("SeatMinimum") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("Id", "PlanType") + .IsUnique(); + + b.ToTable("ProviderPlan", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationApplication", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Applications") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ContentEncryptionKey") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationApplication", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationReport", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ApplicationAtRiskCount") + .HasColumnType("int"); + + b.Property("ApplicationCount") + .HasColumnType("int"); + + b.Property("ApplicationData") + .HasColumnType("longtext"); + + b.Property("ContentEncryptionKey") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("CriticalApplicationAtRiskCount") + .HasColumnType("int"); + + b.Property("CriticalApplicationCount") + .HasColumnType("int"); + + b.Property("CriticalMemberAtRiskCount") + .HasColumnType("int"); + + b.Property("CriticalMemberCount") + .HasColumnType("int"); + + b.Property("CriticalPasswordAtRiskCount") + .HasColumnType("int"); + + b.Property("CriticalPasswordCount") + .HasColumnType("int"); + + b.Property("MemberAtRiskCount") + .HasColumnType("int"); + + b.Property("MemberCount") + .HasColumnType("int"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("PasswordAtRiskCount") + .HasColumnType("int"); + + b.Property("PasswordCount") + .HasColumnType("int"); + + b.Property("ReportData") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("SummaryData") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationReport", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.PasswordHealthReportApplication", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Uri") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("PasswordHealthReportApplication", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Cache", b => + { + b.Property("Id") + .HasMaxLength(449) + .HasColumnType("varchar(449)"); + + b.Property("AbsoluteExpiration") + .HasColumnType("datetime(6)"); + + b.Property("ExpiresAtTime") + .HasColumnType("datetime(6)"); + + b.Property("SlidingExpirationInSeconds") + .HasColumnType("bigint"); + + b.Property("Value") + .IsRequired() + .HasColumnType("longblob"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpiresAtTime") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Cache", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("DefaultUserCollectionEmail") + .HasColumnType("longtext"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Collection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.Property("CollectionId") + .HasColumnType("char(36)"); + + b.Property("CipherId") + .HasColumnType("char(36)"); + + b.HasKey("CollectionId", "CipherId"); + + b.HasIndex("CipherId"); + + b.ToTable("CollectionCipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionGroup", b => + { + b.Property("CollectionId") + .HasColumnType("char(36)"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("HidePasswords") + .HasColumnType("tinyint(1)"); + + b.Property("Manage") + .HasColumnType("tinyint(1)"); + + b.Property("ReadOnly") + .HasColumnType("tinyint(1)"); + + b.HasKey("CollectionId", "GroupId"); + + b.HasIndex("GroupId"); + + b.ToTable("CollectionGroups"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.Property("CollectionId") + .HasColumnType("char(36)"); + + b.Property("OrganizationUserId") + .HasColumnType("char(36)"); + + b.Property("HidePasswords") + .HasColumnType("tinyint(1)"); + + b.Property("Manage") + .HasColumnType("tinyint(1)"); + + b.Property("ReadOnly") + .HasColumnType("tinyint(1)"); + + b.HasKey("CollectionId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Active") + .HasColumnType("tinyint(1)") + .HasDefaultValue(true); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("EncryptedPrivateKey") + .HasColumnType("longtext"); + + b.Property("EncryptedPublicKey") + .HasColumnType("longtext"); + + b.Property("EncryptedUserKey") + .HasColumnType("longtext"); + + b.Property("Identifier") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("PushToken") + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("Identifier") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "Identifier") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Device", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Event", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ActingUserId") + .HasColumnType("char(36)"); + + b.Property("CipherId") + .HasColumnType("char(36)"); + + b.Property("CollectionId") + .HasColumnType("char(36)"); + + b.Property("Date") + .HasColumnType("datetime(6)"); + + b.Property("DeviceType") + .HasColumnType("tinyint unsigned"); + + b.Property("DomainName") + .HasColumnType("longtext"); + + b.Property("GrantedServiceAccountId") + .HasColumnType("char(36)"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("InstallationId") + .HasColumnType("char(36)"); + + b.Property("IpAddress") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("OrganizationUserId") + .HasColumnType("char(36)"); + + b.Property("PolicyId") + .HasColumnType("char(36)"); + + b.Property("ProjectId") + .HasColumnType("char(36)"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("ProviderOrganizationId") + .HasColumnType("char(36)"); + + b.Property("ProviderUserId") + .HasColumnType("char(36)"); + + b.Property("SecretId") + .HasColumnType("char(36)"); + + b.Property("ServiceAccountId") + .HasColumnType("char(36)"); + + b.Property("SystemUser") + .HasColumnType("tinyint unsigned"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("Date", "OrganizationId", "ActingUserId", "CipherId") + .HasDatabaseName("IX_Event_DateOrganizationIdUserId") + .HasAnnotation("SqlServer:Clustered", false) + .HasAnnotation("SqlServer:Include", new[] { "ServiceAccountId", "GrantedServiceAccountId" }); + + b.ToTable("Event", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Group", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("OrganizationUserId") + .HasColumnType("char(36)"); + + b.HasKey("GroupId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("GroupUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Config") + .HasColumnType("longtext"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationConnection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("DomainName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("JobRunCount") + .HasColumnType("int"); + + b.Property("LastCheckedDate") + .HasColumnType("datetime(6)"); + + b.Property("NextRunDate") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Txt") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("VerifiedDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationDomain", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("FriendlyName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("IsAdminInitiated") + .HasColumnType("tinyint(1)"); + + b.Property("LastSyncDate") + .HasColumnType("datetime(6)"); + + b.Property("Notes") + .HasColumnType("longtext"); + + b.Property("OfferedToEmail") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("PlanSponsorshipType") + .HasColumnType("tinyint unsigned"); + + b.Property("SponsoredOrganizationId") + .HasColumnType("char(36)"); + + b.Property("SponsoringOrganizationId") + .HasColumnType("char(36)"); + + b.Property("SponsoringOrganizationUserId") + .HasColumnType("char(36)"); + + b.Property("ToDelete") + .HasColumnType("tinyint(1)"); + + b.Property("ValidUntil") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("SponsoredOrganizationId"); + + b.HasIndex("SponsoringOrganizationId"); + + b.HasIndex("SponsoringOrganizationUserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationSponsorship", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AccessSecretsManager") + .HasColumnType("tinyint(1)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Permissions") + .HasColumnType("longtext"); + + b.Property("ResetPasswordKey") + .HasColumnType("longtext"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AccessCount") + .HasColumnType("int"); + + b.Property("CipherId") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("DeletionDate") + .HasColumnType("datetime(6)"); + + b.Property("Disabled") + .HasColumnType("tinyint(1)"); + + b.Property("Emails") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("ExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("HideEmail") + .HasColumnType("tinyint(1)"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("MaxAccessCount") + .HasColumnType("int"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Password") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("DeletionDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Send", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.TaxRate", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("varchar(40)"); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("Country") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("PostalCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b.Property("Rate") + .HasColumnType("decimal(65,30)"); + + b.Property("State") + .HasMaxLength(2) + .HasColumnType("varchar(2)"); + + b.HasKey("Id"); + + b.ToTable("TaxRate", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Amount") + .HasColumnType("decimal(65,30)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Details") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("Gateway") + .HasColumnType("tinyint unsigned"); + + b.Property("GatewayId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("PaymentMethodType") + .HasColumnType("tinyint unsigned"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("Refunded") + .HasColumnType("tinyint(1)"); + + b.Property("RefundedAmount") + .HasColumnType("decimal(65,30)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId", "CreationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Transaction", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AccountRevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("AvatarColor") + .HasMaxLength(7) + .HasColumnType("varchar(7)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Culture") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("EmailVerified") + .HasColumnType("tinyint(1)"); + + b.Property("EquivalentDomains") + .HasColumnType("longtext"); + + b.Property("ExcludedGlobalEquivalentDomains") + .HasColumnType("longtext"); + + b.Property("FailedLoginCount") + .HasColumnType("int"); + + b.Property("ForcePasswordReset") + .HasColumnType("tinyint(1)"); + + b.Property("Gateway") + .HasColumnType("tinyint unsigned"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Kdf") + .HasColumnType("tinyint unsigned"); + + b.Property("KdfIterations") + .HasColumnType("int"); + + b.Property("KdfMemory") + .HasColumnType("int"); + + b.Property("KdfParallelism") + .HasColumnType("int"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("LastEmailChangeDate") + .HasColumnType("datetime(6)"); + + b.Property("LastFailedLoginDate") + .HasColumnType("datetime(6)"); + + b.Property("LastKdfChangeDate") + .HasColumnType("datetime(6)"); + + b.Property("LastKeyRotationDate") + .HasColumnType("datetime(6)"); + + b.Property("LastPasswordChangeDate") + .HasColumnType("datetime(6)"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("MasterPassword") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("MasterPasswordHint") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("MaxStorageGbIncreased") + .HasColumnType("smallint"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Premium") + .HasColumnType("tinyint(1)"); + + b.Property("PremiumExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("PrivateKey") + .HasColumnType("longtext"); + + b.Property("PublicKey") + .HasColumnType("longtext"); + + b.Property("ReferenceData") + .HasColumnType("longtext"); + + b.Property("RenewalReminderDate") + .HasColumnType("datetime(6)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("SecurityStamp") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("SecurityState") + .HasColumnType("longtext"); + + b.Property("SecurityVersion") + .HasColumnType("int"); + + b.Property("SignedPublicKey") + .HasColumnType("longtext"); + + b.Property("Storage") + .HasColumnType("bigint"); + + b.Property("TwoFactorProviders") + .HasColumnType("longtext"); + + b.Property("TwoFactorRecoveryCode") + .HasMaxLength(32) + .HasColumnType("varchar(32)"); + + b.Property("UsesKeyConnector") + .HasColumnType("tinyint(1)"); + + b.Property("VerifyDevices") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Premium", "PremiumExpirationDate", "RenewalReminderDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("User", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.UserSignatureKeyPair", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("SignatureAlgorithm") + .HasColumnType("tinyint unsigned"); + + b.Property("SigningKey") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.Property("VerifyingKey") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("UserSignatureKeyPair", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Body") + .HasMaxLength(3000) + .HasColumnType("varchar(3000)"); + + b.Property("ClientType") + .HasColumnType("tinyint unsigned"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Global") + .HasColumnType("tinyint(1)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Priority") + .HasColumnType("tinyint unsigned"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("TaskId") + .HasColumnType("char(36)"); + + b.Property("Title") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("TaskId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("ClientType", "Global", "UserId", "OrganizationId", "Priority", "CreationDate") + .IsDescending(false, false, false, false, true, true) + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Notification", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.Property("UserId") + .HasColumnType("char(36)"); + + b.Property("NotificationId") + .HasColumnType("char(36)"); + + b.Property("DeletedDate") + .HasColumnType("datetime(6)"); + + b.Property("ReadDate") + .HasColumnType("datetime(6)"); + + b.HasKey("UserId", "NotificationId") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("NotificationId"); + + b.ToTable("NotificationStatus", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Platform.Installation", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("varchar(150)"); + + b.Property("LastActivityDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.ToTable("Installation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Discriminator") + .IsRequired() + .HasMaxLength(34) + .HasColumnType("varchar(34)"); + + b.Property("Read") + .HasColumnType("tinyint(1)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Write") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.ToTable("AccessPolicy", (string)null); + + b.HasDiscriminator().HasValue("AccessPolicy"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ClientSecretHash") + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("EncryptedPayload") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("varchar(4000)"); + + b.Property("ExpireAt") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("varchar(4000)"); + + b.Property("ServiceAccountId") + .HasColumnType("char(36)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ServiceAccountId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("DeletedDate") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Project", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("DeletedDate") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("Note") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Value") + .HasColumnType("longtext"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Secret", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.SecretVersion", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("EditorOrganizationUserId") + .HasColumnType("char(36)"); + + b.Property("EditorServiceAccountId") + .HasColumnType("char(36)"); + + b.Property("SecretId") + .HasColumnType("char(36)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("VersionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("EditorOrganizationUserId") + .HasDatabaseName("IX_SecretVersion_EditorOrganizationUserId"); + + b.HasIndex("EditorServiceAccountId") + .HasDatabaseName("IX_SecretVersion_EditorServiceAccountId"); + + b.HasIndex("SecretId") + .HasDatabaseName("IX_SecretVersion_SecretId"); + + b.ToTable("SecretVersion"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ServiceAccount", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ArchivedDate") + .HasColumnType("datetime(6)"); + + b.Property("Attachments") + .HasColumnType("longtext"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("DeletedDate") + .HasColumnType("datetime(6)"); + + b.Property("Favorites") + .HasColumnType("longtext"); + + b.Property("Folders") + .HasColumnType("longtext"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Reprompt") + .HasColumnType("tinyint unsigned"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.ToTable("Cipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Folder", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CipherId") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SecurityTask", (string)null); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.Property("ProjectsId") + .HasColumnType("char(36)"); + + b.Property("SecretsId") + .HasColumnType("char(36)"); + + b.HasKey("ProjectsId", "SecretsId"); + + b.HasIndex("SecretsId"); + + b.ToTable("ProjectSecret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedProjectId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedSecretId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedProjectId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedSecretId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedProjectId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedSecretId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegrationConfiguration", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", "OrganizationIntegration") + .WithMany() + .HasForeignKey("OrganizationIntegrationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("OrganizationIntegration"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Policies") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Device", "ResponseDevice") + .WithMany() + .HasForeignKey("ResponseDeviceId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("ResponseDevice"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.EmergencyAccess", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantee") + .WithMany() + .HasForeignKey("GranteeId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantor") + .WithMany() + .HasForeignKey("GrantorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Grantee"); + + b.Navigation("Grantor"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoConfigs") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoUsers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("SsoUsers") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Platform.Installation", "Installation") + .WithMany() + .HasForeignKey("InstallationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Installation"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationApplication", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationReport", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.PasswordHealthReportApplication", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Collections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") + .WithMany("CollectionCiphers") + .HasForeignKey("CipherId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionCiphers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Collection"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionGroup", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionGroups") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionUsers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("CollectionUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Groups") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany("GroupUsers") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("GroupUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Group"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("ApiKeys") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Connections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Domains") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoredOrganization") + .WithMany() + .HasForeignKey("SponsoredOrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoringOrganization") + .WithMany() + .HasForeignKey("SponsoringOrganizationId"); + + b.Navigation("SponsoredOrganization"); + + b.Navigation("SponsoringOrganization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("OrganizationUsers") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("OrganizationUsers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Transactions") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Transactions") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.UserSignatureKeyPair", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", "Task") + .WithMany() + .HasForeignKey("TaskId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("Task"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", "Notification") + .WithMany() + .HasForeignKey("NotificationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Notification"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany("ApiKeys") + .HasForeignKey("ServiceAccountId"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.SecretVersion", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "EditorOrganizationUser") + .WithMany() + .HasForeignKey("EditorOrganizationUserId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "EditorServiceAccount") + .WithMany() + .HasForeignKey("EditorServiceAccountId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "Secret") + .WithMany("SecretVersions") + .HasForeignKey("SecretId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("EditorOrganizationUser"); + + b.Navigation("EditorServiceAccount"); + + b.Navigation("Secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Ciphers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Ciphers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Folders") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") + .WithMany() + .HasForeignKey("CipherId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", null) + .WithMany() + .HasForeignKey("ProjectsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", null) + .WithMany() + .HasForeignKey("SecretsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedProject"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedSecret"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "GrantedServiceAccount") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedServiceAccountId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedServiceAccount"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("ServiceAccountAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany("ProjectAccessPolicies") + .HasForeignKey("ServiceAccountId"); + + b.Navigation("GrantedProject"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("ServiceAccountAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany() + .HasForeignKey("ServiceAccountId"); + + b.Navigation("GrantedSecret"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedProject"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedSecret"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "GrantedServiceAccount") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedServiceAccountId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedServiceAccount"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("Ciphers"); + + b.Navigation("Collections"); + + b.Navigation("Connections"); + + b.Navigation("Domains"); + + b.Navigation("Groups"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("Policies"); + + b.Navigation("SsoConfigs"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.Navigation("CollectionCiphers"); + + b.Navigation("CollectionGroups"); + + b.Navigation("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Navigation("CollectionUsers"); + + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Navigation("Ciphers"); + + b.Navigation("Folders"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ServiceAccountAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Navigation("GroupAccessPolicies"); + + b.Navigation("SecretVersions"); + + b.Navigation("ServiceAccountAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ProjectAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Navigation("CollectionCiphers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/util/MySqlMigrations/Migrations/20251112155802_AddMaxStorageGbIncreasedColumn.cs b/util/MySqlMigrations/Migrations/20251112155802_AddMaxStorageGbIncreasedColumn.cs new file mode 100644 index 0000000000..acdf253901 --- /dev/null +++ b/util/MySqlMigrations/Migrations/20251112155802_AddMaxStorageGbIncreasedColumn.cs @@ -0,0 +1,37 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.MySqlMigrations.Migrations; + +/// +public partial class AddMaxStorageGbIncreasedColumn : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "MaxStorageGbIncreased", + table: "User", + type: "smallint", + nullable: true); + + migrationBuilder.AddColumn( + name: "MaxStorageGbIncreased", + table: "Organization", + type: "smallint", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "MaxStorageGbIncreased", + table: "User"); + + migrationBuilder.DropColumn( + name: "MaxStorageGbIncreased", + table: "Organization"); + } +} diff --git a/util/PostgresMigrations/Migrations/20251112155845_AddMaxStorageGbIncreasedColumn.Designer.cs b/util/PostgresMigrations/Migrations/20251112155845_AddMaxStorageGbIncreasedColumn.Designer.cs new file mode 100644 index 0000000000..99e2535164 --- /dev/null +++ b/util/PostgresMigrations/Migrations/20251112155845_AddMaxStorageGbIncreasedColumn.Designer.cs @@ -0,0 +1,3452 @@ +// +using System; +using Bit.Infrastructure.EntityFramework.Repositories; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Bit.PostgresMigrations.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20251112155845_AddMaxStorageGbIncreasedColumn")] + partial class AddMaxStorageGbIncreasedColumn + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Npgsql:CollationDefinition:postgresIndetermanisticCollation", "en-u-ks-primary,en-u-ks-primary,icu,False") + .HasAnnotation("ProductVersion", "8.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Bit.Core.Dirt.Reports.Models.Data.OrganizationMemberBaseDetail", b => + { + b.Property("CipherId") + .HasColumnType("uuid"); + + b.Property("CollectionId") + .HasColumnType("uuid"); + + b.Property("CollectionName") + .HasColumnType("text"); + + b.Property("Email") + .HasColumnType("text"); + + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("GroupName") + .HasColumnType("text"); + + b.Property("HidePasswords") + .HasColumnType("boolean"); + + b.Property("Manage") + .HasColumnType("boolean"); + + b.Property("ReadOnly") + .HasColumnType("boolean"); + + b.Property("ResetPasswordKey") + .HasColumnType("text"); + + b.Property("TwoFactorProviders") + .HasColumnType("text"); + + b.Property("UserGuid") + .HasColumnType("uuid"); + + b.Property("UserName") + .HasColumnType("text"); + + b.Property("UsesKeyConnector") + .HasColumnType("boolean"); + + b.ToTable("OrganizationMemberBaseDetails"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AllowAdminAccessToAllCollectionItems") + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("BillingEmail") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("BusinessAddress1") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("BusinessAddress2") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("BusinessAddress3") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("BusinessCountry") + .HasMaxLength(2) + .HasColumnType("character varying(2)"); + + b.Property("BusinessName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("BusinessTaxNumber") + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Gateway") + .HasColumnType("smallint"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Identifier") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .UseCollation("postgresIndetermanisticCollation"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("LimitCollectionCreation") + .HasColumnType("boolean"); + + b.Property("LimitCollectionDeletion") + .HasColumnType("boolean"); + + b.Property("LimitItemDeletion") + .HasColumnType("boolean"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("integer"); + + b.Property("MaxAutoscaleSmSeats") + .HasColumnType("integer"); + + b.Property("MaxAutoscaleSmServiceAccounts") + .HasColumnType("integer"); + + b.Property("MaxCollections") + .HasColumnType("smallint"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("MaxStorageGbIncreased") + .HasColumnType("smallint"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("OwnersNotifiedOfAutoscaling") + .HasColumnType("timestamp with time zone"); + + b.Property("Plan") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PlanType") + .HasColumnType("smallint"); + + b.Property("PrivateKey") + .HasColumnType("text"); + + b.Property("PublicKey") + .HasColumnType("text"); + + b.Property("ReferenceData") + .HasColumnType("text"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Seats") + .HasColumnType("integer"); + + b.Property("SelfHost") + .HasColumnType("boolean"); + + b.Property("SmSeats") + .HasColumnType("integer"); + + b.Property("SmServiceAccounts") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Storage") + .HasColumnType("bigint"); + + b.Property("SyncSeats") + .HasColumnType("boolean"); + + b.Property("TwoFactorProviders") + .HasColumnType("text"); + + b.Property("Use2fa") + .HasColumnType("boolean"); + + b.Property("UseAdminSponsoredFamilies") + .HasColumnType("boolean"); + + b.Property("UseApi") + .HasColumnType("boolean"); + + b.Property("UseAutomaticUserConfirmation") + .HasColumnType("boolean"); + + b.Property("UseCustomPermissions") + .HasColumnType("boolean"); + + b.Property("UseDirectory") + .HasColumnType("boolean"); + + b.Property("UseEvents") + .HasColumnType("boolean"); + + b.Property("UseGroups") + .HasColumnType("boolean"); + + b.Property("UseKeyConnector") + .HasColumnType("boolean"); + + b.Property("UseOrganizationDomains") + .HasColumnType("boolean"); + + b.Property("UsePasswordManager") + .HasColumnType("boolean"); + + b.Property("UsePolicies") + .HasColumnType("boolean"); + + b.Property("UseResetPassword") + .HasColumnType("boolean"); + + b.Property("UseRiskInsights") + .HasColumnType("boolean"); + + b.Property("UseScim") + .HasColumnType("boolean"); + + b.Property("UseSecretsManager") + .HasColumnType("boolean"); + + b.Property("UseSso") + .HasColumnType("boolean"); + + b.Property("UseTotp") + .HasColumnType("boolean"); + + b.Property("UsersGetPremium") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("Id", "Enabled"); + + NpgsqlIndexBuilderExtensions.IncludeProperties(b.HasIndex("Id", "Enabled"), new[] { "UseTotp" }); + + b.ToTable("Organization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Configuration") + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "Type") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationIntegration", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegrationConfiguration", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Configuration") + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("EventType") + .HasColumnType("integer"); + + b.Property("Filters") + .HasColumnType("text"); + + b.Property("OrganizationIntegrationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Template") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationIntegrationId"); + + b.ToTable("OrganizationIntegrationConfiguration", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "Type") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Policy", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("BillingEmail") + .HasColumnType("text"); + + b.Property("BillingPhone") + .HasColumnType("text"); + + b.Property("BusinessAddress1") + .HasColumnType("text"); + + b.Property("BusinessAddress2") + .HasColumnType("text"); + + b.Property("BusinessAddress3") + .HasColumnType("text"); + + b.Property("BusinessCountry") + .HasColumnType("text"); + + b.Property("BusinessName") + .HasColumnType("text"); + + b.Property("BusinessTaxNumber") + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("DiscountId") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("Gateway") + .HasColumnType("smallint"); + + b.Property("GatewayCustomerId") + .HasColumnType("text"); + + b.Property("GatewaySubscriptionId") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UseEvents") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.ToTable("Provider", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Settings") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderOrganization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .HasColumnType("text"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("Permissions") + .HasColumnType("text"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId"); + + b.ToTable("ProviderUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccessCode") + .HasMaxLength(25) + .HasColumnType("character varying(25)"); + + b.Property("Approved") + .HasColumnType("boolean"); + + b.Property("AuthenticationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("MasterPasswordHash") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("PublicKey") + .HasColumnType("text"); + + b.Property("RequestCountryName") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("RequestDeviceIdentifier") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("RequestDeviceType") + .HasColumnType("smallint"); + + b.Property("RequestIpAddress") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ResponseDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ResponseDeviceId") + .HasColumnType("uuid"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ResponseDeviceId"); + + b.HasIndex("UserId"); + + b.ToTable("AuthRequest", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.EmergencyAccess", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("GranteeId") + .HasColumnType("uuid"); + + b.Property("GrantorId") + .HasColumnType("uuid"); + + b.Property("KeyEncrypted") + .HasColumnType("text"); + + b.Property("LastNotificationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("RecoveryInitiatedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("WaitTimeDays") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("GranteeId"); + + b.HasIndex("GrantorId"); + + b.ToTable("EmergencyAccess", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.Grant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClientId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ConsumedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .IsRequired() + .HasColumnType("text"); + + b.Property("Description") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("SessionId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("SubjectId") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id") + .HasName("PK_Grant") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpirationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("Grant", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("SsoConfig", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .UseCollation("postgresIndetermanisticCollation"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId"); + + b.HasIndex("OrganizationId", "ExternalId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + NpgsqlIndexBuilderExtensions.IncludeProperties(b.HasIndex("OrganizationId", "ExternalId"), new[] { "UserId" }); + + b.HasIndex("OrganizationId", "UserId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SsoUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AaGuid") + .HasColumnType("uuid"); + + b.Property("Counter") + .HasColumnType("integer"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("CredentialId") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EncryptedPrivateKey") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("EncryptedPublicKey") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("EncryptedUserKey") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PublicKey") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("SupportsPrf") + .HasColumnType("boolean"); + + b.Property("Type") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("WebAuthnCredential", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ClientOrganizationMigrationRecord", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("GatewayCustomerId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("GatewaySubscriptionId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("integer"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("PlanType") + .HasColumnType("smallint"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("Seats") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId", "OrganizationId") + .IsUnique(); + + b.ToTable("ClientOrganizationMigrationRecord", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("InstallationId") + .HasColumnType("uuid"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("InstallationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationInstallation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AssignedSeats") + .HasColumnType("integer"); + + b.Property("ClientId") + .HasColumnType("uuid"); + + b.Property("ClientName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("InvoiceId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("InvoiceNumber") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PlanName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("Total") + .HasColumnType("numeric"); + + b.Property("UsedSeats") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderInvoiceItem", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AllocatedSeats") + .HasColumnType("integer"); + + b.Property("PlanType") + .HasColumnType("smallint"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("PurchasedSeats") + .HasColumnType("integer"); + + b.Property("SeatMinimum") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("Id", "PlanType") + .IsUnique(); + + b.ToTable("ProviderPlan", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationApplication", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Applications") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContentEncryptionKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationApplication", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationReport", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationAtRiskCount") + .HasColumnType("integer"); + + b.Property("ApplicationCount") + .HasColumnType("integer"); + + b.Property("ApplicationData") + .HasColumnType("text"); + + b.Property("ContentEncryptionKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("CriticalApplicationAtRiskCount") + .HasColumnType("integer"); + + b.Property("CriticalApplicationCount") + .HasColumnType("integer"); + + b.Property("CriticalMemberAtRiskCount") + .HasColumnType("integer"); + + b.Property("CriticalMemberCount") + .HasColumnType("integer"); + + b.Property("CriticalPasswordAtRiskCount") + .HasColumnType("integer"); + + b.Property("CriticalPasswordCount") + .HasColumnType("integer"); + + b.Property("MemberAtRiskCount") + .HasColumnType("integer"); + + b.Property("MemberCount") + .HasColumnType("integer"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("PasswordAtRiskCount") + .HasColumnType("integer"); + + b.Property("PasswordCount") + .HasColumnType("integer"); + + b.Property("ReportData") + .IsRequired() + .HasColumnType("text"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("SummaryData") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationReport", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.PasswordHealthReportApplication", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Uri") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("PasswordHealthReportApplication", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Cache", b => + { + b.Property("Id") + .HasMaxLength(449) + .HasColumnType("character varying(449)"); + + b.Property("AbsoluteExpiration") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAtTime") + .HasColumnType("timestamp with time zone"); + + b.Property("SlidingExpirationInSeconds") + .HasColumnType("bigint"); + + b.Property("Value") + .IsRequired() + .HasColumnType("bytea"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpiresAtTime") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Cache", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("DefaultUserCollectionEmail") + .HasColumnType("text"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Collection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.Property("CollectionId") + .HasColumnType("uuid"); + + b.Property("CipherId") + .HasColumnType("uuid"); + + b.HasKey("CollectionId", "CipherId"); + + b.HasIndex("CipherId"); + + b.ToTable("CollectionCipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionGroup", b => + { + b.Property("CollectionId") + .HasColumnType("uuid"); + + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("HidePasswords") + .HasColumnType("boolean"); + + b.Property("Manage") + .HasColumnType("boolean"); + + b.Property("ReadOnly") + .HasColumnType("boolean"); + + b.HasKey("CollectionId", "GroupId"); + + b.HasIndex("GroupId"); + + b.ToTable("CollectionGroups"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.Property("CollectionId") + .HasColumnType("uuid"); + + b.Property("OrganizationUserId") + .HasColumnType("uuid"); + + b.Property("HidePasswords") + .HasColumnType("boolean"); + + b.Property("Manage") + .HasColumnType("boolean"); + + b.Property("ReadOnly") + .HasColumnType("boolean"); + + b.HasKey("CollectionId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Active") + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("EncryptedPrivateKey") + .HasColumnType("text"); + + b.Property("EncryptedPublicKey") + .HasColumnType("text"); + + b.Property("EncryptedUserKey") + .HasColumnType("text"); + + b.Property("Identifier") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PushToken") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("Identifier") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "Identifier") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Device", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Event", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ActingUserId") + .HasColumnType("uuid"); + + b.Property("CipherId") + .HasColumnType("uuid"); + + b.Property("CollectionId") + .HasColumnType("uuid"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("DeviceType") + .HasColumnType("smallint"); + + b.Property("DomainName") + .HasColumnType("text"); + + b.Property("GrantedServiceAccountId") + .HasColumnType("uuid"); + + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("InstallationId") + .HasColumnType("uuid"); + + b.Property("IpAddress") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("OrganizationUserId") + .HasColumnType("uuid"); + + b.Property("PolicyId") + .HasColumnType("uuid"); + + b.Property("ProjectId") + .HasColumnType("uuid"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("ProviderOrganizationId") + .HasColumnType("uuid"); + + b.Property("ProviderUserId") + .HasColumnType("uuid"); + + b.Property("SecretId") + .HasColumnType("uuid"); + + b.Property("ServiceAccountId") + .HasColumnType("uuid"); + + b.Property("SystemUser") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("Date", "OrganizationId", "ActingUserId", "CipherId") + .HasDatabaseName("IX_Event_DateOrganizationIdUserId") + .HasAnnotation("SqlServer:Clustered", false) + .HasAnnotation("SqlServer:Include", new[] { "ServiceAccountId", "GrantedServiceAccountId" }); + + b.ToTable("Event", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Group", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("OrganizationUserId") + .HasColumnType("uuid"); + + b.HasKey("GroupId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("GroupUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Config") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationConnection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("DomainName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("JobRunCount") + .HasColumnType("integer"); + + b.Property("LastCheckedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("NextRunDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Txt") + .IsRequired() + .HasColumnType("text"); + + b.Property("VerifiedDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationDomain", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("FriendlyName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAdminInitiated") + .HasColumnType("boolean"); + + b.Property("LastSyncDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("OfferedToEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PlanSponsorshipType") + .HasColumnType("smallint"); + + b.Property("SponsoredOrganizationId") + .HasColumnType("uuid"); + + b.Property("SponsoringOrganizationId") + .HasColumnType("uuid"); + + b.Property("SponsoringOrganizationUserId") + .HasColumnType("uuid"); + + b.Property("ToDelete") + .HasColumnType("boolean"); + + b.Property("ValidUntil") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("SponsoredOrganizationId"); + + b.HasIndex("SponsoringOrganizationId"); + + b.HasIndex("SponsoringOrganizationUserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationSponsorship", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccessSecretsManager") + .HasColumnType("boolean"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Permissions") + .HasColumnType("text"); + + b.Property("ResetPasswordKey") + .HasColumnType("text"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccessCount") + .HasColumnType("integer"); + + b.Property("CipherId") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("DeletionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Disabled") + .HasColumnType("boolean"); + + b.Property("Emails") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("HideEmail") + .HasColumnType("boolean"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("MaxAccessCount") + .HasColumnType("integer"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Password") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("DeletionDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Send", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.TaxRate", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("Country") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PostalCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("Rate") + .HasColumnType("numeric"); + + b.Property("State") + .HasMaxLength(2) + .HasColumnType("character varying(2)"); + + b.HasKey("Id"); + + b.ToTable("TaxRate", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Amount") + .HasColumnType("numeric"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Details") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Gateway") + .HasColumnType("smallint"); + + b.Property("GatewayId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("PaymentMethodType") + .HasColumnType("smallint"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("Refunded") + .HasColumnType("boolean"); + + b.Property("RefundedAmount") + .HasColumnType("numeric"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId", "CreationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Transaction", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccountRevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("AvatarColor") + .HasMaxLength(7) + .HasColumnType("character varying(7)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Culture") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .UseCollation("postgresIndetermanisticCollation"); + + b.Property("EmailVerified") + .HasColumnType("boolean"); + + b.Property("EquivalentDomains") + .HasColumnType("text"); + + b.Property("ExcludedGlobalEquivalentDomains") + .HasColumnType("text"); + + b.Property("FailedLoginCount") + .HasColumnType("integer"); + + b.Property("ForcePasswordReset") + .HasColumnType("boolean"); + + b.Property("Gateway") + .HasColumnType("smallint"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Kdf") + .HasColumnType("smallint"); + + b.Property("KdfIterations") + .HasColumnType("integer"); + + b.Property("KdfMemory") + .HasColumnType("integer"); + + b.Property("KdfParallelism") + .HasColumnType("integer"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("LastEmailChangeDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastFailedLoginDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastKdfChangeDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastKeyRotationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastPasswordChangeDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("MasterPassword") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("MasterPasswordHint") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("MaxStorageGbIncreased") + .HasColumnType("smallint"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Premium") + .HasColumnType("boolean"); + + b.Property("PremiumExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("PrivateKey") + .HasColumnType("text"); + + b.Property("PublicKey") + .HasColumnType("text"); + + b.Property("ReferenceData") + .HasColumnType("text"); + + b.Property("RenewalReminderDate") + .HasColumnType("timestamp with time zone"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("SecurityStamp") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("SecurityState") + .HasColumnType("text"); + + b.Property("SecurityVersion") + .HasColumnType("integer"); + + b.Property("SignedPublicKey") + .HasColumnType("text"); + + b.Property("Storage") + .HasColumnType("bigint"); + + b.Property("TwoFactorProviders") + .HasColumnType("text"); + + b.Property("TwoFactorRecoveryCode") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("UsesKeyConnector") + .HasColumnType("boolean"); + + b.Property("VerifyDevices") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Premium", "PremiumExpirationDate", "RenewalReminderDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("User", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.UserSignatureKeyPair", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("SignatureAlgorithm") + .HasColumnType("smallint"); + + b.Property("SigningKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("VerifyingKey") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("UserSignatureKeyPair", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Body") + .HasMaxLength(3000) + .HasColumnType("character varying(3000)"); + + b.Property("ClientType") + .HasColumnType("smallint"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Global") + .HasColumnType("boolean"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Priority") + .HasColumnType("smallint"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("TaskId") + .HasColumnType("uuid"); + + b.Property("Title") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("TaskId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("ClientType", "Global", "UserId", "OrganizationId", "Priority", "CreationDate") + .IsDescending(false, false, false, false, true, true) + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Notification", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("NotificationId") + .HasColumnType("uuid"); + + b.Property("DeletedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ReadDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("UserId", "NotificationId") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("NotificationId"); + + b.ToTable("NotificationStatus", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Platform.Installation", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("character varying(150)"); + + b.Property("LastActivityDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Installation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Discriminator") + .IsRequired() + .HasMaxLength(34) + .HasColumnType("character varying(34)"); + + b.Property("Read") + .HasColumnType("boolean"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Write") + .HasColumnType("boolean"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.ToTable("AccessPolicy", (string)null); + + b.HasDiscriminator().HasValue("AccessPolicy"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ClientSecretHash") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("EncryptedPayload") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("ExpireAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("ServiceAccountId") + .HasColumnType("uuid"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ServiceAccountId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Project", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Secret", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.SecretVersion", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("EditorOrganizationUserId") + .HasColumnType("uuid"); + + b.Property("EditorServiceAccountId") + .HasColumnType("uuid"); + + b.Property("SecretId") + .HasColumnType("uuid"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b.Property("VersionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("EditorOrganizationUserId") + .HasDatabaseName("IX_SecretVersion_EditorOrganizationUserId"); + + b.HasIndex("EditorServiceAccountId") + .HasDatabaseName("IX_SecretVersion_EditorServiceAccountId"); + + b.HasIndex("SecretId") + .HasDatabaseName("IX_SecretVersion_SecretId"); + + b.ToTable("SecretVersion"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ServiceAccount", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ArchivedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Attachments") + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("DeletedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Favorites") + .HasColumnType("text"); + + b.Property("Folders") + .HasColumnType("text"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Reprompt") + .HasColumnType("smallint"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.ToTable("Cipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Folder", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CipherId") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SecurityTask", (string)null); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.Property("ProjectsId") + .HasColumnType("uuid"); + + b.Property("SecretsId") + .HasColumnType("uuid"); + + b.HasKey("ProjectsId", "SecretsId"); + + b.HasIndex("SecretsId"); + + b.ToTable("ProjectSecret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedProjectId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedSecretId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedProjectId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedSecretId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedProjectId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedSecretId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegrationConfiguration", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", "OrganizationIntegration") + .WithMany() + .HasForeignKey("OrganizationIntegrationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("OrganizationIntegration"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Policies") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Device", "ResponseDevice") + .WithMany() + .HasForeignKey("ResponseDeviceId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("ResponseDevice"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.EmergencyAccess", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantee") + .WithMany() + .HasForeignKey("GranteeId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantor") + .WithMany() + .HasForeignKey("GrantorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Grantee"); + + b.Navigation("Grantor"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoConfigs") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoUsers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("SsoUsers") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Platform.Installation", "Installation") + .WithMany() + .HasForeignKey("InstallationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Installation"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationApplication", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationReport", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.PasswordHealthReportApplication", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Collections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") + .WithMany("CollectionCiphers") + .HasForeignKey("CipherId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionCiphers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Collection"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionGroup", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionGroups") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionUsers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("CollectionUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Groups") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany("GroupUsers") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("GroupUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Group"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("ApiKeys") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Connections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Domains") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoredOrganization") + .WithMany() + .HasForeignKey("SponsoredOrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoringOrganization") + .WithMany() + .HasForeignKey("SponsoringOrganizationId"); + + b.Navigation("SponsoredOrganization"); + + b.Navigation("SponsoringOrganization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("OrganizationUsers") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("OrganizationUsers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Transactions") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Transactions") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.UserSignatureKeyPair", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", "Task") + .WithMany() + .HasForeignKey("TaskId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("Task"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", "Notification") + .WithMany() + .HasForeignKey("NotificationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Notification"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany("ApiKeys") + .HasForeignKey("ServiceAccountId"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.SecretVersion", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "EditorOrganizationUser") + .WithMany() + .HasForeignKey("EditorOrganizationUserId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "EditorServiceAccount") + .WithMany() + .HasForeignKey("EditorServiceAccountId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "Secret") + .WithMany("SecretVersions") + .HasForeignKey("SecretId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("EditorOrganizationUser"); + + b.Navigation("EditorServiceAccount"); + + b.Navigation("Secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Ciphers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Ciphers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Folders") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") + .WithMany() + .HasForeignKey("CipherId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", null) + .WithMany() + .HasForeignKey("ProjectsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", null) + .WithMany() + .HasForeignKey("SecretsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedProject"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedSecret"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "GrantedServiceAccount") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedServiceAccountId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedServiceAccount"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("ServiceAccountAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany("ProjectAccessPolicies") + .HasForeignKey("ServiceAccountId"); + + b.Navigation("GrantedProject"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("ServiceAccountAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany() + .HasForeignKey("ServiceAccountId"); + + b.Navigation("GrantedSecret"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedProject"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedSecret"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "GrantedServiceAccount") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedServiceAccountId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedServiceAccount"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("Ciphers"); + + b.Navigation("Collections"); + + b.Navigation("Connections"); + + b.Navigation("Domains"); + + b.Navigation("Groups"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("Policies"); + + b.Navigation("SsoConfigs"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.Navigation("CollectionCiphers"); + + b.Navigation("CollectionGroups"); + + b.Navigation("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Navigation("CollectionUsers"); + + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Navigation("Ciphers"); + + b.Navigation("Folders"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ServiceAccountAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Navigation("GroupAccessPolicies"); + + b.Navigation("SecretVersions"); + + b.Navigation("ServiceAccountAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ProjectAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Navigation("CollectionCiphers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/util/PostgresMigrations/Migrations/20251112155845_AddMaxStorageGbIncreasedColumn.cs b/util/PostgresMigrations/Migrations/20251112155845_AddMaxStorageGbIncreasedColumn.cs new file mode 100644 index 0000000000..0b742b9029 --- /dev/null +++ b/util/PostgresMigrations/Migrations/20251112155845_AddMaxStorageGbIncreasedColumn.cs @@ -0,0 +1,37 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.PostgresMigrations.Migrations; + +/// +public partial class AddMaxStorageGbIncreasedColumn : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "MaxStorageGbIncreased", + table: "User", + type: "smallint", + nullable: true); + + migrationBuilder.AddColumn( + name: "MaxStorageGbIncreased", + table: "Organization", + type: "smallint", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "MaxStorageGbIncreased", + table: "User"); + + migrationBuilder.DropColumn( + name: "MaxStorageGbIncreased", + table: "Organization"); + } +} diff --git a/util/RustSdk/rust/Cargo.lock b/util/RustSdk/rust/Cargo.lock index cba06d35ea..aff61935e4 100644 --- a/util/RustSdk/rust/Cargo.lock +++ b/util/RustSdk/rust/Cargo.lock @@ -23,7 +23,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" dependencies = [ - "crypto-common", + "crypto-common 0.1.6", "generic-array", ] @@ -65,9 +65,9 @@ dependencies = [ [[package]] name = "argon2" -version = "0.5.3" +version = "0.6.0-rc.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +checksum = "e1a213fe583d472f454ae47407edc78848bebd950493528b1d4f7327a7dc335f" dependencies = [ "base64ct", "blake2", @@ -135,7 +135,7 @@ checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" [[package]] name = "bitwarden-api-api" version = "1.0.0" -source = "git+https://github.com/bitwarden/sdk-internal.git?rev=1461b3ba6bb6e2d0114770eb4572a1398b4789ef#1461b3ba6bb6e2d0114770eb4572a1398b4789ef" +source = "git+https://github.com/bitwarden/sdk-internal.git?rev=7080159154a42b59028ccb9f5af62bf087e565f9#7080159154a42b59028ccb9f5af62bf087e565f9" dependencies = [ "async-trait", "reqwest", @@ -150,7 +150,7 @@ dependencies = [ [[package]] name = "bitwarden-api-identity" version = "1.0.0" -source = "git+https://github.com/bitwarden/sdk-internal.git?rev=1461b3ba6bb6e2d0114770eb4572a1398b4789ef#1461b3ba6bb6e2d0114770eb4572a1398b4789ef" +source = "git+https://github.com/bitwarden/sdk-internal.git?rev=7080159154a42b59028ccb9f5af62bf087e565f9#7080159154a42b59028ccb9f5af62bf087e565f9" dependencies = [ "async-trait", "reqwest", @@ -165,7 +165,7 @@ dependencies = [ [[package]] name = "bitwarden-core" version = "1.0.0" -source = "git+https://github.com/bitwarden/sdk-internal.git?rev=1461b3ba6bb6e2d0114770eb4572a1398b4789ef#1461b3ba6bb6e2d0114770eb4572a1398b4789ef" +source = "git+https://github.com/bitwarden/sdk-internal.git?rev=7080159154a42b59028ccb9f5af62bf087e565f9#7080159154a42b59028ccb9f5af62bf087e565f9" dependencies = [ "async-trait", "bitwarden-api-api", @@ -196,7 +196,7 @@ dependencies = [ [[package]] name = "bitwarden-crypto" version = "1.0.0" -source = "git+https://github.com/bitwarden/sdk-internal.git?rev=1461b3ba6bb6e2d0114770eb4572a1398b4789ef#1461b3ba6bb6e2d0114770eb4572a1398b4789ef" +source = "git+https://github.com/bitwarden/sdk-internal.git?rev=7080159154a42b59028ccb9f5af62bf087e565f9#7080159154a42b59028ccb9f5af62bf087e565f9" dependencies = [ "aes", "argon2", @@ -234,7 +234,7 @@ dependencies = [ [[package]] name = "bitwarden-encoding" version = "1.0.0" -source = "git+https://github.com/bitwarden/sdk-internal.git?rev=1461b3ba6bb6e2d0114770eb4572a1398b4789ef#1461b3ba6bb6e2d0114770eb4572a1398b4789ef" +source = "git+https://github.com/bitwarden/sdk-internal.git?rev=7080159154a42b59028ccb9f5af62bf087e565f9#7080159154a42b59028ccb9f5af62bf087e565f9" dependencies = [ "data-encoding", "data-encoding-macro", @@ -245,7 +245,7 @@ dependencies = [ [[package]] name = "bitwarden-error" version = "1.0.0" -source = "git+https://github.com/bitwarden/sdk-internal.git?rev=1461b3ba6bb6e2d0114770eb4572a1398b4789ef#1461b3ba6bb6e2d0114770eb4572a1398b4789ef" +source = "git+https://github.com/bitwarden/sdk-internal.git?rev=7080159154a42b59028ccb9f5af62bf087e565f9#7080159154a42b59028ccb9f5af62bf087e565f9" dependencies = [ "bitwarden-error-macro", ] @@ -253,7 +253,7 @@ dependencies = [ [[package]] name = "bitwarden-error-macro" version = "1.0.0" -source = "git+https://github.com/bitwarden/sdk-internal.git?rev=1461b3ba6bb6e2d0114770eb4572a1398b4789ef#1461b3ba6bb6e2d0114770eb4572a1398b4789ef" +source = "git+https://github.com/bitwarden/sdk-internal.git?rev=7080159154a42b59028ccb9f5af62bf087e565f9#7080159154a42b59028ccb9f5af62bf087e565f9" dependencies = [ "darling", "proc-macro2", @@ -264,7 +264,7 @@ dependencies = [ [[package]] name = "bitwarden-state" version = "1.0.0" -source = "git+https://github.com/bitwarden/sdk-internal.git?rev=1461b3ba6bb6e2d0114770eb4572a1398b4789ef#1461b3ba6bb6e2d0114770eb4572a1398b4789ef" +source = "git+https://github.com/bitwarden/sdk-internal.git?rev=7080159154a42b59028ccb9f5af62bf087e565f9#7080159154a42b59028ccb9f5af62bf087e565f9" dependencies = [ "async-trait", "bitwarden-error", @@ -282,7 +282,7 @@ dependencies = [ [[package]] name = "bitwarden-threading" version = "1.0.0" -source = "git+https://github.com/bitwarden/sdk-internal.git?rev=1461b3ba6bb6e2d0114770eb4572a1398b4789ef#1461b3ba6bb6e2d0114770eb4572a1398b4789ef" +source = "git+https://github.com/bitwarden/sdk-internal.git?rev=7080159154a42b59028ccb9f5af62bf087e565f9#7080159154a42b59028ccb9f5af62bf087e565f9" dependencies = [ "bitwarden-error", "log", @@ -295,7 +295,7 @@ dependencies = [ [[package]] name = "bitwarden-uuid" version = "1.0.0" -source = "git+https://github.com/bitwarden/sdk-internal.git?rev=1461b3ba6bb6e2d0114770eb4572a1398b4789ef#1461b3ba6bb6e2d0114770eb4572a1398b4789ef" +source = "git+https://github.com/bitwarden/sdk-internal.git?rev=7080159154a42b59028ccb9f5af62bf087e565f9#7080159154a42b59028ccb9f5af62bf087e565f9" dependencies = [ "bitwarden-uuid-macro", ] @@ -303,7 +303,7 @@ dependencies = [ [[package]] name = "bitwarden-uuid-macro" version = "1.0.0" -source = "git+https://github.com/bitwarden/sdk-internal.git?rev=1461b3ba6bb6e2d0114770eb4572a1398b4789ef#1461b3ba6bb6e2d0114770eb4572a1398b4789ef" +source = "git+https://github.com/bitwarden/sdk-internal.git?rev=7080159154a42b59028ccb9f5af62bf087e565f9#7080159154a42b59028ccb9f5af62bf087e565f9" dependencies = [ "quote", "syn", @@ -311,11 +311,11 @@ dependencies = [ [[package]] name = "blake2" -version = "0.10.6" +version = "0.11.0-rc.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +checksum = "679065eb2b85a078ace42411e657bef3a6afe93a40d1b9cb04e39ca303cc3f36" dependencies = [ - "digest", + "digest 0.11.0-rc.4", ] [[package]] @@ -327,6 +327,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-buffer" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96eb4cdd6cf1b31d671e9efe75c5d1ec614776856cefbe109ca373554a6d514f" +dependencies = [ + "hybrid-array", +] + [[package]] name = "block-padding" version = "0.3.3" @@ -460,7 +469,7 @@ version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ - "crypto-common", + "crypto-common 0.1.6", "inout", "zeroize", ] @@ -558,6 +567,15 @@ dependencies = [ "typenum", ] +[[package]] +name = "crypto-common" +version = "0.2.0-rc.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "919bd05924682a5480aec713596b9e2aabed3a0a6022fab6847f85a99e5f190a" +dependencies = [ + "hybrid-array", +] + [[package]] name = "csbindgen" version = "1.9.3" @@ -577,7 +595,7 @@ dependencies = [ "cfg-if", "cpufeatures", "curve25519-dalek-derive", - "digest", + "digest 0.10.7", "fiat-crypto", "rustc_version", "subtle", @@ -683,9 +701,20 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer", + "block-buffer 0.10.4", "const-oid", - "crypto-common", + "crypto-common 0.1.6", + "subtle", +] + +[[package]] +name = "digest" +version = "0.11.0-rc.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea390c940e465846d64775e55e3115d5dc934acb953de6f6e6360bc232fe2bf7" +dependencies = [ + "block-buffer 0.11.0", + "crypto-common 0.2.0-rc.5", "subtle", ] @@ -952,7 +981,7 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest", + "digest 0.10.7", ] [[package]] @@ -995,6 +1024,15 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" +[[package]] +name = "hybrid-array" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f471e0a81b2f90ffc0cb2f951ae04da57de8baa46fa99112b062a5173a5088d0" +dependencies = [ + "typenum", +] + [[package]] name = "hyper" version = "1.6.0" @@ -1476,12 +1514,12 @@ checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "password-hash" -version = "0.5.0" +version = "0.6.0-rc.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +checksum = "a7d47a2d1aee5a339aa6c740d9128211a8a3d2bdf06a13e01b3f8a0b5c49b9db" dependencies = [ "base64ct", - "rand_core 0.6.4", + "rand_core 0.10.0-rc-2", "subtle", ] @@ -1491,7 +1529,7 @@ version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" dependencies = [ - "digest", + "digest 0.10.7", ] [[package]] @@ -1721,6 +1759,12 @@ dependencies = [ "getrandom 0.3.3", ] +[[package]] +name = "rand_core" +version = "0.10.0-rc-2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "104a23e4e8b77312a823b6b5613edbac78397e2f34320bc7ac4277013ec4478e" + [[package]] name = "rayon" version = "1.10.0" @@ -1851,7 +1895,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b" dependencies = [ "const-oid", - "digest", + "digest 0.10.7", "num-bigint-dig", "num-integer", "num-traits", @@ -2225,7 +2269,7 @@ checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", "cpufeatures", - "digest", + "digest 0.10.7", ] [[package]] @@ -2236,7 +2280,7 @@ checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", "cpufeatures", - "digest", + "digest 0.10.7", ] [[package]] @@ -2251,7 +2295,7 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ - "digest", + "digest 0.10.7", "rand_core 0.6.4", ] @@ -2606,7 +2650,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" dependencies = [ - "crypto-common", + "crypto-common 0.1.6", "subtle", ] diff --git a/util/RustSdk/rust/Cargo.toml b/util/RustSdk/rust/Cargo.toml index 88521277f3..65b0d42e5f 100644 --- a/util/RustSdk/rust/Cargo.toml +++ b/util/RustSdk/rust/Cargo.toml @@ -13,8 +13,8 @@ crate-type = ["cdylib"] [dependencies] base64 = "0.22.1" -bitwarden-core = { git = "https://github.com/bitwarden/sdk-internal.git", rev = "1461b3ba6bb6e2d0114770eb4572a1398b4789ef" } -bitwarden-crypto = { git = "https://github.com/bitwarden/sdk-internal.git", rev = "1461b3ba6bb6e2d0114770eb4572a1398b4789ef" } +bitwarden-core = { git = "https://github.com/bitwarden/sdk-internal.git", rev = "7080159154a42b59028ccb9f5af62bf087e565f9" } +bitwarden-crypto = { git = "https://github.com/bitwarden/sdk-internal.git", rev = "7080159154a42b59028ccb9f5af62bf087e565f9" } serde = "=1.0.219" serde_json = "=1.0.141" diff --git a/util/SqliteMigrations/Migrations/20251112155857_AddMaxStorageGbIncreasedColumn.Designer.cs b/util/SqliteMigrations/Migrations/20251112155857_AddMaxStorageGbIncreasedColumn.Designer.cs new file mode 100644 index 0000000000..28726db203 --- /dev/null +++ b/util/SqliteMigrations/Migrations/20251112155857_AddMaxStorageGbIncreasedColumn.Designer.cs @@ -0,0 +1,3435 @@ +// +using System; +using Bit.Infrastructure.EntityFramework.Repositories; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Bit.SqliteMigrations.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20251112155857_AddMaxStorageGbIncreasedColumn")] + partial class AddMaxStorageGbIncreasedColumn + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.8"); + + modelBuilder.Entity("Bit.Core.Dirt.Reports.Models.Data.OrganizationMemberBaseDetail", b => + { + b.Property("CipherId") + .HasColumnType("TEXT"); + + b.Property("CollectionId") + .HasColumnType("TEXT"); + + b.Property("CollectionName") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasColumnType("TEXT"); + + b.Property("GroupId") + .HasColumnType("TEXT"); + + b.Property("GroupName") + .HasColumnType("TEXT"); + + b.Property("HidePasswords") + .HasColumnType("INTEGER"); + + b.Property("Manage") + .HasColumnType("INTEGER"); + + b.Property("ReadOnly") + .HasColumnType("INTEGER"); + + b.Property("ResetPasswordKey") + .HasColumnType("TEXT"); + + b.Property("TwoFactorProviders") + .HasColumnType("TEXT"); + + b.Property("UserGuid") + .HasColumnType("TEXT"); + + b.Property("UserName") + .HasColumnType("TEXT"); + + b.Property("UsesKeyConnector") + .HasColumnType("INTEGER"); + + b.ToTable("OrganizationMemberBaseDetails"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AllowAdminAccessToAllCollectionItems") + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("BillingEmail") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("BusinessAddress1") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("BusinessAddress2") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("BusinessAddress3") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("BusinessCountry") + .HasMaxLength(2) + .HasColumnType("TEXT"); + + b.Property("BusinessName") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("BusinessTaxNumber") + .HasMaxLength(30) + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("ExpirationDate") + .HasColumnType("TEXT"); + + b.Property("Gateway") + .HasColumnType("INTEGER"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Identifier") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LimitCollectionCreation") + .HasColumnType("INTEGER"); + + b.Property("LimitCollectionDeletion") + .HasColumnType("INTEGER"); + + b.Property("LimitItemDeletion") + .HasColumnType("INTEGER"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("INTEGER"); + + b.Property("MaxAutoscaleSmSeats") + .HasColumnType("INTEGER"); + + b.Property("MaxAutoscaleSmServiceAccounts") + .HasColumnType("INTEGER"); + + b.Property("MaxCollections") + .HasColumnType("INTEGER"); + + b.Property("MaxStorageGb") + .HasColumnType("INTEGER"); + + b.Property("MaxStorageGbIncreased") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("OwnersNotifiedOfAutoscaling") + .HasColumnType("TEXT"); + + b.Property("Plan") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PlanType") + .HasColumnType("INTEGER"); + + b.Property("PrivateKey") + .HasColumnType("TEXT"); + + b.Property("PublicKey") + .HasColumnType("TEXT"); + + b.Property("ReferenceData") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Seats") + .HasColumnType("INTEGER"); + + b.Property("SelfHost") + .HasColumnType("INTEGER"); + + b.Property("SmSeats") + .HasColumnType("INTEGER"); + + b.Property("SmServiceAccounts") + .HasColumnType("INTEGER"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Storage") + .HasColumnType("INTEGER"); + + b.Property("SyncSeats") + .HasColumnType("INTEGER"); + + b.Property("TwoFactorProviders") + .HasColumnType("TEXT"); + + b.Property("Use2fa") + .HasColumnType("INTEGER"); + + b.Property("UseAdminSponsoredFamilies") + .HasColumnType("INTEGER"); + + b.Property("UseApi") + .HasColumnType("INTEGER"); + + b.Property("UseAutomaticUserConfirmation") + .HasColumnType("INTEGER"); + + b.Property("UseCustomPermissions") + .HasColumnType("INTEGER"); + + b.Property("UseDirectory") + .HasColumnType("INTEGER"); + + b.Property("UseEvents") + .HasColumnType("INTEGER"); + + b.Property("UseGroups") + .HasColumnType("INTEGER"); + + b.Property("UseKeyConnector") + .HasColumnType("INTEGER"); + + b.Property("UseOrganizationDomains") + .HasColumnType("INTEGER"); + + b.Property("UsePasswordManager") + .HasColumnType("INTEGER"); + + b.Property("UsePolicies") + .HasColumnType("INTEGER"); + + b.Property("UseResetPassword") + .HasColumnType("INTEGER"); + + b.Property("UseRiskInsights") + .HasColumnType("INTEGER"); + + b.Property("UseScim") + .HasColumnType("INTEGER"); + + b.Property("UseSecretsManager") + .HasColumnType("INTEGER"); + + b.Property("UseSso") + .HasColumnType("INTEGER"); + + b.Property("UseTotp") + .HasColumnType("INTEGER"); + + b.Property("UsersGetPremium") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Id", "Enabled") + .HasAnnotation("Npgsql:IndexInclude", new[] { "UseTotp" }); + + b.ToTable("Organization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Configuration") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "Type") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationIntegration", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegrationConfiguration", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Configuration") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("EventType") + .HasColumnType("INTEGER"); + + b.Property("Filters") + .HasColumnType("TEXT"); + + b.Property("OrganizationIntegrationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Template") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationIntegrationId"); + + b.ToTable("OrganizationIntegrationConfiguration", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Data") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "Type") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Policy", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("BillingEmail") + .HasColumnType("TEXT"); + + b.Property("BillingPhone") + .HasColumnType("TEXT"); + + b.Property("BusinessAddress1") + .HasColumnType("TEXT"); + + b.Property("BusinessAddress2") + .HasColumnType("TEXT"); + + b.Property("BusinessAddress3") + .HasColumnType("TEXT"); + + b.Property("BusinessCountry") + .HasColumnType("TEXT"); + + b.Property("BusinessName") + .HasColumnType("TEXT"); + + b.Property("BusinessTaxNumber") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("DiscountId") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("Gateway") + .HasColumnType("INTEGER"); + + b.Property("GatewayCustomerId") + .HasColumnType("TEXT"); + + b.Property("GatewaySubscriptionId") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UseEvents") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Provider", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Settings") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderOrganization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("Permissions") + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId"); + + b.ToTable("ProviderUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessCode") + .HasMaxLength(25) + .HasColumnType("TEXT"); + + b.Property("Approved") + .HasColumnType("INTEGER"); + + b.Property("AuthenticationDate") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("MasterPasswordHash") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PublicKey") + .HasColumnType("TEXT"); + + b.Property("RequestCountryName") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("RequestDeviceIdentifier") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("RequestDeviceType") + .HasColumnType("INTEGER"); + + b.Property("RequestIpAddress") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("ResponseDate") + .HasColumnType("TEXT"); + + b.Property("ResponseDeviceId") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ResponseDeviceId"); + + b.HasIndex("UserId"); + + b.ToTable("AuthRequest", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.EmergencyAccess", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("GranteeId") + .HasColumnType("TEXT"); + + b.Property("GrantorId") + .HasColumnType("TEXT"); + + b.Property("KeyEncrypted") + .HasColumnType("TEXT"); + + b.Property("LastNotificationDate") + .HasColumnType("TEXT"); + + b.Property("RecoveryInitiatedDate") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("WaitTimeDays") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("GranteeId"); + + b.HasIndex("GrantorId"); + + b.ToTable("EmergencyAccess", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.Grant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("ConsumedDate") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Data") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("ExpirationDate") + .HasColumnType("TEXT"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("SessionId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("SubjectId") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasName("PK_Grant") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpirationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("Grant", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Data") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("SsoConfig", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId"); + + b.HasIndex("OrganizationId", "ExternalId") + .IsUnique() + .HasAnnotation("Npgsql:IndexInclude", new[] { "UserId" }) + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "UserId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SsoUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AaGuid") + .HasColumnType("TEXT"); + + b.Property("Counter") + .HasColumnType("INTEGER"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("CredentialId") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EncryptedPrivateKey") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("EncryptedPublicKey") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("EncryptedUserKey") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PublicKey") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("SupportsPrf") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("WebAuthnCredential", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ClientOrganizationMigrationRecord", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ExpirationDate") + .HasColumnType("TEXT"); + + b.Property("GatewayCustomerId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("GatewaySubscriptionId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("INTEGER"); + + b.Property("MaxStorageGb") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PlanType") + .HasColumnType("INTEGER"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("Seats") + .HasColumnType("INTEGER"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId", "OrganizationId") + .IsUnique(); + + b.ToTable("ClientOrganizationMigrationRecord", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("InstallationId") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("InstallationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationInstallation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AssignedSeats") + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("TEXT"); + + b.Property("ClientName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("InvoiceId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("InvoiceNumber") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PlanName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("Total") + .HasColumnType("TEXT"); + + b.Property("UsedSeats") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderInvoiceItem", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AllocatedSeats") + .HasColumnType("INTEGER"); + + b.Property("PlanType") + .HasColumnType("INTEGER"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("PurchasedSeats") + .HasColumnType("INTEGER"); + + b.Property("SeatMinimum") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("Id", "PlanType") + .IsUnique(); + + b.ToTable("ProviderPlan", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationApplication", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Applications") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ContentEncryptionKey") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationApplication", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationReport", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ApplicationAtRiskCount") + .HasColumnType("INTEGER"); + + b.Property("ApplicationCount") + .HasColumnType("INTEGER"); + + b.Property("ApplicationData") + .HasColumnType("TEXT"); + + b.Property("ContentEncryptionKey") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("CriticalApplicationAtRiskCount") + .HasColumnType("INTEGER"); + + b.Property("CriticalApplicationCount") + .HasColumnType("INTEGER"); + + b.Property("CriticalMemberAtRiskCount") + .HasColumnType("INTEGER"); + + b.Property("CriticalMemberCount") + .HasColumnType("INTEGER"); + + b.Property("CriticalPasswordAtRiskCount") + .HasColumnType("INTEGER"); + + b.Property("CriticalPasswordCount") + .HasColumnType("INTEGER"); + + b.Property("MemberAtRiskCount") + .HasColumnType("INTEGER"); + + b.Property("MemberCount") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PasswordAtRiskCount") + .HasColumnType("INTEGER"); + + b.Property("PasswordCount") + .HasColumnType("INTEGER"); + + b.Property("ReportData") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("SummaryData") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationReport", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.PasswordHealthReportApplication", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Uri") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("PasswordHealthReportApplication", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Cache", b => + { + b.Property("Id") + .HasMaxLength(449) + .HasColumnType("TEXT"); + + b.Property("AbsoluteExpiration") + .HasColumnType("TEXT"); + + b.Property("ExpiresAtTime") + .HasColumnType("TEXT"); + + b.Property("SlidingExpirationInSeconds") + .HasColumnType("INTEGER"); + + b.Property("Value") + .IsRequired() + .HasColumnType("BLOB"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpiresAtTime") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Cache", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("DefaultUserCollectionEmail") + .HasColumnType("TEXT"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Collection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.Property("CollectionId") + .HasColumnType("TEXT"); + + b.Property("CipherId") + .HasColumnType("TEXT"); + + b.HasKey("CollectionId", "CipherId"); + + b.HasIndex("CipherId"); + + b.ToTable("CollectionCipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionGroup", b => + { + b.Property("CollectionId") + .HasColumnType("TEXT"); + + b.Property("GroupId") + .HasColumnType("TEXT"); + + b.Property("HidePasswords") + .HasColumnType("INTEGER"); + + b.Property("Manage") + .HasColumnType("INTEGER"); + + b.Property("ReadOnly") + .HasColumnType("INTEGER"); + + b.HasKey("CollectionId", "GroupId"); + + b.HasIndex("GroupId"); + + b.ToTable("CollectionGroups"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.Property("CollectionId") + .HasColumnType("TEXT"); + + b.Property("OrganizationUserId") + .HasColumnType("TEXT"); + + b.Property("HidePasswords") + .HasColumnType("INTEGER"); + + b.Property("Manage") + .HasColumnType("INTEGER"); + + b.Property("ReadOnly") + .HasColumnType("INTEGER"); + + b.HasKey("CollectionId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Active") + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("EncryptedPrivateKey") + .HasColumnType("TEXT"); + + b.Property("EncryptedPublicKey") + .HasColumnType("TEXT"); + + b.Property("EncryptedUserKey") + .HasColumnType("TEXT"); + + b.Property("Identifier") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PushToken") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Identifier") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "Identifier") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Device", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Event", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ActingUserId") + .HasColumnType("TEXT"); + + b.Property("CipherId") + .HasColumnType("TEXT"); + + b.Property("CollectionId") + .HasColumnType("TEXT"); + + b.Property("Date") + .HasColumnType("TEXT"); + + b.Property("DeviceType") + .HasColumnType("INTEGER"); + + b.Property("DomainName") + .HasColumnType("TEXT"); + + b.Property("GrantedServiceAccountId") + .HasColumnType("TEXT"); + + b.Property("GroupId") + .HasColumnType("TEXT"); + + b.Property("InstallationId") + .HasColumnType("TEXT"); + + b.Property("IpAddress") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("OrganizationUserId") + .HasColumnType("TEXT"); + + b.Property("PolicyId") + .HasColumnType("TEXT"); + + b.Property("ProjectId") + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("ProviderOrganizationId") + .HasColumnType("TEXT"); + + b.Property("ProviderUserId") + .HasColumnType("TEXT"); + + b.Property("SecretId") + .HasColumnType("TEXT"); + + b.Property("ServiceAccountId") + .HasColumnType("TEXT"); + + b.Property("SystemUser") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("Date", "OrganizationId", "ActingUserId", "CipherId") + .HasDatabaseName("IX_Event_DateOrganizationIdUserId") + .HasAnnotation("SqlServer:Clustered", false) + .HasAnnotation("SqlServer:Include", new[] { "ServiceAccountId", "GrantedServiceAccountId" }); + + b.ToTable("Event", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Group", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.Property("GroupId") + .HasColumnType("TEXT"); + + b.Property("OrganizationUserId") + .HasColumnType("TEXT"); + + b.HasKey("GroupId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("GroupUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Config") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationConnection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("DomainName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("JobRunCount") + .HasColumnType("INTEGER"); + + b.Property("LastCheckedDate") + .HasColumnType("TEXT"); + + b.Property("NextRunDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Txt") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("VerifiedDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationDomain", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("FriendlyName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("IsAdminInitiated") + .HasColumnType("INTEGER"); + + b.Property("LastSyncDate") + .HasColumnType("TEXT"); + + b.Property("Notes") + .HasColumnType("TEXT"); + + b.Property("OfferedToEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PlanSponsorshipType") + .HasColumnType("INTEGER"); + + b.Property("SponsoredOrganizationId") + .HasColumnType("TEXT"); + + b.Property("SponsoringOrganizationId") + .HasColumnType("TEXT"); + + b.Property("SponsoringOrganizationUserId") + .HasColumnType("TEXT"); + + b.Property("ToDelete") + .HasColumnType("INTEGER"); + + b.Property("ValidUntil") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("SponsoredOrganizationId"); + + b.HasIndex("SponsoringOrganizationId"); + + b.HasIndex("SponsoringOrganizationUserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationSponsorship", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessSecretsManager") + .HasColumnType("INTEGER"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Permissions") + .HasColumnType("TEXT"); + + b.Property("ResetPasswordKey") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessCount") + .HasColumnType("INTEGER"); + + b.Property("CipherId") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Data") + .HasColumnType("TEXT"); + + b.Property("DeletionDate") + .HasColumnType("TEXT"); + + b.Property("Disabled") + .HasColumnType("INTEGER"); + + b.Property("Emails") + .HasMaxLength(1024) + .HasColumnType("TEXT"); + + b.Property("ExpirationDate") + .HasColumnType("TEXT"); + + b.Property("HideEmail") + .HasColumnType("INTEGER"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("MaxAccessCount") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Password") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DeletionDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Send", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.TaxRate", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("Country") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PostalCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("TEXT"); + + b.Property("Rate") + .HasColumnType("TEXT"); + + b.Property("State") + .HasMaxLength(2) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("TaxRate", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Amount") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Details") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Gateway") + .HasColumnType("INTEGER"); + + b.Property("GatewayId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PaymentMethodType") + .HasColumnType("INTEGER"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("Refunded") + .HasColumnType("INTEGER"); + + b.Property("RefundedAmount") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId", "CreationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Transaction", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccountRevisionDate") + .HasColumnType("TEXT"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("TEXT"); + + b.Property("AvatarColor") + .HasMaxLength(7) + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Culture") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailVerified") + .HasColumnType("INTEGER"); + + b.Property("EquivalentDomains") + .HasColumnType("TEXT"); + + b.Property("ExcludedGlobalEquivalentDomains") + .HasColumnType("TEXT"); + + b.Property("FailedLoginCount") + .HasColumnType("INTEGER"); + + b.Property("ForcePasswordReset") + .HasColumnType("INTEGER"); + + b.Property("Gateway") + .HasColumnType("INTEGER"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Kdf") + .HasColumnType("INTEGER"); + + b.Property("KdfIterations") + .HasColumnType("INTEGER"); + + b.Property("KdfMemory") + .HasColumnType("INTEGER"); + + b.Property("KdfParallelism") + .HasColumnType("INTEGER"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("LastEmailChangeDate") + .HasColumnType("TEXT"); + + b.Property("LastFailedLoginDate") + .HasColumnType("TEXT"); + + b.Property("LastKdfChangeDate") + .HasColumnType("TEXT"); + + b.Property("LastKeyRotationDate") + .HasColumnType("TEXT"); + + b.Property("LastPasswordChangeDate") + .HasColumnType("TEXT"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("MasterPassword") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("MasterPasswordHint") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("MaxStorageGb") + .HasColumnType("INTEGER"); + + b.Property("MaxStorageGbIncreased") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Premium") + .HasColumnType("INTEGER"); + + b.Property("PremiumExpirationDate") + .HasColumnType("TEXT"); + + b.Property("PrivateKey") + .HasColumnType("TEXT"); + + b.Property("PublicKey") + .HasColumnType("TEXT"); + + b.Property("ReferenceData") + .HasColumnType("TEXT"); + + b.Property("RenewalReminderDate") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("SecurityStamp") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("SecurityState") + .HasColumnType("TEXT"); + + b.Property("SecurityVersion") + .HasColumnType("INTEGER"); + + b.Property("SignedPublicKey") + .HasColumnType("TEXT"); + + b.Property("Storage") + .HasColumnType("INTEGER"); + + b.Property("TwoFactorProviders") + .HasColumnType("TEXT"); + + b.Property("TwoFactorRecoveryCode") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("UsesKeyConnector") + .HasColumnType("INTEGER"); + + b.Property("VerifyDevices") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Premium", "PremiumExpirationDate", "RenewalReminderDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("User", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.UserSignatureKeyPair", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("SignatureAlgorithm") + .HasColumnType("INTEGER"); + + b.Property("SigningKey") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("VerifyingKey") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("UserSignatureKeyPair", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Body") + .HasMaxLength(3000) + .HasColumnType("TEXT"); + + b.Property("ClientType") + .HasColumnType("INTEGER"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Global") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Priority") + .HasColumnType("INTEGER"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("TaskId") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("TaskId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("ClientType", "Global", "UserId", "OrganizationId", "Priority", "CreationDate") + .IsDescending(false, false, false, false, true, true) + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Notification", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("NotificationId") + .HasColumnType("TEXT"); + + b.Property("DeletedDate") + .HasColumnType("TEXT"); + + b.Property("ReadDate") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "NotificationId") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("NotificationId"); + + b.ToTable("NotificationStatus", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Platform.Installation", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("TEXT"); + + b.Property("LastActivityDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Installation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Discriminator") + .IsRequired() + .HasMaxLength(34) + .HasColumnType("TEXT"); + + b.Property("Read") + .HasColumnType("INTEGER"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Write") + .HasColumnType("INTEGER"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.ToTable("AccessPolicy", (string)null); + + b.HasDiscriminator().HasValue("AccessPolicy"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ClientSecretHash") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("EncryptedPayload") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("TEXT"); + + b.Property("ExpireAt") + .HasColumnType("TEXT"); + + b.Property("Key") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("TEXT"); + + b.Property("ServiceAccountId") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ServiceAccountId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("DeletedDate") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Project", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("DeletedDate") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("Note") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Secret", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.SecretVersion", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("EditorOrganizationUserId") + .HasColumnType("TEXT"); + + b.Property("EditorServiceAccountId") + .HasColumnType("TEXT"); + + b.Property("SecretId") + .HasColumnType("TEXT"); + + b.Property("Value") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("VersionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("EditorOrganizationUserId") + .HasDatabaseName("IX_SecretVersion_EditorOrganizationUserId"); + + b.HasIndex("EditorServiceAccountId") + .HasDatabaseName("IX_SecretVersion_EditorServiceAccountId"); + + b.HasIndex("SecretId") + .HasDatabaseName("IX_SecretVersion_SecretId"); + + b.ToTable("SecretVersion"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ServiceAccount", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ArchivedDate") + .HasColumnType("TEXT"); + + b.Property("Attachments") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Data") + .HasColumnType("TEXT"); + + b.Property("DeletedDate") + .HasColumnType("TEXT"); + + b.Property("Favorites") + .HasColumnType("TEXT"); + + b.Property("Folders") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Reprompt") + .HasColumnType("INTEGER"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.ToTable("Cipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Folder", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CipherId") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SecurityTask", (string)null); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.Property("ProjectsId") + .HasColumnType("TEXT"); + + b.Property("SecretsId") + .HasColumnType("TEXT"); + + b.HasKey("ProjectsId", "SecretsId"); + + b.HasIndex("SecretsId"); + + b.ToTable("ProjectSecret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedProjectId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedSecretId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedProjectId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedSecretId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedProjectId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedSecretId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegrationConfiguration", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", "OrganizationIntegration") + .WithMany() + .HasForeignKey("OrganizationIntegrationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("OrganizationIntegration"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Policies") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Device", "ResponseDevice") + .WithMany() + .HasForeignKey("ResponseDeviceId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("ResponseDevice"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.EmergencyAccess", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantee") + .WithMany() + .HasForeignKey("GranteeId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantor") + .WithMany() + .HasForeignKey("GrantorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Grantee"); + + b.Navigation("Grantor"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoConfigs") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoUsers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("SsoUsers") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Platform.Installation", "Installation") + .WithMany() + .HasForeignKey("InstallationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Installation"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationApplication", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationReport", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.PasswordHealthReportApplication", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Collections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") + .WithMany("CollectionCiphers") + .HasForeignKey("CipherId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionCiphers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Collection"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionGroup", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionGroups") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionUsers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("CollectionUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Groups") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany("GroupUsers") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("GroupUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Group"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("ApiKeys") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Connections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Domains") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoredOrganization") + .WithMany() + .HasForeignKey("SponsoredOrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoringOrganization") + .WithMany() + .HasForeignKey("SponsoringOrganizationId"); + + b.Navigation("SponsoredOrganization"); + + b.Navigation("SponsoringOrganization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("OrganizationUsers") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("OrganizationUsers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Transactions") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Transactions") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.UserSignatureKeyPair", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", "Task") + .WithMany() + .HasForeignKey("TaskId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("Task"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", "Notification") + .WithMany() + .HasForeignKey("NotificationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Notification"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany("ApiKeys") + .HasForeignKey("ServiceAccountId"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.SecretVersion", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "EditorOrganizationUser") + .WithMany() + .HasForeignKey("EditorOrganizationUserId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "EditorServiceAccount") + .WithMany() + .HasForeignKey("EditorServiceAccountId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "Secret") + .WithMany("SecretVersions") + .HasForeignKey("SecretId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("EditorOrganizationUser"); + + b.Navigation("EditorServiceAccount"); + + b.Navigation("Secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Ciphers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Ciphers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Folders") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") + .WithMany() + .HasForeignKey("CipherId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", null) + .WithMany() + .HasForeignKey("ProjectsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", null) + .WithMany() + .HasForeignKey("SecretsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedProject"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedSecret"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "GrantedServiceAccount") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedServiceAccountId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedServiceAccount"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("ServiceAccountAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany("ProjectAccessPolicies") + .HasForeignKey("ServiceAccountId"); + + b.Navigation("GrantedProject"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("ServiceAccountAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany() + .HasForeignKey("ServiceAccountId"); + + b.Navigation("GrantedSecret"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedProject"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedSecret"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "GrantedServiceAccount") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedServiceAccountId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedServiceAccount"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("Ciphers"); + + b.Navigation("Collections"); + + b.Navigation("Connections"); + + b.Navigation("Domains"); + + b.Navigation("Groups"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("Policies"); + + b.Navigation("SsoConfigs"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.Navigation("CollectionCiphers"); + + b.Navigation("CollectionGroups"); + + b.Navigation("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Navigation("CollectionUsers"); + + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Navigation("Ciphers"); + + b.Navigation("Folders"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ServiceAccountAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Navigation("GroupAccessPolicies"); + + b.Navigation("SecretVersions"); + + b.Navigation("ServiceAccountAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ProjectAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Navigation("CollectionCiphers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/util/SqliteMigrations/Migrations/20251112155857_AddMaxStorageGbIncreasedColumn.cs b/util/SqliteMigrations/Migrations/20251112155857_AddMaxStorageGbIncreasedColumn.cs new file mode 100644 index 0000000000..a80458fab9 --- /dev/null +++ b/util/SqliteMigrations/Migrations/20251112155857_AddMaxStorageGbIncreasedColumn.cs @@ -0,0 +1,37 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.SqliteMigrations.Migrations; + +/// +public partial class AddMaxStorageGbIncreasedColumn : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "MaxStorageGbIncreased", + table: "User", + type: "INTEGER", + nullable: true); + + migrationBuilder.AddColumn( + name: "MaxStorageGbIncreased", + table: "Organization", + type: "INTEGER", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "MaxStorageGbIncreased", + table: "User"); + + migrationBuilder.DropColumn( + name: "MaxStorageGbIncreased", + table: "Organization"); + } +}