diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index ada906329..d56bb2796 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "swashbuckle.aspnetcore.cli": { - "version": "6.8.1", + "version": "6.9.0", "commands": ["swagger"] }, "dotnet-ef": { diff --git a/.github/workflows/_move_finalization_db_scripts.yml b/.github/workflows/_move_finalization_db_scripts.yml index 6e3825733..d89787539 100644 --- a/.github/workflows/_move_finalization_db_scripts.yml +++ b/.github/workflows/_move_finalization_db_scripts.yml @@ -29,7 +29,7 @@ jobs: secrets: "github-pat-bitwarden-devops-bot-repo-scope" - name: Check out branch - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: token: ${{ steps.retrieve-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }} @@ -53,7 +53,7 @@ jobs: if: ${{ needs.setup.outputs.copy_finalization_scripts == 'true' }} steps: - name: Checkout - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 0 @@ -107,7 +107,7 @@ jobs: devops-alerts-slack-webhook-url" - name: Import GPG keys - uses: crazy-max/ghaction-import-gpg@01dd5d3ca463c7f10f7f4f7b4f177225ac661ee4 # v6.1.0 + uses: crazy-max/ghaction-import-gpg@cb9bde2e2525e640591a934b1fd28eef1dcaf5e5 # v6.2.0 with: gpg_private_key: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key }} passphrase: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key-passphrase }} diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6043e1e21..17e3e999e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,10 +18,10 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Check out repo - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up .NET - uses: actions/setup-dotnet@6bd8b7f7774af54e05809fcc5431931b3eb1ddee # v4.0.1 + uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0 - name: Verify format run: dotnet format --verify-no-changes @@ -67,13 +67,13 @@ jobs: node: true steps: - name: Check out repo - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up .NET - uses: actions/setup-dotnet@6bd8b7f7774af54e05809fcc5431931b3eb1ddee # v4.0.1 + uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0 - name: Set up Node - uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4 + uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 with: cache: "npm" cache-dependency-path: "**/package-lock.json" @@ -172,7 +172,7 @@ jobs: dotnet: true steps: - name: Check out repo - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Check branch to publish env: @@ -274,14 +274,14 @@ jobs: - name: Scan Docker image id: container-scan - uses: anchore/scan-action@49e50b215b647c5ec97abb66f69af73c46a4ca08 # v5.0.1 + uses: anchore/scan-action@5ed195cc06065322983cae4bb31e2a751feb86fd # v5.2.0 with: image: ${{ steps.image-tags.outputs.primary_tag }} fail-build: false output-format: sarif - name: Upload Grype results to GitHub - uses: github/codeql-action/upload-sarif@f779452ac5af1c261dce0346a8f964149f49322b # v3.26.13 + uses: github/codeql-action/upload-sarif@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0 with: sarif_file: ${{ steps.container-scan.outputs.sarif }} @@ -291,10 +291,10 @@ jobs: needs: build-docker steps: - name: Check out repo - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up .NET - uses: actions/setup-dotnet@6bd8b7f7774af54e05809fcc5431931b3eb1ddee # v4.0.1 + uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0 - name: Log in to Azure - production subscription uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 @@ -466,10 +466,10 @@ jobs: - win-x64 steps: - name: Check out repo - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up .NET - uses: actions/setup-dotnet@6bd8b7f7774af54e05809fcc5431931b3eb1ddee # v4.0.1 + uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0 - name: Print environment run: | diff --git a/.github/workflows/cleanup-ephemeral-environment.yml b/.github/workflows/cleanup-ephemeral-environment.yml index d5c34a7bb..91e8ff083 100644 --- a/.github/workflows/cleanup-ephemeral-environment.yml +++ b/.github/workflows/cleanup-ephemeral-environment.yml @@ -12,7 +12,7 @@ jobs: config-exists: ${{ steps.validate-config.outputs.config-exists }} steps: - name: Checkout PR - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Validate config exists in path id: validate-config diff --git a/.github/workflows/cleanup-rc-branch.yml b/.github/workflows/cleanup-rc-branch.yml index e037c18f9..1ea2eab08 100644 --- a/.github/workflows/cleanup-rc-branch.yml +++ b/.github/workflows/cleanup-rc-branch.yml @@ -23,7 +23,7 @@ jobs: secrets: "github-pat-bitwarden-devops-bot-repo-scope" - name: Checkout main - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: ref: main token: ${{ steps.retrieve-bot-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }} diff --git a/.github/workflows/code-references.yml b/.github/workflows/code-references.yml index 855241fdb..eeb84f745 100644 --- a/.github/workflows/code-references.yml +++ b/.github/workflows/code-references.yml @@ -33,7 +33,7 @@ jobs: steps: - name: Check out repository - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Collect id: collect diff --git a/.github/workflows/protect-files.yml b/.github/workflows/protect-files.yml index 95d57180d..89d6d4c6d 100644 --- a/.github/workflows/protect-files.yml +++ b/.github/workflows/protect-files.yml @@ -28,7 +28,7 @@ jobs: label: "DB-migrations-changed" steps: - name: Check out repo - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 2 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 4454ea1f3..55220390c 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -98,7 +98,7 @@ jobs: echo "Github Release Option: $RELEASE_OPTION" - name: Check out repo - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up project name id: setup diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9d5dcb74d..0809ff833 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -36,7 +36,7 @@ jobs: fi - name: Check out repo - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Check release version id: version diff --git a/.github/workflows/repository-management.yml b/.github/workflows/repository-management.yml index eb4187c59..8b0e3bcc0 100644 --- a/.github/workflows/repository-management.yml +++ b/.github/workflows/repository-management.yml @@ -27,7 +27,7 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Check out target ref - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: ref: ${{ inputs.target_ref }} @@ -62,7 +62,7 @@ jobs: version: ${{ inputs.version_number_override }} - name: Check out branch - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: ref: main @@ -135,13 +135,61 @@ jobs: git config --local user.email "actions@github.com" git config --local user.name "Github Actions" + - name: Create version branch + id: create-branch + run: | + NAME=version_bump_${{ github.ref_name }}_$(date +"%Y-%m-%d") + git switch -c $NAME + echo "name=$NAME" >> $GITHUB_OUTPUT + - name: Commit files run: git commit -m "Bumped version to ${{ steps.set-final-version-output.outputs.version }}" -a - name: Push changes + run: git push + + - name: Generate GH App token + uses: actions/create-github-app-token@5d869da34e18e7287c1daad50e0b8ea0f506ce69 # v1.11.0 + id: app-token + with: + app-id: ${{ secrets.BW_GHAPP_ID }} + private-key: ${{ secrets.BW_GHAPP_KEY }} + owner: ${{ github.repository_owner }} + + - name: Create version PR + id: create-pr + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + PR_BRANCH: ${{ steps.create-branch.outputs.name }} + TITLE: "Bump version to ${{ steps.set-final-version-output.outputs.version }}" run: | - git pull -pt - git push + PR_URL=$(gh pr create --title "$TITLE" \ + --base "main" \ + --head "$PR_BRANCH" \ + --label "version update" \ + --label "automated pr" \ + --body " + ## Type of change + - [ ] Bug fix + - [ ] New feature development + - [ ] Tech debt (refactoring, code cleanup, dependency upgrades, etc) + - [ ] Build/deploy pipeline (DevOps) + - [X] Other + ## Objective + Automated version bump to ${{ steps.set-final-version-output.outputs.version }}") + echo "pr_number=${PR_URL##*/}" >> $GITHUB_OUTPUT + + - name: Approve PR + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_NUMBER: ${{ steps.create-pr.outputs.pr_number }} + run: gh pr review $PR_NUMBER --approve + + - name: Merge PR + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + PR_NUMBER: ${{ steps.create-pr.outputs.pr_number }} + run: gh pr merge $PR_NUMBER --squash --auto --delete-branch cherry_pick: @@ -150,10 +198,10 @@ jobs: needs: bump_version steps: - name: Check out main branch - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: ref: main - + - name: Install xmllint run: | sudo apt-get update @@ -189,7 +237,7 @@ jobs: RC_VERSION=$(xmllint -xpath "/Project/PropertyGroup/Version/text()" Directory.Build.props) echo "rc_version=$RC_VERSION" >> $GITHUB_OUTPUT fi - + - name: Configure Git run: | git config --local user.email "actions@github.com" diff --git a/.github/workflows/scan.yml b/.github/workflows/scan.yml index 8703bac5e..f071cb4ec 100644 --- a/.github/workflows/scan.yml +++ b/.github/workflows/scan.yml @@ -26,7 +26,7 @@ jobs: steps: - name: Check out repo - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: ref: ${{ github.event.pull_request.head.sha }} @@ -46,7 +46,7 @@ jobs: --output-path . ${{ env.INCREMENTAL }} - name: Upload Checkmarx results to GitHub - uses: github/codeql-action/upload-sarif@f779452ac5af1c261dce0346a8f964149f49322b # v3.26.13 + uses: github/codeql-action/upload-sarif@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0 with: sarif_file: cx_result.sarif @@ -60,19 +60,19 @@ jobs: steps: - name: Set up JDK 17 - uses: actions/setup-java@b36c23c0d998641eff861008f374ee103c25ac73 # v4.4.0 + uses: actions/setup-java@8df1039502a15bceb9433410b1a100fbe190c53b # v4.5.0 with: java-version: 17 distribution: "zulu" - name: Check out repo - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 0 ref: ${{ github.event.pull_request.head.sha }} - name: Set up .NET - uses: actions/setup-dotnet@6bd8b7f7774af54e05809fcc5431931b3eb1ddee # v4.0.1 + uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0 - name: Install SonarCloud scanner run: dotnet tool install dotnet-sonarscanner -g diff --git a/.github/workflows/test-database.yml b/.github/workflows/test-database.yml index 7a38b0f3b..e16c080bc 100644 --- a/.github/workflows/test-database.yml +++ b/.github/workflows/test-database.yml @@ -35,10 +35,10 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Check out repo - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up .NET - uses: actions/setup-dotnet@6bd8b7f7774af54e05809fcc5431931b3eb1ddee # v4.0.1 + uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0 - name: Restore tools run: dotnet tool restore @@ -146,10 +146,10 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Check out repo - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up .NET - uses: actions/setup-dotnet@6bd8b7f7774af54e05809fcc5431931b3eb1ddee # v4.0.1 + uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0 - name: Print environment run: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index bd9e358df..5f3b9871b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -46,10 +46,10 @@ jobs: steps: - name: Check out repo - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up .NET - uses: actions/setup-dotnet@6bd8b7f7774af54e05809fcc5431931b3eb1ddee # v4.0.1 + uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0 - name: Print environment run: | diff --git a/Directory.Build.props b/Directory.Build.props index 5cd12bfb7..4e252c82e 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -3,7 +3,7 @@ net8.0 - 2024.10.1 + 2024.11.0 Bit.$(MSBuildProjectName) enable diff --git a/bitwarden_license/src/Commercial.Core/AdminConsole/Providers/CreateProviderCommand.cs b/bitwarden_license/src/Commercial.Core/AdminConsole/Providers/CreateProviderCommand.cs index 09157d72c..3b01370ef 100644 --- a/bitwarden_license/src/Commercial.Core/AdminConsole/Providers/CreateProviderCommand.cs +++ b/bitwarden_license/src/Commercial.Core/AdminConsole/Providers/CreateProviderCommand.cs @@ -40,6 +40,36 @@ public class CreateProviderCommand : ICreateProviderCommand } public async Task CreateMspAsync(Provider provider, string ownerEmail, int teamsMinimumSeats, int enterpriseMinimumSeats) + { + var providerId = await CreateProviderAsync(provider, ownerEmail); + + var isConsolidatedBillingEnabled = _featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling); + + if (isConsolidatedBillingEnabled) + { + await CreateProviderPlanAsync(providerId, PlanType.TeamsMonthly, teamsMinimumSeats); + await CreateProviderPlanAsync(providerId, PlanType.EnterpriseMonthly, enterpriseMinimumSeats); + } + } + + public async Task CreateResellerAsync(Provider provider) + { + await ProviderRepositoryCreateAsync(provider, ProviderStatusType.Created); + } + + public async Task CreateMultiOrganizationEnterpriseAsync(Provider provider, string ownerEmail, PlanType plan, int minimumSeats) + { + var providerId = await CreateProviderAsync(provider, ownerEmail); + + var isConsolidatedBillingEnabled = _featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling); + + if (isConsolidatedBillingEnabled) + { + await CreateProviderPlanAsync(providerId, plan, minimumSeats); + } + } + + private async Task CreateProviderAsync(Provider provider, string ownerEmail) { var owner = await _userRepository.GetByEmailAsync(ownerEmail); if (owner == null) @@ -64,27 +94,10 @@ public class CreateProviderCommand : ICreateProviderCommand Status = ProviderUserStatusType.Confirmed, }; - if (isConsolidatedBillingEnabled) - { - var providerPlans = new List - { - CreateProviderPlan(provider.Id, PlanType.TeamsMonthly, teamsMinimumSeats), - CreateProviderPlan(provider.Id, PlanType.EnterpriseMonthly, enterpriseMinimumSeats) - }; - - foreach (var providerPlan in providerPlans) - { - await _providerPlanRepository.CreateAsync(providerPlan); - } - } - await _providerUserRepository.CreateAsync(providerUser); await _providerService.SendProviderSetupInviteEmailAsync(provider, owner.Email); - } - public async Task CreateResellerAsync(Provider provider) - { - await ProviderRepositoryCreateAsync(provider, ProviderStatusType.Created); + return provider.Id; } private async Task ProviderRepositoryCreateAsync(Provider provider, ProviderStatusType status) @@ -95,9 +108,9 @@ public class CreateProviderCommand : ICreateProviderCommand await _providerRepository.CreateAsync(provider); } - private ProviderPlan CreateProviderPlan(Guid providerId, PlanType planType, int seatMinimum) + private async Task CreateProviderPlanAsync(Guid providerId, PlanType planType, int seatMinimum) { - return new ProviderPlan + var plan = new ProviderPlan { ProviderId = providerId, PlanType = planType, @@ -105,5 +118,6 @@ public class CreateProviderCommand : ICreateProviderCommand PurchasedSeats = 0, AllocatedSeats = 0 }; + await _providerPlanRepository.CreateAsync(plan); } } diff --git a/bitwarden_license/src/Commercial.Core/Billing/ProviderBillingService.cs b/bitwarden_license/src/Commercial.Core/Billing/ProviderBillingService.cs index 787a11d1e..32698eaaf 100644 --- a/bitwarden_license/src/Commercial.Core/Billing/ProviderBillingService.cs +++ b/bitwarden_license/src/Commercial.Core/Billing/ProviderBillingService.cs @@ -11,6 +11,7 @@ using Bit.Core.Billing.Enums; using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Repositories; using Bit.Core.Billing.Services; +using Bit.Core.Billing.Services.Contracts; using Bit.Core.Context; using Bit.Core.Enums; using Bit.Core.Exceptions; @@ -379,42 +380,23 @@ public class ProviderBillingService( var subscriptionItemOptionsList = new List(); - var teamsProviderPlan = - providerPlans.SingleOrDefault(providerPlan => providerPlan.PlanType == PlanType.TeamsMonthly); - - if (teamsProviderPlan == null || !teamsProviderPlan.IsConfigured()) + foreach (var providerPlan in providerPlans) { - logger.LogError("Cannot start subscription for provider ({ProviderID}) that has no configured Teams plan", provider.Id); + var plan = StaticStore.GetPlan(providerPlan.PlanType); - throw new BillingException(); + if (!providerPlan.IsConfigured()) + { + logger.LogError("Cannot start subscription for provider ({ProviderID}) that has no configured {ProviderName} plan", provider.Id, plan.Name); + throw new BillingException(); + } + + subscriptionItemOptionsList.Add(new SubscriptionItemOptions + { + Price = plan.PasswordManager.StripeProviderPortalSeatPlanId, + Quantity = providerPlan.SeatMinimum + }); } - var teamsPlan = StaticStore.GetPlan(PlanType.TeamsMonthly); - - subscriptionItemOptionsList.Add(new SubscriptionItemOptions - { - Price = teamsPlan.PasswordManager.StripeProviderPortalSeatPlanId, - Quantity = teamsProviderPlan.SeatMinimum - }); - - var enterpriseProviderPlan = - providerPlans.SingleOrDefault(providerPlan => providerPlan.PlanType == PlanType.EnterpriseMonthly); - - if (enterpriseProviderPlan == null || !enterpriseProviderPlan.IsConfigured()) - { - logger.LogError("Cannot start subscription for provider ({ProviderID}) that has no configured Enterprise plan", provider.Id); - - throw new BillingException(); - } - - var enterprisePlan = StaticStore.GetPlan(PlanType.EnterpriseMonthly); - - subscriptionItemOptionsList.Add(new SubscriptionItemOptions - { - Price = enterprisePlan.PasswordManager.StripeProviderPortalSeatPlanId, - Quantity = enterpriseProviderPlan.SeatMinimum - }); - var subscriptionCreateOptions = new SubscriptionCreateOptions { AutomaticTax = new SubscriptionAutomaticTaxOptions @@ -456,144 +438,142 @@ public class ProviderBillingService( } } - public async Task UpdateSeatMinimums( - Provider provider, - int enterpriseSeatMinimum, - int teamsSeatMinimum) + public async Task ChangePlan(ChangeProviderPlanCommand command) { - ArgumentNullException.ThrowIfNull(provider); + var plan = await providerPlanRepository.GetByIdAsync(command.ProviderPlanId); - if (enterpriseSeatMinimum < 0 || teamsSeatMinimum < 0) + if (plan == null) + { + throw new BadRequestException("Provider plan not found."); + } + + if (plan.PlanType == command.NewPlan) + { + return; + } + + var oldPlanConfiguration = StaticStore.GetPlan(plan.PlanType); + + plan.PlanType = command.NewPlan; + await providerPlanRepository.ReplaceAsync(plan); + + Subscription subscription; + try + { + subscription = await stripeAdapter.ProviderSubscriptionGetAsync(command.GatewaySubscriptionId, plan.ProviderId); + } + catch (InvalidOperationException) + { + throw new ConflictException("Subscription not found."); + } + + var oldSubscriptionItem = subscription.Items.SingleOrDefault(x => + x.Price.Id == oldPlanConfiguration.PasswordManager.StripeProviderPortalSeatPlanId); + + var updateOptions = new SubscriptionUpdateOptions + { + Items = + [ + new SubscriptionItemOptions + { + Price = StaticStore.GetPlan(command.NewPlan).PasswordManager.StripeProviderPortalSeatPlanId, + Quantity = oldSubscriptionItem!.Quantity + }, + new SubscriptionItemOptions + { + Id = oldSubscriptionItem.Id, + Deleted = true + } + ] + }; + + await stripeAdapter.SubscriptionUpdateAsync(command.GatewaySubscriptionId, updateOptions); + } + + public async Task UpdateSeatMinimums(UpdateProviderSeatMinimumsCommand command) + { + if (command.Configuration.Any(x => x.SeatsMinimum < 0)) { throw new BadRequestException("Provider seat minimums must be at least 0."); } - var subscription = await stripeAdapter.SubscriptionGetAsync(provider.GatewaySubscriptionId); + Subscription subscription; + try + { + subscription = await stripeAdapter.ProviderSubscriptionGetAsync(command.GatewaySubscriptionId, command.Id); + } + catch (InvalidOperationException) + { + throw new ConflictException("Subscription not found."); + } var subscriptionItemOptionsList = new List(); - var providerPlans = await providerPlanRepository.GetByProviderId(provider.Id); + var providerPlans = await providerPlanRepository.GetByProviderId(command.Id); - var enterpriseProviderPlan = - providerPlans.Single(providerPlan => providerPlan.PlanType == PlanType.EnterpriseMonthly); - - if (enterpriseProviderPlan.SeatMinimum != enterpriseSeatMinimum) + foreach (var newPlanConfiguration in command.Configuration) { - var enterprisePriceId = StaticStore.GetPlan(PlanType.EnterpriseMonthly).PasswordManager - .StripeProviderPortalSeatPlanId; + var providerPlan = + providerPlans.Single(providerPlan => providerPlan.PlanType == newPlanConfiguration.Plan); - var enterpriseSubscriptionItem = subscription.Items.First(item => item.Price.Id == enterprisePriceId); - - if (enterpriseProviderPlan.PurchasedSeats == 0) + if (providerPlan.SeatMinimum != newPlanConfiguration.SeatsMinimum) { - if (enterpriseProviderPlan.AllocatedSeats > enterpriseSeatMinimum) - { - enterpriseProviderPlan.PurchasedSeats = - enterpriseProviderPlan.AllocatedSeats - enterpriseSeatMinimum; + var priceId = StaticStore.GetPlan(newPlanConfiguration.Plan).PasswordManager + .StripeProviderPortalSeatPlanId; + var subscriptionItem = subscription.Items.First(item => item.Price.Id == priceId); - subscriptionItemOptionsList.Add(new SubscriptionItemOptions + if (providerPlan.PurchasedSeats == 0) + { + if (providerPlan.AllocatedSeats > newPlanConfiguration.SeatsMinimum) { - Id = enterpriseSubscriptionItem.Id, - Price = enterprisePriceId, - Quantity = enterpriseProviderPlan.AllocatedSeats - }); + providerPlan.PurchasedSeats = providerPlan.AllocatedSeats - newPlanConfiguration.SeatsMinimum; + + subscriptionItemOptionsList.Add(new SubscriptionItemOptions + { + Id = subscriptionItem.Id, + Price = priceId, + Quantity = providerPlan.AllocatedSeats + }); + } + else + { + subscriptionItemOptionsList.Add(new SubscriptionItemOptions + { + Id = subscriptionItem.Id, + Price = priceId, + Quantity = newPlanConfiguration.SeatsMinimum + }); + } } else { - subscriptionItemOptionsList.Add(new SubscriptionItemOptions + var totalSeats = providerPlan.SeatMinimum + providerPlan.PurchasedSeats; + + if (newPlanConfiguration.SeatsMinimum <= totalSeats) { - Id = enterpriseSubscriptionItem.Id, - Price = enterprisePriceId, - Quantity = enterpriseSeatMinimum - }); + providerPlan.PurchasedSeats = totalSeats - newPlanConfiguration.SeatsMinimum; + } + else + { + providerPlan.PurchasedSeats = 0; + subscriptionItemOptionsList.Add(new SubscriptionItemOptions + { + Id = subscriptionItem.Id, + Price = priceId, + Quantity = newPlanConfiguration.SeatsMinimum + }); + } } + + providerPlan.SeatMinimum = newPlanConfiguration.SeatsMinimum; + + await providerPlanRepository.ReplaceAsync(providerPlan); } - else - { - var totalEnterpriseSeats = enterpriseProviderPlan.SeatMinimum + enterpriseProviderPlan.PurchasedSeats; - - if (enterpriseSeatMinimum <= totalEnterpriseSeats) - { - enterpriseProviderPlan.PurchasedSeats = totalEnterpriseSeats - enterpriseSeatMinimum; - } - else - { - enterpriseProviderPlan.PurchasedSeats = 0; - subscriptionItemOptionsList.Add(new SubscriptionItemOptions - { - Id = enterpriseSubscriptionItem.Id, - Price = enterprisePriceId, - Quantity = enterpriseSeatMinimum - }); - } - } - - enterpriseProviderPlan.SeatMinimum = enterpriseSeatMinimum; - - await providerPlanRepository.ReplaceAsync(enterpriseProviderPlan); - } - - var teamsProviderPlan = - providerPlans.Single(providerPlan => providerPlan.PlanType == PlanType.TeamsMonthly); - - if (teamsProviderPlan.SeatMinimum != teamsSeatMinimum) - { - var teamsPriceId = StaticStore.GetPlan(PlanType.TeamsMonthly).PasswordManager - .StripeProviderPortalSeatPlanId; - - var teamsSubscriptionItem = subscription.Items.First(item => item.Price.Id == teamsPriceId); - - if (teamsProviderPlan.PurchasedSeats == 0) - { - if (teamsProviderPlan.AllocatedSeats > teamsSeatMinimum) - { - teamsProviderPlan.PurchasedSeats = teamsProviderPlan.AllocatedSeats - teamsSeatMinimum; - - subscriptionItemOptionsList.Add(new SubscriptionItemOptions - { - Id = teamsSubscriptionItem.Id, - Price = teamsPriceId, - Quantity = teamsProviderPlan.AllocatedSeats - }); - } - else - { - subscriptionItemOptionsList.Add(new SubscriptionItemOptions - { - Id = teamsSubscriptionItem.Id, - Price = teamsPriceId, - Quantity = teamsSeatMinimum - }); - } - } - else - { - var totalTeamsSeats = teamsProviderPlan.SeatMinimum + teamsProviderPlan.PurchasedSeats; - - if (teamsSeatMinimum <= totalTeamsSeats) - { - teamsProviderPlan.PurchasedSeats = totalTeamsSeats - teamsSeatMinimum; - } - else - { - teamsProviderPlan.PurchasedSeats = 0; - subscriptionItemOptionsList.Add(new SubscriptionItemOptions - { - Id = teamsSubscriptionItem.Id, - Price = teamsPriceId, - Quantity = teamsSeatMinimum - }); - } - } - - teamsProviderPlan.SeatMinimum = teamsSeatMinimum; - - await providerPlanRepository.ReplaceAsync(teamsProviderPlan); } if (subscriptionItemOptionsList.Count > 0) { - await stripeAdapter.SubscriptionUpdateAsync(provider.GatewaySubscriptionId, + await stripeAdapter.SubscriptionUpdateAsync(command.GatewaySubscriptionId, new SubscriptionUpdateOptions { Items = subscriptionItemOptionsList }); } } diff --git a/bitwarden_license/test/Commercial.Core.Test/AdminConsole/ProviderFeatures/CreateProviderCommandTests.cs b/bitwarden_license/test/Commercial.Core.Test/AdminConsole/ProviderFeatures/CreateProviderCommandTests.cs index 787d5a17b..e354e4417 100644 --- a/bitwarden_license/test/Commercial.Core.Test/AdminConsole/ProviderFeatures/CreateProviderCommandTests.cs +++ b/bitwarden_license/test/Commercial.Core.Test/AdminConsole/ProviderFeatures/CreateProviderCommandTests.cs @@ -3,6 +3,7 @@ using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Services; +using Bit.Core.Billing.Enums; using Bit.Core.Entities; using Bit.Core.Exceptions; using Bit.Core.Repositories; @@ -19,23 +20,30 @@ public class CreateProviderCommandTests [Theory, BitAutoData] public async Task CreateMspAsync_UserIdIsInvalid_Throws(Provider provider, SutProvider sutProvider) { + // Arrange provider.Type = ProviderType.Msp; + // Act var exception = await Assert.ThrowsAsync( () => sutProvider.Sut.CreateMspAsync(provider, default, default, default)); + + // Assert Assert.Contains("Invalid owner.", exception.Message); } [Theory, BitAutoData] public async Task CreateMspAsync_Success(Provider provider, User user, SutProvider sutProvider) { + // Arrange provider.Type = ProviderType.Msp; var userRepository = sutProvider.GetDependency(); userRepository.GetByEmailAsync(user.Email).Returns(user); + // Act await sutProvider.Sut.CreateMspAsync(provider, user.Email, default, default); + // Assert await sutProvider.GetDependency().ReceivedWithAnyArgs().CreateAsync(default); await sutProvider.GetDependency().Received(1).SendProviderSetupInviteEmailAsync(provider, user.Email); } @@ -43,11 +51,52 @@ public class CreateProviderCommandTests [Theory, BitAutoData] public async Task CreateResellerAsync_Success(Provider provider, SutProvider sutProvider) { + // Arrange provider.Type = ProviderType.Reseller; + // Act await sutProvider.Sut.CreateResellerAsync(provider); + // Assert await sutProvider.GetDependency().ReceivedWithAnyArgs().CreateAsync(default); await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().SendProviderSetupInviteEmailAsync(default, default); } + + [Theory, BitAutoData] + public async Task CreateMultiOrganizationEnterpriseAsync_Success( + Provider provider, + User user, + PlanType plan, + int minimumSeats, + SutProvider sutProvider) + { + // Arrange + provider.Type = ProviderType.MultiOrganizationEnterprise; + + var userRepository = sutProvider.GetDependency(); + userRepository.GetByEmailAsync(user.Email).Returns(user); + + // Act + await sutProvider.Sut.CreateMultiOrganizationEnterpriseAsync(provider, user.Email, plan, minimumSeats); + + // Assert + await sutProvider.GetDependency().ReceivedWithAnyArgs().CreateAsync(provider); + await sutProvider.GetDependency().Received(1).SendProviderSetupInviteEmailAsync(provider, user.Email); + } + + [Theory, BitAutoData] + public async Task CreateMultiOrganizationEnterpriseAsync_UserIdIsInvalid_Throws( + Provider provider, + SutProvider sutProvider) + { + // Arrange + provider.Type = ProviderType.Msp; + + // Act + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.CreateMultiOrganizationEnterpriseAsync(provider, default, default, default)); + + // Assert + Assert.Contains("Invalid owner.", exception.Message); + } } diff --git a/bitwarden_license/test/Commercial.Core.Test/Billing/ProviderBillingServiceTests.cs b/bitwarden_license/test/Commercial.Core.Test/Billing/ProviderBillingServiceTests.cs index d9ae9a559..7c3e8cad8 100644 --- a/bitwarden_license/test/Commercial.Core.Test/Billing/ProviderBillingServiceTests.cs +++ b/bitwarden_license/test/Commercial.Core.Test/Billing/ProviderBillingServiceTests.cs @@ -13,6 +13,7 @@ using Bit.Core.Billing.Entities; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Repositories; using Bit.Core.Billing.Services; +using Bit.Core.Billing.Services.Contracts; using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Enums; @@ -1011,26 +1012,192 @@ public class ProviderBillingServiceTests #endregion - #region UpdateSeatMinimums + #region ChangePlan [Theory, BitAutoData] - public async Task UpdateSeatMinimums_NullProvider_ThrowsArgumentNullException( - SutProvider sutProvider) => - await Assert.ThrowsAsync(() => sutProvider.Sut.UpdateSeatMinimums(null, 0, 0)); + public async Task ChangePlan_NullProviderPlan_ThrowsBadRequestException( + ChangeProviderPlanCommand command, + SutProvider sutProvider) + { + // Arrange + var providerPlanRepository = sutProvider.GetDependency(); + providerPlanRepository.GetByIdAsync(Arg.Any()).Returns((ProviderPlan)null); + + // Act + var actual = await Assert.ThrowsAsync(() => sutProvider.Sut.ChangePlan(command)); + + // Assert + Assert.Equal("Provider plan not found.", actual.Message); + } + + [Theory, BitAutoData] + public async Task ChangePlan_ProviderNotFound_DoesNothing( + ChangeProviderPlanCommand command, + SutProvider sutProvider) + { + // Arrange + var providerPlanRepository = sutProvider.GetDependency(); + var stripeAdapter = sutProvider.GetDependency(); + var existingPlan = new ProviderPlan + { + Id = command.ProviderPlanId, + PlanType = command.NewPlan, + PurchasedSeats = 0, + AllocatedSeats = 0, + SeatMinimum = 0 + }; + providerPlanRepository + .GetByIdAsync(Arg.Is(p => p == command.ProviderPlanId)) + .Returns(existingPlan); + + // Act + await sutProvider.Sut.ChangePlan(command); + + // Assert + await providerPlanRepository.Received(0).ReplaceAsync(Arg.Any()); + await stripeAdapter.Received(0).SubscriptionUpdateAsync(Arg.Any(), Arg.Any()); + } + + [Theory, BitAutoData] + public async Task ChangePlan_SameProviderPlan_DoesNothing( + ChangeProviderPlanCommand command, + SutProvider sutProvider) + { + // Arrange + var providerPlanRepository = sutProvider.GetDependency(); + var stripeAdapter = sutProvider.GetDependency(); + var existingPlan = new ProviderPlan + { + Id = command.ProviderPlanId, + PlanType = command.NewPlan, + PurchasedSeats = 0, + AllocatedSeats = 0, + SeatMinimum = 0 + }; + providerPlanRepository + .GetByIdAsync(Arg.Is(p => p == command.ProviderPlanId)) + .Returns(existingPlan); + + // Act + await sutProvider.Sut.ChangePlan(command); + + // Assert + await providerPlanRepository.Received(0).ReplaceAsync(Arg.Any()); + await stripeAdapter.Received(0).SubscriptionUpdateAsync(Arg.Any(), Arg.Any()); + } + + [Theory, BitAutoData] + public async Task ChangePlan_UpdatesSubscriptionCorrectly( + Guid providerPlanId, + Provider provider, + SutProvider sutProvider) + { + // Arrange + var providerPlanRepository = sutProvider.GetDependency(); + var existingPlan = new ProviderPlan + { + Id = providerPlanId, + ProviderId = provider.Id, + PlanType = PlanType.EnterpriseAnnually, + PurchasedSeats = 2, + AllocatedSeats = 10, + SeatMinimum = 8 + }; + providerPlanRepository + .GetByIdAsync(Arg.Is(p => p == providerPlanId)) + .Returns(existingPlan); + + var providerRepository = sutProvider.GetDependency(); + providerRepository.GetByIdAsync(Arg.Is(existingPlan.ProviderId)).Returns(provider); + + var stripeAdapter = sutProvider.GetDependency(); + stripeAdapter.ProviderSubscriptionGetAsync( + Arg.Is(provider.GatewaySubscriptionId), + Arg.Is(provider.Id)) + .Returns(new Subscription + { + Id = provider.GatewaySubscriptionId, + Items = new StripeList + { + Data = + [ + new SubscriptionItem + { + Id = "si_ent_annual", + Price = new Price + { + Id = StaticStore.GetPlan(PlanType.EnterpriseAnnually).PasswordManager + .StripeProviderPortalSeatPlanId + }, + Quantity = 10 + } + ] + } + }); + + var command = + new ChangeProviderPlanCommand(providerPlanId, PlanType.EnterpriseMonthly, provider.GatewaySubscriptionId); + + // Act + await sutProvider.Sut.ChangePlan(command); + + // Assert + await providerPlanRepository.Received(1) + .ReplaceAsync(Arg.Is(p => p.PlanType == PlanType.EnterpriseMonthly)); + + await stripeAdapter.Received(1) + .SubscriptionUpdateAsync( + Arg.Is(provider.GatewaySubscriptionId), + Arg.Is(p => + p.Items.Count(si => si.Id == "si_ent_annual" && si.Deleted == true) == 1)); + + var newPlanCfg = StaticStore.GetPlan(command.NewPlan); + await stripeAdapter.Received(1) + .SubscriptionUpdateAsync( + Arg.Is(provider.GatewaySubscriptionId), + Arg.Is(p => + p.Items.Count(si => + si.Price == newPlanCfg.PasswordManager.StripeProviderPortalSeatPlanId && + si.Deleted == default && + si.Quantity == 10) == 1)); + } + + #endregion + + #region UpdateSeatMinimums [Theory, BitAutoData] public async Task UpdateSeatMinimums_NegativeSeatMinimum_ThrowsBadRequestException( Provider provider, - SutProvider sutProvider) => - await Assert.ThrowsAsync(() => sutProvider.Sut.UpdateSeatMinimums(provider, -10, 100)); + SutProvider sutProvider) + { + // Arrange + var providerRepository = sutProvider.GetDependency(); + providerRepository.GetByIdAsync(provider.Id).Returns(provider); + var command = new UpdateProviderSeatMinimumsCommand( + provider.Id, + provider.GatewaySubscriptionId, + [ + (PlanType.TeamsMonthly, -10), + (PlanType.EnterpriseMonthly, 50) + ]); + + // Act + var actual = await Assert.ThrowsAsync(() => sutProvider.Sut.UpdateSeatMinimums(command)); + + // Assert + Assert.Equal("Provider seat minimums must be at least 0.", actual.Message); + } [Theory, BitAutoData] public async Task UpdateSeatMinimums_NoPurchasedSeats_AllocatedHigherThanIncomingMinimum_UpdatesPurchasedSeats_SyncsStripeWithNewSeatMinimum( Provider provider, SutProvider sutProvider) { + // Arrange var stripeAdapter = sutProvider.GetDependency(); var providerPlanRepository = sutProvider.GetDependency(); + var providerRepository = sutProvider.GetDependency(); const string enterpriseLineItemId = "enterprise_line_item_id"; const string teamsLineItemId = "teams_line_item_id"; @@ -1058,7 +1225,9 @@ public class ProviderBillingServiceTests } }; - stripeAdapter.SubscriptionGetAsync(provider.GatewaySubscriptionId).Returns(subscription); + stripeAdapter.ProviderSubscriptionGetAsync( + provider.GatewaySubscriptionId, + provider.Id).Returns(subscription); var providerPlans = new List { @@ -1066,10 +1235,21 @@ public class ProviderBillingServiceTests new() { PlanType = PlanType.TeamsMonthly, SeatMinimum = 30, PurchasedSeats = 0, AllocatedSeats = 25 } }; + providerRepository.GetByIdAsync(provider.Id).Returns(provider); providerPlanRepository.GetByProviderId(provider.Id).Returns(providerPlans); - await sutProvider.Sut.UpdateSeatMinimums(provider, 30, 20); + var command = new UpdateProviderSeatMinimumsCommand( + provider.Id, + provider.GatewaySubscriptionId, + [ + (PlanType.EnterpriseMonthly, 30), + (PlanType.TeamsMonthly, 20) + ]); + // Act + await sutProvider.Sut.UpdateSeatMinimums(command); + + // Assert await providerPlanRepository.Received(1).ReplaceAsync(Arg.Is( providerPlan => providerPlan.PlanType == PlanType.EnterpriseMonthly && providerPlan.SeatMinimum == 30)); @@ -1091,8 +1271,11 @@ public class ProviderBillingServiceTests Provider provider, SutProvider sutProvider) { + // Arrange var stripeAdapter = sutProvider.GetDependency(); var providerPlanRepository = sutProvider.GetDependency(); + var providerRepository = sutProvider.GetDependency(); + providerRepository.GetByIdAsync(provider.Id).Returns(provider); const string enterpriseLineItemId = "enterprise_line_item_id"; const string teamsLineItemId = "teams_line_item_id"; @@ -1120,7 +1303,7 @@ public class ProviderBillingServiceTests } }; - stripeAdapter.SubscriptionGetAsync(provider.GatewaySubscriptionId).Returns(subscription); + stripeAdapter.ProviderSubscriptionGetAsync(provider.GatewaySubscriptionId, provider.Id).Returns(subscription); var providerPlans = new List { @@ -1130,8 +1313,18 @@ public class ProviderBillingServiceTests providerPlanRepository.GetByProviderId(provider.Id).Returns(providerPlans); - await sutProvider.Sut.UpdateSeatMinimums(provider, 70, 50); + var command = new UpdateProviderSeatMinimumsCommand( + provider.Id, + provider.GatewaySubscriptionId, + [ + (PlanType.EnterpriseMonthly, 70), + (PlanType.TeamsMonthly, 50) + ]); + // Act + await sutProvider.Sut.UpdateSeatMinimums(command); + + // Assert await providerPlanRepository.Received(1).ReplaceAsync(Arg.Is( providerPlan => providerPlan.PlanType == PlanType.EnterpriseMonthly && providerPlan.SeatMinimum == 70)); @@ -1153,8 +1346,11 @@ public class ProviderBillingServiceTests Provider provider, SutProvider sutProvider) { + // Arrange var stripeAdapter = sutProvider.GetDependency(); var providerPlanRepository = sutProvider.GetDependency(); + var providerRepository = sutProvider.GetDependency(); + providerRepository.GetByIdAsync(provider.Id).Returns(provider); const string enterpriseLineItemId = "enterprise_line_item_id"; const string teamsLineItemId = "teams_line_item_id"; @@ -1182,7 +1378,7 @@ public class ProviderBillingServiceTests } }; - stripeAdapter.SubscriptionGetAsync(provider.GatewaySubscriptionId).Returns(subscription); + stripeAdapter.ProviderSubscriptionGetAsync(provider.GatewaySubscriptionId, provider.Id).Returns(subscription); var providerPlans = new List { @@ -1192,8 +1388,18 @@ public class ProviderBillingServiceTests providerPlanRepository.GetByProviderId(provider.Id).Returns(providerPlans); - await sutProvider.Sut.UpdateSeatMinimums(provider, 60, 60); + var command = new UpdateProviderSeatMinimumsCommand( + provider.Id, + provider.GatewaySubscriptionId, + [ + (PlanType.EnterpriseMonthly, 60), + (PlanType.TeamsMonthly, 60) + ]); + // Act + await sutProvider.Sut.UpdateSeatMinimums(command); + + // Assert await providerPlanRepository.Received(1).ReplaceAsync(Arg.Is( providerPlan => providerPlan.PlanType == PlanType.EnterpriseMonthly && providerPlan.SeatMinimum == 60 && providerPlan.PurchasedSeats == 10)); @@ -1209,8 +1415,11 @@ public class ProviderBillingServiceTests Provider provider, SutProvider sutProvider) { + // Arrange var stripeAdapter = sutProvider.GetDependency(); var providerPlanRepository = sutProvider.GetDependency(); + var providerRepository = sutProvider.GetDependency(); + providerRepository.GetByIdAsync(provider.Id).Returns(provider); const string enterpriseLineItemId = "enterprise_line_item_id"; const string teamsLineItemId = "teams_line_item_id"; @@ -1238,7 +1447,7 @@ public class ProviderBillingServiceTests } }; - stripeAdapter.SubscriptionGetAsync(provider.GatewaySubscriptionId).Returns(subscription); + stripeAdapter.ProviderSubscriptionGetAsync(provider.GatewaySubscriptionId, provider.Id).Returns(subscription); var providerPlans = new List { @@ -1248,8 +1457,18 @@ public class ProviderBillingServiceTests providerPlanRepository.GetByProviderId(provider.Id).Returns(providerPlans); - await sutProvider.Sut.UpdateSeatMinimums(provider, 80, 80); + var command = new UpdateProviderSeatMinimumsCommand( + provider.Id, + provider.GatewaySubscriptionId, + [ + (PlanType.EnterpriseMonthly, 80), + (PlanType.TeamsMonthly, 80) + ]); + // Act + await sutProvider.Sut.UpdateSeatMinimums(command); + + // Assert await providerPlanRepository.Received(1).ReplaceAsync(Arg.Is( providerPlan => providerPlan.PlanType == PlanType.EnterpriseMonthly && providerPlan.SeatMinimum == 80 && providerPlan.PurchasedSeats == 0)); @@ -1271,8 +1490,11 @@ public class ProviderBillingServiceTests Provider provider, SutProvider sutProvider) { + // Arrange var stripeAdapter = sutProvider.GetDependency(); var providerPlanRepository = sutProvider.GetDependency(); + var providerRepository = sutProvider.GetDependency(); + providerRepository.GetByIdAsync(provider.Id).Returns(provider); const string enterpriseLineItemId = "enterprise_line_item_id"; const string teamsLineItemId = "teams_line_item_id"; @@ -1300,7 +1522,7 @@ public class ProviderBillingServiceTests } }; - stripeAdapter.SubscriptionGetAsync(provider.GatewaySubscriptionId).Returns(subscription); + stripeAdapter.ProviderSubscriptionGetAsync(provider.GatewaySubscriptionId, provider.Id).Returns(subscription); var providerPlans = new List { @@ -1310,8 +1532,18 @@ public class ProviderBillingServiceTests providerPlanRepository.GetByProviderId(provider.Id).Returns(providerPlans); - await sutProvider.Sut.UpdateSeatMinimums(provider, 70, 30); + var command = new UpdateProviderSeatMinimumsCommand( + provider.Id, + provider.GatewaySubscriptionId, + [ + (PlanType.EnterpriseMonthly, 70), + (PlanType.TeamsMonthly, 30) + ]); + // Act + await sutProvider.Sut.UpdateSeatMinimums(command); + + // Assert await providerPlanRepository.Received(1).ReplaceAsync(Arg.Is( providerPlan => providerPlan.PlanType == PlanType.EnterpriseMonthly && providerPlan.SeatMinimum == 70)); diff --git a/bitwarden_license/test/Scim.IntegrationTest/Scim.IntegrationTest.csproj b/bitwarden_license/test/Scim.IntegrationTest/Scim.IntegrationTest.csproj index a84813fd7..4fc79f202 100644 --- a/bitwarden_license/test/Scim.IntegrationTest/Scim.IntegrationTest.csproj +++ b/bitwarden_license/test/Scim.IntegrationTest/Scim.IntegrationTest.csproj @@ -9,7 +9,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - + diff --git a/src/Admin/AdminConsole/Controllers/ProvidersController.cs b/src/Admin/AdminConsole/Controllers/ProvidersController.cs index 12e2c4d43..83e4ce7d5 100644 --- a/src/Admin/AdminConsole/Controllers/ProvidersController.cs +++ b/src/Admin/AdminConsole/Controllers/ProvidersController.cs @@ -14,6 +14,7 @@ using Bit.Core.Billing.Enums; using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Repositories; using Bit.Core.Billing.Services; +using Bit.Core.Billing.Services.Contracts; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Repositories; @@ -107,9 +108,15 @@ public class ProvidersController : Controller }); } - public IActionResult Create(int teamsMinimumSeats, int enterpriseMinimumSeats, string ownerEmail = null) + public IActionResult Create() { - return View(new CreateProviderModel + return View(new CreateProviderModel()); + } + + [HttpGet("providers/create/msp")] + public IActionResult CreateMsp(int teamsMinimumSeats, int enterpriseMinimumSeats, string ownerEmail = null) + { + return View(new CreateMspProviderModel { OwnerEmail = ownerEmail, TeamsMonthlySeatMinimum = teamsMinimumSeats, @@ -117,10 +124,50 @@ public class ProvidersController : Controller }); } + [HttpGet("providers/create/reseller")] + public IActionResult CreateReseller() + { + return View(new CreateResellerProviderModel()); + } + + [HttpGet("providers/create/multi-organization-enterprise")] + public IActionResult CreateMultiOrganizationEnterprise(int enterpriseMinimumSeats, string ownerEmail = null) + { + if (!_featureService.IsEnabled(FeatureFlagKeys.PM12275_MultiOrganizationEnterprises)) + { + return RedirectToAction("Create"); + } + + return View(new CreateMultiOrganizationEnterpriseProviderModel + { + OwnerEmail = ownerEmail, + EnterpriseSeatMinimum = enterpriseMinimumSeats + }); + } + [HttpPost] [ValidateAntiForgeryToken] [RequirePermission(Permission.Provider_Create)] - public async Task Create(CreateProviderModel model) + public IActionResult Create(CreateProviderModel model) + { + if (!ModelState.IsValid) + { + return View(model); + } + + return model.Type switch + { + ProviderType.Msp => RedirectToAction("CreateMsp"), + ProviderType.Reseller => RedirectToAction("CreateReseller"), + ProviderType.MultiOrganizationEnterprise => RedirectToAction("CreateMultiOrganizationEnterprise"), + _ => View(model) + }; + } + + [HttpPost("providers/create/msp")] + [ValidateAntiForgeryToken] + [RequirePermission(Permission.Provider_Create)] + public async Task CreateMsp(CreateMspProviderModel model) { if (!ModelState.IsValid) { @@ -128,19 +175,51 @@ public class ProvidersController : Controller } var provider = model.ToProvider(); - switch (provider.Type) + + await _createProviderCommand.CreateMspAsync( + provider, + model.OwnerEmail, + model.TeamsMonthlySeatMinimum, + model.EnterpriseMonthlySeatMinimum); + + return RedirectToAction("Edit", new { id = provider.Id }); + } + + [HttpPost("providers/create/reseller")] + [ValidateAntiForgeryToken] + [RequirePermission(Permission.Provider_Create)] + public async Task CreateReseller(CreateResellerProviderModel model) + { + if (!ModelState.IsValid) { - case ProviderType.Msp: - await _createProviderCommand.CreateMspAsync( - provider, - model.OwnerEmail, - model.TeamsMonthlySeatMinimum, - model.EnterpriseMonthlySeatMinimum); - break; - case ProviderType.Reseller: - await _createProviderCommand.CreateResellerAsync(provider); - break; + return View(model); } + var provider = model.ToProvider(); + await _createProviderCommand.CreateResellerAsync(provider); + + return RedirectToAction("Edit", new { id = provider.Id }); + } + + [HttpPost("providers/create/multi-organization-enterprise")] + [ValidateAntiForgeryToken] + [RequirePermission(Permission.Provider_Create)] + public async Task CreateMultiOrganizationEnterprise(CreateMultiOrganizationEnterpriseProviderModel model) + { + if (!ModelState.IsValid) + { + return View(model); + } + var provider = model.ToProvider(); + + if (!_featureService.IsEnabled(FeatureFlagKeys.PM12275_MultiOrganizationEnterprises)) + { + return RedirectToAction("Create"); + } + await _createProviderCommand.CreateMultiOrganizationEnterpriseAsync( + provider, + model.OwnerEmail, + model.Plan.Value, + model.EnterpriseSeatMinimum); return RedirectToAction("Edit", new { id = provider.Id }); } @@ -212,25 +291,39 @@ public class ProvidersController : Controller var providerPlans = await _providerPlanRepository.GetByProviderId(id); - if (providerPlans.Count == 0) + switch (provider.Type) { - var newProviderPlans = new List - { - new () { ProviderId = provider.Id, PlanType = PlanType.TeamsMonthly, SeatMinimum = model.TeamsMonthlySeatMinimum, PurchasedSeats = 0, AllocatedSeats = 0 }, - new () { ProviderId = provider.Id, PlanType = PlanType.EnterpriseMonthly, SeatMinimum = model.EnterpriseMonthlySeatMinimum, PurchasedSeats = 0, AllocatedSeats = 0 } - }; + case ProviderType.Msp: + var updateMspSeatMinimumsCommand = new UpdateProviderSeatMinimumsCommand( + provider.Id, + provider.GatewaySubscriptionId, + [ + (Plan: PlanType.TeamsMonthly, SeatsMinimum: model.TeamsMonthlySeatMinimum), + (Plan: PlanType.EnterpriseMonthly, SeatsMinimum: model.EnterpriseMonthlySeatMinimum) + ]); + await _providerBillingService.UpdateSeatMinimums(updateMspSeatMinimumsCommand); + break; + case ProviderType.MultiOrganizationEnterprise: + { + var existingMoePlan = providerPlans.Single(); - foreach (var newProviderPlan in newProviderPlans) - { - await _providerPlanRepository.CreateAsync(newProviderPlan); - } - } - else - { - await _providerBillingService.UpdateSeatMinimums( - provider, - model.EnterpriseMonthlySeatMinimum, - model.TeamsMonthlySeatMinimum); + // 1. Change the plan and take over any old values. + var changeMoePlanCommand = new ChangeProviderPlanCommand( + existingMoePlan.Id, + model.Plan!.Value, + provider.GatewaySubscriptionId); + await _providerBillingService.ChangePlan(changeMoePlanCommand); + + // 2. Update the seat minimums. + var updateMoeSeatMinimumsCommand = new UpdateProviderSeatMinimumsCommand( + provider.Id, + provider.GatewaySubscriptionId, + [ + (Plan: model.Plan!.Value, SeatsMinimum: model.EnterpriseMinimumSeats!.Value) + ]); + await _providerBillingService.UpdateSeatMinimums(updateMoeSeatMinimumsCommand); + break; + } } return RedirectToAction("Edit", new { id }); diff --git a/src/Admin/AdminConsole/Models/CreateMspProviderModel.cs b/src/Admin/AdminConsole/Models/CreateMspProviderModel.cs new file mode 100644 index 000000000..f48cf2176 --- /dev/null +++ b/src/Admin/AdminConsole/Models/CreateMspProviderModel.cs @@ -0,0 +1,45 @@ +using System.ComponentModel.DataAnnotations; +using Bit.Core.AdminConsole.Entities.Provider; +using Bit.Core.AdminConsole.Enums.Provider; +using Bit.SharedWeb.Utilities; + +namespace Bit.Admin.AdminConsole.Models; + +public class CreateMspProviderModel : IValidatableObject +{ + [Display(Name = "Owner Email")] + public string OwnerEmail { get; set; } + + [Display(Name = "Teams (Monthly) Seat Minimum")] + public int TeamsMonthlySeatMinimum { get; set; } + + [Display(Name = "Enterprise (Monthly) Seat Minimum")] + public int EnterpriseMonthlySeatMinimum { get; set; } + + public virtual Provider ToProvider() + { + return new Provider + { + Type = ProviderType.Msp + }; + } + + public IEnumerable Validate(ValidationContext validationContext) + { + if (string.IsNullOrWhiteSpace(OwnerEmail)) + { + var ownerEmailDisplayName = nameof(OwnerEmail).GetDisplayAttribute()?.GetName() ?? nameof(OwnerEmail); + yield return new ValidationResult($"The {ownerEmailDisplayName} field is required."); + } + if (TeamsMonthlySeatMinimum < 0) + { + var teamsMinimumSeatsDisplayName = nameof(TeamsMonthlySeatMinimum).GetDisplayAttribute()?.GetName() ?? nameof(TeamsMonthlySeatMinimum); + yield return new ValidationResult($"The {teamsMinimumSeatsDisplayName} field can not be negative."); + } + if (EnterpriseMonthlySeatMinimum < 0) + { + var enterpriseMinimumSeatsDisplayName = nameof(EnterpriseMonthlySeatMinimum).GetDisplayAttribute()?.GetName() ?? nameof(EnterpriseMonthlySeatMinimum); + yield return new ValidationResult($"The {enterpriseMinimumSeatsDisplayName} field can not be negative."); + } + } +} diff --git a/src/Admin/AdminConsole/Models/CreateMultiOrganizationEnterpriseProviderModel.cs b/src/Admin/AdminConsole/Models/CreateMultiOrganizationEnterpriseProviderModel.cs new file mode 100644 index 000000000..ef7210a9e --- /dev/null +++ b/src/Admin/AdminConsole/Models/CreateMultiOrganizationEnterpriseProviderModel.cs @@ -0,0 +1,47 @@ +using System.ComponentModel.DataAnnotations; +using Bit.Core.AdminConsole.Entities.Provider; +using Bit.Core.AdminConsole.Enums.Provider; +using Bit.Core.Billing.Enums; +using Bit.SharedWeb.Utilities; + +namespace Bit.Admin.AdminConsole.Models; + +public class CreateMultiOrganizationEnterpriseProviderModel : IValidatableObject +{ + [Display(Name = "Owner Email")] + public string OwnerEmail { get; set; } + + [Display(Name = "Enterprise Seat Minimum")] + public int EnterpriseSeatMinimum { get; set; } + + [Display(Name = "Plan")] + [Required] + public PlanType? Plan { get; set; } + + public virtual Provider ToProvider() + { + return new Provider + { + Type = ProviderType.MultiOrganizationEnterprise + }; + } + + public IEnumerable Validate(ValidationContext validationContext) + { + if (string.IsNullOrWhiteSpace(OwnerEmail)) + { + var ownerEmailDisplayName = nameof(OwnerEmail).GetDisplayAttribute()?.GetName() ?? nameof(OwnerEmail); + yield return new ValidationResult($"The {ownerEmailDisplayName} field is required."); + } + if (EnterpriseSeatMinimum < 0) + { + var enterpriseSeatMinimumDisplayName = nameof(EnterpriseSeatMinimum).GetDisplayAttribute()?.GetName() ?? nameof(EnterpriseSeatMinimum); + yield return new ValidationResult($"The {enterpriseSeatMinimumDisplayName} field can not be negative."); + } + if (Plan != PlanType.EnterpriseAnnually && Plan != PlanType.EnterpriseMonthly) + { + var planDisplayName = nameof(Plan).GetDisplayAttribute()?.GetName() ?? nameof(Plan); + yield return new ValidationResult($"The {planDisplayName} field must be set to Enterprise Annually or Enterprise Monthly."); + } + } +} diff --git a/src/Admin/AdminConsole/Models/CreateProviderModel.cs b/src/Admin/AdminConsole/Models/CreateProviderModel.cs index 07bb1b6e4..da73787a9 100644 --- a/src/Admin/AdminConsole/Models/CreateProviderModel.cs +++ b/src/Admin/AdminConsole/Models/CreateProviderModel.cs @@ -1,84 +1,8 @@ -using System.ComponentModel.DataAnnotations; -using Bit.Core.AdminConsole.Entities.Provider; -using Bit.Core.AdminConsole.Enums.Provider; -using Bit.SharedWeb.Utilities; +using Bit.Core.AdminConsole.Enums.Provider; namespace Bit.Admin.AdminConsole.Models; -public class CreateProviderModel : IValidatableObject +public class CreateProviderModel { - public CreateProviderModel() { } - - [Display(Name = "Provider Type")] public ProviderType Type { get; set; } - - [Display(Name = "Owner Email")] - public string OwnerEmail { get; set; } - - [Display(Name = "Name")] - public string Name { get; set; } - - [Display(Name = "Business Name")] - public string BusinessName { get; set; } - - [Display(Name = "Primary Billing Email")] - public string BillingEmail { get; set; } - - [Display(Name = "Teams (Monthly) Seat Minimum")] - public int TeamsMonthlySeatMinimum { get; set; } - - [Display(Name = "Enterprise (Monthly) Seat Minimum")] - public int EnterpriseMonthlySeatMinimum { get; set; } - - public virtual Provider ToProvider() - { - return new Provider() - { - Type = Type, - Name = Name, - BusinessName = BusinessName, - BillingEmail = BillingEmail?.ToLowerInvariant().Trim() - }; - } - - public IEnumerable Validate(ValidationContext validationContext) - { - switch (Type) - { - case ProviderType.Msp: - if (string.IsNullOrWhiteSpace(OwnerEmail)) - { - var ownerEmailDisplayName = nameof(OwnerEmail).GetDisplayAttribute()?.GetName() ?? nameof(OwnerEmail); - yield return new ValidationResult($"The {ownerEmailDisplayName} field is required."); - } - if (TeamsMonthlySeatMinimum < 0) - { - var teamsMinimumSeatsDisplayName = nameof(TeamsMonthlySeatMinimum).GetDisplayAttribute()?.GetName() ?? nameof(TeamsMonthlySeatMinimum); - yield return new ValidationResult($"The {teamsMinimumSeatsDisplayName} field can not be negative."); - } - if (EnterpriseMonthlySeatMinimum < 0) - { - var enterpriseMinimumSeatsDisplayName = nameof(EnterpriseMonthlySeatMinimum).GetDisplayAttribute()?.GetName() ?? nameof(EnterpriseMonthlySeatMinimum); - yield return new ValidationResult($"The {enterpriseMinimumSeatsDisplayName} field can not be negative."); - } - break; - case ProviderType.Reseller: - if (string.IsNullOrWhiteSpace(Name)) - { - var nameDisplayName = nameof(Name).GetDisplayAttribute()?.GetName() ?? nameof(Name); - yield return new ValidationResult($"The {nameDisplayName} field is required."); - } - if (string.IsNullOrWhiteSpace(BusinessName)) - { - var businessNameDisplayName = nameof(BusinessName).GetDisplayAttribute()?.GetName() ?? nameof(BusinessName); - yield return new ValidationResult($"The {businessNameDisplayName} field is required."); - } - if (string.IsNullOrWhiteSpace(BillingEmail)) - { - var billingEmailDisplayName = nameof(BillingEmail).GetDisplayAttribute()?.GetName() ?? nameof(BillingEmail); - yield return new ValidationResult($"The {billingEmailDisplayName} field is required."); - } - break; - } - } } diff --git a/src/Admin/AdminConsole/Models/CreateResellerProviderModel.cs b/src/Admin/AdminConsole/Models/CreateResellerProviderModel.cs new file mode 100644 index 000000000..958faf3f8 --- /dev/null +++ b/src/Admin/AdminConsole/Models/CreateResellerProviderModel.cs @@ -0,0 +1,48 @@ +using System.ComponentModel.DataAnnotations; +using Bit.Core.AdminConsole.Entities.Provider; +using Bit.Core.AdminConsole.Enums.Provider; +using Bit.SharedWeb.Utilities; + +namespace Bit.Admin.AdminConsole.Models; + +public class CreateResellerProviderModel : IValidatableObject +{ + [Display(Name = "Name")] + public string Name { get; set; } + + [Display(Name = "Business Name")] + public string BusinessName { get; set; } + + [Display(Name = "Primary Billing Email")] + public string BillingEmail { get; set; } + + public virtual Provider ToProvider() + { + return new Provider + { + Name = Name, + BusinessName = BusinessName, + BillingEmail = BillingEmail?.ToLowerInvariant().Trim(), + Type = ProviderType.Reseller + }; + } + + public IEnumerable Validate(ValidationContext validationContext) + { + if (string.IsNullOrWhiteSpace(Name)) + { + var nameDisplayName = nameof(Name).GetDisplayAttribute()?.GetName() ?? nameof(Name); + yield return new ValidationResult($"The {nameDisplayName} field is required."); + } + if (string.IsNullOrWhiteSpace(BusinessName)) + { + var businessNameDisplayName = nameof(BusinessName).GetDisplayAttribute()?.GetName() ?? nameof(BusinessName); + yield return new ValidationResult($"The {businessNameDisplayName} field is required."); + } + if (string.IsNullOrWhiteSpace(BillingEmail)) + { + var billingEmailDisplayName = nameof(BillingEmail).GetDisplayAttribute()?.GetName() ?? nameof(BillingEmail); + yield return new ValidationResult($"The {billingEmailDisplayName} field is required."); + } + } +} diff --git a/src/Admin/AdminConsole/Models/ProviderEditModel.cs b/src/Admin/AdminConsole/Models/ProviderEditModel.cs index dd9b9f5a5..7fd5c765c 100644 --- a/src/Admin/AdminConsole/Models/ProviderEditModel.cs +++ b/src/Admin/AdminConsole/Models/ProviderEditModel.cs @@ -33,6 +33,13 @@ public class ProviderEditModel : ProviderViewModel, IValidatableObject GatewayCustomerUrl = gatewayCustomerUrl; GatewaySubscriptionUrl = gatewaySubscriptionUrl; Type = provider.Type; + + if (Type == ProviderType.MultiOrganizationEnterprise) + { + var plan = providerPlans.SingleOrDefault(); + EnterpriseMinimumSeats = plan?.SeatMinimum ?? 0; + Plan = plan?.PlanType; + } } [Display(Name = "Billing Email")] @@ -58,13 +65,24 @@ public class ProviderEditModel : ProviderViewModel, IValidatableObject [Display(Name = "Provider Type")] public ProviderType Type { get; set; } + [Display(Name = "Plan")] + public PlanType? Plan { get; set; } + + [Display(Name = "Enterprise Seats Minimum")] + public int? EnterpriseMinimumSeats { get; set; } + public virtual Provider ToProvider(Provider existingProvider) { existingProvider.BillingEmail = BillingEmail?.ToLowerInvariant().Trim(); existingProvider.BillingPhone = BillingPhone?.ToLowerInvariant().Trim(); - existingProvider.Gateway = Gateway; - existingProvider.GatewayCustomerId = GatewayCustomerId; - existingProvider.GatewaySubscriptionId = GatewaySubscriptionId; + switch (Type) + { + case ProviderType.Msp: + existingProvider.Gateway = Gateway; + existingProvider.GatewayCustomerId = GatewayCustomerId; + existingProvider.GatewaySubscriptionId = GatewaySubscriptionId; + break; + } return existingProvider; } @@ -82,6 +100,23 @@ public class ProviderEditModel : ProviderViewModel, IValidatableObject yield return new ValidationResult($"The {billingEmailDisplayName} field is required."); } break; + case ProviderType.MultiOrganizationEnterprise: + if (Plan == null) + { + var displayName = nameof(Plan).GetDisplayAttribute()?.GetName() ?? nameof(Plan); + yield return new ValidationResult($"The {displayName} field is required."); + } + if (EnterpriseMinimumSeats == null) + { + var displayName = nameof(EnterpriseMinimumSeats).GetDisplayAttribute()?.GetName() ?? nameof(EnterpriseMinimumSeats); + yield return new ValidationResult($"The {displayName} field is required."); + } + if (EnterpriseMinimumSeats < 0) + { + var displayName = nameof(EnterpriseMinimumSeats).GetDisplayAttribute()?.GetName() ?? nameof(EnterpriseMinimumSeats); + yield return new ValidationResult($"The {displayName} field cannot be less than 0."); + } + break; } } } diff --git a/src/Admin/AdminConsole/Views/Providers/Create.cshtml b/src/Admin/AdminConsole/Views/Providers/Create.cshtml index 41855895e..8f43a4f85 100644 --- a/src/Admin/AdminConsole/Views/Providers/Create.cshtml +++ b/src/Admin/AdminConsole/Views/Providers/Create.cshtml @@ -1,80 +1,48 @@ @using Bit.SharedWeb.Utilities @using Bit.Core.AdminConsole.Enums.Provider @using Bit.Core + @model CreateProviderModel + @inject Bit.Core.Services.IFeatureService FeatureService + @{ ViewData["Title"] = "Create Provider"; -} -@section Scripts { - + var providerTypes = Enum.GetValues() + .OrderBy(x => x.GetDisplayAttribute().Order) + .ToList(); + + if (!FeatureService.IsEnabled(FeatureFlagKeys.PM12275_MultiOrganizationEnterprises)) + { + providerTypes.Remove(ProviderType.MultiOrganizationEnterprise); + } }

Create Provider

- -
+
-
- @foreach(ProviderType providerType in Enum.GetValues(typeof(ProviderType))) + @foreach (var providerType in providerTypes) { var providerTypeValue = (int)providerType; -
- @Html.RadioButtonFor(m => m.Type, providerType, new { id = $"providerType-{providerTypeValue}", @class = "form-check-input", onclick=$"toggleProviderTypeInfo({providerTypeValue})" }) - @Html.LabelFor(m => m.Type, providerType.GetDisplayAttribute()?.GetName(), new { @class = "form-check-label align-middle", @for = $"providerType-{providerTypeValue}" }) -
- @Html.LabelFor(m => m.Type, providerType.GetDisplayAttribute()?.GetDescription(), new { @class = "form-check-label small text-muted ml-3 align-top", @for = $"providerType-{providerTypeValue}" }) -
- } -
- -
-

MSP Info

-
- - -
- @if (FeatureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling)) - { -
-
-
- - +
+
+
+
+ @Html.RadioButtonFor(m => m.Type, providerType, new { id = $"providerType-{providerTypeValue}", @class = "form-check-input" }) + @Html.LabelFor(m => m.Type, providerType.GetDisplayAttribute()?.GetName(), new { @class = "form-check-label align-middle", @for = $"providerType-{providerTypeValue}" }) +
-
-
- - +
+
+ @Html.LabelFor(m => m.Type, providerType.GetDisplayAttribute()?.GetDescription(), new { @class = "form-check-label small text-muted align-top", @for = $"providerType-{providerTypeValue}" })
}
- -
-

Reseller Info

-
- - -
-
- - -
-
- - -
-
- - + diff --git a/src/Admin/AdminConsole/Views/Providers/CreateMsp.cshtml b/src/Admin/AdminConsole/Views/Providers/CreateMsp.cshtml new file mode 100644 index 000000000..dde62b58a --- /dev/null +++ b/src/Admin/AdminConsole/Views/Providers/CreateMsp.cshtml @@ -0,0 +1,39 @@ +@using Bit.Core.AdminConsole.Enums.Provider +@using Bit.Core + +@model CreateMspProviderModel + +@inject Bit.Core.Services.IFeatureService FeatureService + +@{ + ViewData["Title"] = "Create Managed Service Provider"; +} + +

Create Managed Service Provider

+
+
+
+
+ + +
+ @if (FeatureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling)) + { +
+
+
+ + +
+
+
+
+ + +
+
+
+ } + +
+
diff --git a/src/Admin/AdminConsole/Views/Providers/CreateMultiOrganizationEnterprise.cshtml b/src/Admin/AdminConsole/Views/Providers/CreateMultiOrganizationEnterprise.cshtml new file mode 100644 index 000000000..997fa32ef --- /dev/null +++ b/src/Admin/AdminConsole/Views/Providers/CreateMultiOrganizationEnterprise.cshtml @@ -0,0 +1,43 @@ +@using Bit.Core.Billing.Enums +@using Microsoft.AspNetCore.Mvc.TagHelpers + +@model CreateMultiOrganizationEnterpriseProviderModel + +@{ + ViewData["Title"] = "Create Multi-organization Enterprise Provider"; +} + +

Create Multi-organization Enterprise Provider

+
+
+
+
+ + +
+
+
+
+ @{ + var multiOrgPlans = new List + { + PlanType.EnterpriseAnnually, + PlanType.EnterpriseMonthly + }; + } + + +
+
+
+
+ + +
+
+
+ +
+
diff --git a/src/Admin/AdminConsole/Views/Providers/CreateReseller.cshtml b/src/Admin/AdminConsole/Views/Providers/CreateReseller.cshtml new file mode 100644 index 000000000..320ff7a4b --- /dev/null +++ b/src/Admin/AdminConsole/Views/Providers/CreateReseller.cshtml @@ -0,0 +1,25 @@ +@model CreateResellerProviderModel + +@{ + ViewData["Title"] = "Create Reseller Provider"; +} + +

Create Reseller Provider

+
+
+
+
+ + +
+
+ + +
+
+ + +
+ +
+
diff --git a/src/Admin/AdminConsole/Views/Providers/Edit.cshtml b/src/Admin/AdminConsole/Views/Providers/Edit.cshtml index 37cda8417..53944d0fc 100644 --- a/src/Admin/AdminConsole/Views/Providers/Edit.cshtml +++ b/src/Admin/AdminConsole/Views/Providers/Edit.cshtml @@ -1,6 +1,9 @@ @using Bit.Admin.Enums; @using Bit.Core +@using Bit.Core.AdminConsole.Enums.Provider +@using Bit.Core.Billing.Enums @using Bit.Core.Billing.Extensions +@using Microsoft.AspNetCore.Mvc.TagHelpers @inject Bit.Admin.Services.IAccessControlService AccessControlService @inject Bit.Core.Services.IFeatureService FeatureService @@ -47,60 +50,97 @@
@if (FeatureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling) && Model.Provider.IsBillable()) { -
-
-
- - -
-
-
-
- - -
-
-
-
-
-
-
- - + switch (Model.Provider.Type) + { + case ProviderType.Msp: + { +
+
+
+ + +
-
-
-
-
-
-
- -
- -
- - - +
+
+ +
-
-
-
- -
- -
- - - +
+
+
+
+ + +
-
-
+
+
+
+ +
+ +
+ + + +
+
+
+
+
+
+ +
+ +
+ + + +
+
+
+
+
+ break; + } + case ProviderType.MultiOrganizationEnterprise: + { + @if (FeatureService.IsEnabled(FeatureFlagKeys.PM12275_MultiOrganizationEnterprises) && Model.Provider.Type == ProviderType.MultiOrganizationEnterprise) + { +
+
+
+ @{ + var multiOrgPlans = new List + { + PlanType.EnterpriseAnnually, + PlanType.EnterpriseMonthly + }; + } + + +
+
+
+
+ + +
+
+
+ } + break; + } + } } @await Html.PartialAsync("Organizations", Model) diff --git a/src/Admin/Controllers/UsersController.cs b/src/Admin/Controllers/UsersController.cs index 2842efcdb..54e43d8b4 100644 --- a/src/Admin/Controllers/UsersController.cs +++ b/src/Admin/Controllers/UsersController.cs @@ -4,6 +4,7 @@ using Bit.Admin.Enums; using Bit.Admin.Models; using Bit.Admin.Services; using Bit.Admin.Utilities; +using Bit.Core; using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; using Bit.Core.Repositories; using Bit.Core.Services; @@ -24,6 +25,8 @@ public class UsersController : Controller private readonly GlobalSettings _globalSettings; private readonly IAccessControlService _accessControlService; private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery; + private readonly IUserService _userService; + private readonly IFeatureService _featureService; public UsersController( IUserRepository userRepository, @@ -31,7 +34,9 @@ public class UsersController : Controller IPaymentService paymentService, GlobalSettings globalSettings, IAccessControlService accessControlService, - ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery) + ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery, + IUserService userService, + IFeatureService featureService) { _userRepository = userRepository; _cipherRepository = cipherRepository; @@ -39,6 +44,8 @@ public class UsersController : Controller _globalSettings = globalSettings; _accessControlService = accessControlService; _twoFactorIsEnabledQuery = twoFactorIsEnabledQuery; + _userService = userService; + _featureService = featureService; } [RequirePermission(Permission.User_List_View)] @@ -82,8 +89,8 @@ public class UsersController : Controller var ciphers = await _cipherRepository.GetManyByUserIdAsync(id); var isTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user); - - return View(UserViewModel.MapViewModel(user, isTwoFactorEnabled, ciphers)); + var verifiedDomain = await AccountDeprovisioningEnabled(user.Id); + return View(UserViewModel.MapViewModel(user, isTwoFactorEnabled, ciphers, verifiedDomain)); } [SelfHosted(NotSelfHostedOnly = true)] @@ -99,7 +106,8 @@ public class UsersController : Controller var billingInfo = await _paymentService.GetBillingAsync(user); var billingHistoryInfo = await _paymentService.GetBillingHistoryAsync(user); var isTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user); - return View(new UserEditModel(user, isTwoFactorEnabled, ciphers, billingInfo, billingHistoryInfo, _globalSettings)); + var verifiedDomain = await AccountDeprovisioningEnabled(user.Id); + return View(new UserEditModel(user, isTwoFactorEnabled, ciphers, billingInfo, billingHistoryInfo, _globalSettings, verifiedDomain)); } [HttpPost] @@ -153,4 +161,12 @@ public class UsersController : Controller return RedirectToAction("Index"); } + + // TODO: Feature flag to be removed in PM-14207 + private async Task AccountDeprovisioningEnabled(Guid userId) + { + return _featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning) + ? await _userService.IsManagedByAnyOrganizationAsync(userId) + : null; + } } diff --git a/src/Admin/Enums/HtmlHelperExtensions.cs b/src/Admin/Enums/HtmlHelperExtensions.cs new file mode 100644 index 000000000..a5fb89303 --- /dev/null +++ b/src/Admin/Enums/HtmlHelperExtensions.cs @@ -0,0 +1,19 @@ + +using Bit.SharedWeb.Utilities; + +// ReSharper disable once CheckNamespace +namespace Microsoft.AspNetCore.Mvc.Rendering; + +public static class HtmlHelper +{ + public static IEnumerable GetEnumSelectList(this IHtmlHelper htmlHelper, IEnumerable values) + where T : Enum + { + return values.Select(v => new SelectListItem + { + Text = v.GetDisplayAttribute().Name, + Value = v.ToString() + }); + } + +} diff --git a/src/Admin/Models/UserEditModel.cs b/src/Admin/Models/UserEditModel.cs index 52cdb4c80..2ad0b27cb 100644 --- a/src/Admin/Models/UserEditModel.cs +++ b/src/Admin/Models/UserEditModel.cs @@ -20,9 +20,11 @@ public class UserEditModel IEnumerable ciphers, BillingInfo billingInfo, BillingHistoryInfo billingHistoryInfo, - GlobalSettings globalSettings) + GlobalSettings globalSettings, + bool? domainVerified + ) { - User = UserViewModel.MapViewModel(user, isTwoFactorEnabled, ciphers); + User = UserViewModel.MapViewModel(user, isTwoFactorEnabled, ciphers, domainVerified); BillingInfo = billingInfo; BillingHistoryInfo = billingHistoryInfo; diff --git a/src/Admin/Models/UserViewModel.cs b/src/Admin/Models/UserViewModel.cs index 09b3d5577..75c089ee5 100644 --- a/src/Admin/Models/UserViewModel.cs +++ b/src/Admin/Models/UserViewModel.cs @@ -14,6 +14,7 @@ public class UserViewModel public bool Premium { get; } public short? MaxStorageGb { get; } public bool EmailVerified { get; } + public bool? DomainVerified { get; } public bool TwoFactorEnabled { get; } public DateTime AccountRevisionDate { get; } public DateTime RevisionDate { get; } @@ -35,6 +36,7 @@ public class UserViewModel bool premium, short? maxStorageGb, bool emailVerified, + bool? domainVerified, bool twoFactorEnabled, DateTime accountRevisionDate, DateTime revisionDate, @@ -56,6 +58,7 @@ public class UserViewModel Premium = premium; MaxStorageGb = maxStorageGb; EmailVerified = emailVerified; + DomainVerified = domainVerified; TwoFactorEnabled = twoFactorEnabled; AccountRevisionDate = accountRevisionDate; RevisionDate = revisionDate; @@ -73,10 +76,10 @@ public class UserViewModel public static IEnumerable MapViewModels( IEnumerable users, IEnumerable<(Guid userId, bool twoFactorIsEnabled)> lookup) => - users.Select(user => MapViewModel(user, lookup)); + users.Select(user => MapViewModel(user, lookup, false)); public static UserViewModel MapViewModel(User user, - IEnumerable<(Guid userId, bool twoFactorIsEnabled)> lookup) => + IEnumerable<(Guid userId, bool twoFactorIsEnabled)> lookup, bool? domainVerified) => new( user.Id, user.Name, @@ -86,6 +89,7 @@ public class UserViewModel user.Premium, user.MaxStorageGb, user.EmailVerified, + domainVerified, IsTwoFactorEnabled(user, lookup), user.AccountRevisionDate, user.RevisionDate, @@ -100,9 +104,9 @@ public class UserViewModel Array.Empty()); public static UserViewModel MapViewModel(User user, bool isTwoFactorEnabled) => - MapViewModel(user, isTwoFactorEnabled, Array.Empty()); + MapViewModel(user, isTwoFactorEnabled, Array.Empty(), false); - public static UserViewModel MapViewModel(User user, bool isTwoFactorEnabled, IEnumerable ciphers) => + public static UserViewModel MapViewModel(User user, bool isTwoFactorEnabled, IEnumerable ciphers, bool? domainVerified) => new( user.Id, user.Name, @@ -112,6 +116,7 @@ public class UserViewModel user.Premium, user.MaxStorageGb, user.EmailVerified, + domainVerified, isTwoFactorEnabled, user.AccountRevisionDate, user.RevisionDate, diff --git a/src/Admin/Utilities/RolePermissionMapping.cs b/src/Admin/Utilities/RolePermissionMapping.cs index e260c264f..ec357c7e9 100644 --- a/src/Admin/Utilities/RolePermissionMapping.cs +++ b/src/Admin/Utilities/RolePermissionMapping.cs @@ -110,6 +110,7 @@ public static class RolePermissionMapping Permission.User_Licensing_View, Permission.User_Billing_View, Permission.User_Billing_LaunchGateway, + Permission.User_Delete, Permission.Org_List_View, Permission.Org_OrgInformation_View, Permission.Org_GeneralDetails_View, diff --git a/src/Admin/Views/Users/_ViewInformation.cshtml b/src/Admin/Views/Users/_ViewInformation.cshtml index 490ebd78d..00afcc19d 100644 --- a/src/Admin/Views/Users/_ViewInformation.cshtml +++ b/src/Admin/Views/Users/_ViewInformation.cshtml @@ -1,4 +1,4 @@ -@model UserViewModel +@model UserViewModel
Id
@Model.Id
@@ -12,6 +12,11 @@
Email Verified
@(Model.EmailVerified ? "Yes" : "No")
+ @if(Model.DomainVerified.HasValue){ +
Domain Verified
+
@(Model.DomainVerified.Value == true ? "Yes" : "No")
+ } +
Using 2FA
@(Model.TwoFactorEnabled ? "Yes" : "No")
diff --git a/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs b/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs index af0aede5a..b81cb068f 100644 --- a/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs +++ b/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs @@ -1,6 +1,5 @@ using Bit.Api.AdminConsole.Models.Request.Organizations; using Bit.Api.AdminConsole.Models.Response.Organizations; -using Bit.Api.Auth.Models.Request.Accounts; using Bit.Api.Models.Request.Organizations; using Bit.Api.Models.Response; using Bit.Api.Vault.AuthorizationHandlers.Collections; @@ -53,6 +52,8 @@ public class OrganizationUsersController : Controller private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery; private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand; private readonly IDeleteManagedOrganizationUserAccountCommand _deleteManagedOrganizationUserAccountCommand; + private readonly IGetOrganizationUsersManagementStatusQuery _getOrganizationUsersManagementStatusQuery; + private readonly IFeatureService _featureService; public OrganizationUsersController( IOrganizationRepository organizationRepository, @@ -73,7 +74,9 @@ public class OrganizationUsersController : Controller IOrganizationUserUserDetailsQuery organizationUserUserDetailsQuery, ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery, IRemoveOrganizationUserCommand removeOrganizationUserCommand, - IDeleteManagedOrganizationUserAccountCommand deleteManagedOrganizationUserAccountCommand) + IDeleteManagedOrganizationUserAccountCommand deleteManagedOrganizationUserAccountCommand, + IGetOrganizationUsersManagementStatusQuery getOrganizationUsersManagementStatusQuery, + IFeatureService featureService) { _organizationRepository = organizationRepository; _organizationUserRepository = organizationUserRepository; @@ -94,29 +97,34 @@ public class OrganizationUsersController : Controller _twoFactorIsEnabledQuery = twoFactorIsEnabledQuery; _removeOrganizationUserCommand = removeOrganizationUserCommand; _deleteManagedOrganizationUserAccountCommand = deleteManagedOrganizationUserAccountCommand; + _getOrganizationUsersManagementStatusQuery = getOrganizationUsersManagementStatusQuery; + _featureService = featureService; } [HttpGet("{id}")] - public async Task Get(string id, bool includeGroups = false) + public async Task Get(Guid id, bool includeGroups = false) { - var organizationUser = await _organizationUserRepository.GetDetailsByIdWithCollectionsAsync(new Guid(id)); - if (organizationUser == null || !await _currentContext.ManageUsers(organizationUser.Item1.OrganizationId)) + var (organizationUser, collections) = await _organizationUserRepository.GetDetailsByIdWithCollectionsAsync(id); + if (organizationUser == null || !await _currentContext.ManageUsers(organizationUser.OrganizationId)) { throw new NotFoundException(); } - var response = new OrganizationUserDetailsResponseModel(organizationUser.Item1, organizationUser.Item2); + var managedByOrganization = await GetManagedByOrganizationStatusAsync( + organizationUser.OrganizationId, + [organizationUser.Id]); + + var response = new OrganizationUserDetailsResponseModel(organizationUser, managedByOrganization[organizationUser.Id], collections); if (includeGroups) { - response.Groups = await _groupRepository.GetManyIdsByUserIdAsync(organizationUser.Item1.Id); + response.Groups = await _groupRepository.GetManyIdsByUserIdAsync(organizationUser.Id); } return response; } [HttpGet("mini-details")] - [RequireFeature(FeatureFlagKeys.Pm3478RefactorOrganizationUserApi)] public async Task> GetMiniDetails(Guid orgId) { var authorizationResult = await _authorizationService.AuthorizeAsync(User, new OrganizationScope(orgId), @@ -150,11 +158,13 @@ public class OrganizationUsersController : Controller } ); var organizationUsersTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(organizationUsers); + var organizationUsersManagementStatus = await GetManagedByOrganizationStatusAsync(orgId, organizationUsers.Select(o => o.Id)); var responses = organizationUsers .Select(o => { var userTwoFactorEnabled = organizationUsersTwoFactorEnabled.FirstOrDefault(u => u.user.Id == o.Id).twoFactorIsEnabled; - var orgUser = new OrganizationUserUserDetailsResponseModel(o, userTwoFactorEnabled); + var managedByOrganization = organizationUsersManagementStatus[o.Id]; + var orgUser = new OrganizationUserUserDetailsResponseModel(o, userTwoFactorEnabled, managedByOrganization); return orgUser; }); @@ -534,7 +544,7 @@ public class OrganizationUsersController : Controller [RequireFeature(FeatureFlagKeys.AccountDeprovisioning)] [HttpDelete("{id}/delete-account")] [HttpPost("{id}/delete-account")] - public async Task DeleteAccount(Guid orgId, Guid id, [FromBody] SecretVerificationRequestModel model) + public async Task DeleteAccount(Guid orgId, Guid id) { if (!await _currentContext.ManageUsers(orgId)) { @@ -547,19 +557,13 @@ public class OrganizationUsersController : Controller throw new UnauthorizedAccessException(); } - if (!await _userService.VerifySecretAsync(currentUser, model.Secret)) - { - await Task.Delay(2000); - throw new BadRequestException(string.Empty, "User verification failed."); - } - await _deleteManagedOrganizationUserAccountCommand.DeleteUserAsync(orgId, id, currentUser.Id); } [RequireFeature(FeatureFlagKeys.AccountDeprovisioning)] [HttpDelete("delete-account")] [HttpPost("delete-account")] - public async Task> BulkDeleteAccount(Guid orgId, [FromBody] SecureOrganizationUserBulkRequestModel model) + public async Task> BulkDeleteAccount(Guid orgId, [FromBody] OrganizationUserBulkRequestModel model) { if (!await _currentContext.ManageUsers(orgId)) { @@ -572,12 +576,6 @@ public class OrganizationUsersController : Controller throw new UnauthorizedAccessException(); } - if (!await _userService.VerifySecretAsync(currentUser, model.Secret)) - { - await Task.Delay(2000); - throw new BadRequestException(string.Empty, "User verification failed."); - } - var results = await _deleteManagedOrganizationUserAccountCommand.DeleteManyUsersAsync(orgId, model.Ids, currentUser.Id); return new ListResponseModel(results.Select(r => @@ -682,4 +680,15 @@ public class OrganizationUsersController : Controller return new ListResponseModel(result.Select(r => new OrganizationUserBulkResponseModel(r.Item1.Id, r.Item2))); } + + private async Task> GetManagedByOrganizationStatusAsync(Guid orgId, IEnumerable userIds) + { + if (!_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)) + { + return userIds.ToDictionary(kvp => kvp, kvp => false); + } + + var usersOrganizationManagementStatus = await _getOrganizationUsersManagementStatusQuery.GetUsersOrganizationManagementStatusAsync(orgId, userIds); + return usersOrganizationManagementStatus; + } } diff --git a/src/Api/AdminConsole/Controllers/PoliciesController.cs b/src/Api/AdminConsole/Controllers/PoliciesController.cs index 7bfd13c40..4a1becc0b 100644 --- a/src/Api/AdminConsole/Controllers/PoliciesController.cs +++ b/src/Api/AdminConsole/Controllers/PoliciesController.cs @@ -16,6 +16,7 @@ using Bit.Core.Utilities; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Mvc; +using AdminConsoleEntities = Bit.Core.AdminConsole.Entities; namespace Bit.Api.AdminConsole.Controllers; @@ -55,17 +56,16 @@ public class PoliciesController : Controller } [HttpGet("{type}")] - public async Task Get(string orgId, int type) + public async Task Get(Guid orgId, int type) { - var orgIdGuid = new Guid(orgId); - if (!await _currentContext.ManagePolicies(orgIdGuid)) + if (!await _currentContext.ManagePolicies(orgId)) { throw new NotFoundException(); } - var policy = await _policyRepository.GetByOrganizationIdTypeAsync(orgIdGuid, (PolicyType)type); + var policy = await _policyRepository.GetByOrganizationIdTypeAsync(orgId, (PolicyType)type); if (policy == null) { - throw new NotFoundException(); + return new PolicyResponseModel(new AdminConsoleEntities.Policy() { Type = (PolicyType)type, Enabled = false }); } return new PolicyResponseModel(policy); diff --git a/src/Api/AdminConsole/Models/Request/Organizations/SecureOrganizationUserBulkRequestModel.cs b/src/Api/AdminConsole/Models/Request/Organizations/SecureOrganizationUserBulkRequestModel.cs deleted file mode 100644 index f8edb08ba..000000000 --- a/src/Api/AdminConsole/Models/Request/Organizations/SecureOrganizationUserBulkRequestModel.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using Bit.Api.Auth.Models.Request.Accounts; - -namespace Bit.Api.AdminConsole.Models.Request.Organizations; - -public class SecureOrganizationUserBulkRequestModel : SecretVerificationRequestModel -{ - [Required] - public IEnumerable Ids { get; set; } -} diff --git a/src/Api/AdminConsole/Models/Response/Organizations/OrganizationUserResponseModel.cs b/src/Api/AdminConsole/Models/Response/Organizations/OrganizationUserResponseModel.cs index 874169486..64dca73aa 100644 --- a/src/Api/AdminConsole/Models/Response/Organizations/OrganizationUserResponseModel.cs +++ b/src/Api/AdminConsole/Models/Response/Organizations/OrganizationUserResponseModel.cs @@ -64,20 +64,27 @@ public class OrganizationUserResponseModel : ResponseModel public class OrganizationUserDetailsResponseModel : OrganizationUserResponseModel { - public OrganizationUserDetailsResponseModel(OrganizationUser organizationUser, + public OrganizationUserDetailsResponseModel( + OrganizationUser organizationUser, + bool managedByOrganization, IEnumerable collections) : base(organizationUser, "organizationUserDetails") { + ManagedByOrganization = managedByOrganization; Collections = collections.Select(c => new SelectionReadOnlyResponseModel(c)); } public OrganizationUserDetailsResponseModel(OrganizationUserUserDetails organizationUser, + bool managedByOrganization, IEnumerable collections) : base(organizationUser, "organizationUserDetails") { + ManagedByOrganization = managedByOrganization; Collections = collections.Select(c => new SelectionReadOnlyResponseModel(c)); } + public bool ManagedByOrganization { get; set; } + public IEnumerable Collections { get; set; } [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] @@ -110,7 +117,7 @@ public class OrganizationUserUserMiniDetailsResponseModel : ResponseModel public class OrganizationUserUserDetailsResponseModel : OrganizationUserResponseModel { public OrganizationUserUserDetailsResponseModel(OrganizationUserUserDetails organizationUser, - bool twoFactorEnabled, string obj = "organizationUserUserDetails") + bool twoFactorEnabled, bool managedByOrganization, string obj = "organizationUserUserDetails") : base(organizationUser, obj) { if (organizationUser == null) @@ -127,6 +134,7 @@ public class OrganizationUserUserDetailsResponseModel : OrganizationUserResponse Groups = organizationUser.Groups; // Prevent reset password when using key connector. ResetPasswordEnrolled = ResetPasswordEnrolled && !organizationUser.UsesKeyConnector; + ManagedByOrganization = managedByOrganization; } public string Name { get; set; } @@ -134,6 +142,11 @@ public class OrganizationUserUserDetailsResponseModel : OrganizationUserResponse public string AvatarColor { get; set; } public bool TwoFactorEnabled { get; set; } public bool SsoBound { get; set; } + /// + /// Indicates if the organization manages the user. If a user is "managed" by an organization, + /// the organization has greater control over their account, and some user actions are restricted. + /// + public bool ManagedByOrganization { get; set; } public IEnumerable Collections { get; set; } public IEnumerable Groups { get; set; } } diff --git a/src/Api/AdminConsole/Public/Controllers/MembersController.cs b/src/Api/AdminConsole/Public/Controllers/MembersController.cs index 7515111d2..4e99353d4 100644 --- a/src/Api/AdminConsole/Public/Controllers/MembersController.cs +++ b/src/Api/AdminConsole/Public/Controllers/MembersController.cs @@ -71,14 +71,13 @@ public class MembersController : Controller [ProducesResponseType((int)HttpStatusCode.NotFound)] public async Task Get(Guid id) { - var userDetails = await _organizationUserRepository.GetDetailsByIdWithCollectionsAsync(id); - var orgUser = userDetails?.Item1; + var (orgUser, collections) = await _organizationUserRepository.GetDetailsByIdWithCollectionsAsync(id); if (orgUser == null || orgUser.OrganizationId != _currentContext.OrganizationId) { return new NotFoundResult(); } var response = new MemberResponseModel(orgUser, await _userService.TwoFactorIsEnabledAsync(orgUser), - userDetails.Item2); + collections); return new JsonResult(response); } diff --git a/src/Api/Api.csproj b/src/Api/Api.csproj index 13066ed01..d6d055e90 100644 --- a/src/Api/Api.csproj +++ b/src/Api/Api.csproj @@ -35,7 +35,7 @@ - + diff --git a/src/Api/Auth/Controllers/AccountsController.cs b/src/Api/Auth/Controllers/AccountsController.cs index a0c01752a..193077dc1 100644 --- a/src/Api/Auth/Controllers/AccountsController.cs +++ b/src/Api/Auth/Controllers/AccountsController.cs @@ -148,6 +148,13 @@ public class AccountsController : Controller throw new BadRequestException("MasterPasswordHash", "Invalid password."); } + // If Account Deprovisioning is enabled, we need to check if the user is managed by any organization. + if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning) + && await _userService.IsManagedByAnyOrganizationAsync(user.Id)) + { + throw new BadRequestException("Cannot change emails for accounts owned by an organization. Contact your organization administrator for additional details."); + } + await _userService.InitiateEmailChangeAsync(user, model.NewEmail); } @@ -165,6 +172,13 @@ public class AccountsController : Controller throw new BadRequestException("You cannot change your email when using Key Connector."); } + // If Account Deprovisioning is enabled, we need to check if the user is managed by any organization. + if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning) + && await _userService.IsManagedByAnyOrganizationAsync(user.Id)) + { + throw new BadRequestException("Cannot change emails for accounts owned by an organization. Contact your organization administrator for additional details."); + } + var result = await _userService.ChangeEmailAsync(user, model.MasterPasswordHash, model.NewEmail, model.NewMasterPasswordHash, model.Token, model.Key); if (result.Succeeded) @@ -566,6 +580,13 @@ public class AccountsController : Controller } else { + // If Account Deprovisioning is enabled, we need to check if the user is managed by any organization. + if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning) + && await _userService.IsManagedByAnyOrganizationAsync(user.Id)) + { + throw new BadRequestException("Cannot delete accounts owned by an organization. Contact your organization administrator for additional details."); + } + var result = await _userService.DeleteAsync(user); if (result.Succeeded) { diff --git a/src/Api/Billing/Controllers/OrganizationBillingController.cs b/src/Api/Billing/Controllers/OrganizationBillingController.cs index f6ba87c71..b6a26f240 100644 --- a/src/Api/Billing/Controllers/OrganizationBillingController.cs +++ b/src/Api/Billing/Controllers/OrganizationBillingController.cs @@ -26,7 +26,7 @@ public class OrganizationBillingController( [HttpGet("metadata")] public async Task GetMetadataAsync([FromRoute] Guid organizationId) { - if (!await currentContext.AccessMembersTab(organizationId)) + if (!await currentContext.OrganizationUser(organizationId)) { return Error.Unauthorized(); } diff --git a/src/Api/Billing/Models/Responses/OrganizationMetadataResponse.cs b/src/Api/Billing/Models/Responses/OrganizationMetadataResponse.cs index b5f9ab2f5..960cd53ea 100644 --- a/src/Api/Billing/Models/Responses/OrganizationMetadataResponse.cs +++ b/src/Api/Billing/Models/Responses/OrganizationMetadataResponse.cs @@ -4,10 +4,14 @@ namespace Bit.Api.Billing.Models.Responses; public record OrganizationMetadataResponse( bool IsEligibleForSelfHost, - bool IsOnSecretsManagerStandalone) + bool IsManaged, + bool IsOnSecretsManagerStandalone, + bool IsSubscriptionUnpaid) { public static OrganizationMetadataResponse From(OrganizationMetadata metadata) => new( metadata.IsEligibleForSelfHost, - metadata.IsOnSecretsManagerStandalone); + metadata.IsManaged, + metadata.IsOnSecretsManagerStandalone, + metadata.IsSubscriptionUnpaid); } diff --git a/src/Api/Controllers/DevicesController.cs b/src/Api/Controllers/DevicesController.cs index bbb2b7005..f55b30eb2 100644 --- a/src/Api/Controllers/DevicesController.cs +++ b/src/Api/Controllers/DevicesController.cs @@ -196,8 +196,8 @@ public class DevicesController : Controller } [HttpDelete("{id}")] - [HttpPost("{id}/delete")] - public async Task Delete(string id) + [HttpPost("{id}/deactivate")] + public async Task Deactivate(string id) { var device = await _deviceRepository.GetByIdAsync(new Guid(id), _userService.GetProperUserId(User).Value); if (device == null) @@ -205,7 +205,7 @@ public class DevicesController : Controller throw new NotFoundException(); } - await _deviceService.DeleteAsync(device); + await _deviceService.DeactivateAsync(device); } [AllowAnonymous] diff --git a/src/Core/AdminConsole/Enums/Provider/ProviderType.cs b/src/Core/AdminConsole/Enums/Provider/ProviderType.cs index a159fe2b6..50c344ec9 100644 --- a/src/Core/AdminConsole/Enums/Provider/ProviderType.cs +++ b/src/Core/AdminConsole/Enums/Provider/ProviderType.cs @@ -4,8 +4,10 @@ namespace Bit.Core.AdminConsole.Enums.Provider; public enum ProviderType : byte { - [Display(ShortName = "MSP", Name = "Managed Service Provider", Description = "Access to clients organization")] + [Display(ShortName = "MSP", Name = "Managed Service Provider", Description = "Access to clients organization", Order = 0)] Msp = 0, - [Display(ShortName = "Reseller", Name = "Reseller", Description = "Access to clients billing")] + [Display(ShortName = "Reseller", Name = "Reseller", Description = "Access to clients billing", Order = 1000)] Reseller = 1, + [Display(ShortName = "MOE", Name = "Multi-organization Enterprise", Description = "Access to multiple organizations", Order = 1)] + MultiOrganizationEnterprise = 2, } diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationDomains/Interfaces/IOrganizationHasVerifiedDomainsQuery.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationDomains/Interfaces/IOrganizationHasVerifiedDomainsQuery.cs new file mode 100644 index 000000000..b7df7f83e --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationDomains/Interfaces/IOrganizationHasVerifiedDomainsQuery.cs @@ -0,0 +1,6 @@ +namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces; + +public interface IOrganizationHasVerifiedDomainsQuery +{ + Task HasVerifiedDomainsAsync(Guid orgId); +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationDomains/OrganizationHasVerifiedDomainsQuery.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationDomains/OrganizationHasVerifiedDomainsQuery.cs new file mode 100644 index 000000000..15a36e4f0 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationDomains/OrganizationHasVerifiedDomainsQuery.cs @@ -0,0 +1,10 @@ +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces; +using Bit.Core.Repositories; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains; + +public class OrganizationHasVerifiedDomainsQuery(IOrganizationDomainRepository domainRepository) : IOrganizationHasVerifiedDomainsQuery +{ + public async Task HasVerifiedDomainsAsync(Guid orgId) => + (await domainRepository.GetDomainsByOrganizationIdAsync(orgId)).Any(od => od.VerifiedDate is not null); +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationDomains/VerifyOrganizationDomainCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationDomains/VerifyOrganizationDomainCommand.cs index 8e1a4d573..4a597a290 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationDomains/VerifyOrganizationDomainCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationDomains/VerifyOrganizationDomainCommand.cs @@ -1,4 +1,7 @@ -using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces; +using Bit.Core.AdminConsole.Services; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; @@ -15,6 +18,9 @@ public class VerifyOrganizationDomainCommand : IVerifyOrganizationDomainCommand private readonly IDnsResolverService _dnsResolverService; private readonly IEventService _eventService; private readonly IGlobalSettings _globalSettings; + private readonly IPolicyService _policyService; + private readonly IFeatureService _featureService; + private readonly IOrganizationService _organizationService; private readonly ILogger _logger; public VerifyOrganizationDomainCommand( @@ -22,12 +28,18 @@ public class VerifyOrganizationDomainCommand : IVerifyOrganizationDomainCommand IDnsResolverService dnsResolverService, IEventService eventService, IGlobalSettings globalSettings, + IPolicyService policyService, + IFeatureService featureService, + IOrganizationService organizationService, ILogger logger) { _organizationDomainRepository = organizationDomainRepository; _dnsResolverService = dnsResolverService; _eventService = eventService; _globalSettings = globalSettings; + _policyService = policyService; + _featureService = featureService; + _organizationService = organizationService; _logger = logger; } @@ -102,6 +114,8 @@ public class VerifyOrganizationDomainCommand : IVerifyOrganizationDomainCommand if (await _dnsResolverService.ResolveAsync(domain.DomainName, domain.Txt)) { domain.SetVerifiedDate(); + + await EnableSingleOrganizationPolicyAsync(domain.OrganizationId); } } catch (Exception e) @@ -112,4 +126,13 @@ public class VerifyOrganizationDomainCommand : IVerifyOrganizationDomainCommand return domain; } + + private async Task EnableSingleOrganizationPolicyAsync(Guid organizationId) + { + if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)) + { + await _policyService.SaveAsync( + new Policy { OrganizationId = organizationId, Type = PolicyType.SingleOrg, Enabled = true }, null); + } + } } diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Authorization/OrganizationUserUserDetailsAuthorizationHandler.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Authorization/OrganizationUserUserDetailsAuthorizationHandler.cs index dcfe630e3..e890e4d9f 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Authorization/OrganizationUserUserDetailsAuthorizationHandler.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Authorization/OrganizationUserUserDetailsAuthorizationHandler.cs @@ -1,7 +1,6 @@ #nullable enable using Bit.Core.Context; using Bit.Core.Enums; -using Bit.Core.Services; using Microsoft.AspNetCore.Authorization; namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Authorization; @@ -10,12 +9,10 @@ public class OrganizationUserUserDetailsAuthorizationHandler : AuthorizationHandler { private readonly ICurrentContext _currentContext; - private readonly IFeatureService _featureService; - public OrganizationUserUserDetailsAuthorizationHandler(ICurrentContext currentContext, IFeatureService featureService) + public OrganizationUserUserDetailsAuthorizationHandler(ICurrentContext currentContext) { _currentContext = currentContext; - _featureService = featureService; } protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, @@ -37,29 +34,6 @@ public class OrganizationUserUserDetailsAuthorizationHandler } private async Task CanReadAllAsync(Guid organizationId) - { - if (_featureService.IsEnabled(FeatureFlagKeys.Pm3478RefactorOrganizationUserApi)) - { - return await CanReadAllAsync_vNext(organizationId); - } - - return await CanReadAllAsync_vCurrent(organizationId); - } - - private async Task CanReadAllAsync_vCurrent(Guid organizationId) - { - // All users of an organization can read all other users of that organization for collection access management - var org = _currentContext.GetOrganization(organizationId); - if (org is not null) - { - return true; - } - - // Allow provider users to read all organization users if they are a provider for the target organization - return await _currentContext.ProviderUserForOrgAsync(organizationId); - } - - private async Task CanReadAllAsync_vNext(Guid organizationId) { // Admins can access this for general user management var organization = _currentContext.GetOrganization(organizationId); diff --git a/src/Core/AdminConsole/Providers/Interfaces/ICreateProviderCommand.cs b/src/Core/AdminConsole/Providers/Interfaces/ICreateProviderCommand.cs index 800ec1405..bea3c08a8 100644 --- a/src/Core/AdminConsole/Providers/Interfaces/ICreateProviderCommand.cs +++ b/src/Core/AdminConsole/Providers/Interfaces/ICreateProviderCommand.cs @@ -1,4 +1,5 @@ using Bit.Core.AdminConsole.Entities.Provider; +using Bit.Core.Billing.Enums; namespace Bit.Core.AdminConsole.Providers.Interfaces; @@ -6,4 +7,5 @@ public interface ICreateProviderCommand { Task CreateMspAsync(Provider provider, string ownerEmail, int teamsMinimumSeats, int enterpriseMinimumSeats); Task CreateResellerAsync(Provider provider); + Task CreateMultiOrganizationEnterpriseAsync(Provider provider, string ownerEmail, PlanType plan, int minimumSeats); } diff --git a/src/Core/AdminConsole/Repositories/IOrganizationUserRepository.cs b/src/Core/AdminConsole/Repositories/IOrganizationUserRepository.cs index 54040e6dc..a3a68b5de 100644 --- a/src/Core/AdminConsole/Repositories/IOrganizationUserRepository.cs +++ b/src/Core/AdminConsole/Repositories/IOrganizationUserRepository.cs @@ -22,8 +22,7 @@ public interface IOrganizationUserRepository : IRepository GetByOrganizationAsync(Guid organizationId, Guid userId); Task>> GetByIdWithCollectionsAsync(Guid id); Task GetDetailsByIdAsync(Guid id); - Task>> - GetDetailsByIdWithCollectionsAsync(Guid id); + Task<(OrganizationUserUserDetails? OrganizationUser, ICollection Collections)> GetDetailsByIdWithCollectionsAsync(Guid id); Task> GetManyDetailsByOrganizationAsync(Guid organizationId, bool includeGroups = false, bool includeCollections = false); Task> GetManyDetailsByUserAsync(Guid userId, OrganizationUserStatusType? status = null); diff --git a/src/Core/AdminConsole/Services/IOrganizationDomainService.cs b/src/Core/AdminConsole/Services/IOrganizationDomainService.cs index 8ed543f0e..463371c14 100644 --- a/src/Core/AdminConsole/Services/IOrganizationDomainService.cs +++ b/src/Core/AdminConsole/Services/IOrganizationDomainService.cs @@ -4,8 +4,4 @@ public interface IOrganizationDomainService { Task ValidateOrganizationsDomainAsync(); Task OrganizationDomainMaintenanceAsync(); - /// - /// Indicates if the organization has any verified domains. - /// - Task HasVerifiedDomainsAsync(Guid orgId); } diff --git a/src/Core/AdminConsole/Services/Implementations/OrganizationDomainService.cs b/src/Core/AdminConsole/Services/Implementations/OrganizationDomainService.cs index 890042b31..4ce33f3b5 100644 --- a/src/Core/AdminConsole/Services/Implementations/OrganizationDomainService.cs +++ b/src/Core/AdminConsole/Services/Implementations/OrganizationDomainService.cs @@ -106,12 +106,6 @@ public class OrganizationDomainService : IOrganizationDomainService } } - public async Task HasVerifiedDomainsAsync(Guid orgId) - { - var orgDomains = await _domainRepository.GetDomainsByOrganizationIdAsync(orgId); - return orgDomains.Any(od => od.VerifiedDate != null); - } - private async Task> GetAdminEmailsAsync(Guid organizationId) { var orgUsers = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(organizationId); diff --git a/src/Core/AdminConsole/Services/Implementations/PolicyService.cs b/src/Core/AdminConsole/Services/Implementations/PolicyService.cs index 6ab90afe0..072aa8283 100644 --- a/src/Core/AdminConsole/Services/Implementations/PolicyService.cs +++ b/src/Core/AdminConsole/Services/Implementations/PolicyService.cs @@ -1,6 +1,7 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.Policies; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models; @@ -32,6 +33,7 @@ public class PolicyService : IPolicyService private readonly IFeatureService _featureService; private readonly ISavePolicyCommand _savePolicyCommand; private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand; + private readonly IOrganizationHasVerifiedDomainsQuery _organizationHasVerifiedDomainsQuery; public PolicyService( IApplicationCacheService applicationCacheService, @@ -45,7 +47,8 @@ public class PolicyService : IPolicyService ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery, IFeatureService featureService, ISavePolicyCommand savePolicyCommand, - IRemoveOrganizationUserCommand removeOrganizationUserCommand) + IRemoveOrganizationUserCommand removeOrganizationUserCommand, + IOrganizationHasVerifiedDomainsQuery organizationHasVerifiedDomainsQuery) { _applicationCacheService = applicationCacheService; _eventService = eventService; @@ -59,6 +62,7 @@ public class PolicyService : IPolicyService _featureService = featureService; _savePolicyCommand = savePolicyCommand; _removeOrganizationUserCommand = removeOrganizationUserCommand; + _organizationHasVerifiedDomainsQuery = organizationHasVerifiedDomainsQuery; } public async Task SaveAsync(Policy policy, Guid? savingUserId) @@ -239,6 +243,7 @@ public class PolicyService : IPolicyService case PolicyType.SingleOrg: if (!policy.Enabled) { + await HasVerifiedDomainsAsync(org); await RequiredBySsoAsync(org); await RequiredByVaultTimeoutAsync(org); await RequiredByKeyConnectorAsync(org); @@ -279,6 +284,15 @@ public class PolicyService : IPolicyService } } + private async Task HasVerifiedDomainsAsync(Organization org) + { + if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning) + && await _organizationHasVerifiedDomainsQuery.HasVerifiedDomainsAsync(org.Id)) + { + throw new BadRequestException("Organization has verified domains."); + } + } + private async Task SetPolicyConfiguration(Policy policy) { await _policyRepository.UpsertAsync(policy); diff --git a/src/Core/Auth/Models/Api/Request/Accounts/RegisterFinishRequestModel.cs b/src/Core/Auth/Models/Api/Request/Accounts/RegisterFinishRequestModel.cs index 9036651fd..0ac7dbbcb 100644 --- a/src/Core/Auth/Models/Api/Request/Accounts/RegisterFinishRequestModel.cs +++ b/src/Core/Auth/Models/Api/Request/Accounts/RegisterFinishRequestModel.cs @@ -6,6 +6,14 @@ using Bit.Core.Utilities; namespace Bit.Core.Auth.Models.Api.Request.Accounts; using System.ComponentModel.DataAnnotations; +public enum RegisterFinishTokenType : byte +{ + EmailVerification = 1, + OrganizationInvite = 2, + OrgSponsoredFreeFamilyPlan = 3, + EmergencyAccessInvite = 4, + ProviderInvite = 5, +} public class RegisterFinishRequestModel : IValidatableObject { @@ -36,6 +44,10 @@ public class RegisterFinishRequestModel : IValidatableObject public string? AcceptEmergencyAccessInviteToken { get; set; } public Guid? AcceptEmergencyAccessId { get; set; } + public string? ProviderInviteToken { get; set; } + + public Guid? ProviderUserId { get; set; } + public User ToUser() { var user = new User @@ -54,6 +66,32 @@ public class RegisterFinishRequestModel : IValidatableObject return user; } + public RegisterFinishTokenType GetTokenType() + { + if (!string.IsNullOrWhiteSpace(EmailVerificationToken)) + { + return RegisterFinishTokenType.EmailVerification; + } + if (!string.IsNullOrEmpty(OrgInviteToken) && OrganizationUserId.HasValue) + { + return RegisterFinishTokenType.OrganizationInvite; + } + if (!string.IsNullOrWhiteSpace(OrgSponsoredFreeFamilyPlanToken)) + { + return RegisterFinishTokenType.OrgSponsoredFreeFamilyPlan; + } + if (!string.IsNullOrWhiteSpace(AcceptEmergencyAccessInviteToken) && AcceptEmergencyAccessId.HasValue) + { + return RegisterFinishTokenType.EmergencyAccessInvite; + } + if (!string.IsNullOrWhiteSpace(ProviderInviteToken) && ProviderUserId.HasValue) + { + return RegisterFinishTokenType.ProviderInvite; + } + + throw new InvalidOperationException("Invalid token type."); + } + public IEnumerable Validate(ValidationContext validationContext) { diff --git a/src/Core/Auth/Models/Mail/CannotDeleteManagedAccountViewModel.cs b/src/Core/Auth/Models/Mail/CannotDeleteManagedAccountViewModel.cs new file mode 100644 index 000000000..02549a959 --- /dev/null +++ b/src/Core/Auth/Models/Mail/CannotDeleteManagedAccountViewModel.cs @@ -0,0 +1,7 @@ +using Bit.Core.Models.Mail; + +namespace Bit.Core.Auth.Models.Mail; + +public class CannotDeleteManagedAccountViewModel : BaseMailModel +{ +} diff --git a/src/Core/Auth/UserFeatures/Registration/IRegisterUserCommand.cs b/src/Core/Auth/UserFeatures/Registration/IRegisterUserCommand.cs index d507cda4e..f61cce895 100644 --- a/src/Core/Auth/UserFeatures/Registration/IRegisterUserCommand.cs +++ b/src/Core/Auth/UserFeatures/Registration/IRegisterUserCommand.cs @@ -61,4 +61,16 @@ public interface IRegisterUserCommand public Task RegisterUserViaAcceptEmergencyAccessInviteToken(User user, string masterPasswordHash, string acceptEmergencyAccessInviteToken, Guid acceptEmergencyAccessId); + /// + /// Creates a new user with a given master password hash, sends a welcome email, and raises the signup reference event. + /// If a valid token is provided, the user will be created with their email verified. + /// If the token is invalid or expired, an error will be thrown. + /// + /// The to create + /// The hashed master password the user entered + /// The provider invite token sent to the user via email + /// The provider user id which is used to validate the invite token + /// + public Task RegisterUserViaProviderInviteToken(User user, string masterPasswordHash, string providerInviteToken, Guid providerUserId); + } diff --git a/src/Core/Auth/UserFeatures/Registration/Implementations/RegisterUserCommand.cs b/src/Core/Auth/UserFeatures/Registration/Implementations/RegisterUserCommand.cs index 3bbdaaf0a..8174d7d36 100644 --- a/src/Core/Auth/UserFeatures/Registration/Implementations/RegisterUserCommand.cs +++ b/src/Core/Auth/UserFeatures/Registration/Implementations/RegisterUserCommand.cs @@ -32,6 +32,7 @@ public class RegisterUserCommand : IRegisterUserCommand private readonly IDataProtectorTokenFactory _orgUserInviteTokenDataFactory; private readonly IDataProtectorTokenFactory _registrationEmailVerificationTokenDataFactory; private readonly IDataProtector _organizationServiceDataProtector; + private readonly IDataProtector _providerServiceDataProtector; private readonly ICurrentContext _currentContext; @@ -75,6 +76,8 @@ public class RegisterUserCommand : IRegisterUserCommand _validateRedemptionTokenCommand = validateRedemptionTokenCommand; _emergencyAccessInviteTokenDataFactory = emergencyAccessInviteTokenDataFactory; + + _providerServiceDataProtector = dataProtectionProvider.CreateProtector("ProviderServiceDataProtector"); } @@ -303,6 +306,25 @@ public class RegisterUserCommand : IRegisterUserCommand return result; } + public async Task RegisterUserViaProviderInviteToken(User user, string masterPasswordHash, + string providerInviteToken, Guid providerUserId) + { + ValidateOpenRegistrationAllowed(); + ValidateProviderInviteToken(providerInviteToken, providerUserId, user.Email); + + user.EmailVerified = true; + user.ApiKey = CoreHelpers.SecureRandomString(30); // API key can't be null. + + var result = await _userService.CreateUserAsync(user, masterPasswordHash); + if (result == IdentityResult.Success) + { + await _mailService.SendWelcomeEmailAsync(user); + await _referenceEventService.RaiseEventAsync(new ReferenceEvent(ReferenceEventType.Signup, user, _currentContext)); + } + + return result; + } + private void ValidateOpenRegistrationAllowed() { // We validate open registration on send of initial email and here b/c a user could technically start the @@ -333,6 +355,15 @@ public class RegisterUserCommand : IRegisterUserCommand } } + private void ValidateProviderInviteToken(string providerInviteToken, Guid providerUserId, string userEmail) + { + if (!CoreHelpers.TokenIsValid("ProviderUserInvite", _providerServiceDataProtector, providerInviteToken, userEmail, providerUserId, + _globalSettings.OrganizationInviteExpirationHours)) + { + throw new BadRequestException("Invalid provider invite token."); + } + } + private RegistrationEmailVerificationTokenable ValidateRegistrationEmailVerificationTokenable(string emailVerificationToken, string userEmail) { diff --git a/src/Core/Billing/Extensions/BillingExtensions.cs b/src/Core/Billing/Extensions/BillingExtensions.cs index d6fa0988b..21974b318 100644 --- a/src/Core/Billing/Extensions/BillingExtensions.cs +++ b/src/Core/Billing/Extensions/BillingExtensions.cs @@ -11,11 +11,10 @@ namespace Bit.Core.Billing.Extensions; public static class BillingExtensions { public static bool IsBillable(this Provider provider) => - provider is - { - Type: ProviderType.Msp, - Status: ProviderStatusType.Billable - }; + provider.SupportsConsolidatedBilling() && provider.Status == ProviderStatusType.Billable; + + public static bool SupportsConsolidatedBilling(this Provider provider) + => provider.Type is ProviderType.Msp or ProviderType.MultiOrganizationEnterprise; public static bool IsValidClient(this Organization organization) => organization is diff --git a/src/Core/Billing/Migration/Services/Implementations/ProviderMigrator.cs b/src/Core/Billing/Migration/Services/Implementations/ProviderMigrator.cs index 9ca515a26..ea490d0d6 100644 --- a/src/Core/Billing/Migration/Services/Implementations/ProviderMigrator.cs +++ b/src/Core/Billing/Migration/Services/Implementations/ProviderMigrator.cs @@ -8,6 +8,7 @@ using Bit.Core.Billing.Enums; using Bit.Core.Billing.Migration.Models; using Bit.Core.Billing.Repositories; using Bit.Core.Billing.Services; +using Bit.Core.Billing.Services.Contracts; using Bit.Core.Repositories; using Bit.Core.Services; using Microsoft.Extensions.Logging; @@ -307,7 +308,14 @@ public class ProviderMigrator( .FirstOrDefault(providerPlan => providerPlan.PlanType == PlanType.TeamsMonthly)? .SeatMinimum ?? 0; - await providerBillingService.UpdateSeatMinimums(provider, enterpriseSeatMinimum, teamsSeatMinimum); + var updateSeatMinimumsCommand = new UpdateProviderSeatMinimumsCommand( + provider.Id, + provider.GatewaySubscriptionId, + [ + (Plan: PlanType.EnterpriseMonthly, SeatsMinimum: enterpriseSeatMinimum), + (Plan: PlanType.TeamsMonthly, SeatsMinimum: teamsSeatMinimum) + ]); + await providerBillingService.UpdateSeatMinimums(updateSeatMinimumsCommand); logger.LogInformation( "CB: Updated Stripe subscription for provider ({ProviderID}) with current seat minimums", provider.Id); @@ -325,13 +333,16 @@ public class ProviderMigrator( var organizationCancellationCredit = organizationCustomers.Sum(customer => customer.Balance); - await stripeAdapter.CustomerBalanceTransactionCreate(provider.GatewayCustomerId, - new CustomerBalanceTransactionCreateOptions - { - Amount = organizationCancellationCredit, - Currency = "USD", - Description = "Unused, prorated time for client organization subscriptions." - }); + if (organizationCancellationCredit != 0) + { + await stripeAdapter.CustomerBalanceTransactionCreate(provider.GatewayCustomerId, + new CustomerBalanceTransactionCreateOptions + { + Amount = organizationCancellationCredit, + Currency = "USD", + Description = "Unused, prorated time for client organization subscriptions." + }); + } var migrationRecords = await Task.WhenAll(organizations.Select(organization => clientOrganizationMigrationRecordRepository.GetByOrganizationId(organization.Id))); diff --git a/src/Core/Billing/Models/OrganizationMetadata.cs b/src/Core/Billing/Models/OrganizationMetadata.cs index 136964d7c..138fb6aef 100644 --- a/src/Core/Billing/Models/OrganizationMetadata.cs +++ b/src/Core/Billing/Models/OrganizationMetadata.cs @@ -2,9 +2,6 @@ public record OrganizationMetadata( bool IsEligibleForSelfHost, - bool IsOnSecretsManagerStandalone) -{ - public static OrganizationMetadata Default() => new( - IsEligibleForSelfHost: false, - IsOnSecretsManagerStandalone: false); -} + bool IsManaged, + bool IsOnSecretsManagerStandalone, + bool IsSubscriptionUnpaid); diff --git a/src/Core/Billing/Models/StaticStore/Plans/EnterprisePlan.cs b/src/Core/Billing/Models/StaticStore/Plans/EnterprisePlan.cs index f1fff0762..2d498a765 100644 --- a/src/Core/Billing/Models/StaticStore/Plans/EnterprisePlan.cs +++ b/src/Core/Billing/Models/StaticStore/Plans/EnterprisePlan.cs @@ -87,7 +87,9 @@ public record EnterprisePlan : Plan AdditionalStoragePricePerGb = 4; StripeStoragePlanId = "storage-gb-annually"; StripeSeatPlanId = "2023-enterprise-org-seat-annually"; + StripeProviderPortalSeatPlanId = "password-manager-provider-portal-enterprise-annually-2024"; SeatPrice = 72; + ProviderPortalSeatPrice = 72; } else { diff --git a/src/Core/Billing/Services/Contracts/ChangeProviderPlansCommand.cs b/src/Core/Billing/Services/Contracts/ChangeProviderPlansCommand.cs new file mode 100644 index 000000000..3e8fffdd1 --- /dev/null +++ b/src/Core/Billing/Services/Contracts/ChangeProviderPlansCommand.cs @@ -0,0 +1,8 @@ +using Bit.Core.Billing.Enums; + +namespace Bit.Core.Billing.Services.Contracts; + +public record ChangeProviderPlanCommand( + Guid ProviderPlanId, + PlanType NewPlan, + string GatewaySubscriptionId); diff --git a/src/Core/Billing/Services/Contracts/UpdateProviderSeatMinimumsCommand.cs b/src/Core/Billing/Services/Contracts/UpdateProviderSeatMinimumsCommand.cs new file mode 100644 index 000000000..86a596ffb --- /dev/null +++ b/src/Core/Billing/Services/Contracts/UpdateProviderSeatMinimumsCommand.cs @@ -0,0 +1,10 @@ +using Bit.Core.Billing.Enums; + +namespace Bit.Core.Billing.Services.Contracts; + +/// The ID of the provider to update the seat minimums for. +/// The new seat minimums for the provider. +public record UpdateProviderSeatMinimumsCommand( + Guid Id, + string GatewaySubscriptionId, + IReadOnlyCollection<(PlanType Plan, int SeatsMinimum)> Configuration); diff --git a/src/Core/Billing/Services/IProviderBillingService.cs b/src/Core/Billing/Services/IProviderBillingService.cs index 2514ca785..e353e5515 100644 --- a/src/Core/Billing/Services/IProviderBillingService.cs +++ b/src/Core/Billing/Services/IProviderBillingService.cs @@ -3,6 +3,7 @@ using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.Billing.Entities; using Bit.Core.Billing.Enums; +using Bit.Core.Billing.Services.Contracts; using Bit.Core.Models.Business; using Stripe; @@ -89,8 +90,12 @@ public interface IProviderBillingService Task SetupSubscription( Provider provider); - Task UpdateSeatMinimums( - Provider provider, - int enterpriseSeatMinimum, - int teamsSeatMinimum); + /// + /// Changes the assigned provider plan for the provider. + /// + /// The command to change the provider plan. + /// + Task ChangePlan(ChangeProviderPlanCommand command); + + Task UpdateSeatMinimums(UpdateProviderSeatMinimumsCommand command); } diff --git a/src/Core/Billing/Services/Implementations/OrganizationBillingService.cs b/src/Core/Billing/Services/Implementations/OrganizationBillingService.cs index 7db886203..bdcf0fbf6 100644 --- a/src/Core/Billing/Services/Implementations/OrganizationBillingService.cs +++ b/src/Core/Billing/Services/Implementations/OrganizationBillingService.cs @@ -1,5 +1,4 @@ using Bit.Core.AdminConsole.Entities; -using Bit.Core.AdminConsole.Repositories; using Bit.Core.Billing.Caches; using Bit.Core.Billing.Constants; using Bit.Core.Billing.Models; @@ -27,7 +26,6 @@ public class OrganizationBillingService( IGlobalSettings globalSettings, ILogger logger, IOrganizationRepository organizationRepository, - IProviderRepository providerRepository, ISetupIntentCache setupIntentCache, IStripeAdapter stripeAdapter, ISubscriberService subscriberService) : IOrganizationBillingService @@ -64,18 +62,18 @@ public class OrganizationBillingService( return null; } - var customer = await subscriberService.GetCustomer(organization, new CustomerGetOptions - { - Expand = ["discount.coupon.applies_to"] - }); + var customer = await subscriberService.GetCustomer(organization, + new CustomerGetOptions { Expand = ["discount.coupon.applies_to"] }); var subscription = await subscriberService.GetSubscription(organization); - var isEligibleForSelfHost = await IsEligibleForSelfHost(organization, subscription); - + var isEligibleForSelfHost = IsEligibleForSelfHost(organization); + var isManaged = organization.Status == OrganizationStatusType.Managed; var isOnSecretsManagerStandalone = IsOnSecretsManagerStandalone(organization, customer, subscription); + var isSubscriptionUnpaid = IsSubscriptionUnpaid(subscription); - return new OrganizationMetadata(isEligibleForSelfHost, isOnSecretsManagerStandalone); + return new OrganizationMetadata(isEligibleForSelfHost, isManaged, isOnSecretsManagerStandalone, + isSubscriptionUnpaid); } public async Task UpdatePaymentMethod( @@ -339,26 +337,12 @@ public class OrganizationBillingService( return await stripeAdapter.SubscriptionCreateAsync(subscriptionCreateOptions); } - private async Task IsEligibleForSelfHost( - Organization organization, - Subscription? organizationSubscription) + private static bool IsEligibleForSelfHost( + Organization organization) { - if (organization.Status != OrganizationStatusType.Managed) - { - return organization.Plan.Contains("Families") || - organization.Plan.Contains("Enterprise") && IsActive(organizationSubscription); - } + var eligibleSelfHostPlans = StaticStore.Plans.Where(plan => plan.HasSelfHost).Select(plan => plan.Type); - var provider = await providerRepository.GetByOrganizationIdAsync(organization.Id); - - var providerSubscription = await subscriberService.GetSubscriptionOrThrow(provider); - - return organization.Plan.Contains("Enterprise") && IsActive(providerSubscription); - - bool IsActive(Subscription? subscription) => subscription?.Status is - StripeConstants.SubscriptionStatus.Active or - StripeConstants.SubscriptionStatus.Trialing or - StripeConstants.SubscriptionStatus.PastDue; + return eligibleSelfHostPlans.Contains(organization.PlanType); } private static bool IsOnSecretsManagerStandalone( @@ -392,5 +376,16 @@ public class OrganizationBillingService( return subscriptionProductIds.Intersect(couponAppliesTo ?? []).Any(); } + private static bool IsSubscriptionUnpaid(Subscription subscription) + { + if (subscription == null) + { + return false; + } + + return subscription.Status == "unpaid"; + } + + #endregion } diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index ecbe190cc..52931582e 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -106,7 +106,6 @@ public static class FeatureFlagKeys public const string UseTreeWalkerApiForPageDetailsCollection = "use-tree-walker-api-for-page-details-collection"; public const string ItemShare = "item-share"; public const string DuoRedirect = "duo-redirect"; - public const string PM5864DollarThreshold = "PM-5864-dollar-threshold"; public const string AC2101UpdateTrialInitiationEmail = "AC-2101-update-trial-initiation-email"; public const string EnableConsolidatedBilling = "enable-consolidated-billing"; public const string AC1795_UpdatedSubscriptionStatusSection = "AC-1795_updated-subscription-status-section"; @@ -117,7 +116,6 @@ public static class FeatureFlagKeys public const string RestrictProviderAccess = "restrict-provider-access"; public const string PM4154BulkEncryptionService = "PM-4154-bulk-encryption-service"; public const string VaultBulkManagementAction = "vault-bulk-management-action"; - public const string BulkDeviceApproval = "bulk-device-approval"; public const string MemberAccessReport = "ac-2059-member-access-report"; public const string BlockLegacyUsers = "block-legacy-users"; public const string InlineMenuFieldQualification = "inline-menu-field-qualification"; @@ -142,12 +140,14 @@ public static class FeatureFlagKeys public const string EnableNewCardCombinedExpiryAutofill = "enable-new-card-combined-expiry-autofill"; public const string StorageReseedRefactor = "storage-reseed-refactor"; public const string TrialPayment = "PM-8163-trial-payment"; - public const string Pm3478RefactorOrganizationUserApi = "pm-3478-refactor-organizationuser-api"; public const string RemoveServerVersionHeader = "remove-server-version-header"; public const string AccessIntelligence = "pm-13227-access-intelligence"; public const string VerifiedSsoDomainEndpoint = "pm-12337-refactor-sso-details-endpoint"; + public const string PM12275_MultiOrganizationEnterprises = "pm-12275-multi-organization-enterprises"; public const string Pm13322AddPolicyDefinitions = "pm-13322-add-policy-definitions"; public const string LimitCollectionCreationDeletionSplit = "pm-10863-limit-collection-creation-deletion-split"; + public const string GeneratorToolsModernization = "generator-tools-modernization"; + public const string NewDeviceVerification = "new-device-verification"; public static List GetAllKeys() { @@ -163,7 +163,6 @@ public static class FeatureFlagKeys return new Dictionary() { { DuoRedirect, "true" }, - { BulkDeviceApproval, "true" }, { CipherKeyEncryption, "true" }, }; } diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index 5174543c6..6913a1e89 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -21,11 +21,11 @@ - - + + - + @@ -35,22 +35,22 @@ - - + + - + - + - + @@ -58,8 +58,8 @@ - - + + diff --git a/src/Core/Entities/Device.cs b/src/Core/Entities/Device.cs index 44929fa2d..efb011861 100644 --- a/src/Core/Entities/Device.cs +++ b/src/Core/Entities/Device.cs @@ -38,6 +38,10 @@ public class Device : ITableObject /// public string? EncryptedPrivateKey { get; set; } + /// + /// Whether the device is active for the user. + /// + public bool Active { get; set; } = true; public void SetNewId() { diff --git a/src/Core/MailTemplates/Handlebars/AdminConsole/CannotDeleteManagedAccount.html.hbs b/src/Core/MailTemplates/Handlebars/AdminConsole/CannotDeleteManagedAccount.html.hbs new file mode 100644 index 000000000..e867bf4f1 --- /dev/null +++ b/src/Core/MailTemplates/Handlebars/AdminConsole/CannotDeleteManagedAccount.html.hbs @@ -0,0 +1,15 @@ +{{#>FullHtmlLayout}} + + + + + + + +
+ You have requested to delete your account. This action cannot be completed because your account is owned by an organization. +
+ Please contact your organization administrator for additional details. +
+
+{{/FullHtmlLayout}} diff --git a/src/Core/MailTemplates/Handlebars/AdminConsole/CannotDeleteManagedAccount.text.hbs b/src/Core/MailTemplates/Handlebars/AdminConsole/CannotDeleteManagedAccount.text.hbs new file mode 100644 index 000000000..3b71a1b1f --- /dev/null +++ b/src/Core/MailTemplates/Handlebars/AdminConsole/CannotDeleteManagedAccount.text.hbs @@ -0,0 +1,6 @@ +{{#>BasicTextLayout}} +You have requested to delete your account. This action cannot be completed because your account is owned by an organization. + +Please contact your organization administrator for additional details. + +{{/BasicTextLayout}} diff --git a/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs b/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs index 58eb65faf..d11da2119 100644 --- a/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs +++ b/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs @@ -130,6 +130,7 @@ public static class OrganizationServiceCollectionExtensions services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); } private static void AddOrganizationAuthCommands(this IServiceCollection services) diff --git a/src/Core/Services/IDeviceService.cs b/src/Core/Services/IDeviceService.cs index cadc3e4be..b5f3a0b8f 100644 --- a/src/Core/Services/IDeviceService.cs +++ b/src/Core/Services/IDeviceService.cs @@ -7,7 +7,7 @@ public interface IDeviceService { Task SaveAsync(Device device); Task ClearTokenAsync(Device device); - Task DeleteAsync(Device device); + Task DeactivateAsync(Device device); Task UpdateDevicesTrustAsync(string currentDeviceIdentifier, Guid currentUserId, DeviceKeysUpdateRequestModel currentDeviceUpdate, diff --git a/src/Core/Services/IMailService.cs b/src/Core/Services/IMailService.cs index 5e786bbe0..15ed9e2ea 100644 --- a/src/Core/Services/IMailService.cs +++ b/src/Core/Services/IMailService.cs @@ -18,6 +18,7 @@ public interface IMailService ProductTierType productTier, IEnumerable products); Task SendVerifyDeleteEmailAsync(string email, Guid userId, string token); + Task SendCannotDeleteManagedAccountEmailAsync(string email); Task SendChangeEmailAlreadyExistsEmailAsync(string fromEmail, string toEmail); Task SendChangeEmailEmailAsync(string newEmailAddress, string token); Task SendTwoFactorEmailAsync(string email, string token); diff --git a/src/Core/Services/IStripeAdapter.cs b/src/Core/Services/IStripeAdapter.cs index a288e1cbe..30583ef0b 100644 --- a/src/Core/Services/IStripeAdapter.cs +++ b/src/Core/Services/IStripeAdapter.cs @@ -14,6 +14,17 @@ public interface IStripeAdapter CustomerBalanceTransactionCreateOptions options); Task SubscriptionCreateAsync(Stripe.SubscriptionCreateOptions subscriptionCreateOptions); Task SubscriptionGetAsync(string id, Stripe.SubscriptionGetOptions options = null); + + /// + /// Retrieves a subscription object for a provider. + /// + /// The subscription ID. + /// The provider ID. + /// Additional options. + /// The subscription object. + /// Thrown when the subscription doesn't belong to the provider. + Task ProviderSubscriptionGetAsync(string id, Guid providerId, Stripe.SubscriptionGetOptions options = null); + Task> SubscriptionListAsync(StripeSubscriptionListOptions subscriptionSearchOptions); Task SubscriptionUpdateAsync(string id, Stripe.SubscriptionUpdateOptions options = null); Task SubscriptionCancelAsync(string Id, Stripe.SubscriptionCancelOptions options = null); diff --git a/src/Core/Services/Implementations/DeviceService.cs b/src/Core/Services/Implementations/DeviceService.cs index 5b1e4b0f0..638e4c5e0 100644 --- a/src/Core/Services/Implementations/DeviceService.cs +++ b/src/Core/Services/Implementations/DeviceService.cs @@ -41,9 +41,18 @@ public class DeviceService : IDeviceService await _pushRegistrationService.DeleteRegistrationAsync(device.Id.ToString()); } - public async Task DeleteAsync(Device device) + public async Task DeactivateAsync(Device device) { - await _deviceRepository.DeleteAsync(device); + // already deactivated + if (!device.Active) + { + return; + } + + device.Active = false; + device.RevisionDate = DateTime.UtcNow; + await _deviceRepository.UpsertAsync(device); + await _pushRegistrationService.DeleteRegistrationAsync(device.Id.ToString()); } diff --git a/src/Core/Services/Implementations/HandlebarsMailService.cs b/src/Core/Services/Implementations/HandlebarsMailService.cs index 455b775c2..dbf056c02 100644 --- a/src/Core/Services/Implementations/HandlebarsMailService.cs +++ b/src/Core/Services/Implementations/HandlebarsMailService.cs @@ -112,6 +112,19 @@ public class HandlebarsMailService : IMailService await _mailDeliveryService.SendEmailAsync(message); } + public async Task SendCannotDeleteManagedAccountEmailAsync(string email) + { + var message = CreateDefaultMessage("Delete Your Account", email); + var model = new CannotDeleteManagedAccountViewModel + { + WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash, + SiteName = _globalSettings.SiteName, + }; + await AddMessageContentAsync(message, "AdminConsole.CannotDeleteManagedAccount", model); + message.Category = "CannotDeleteManagedAccount"; + await _mailDeliveryService.SendEmailAsync(message); + } + public async Task SendChangeEmailAlreadyExistsEmailAsync(string fromEmail, string toEmail) { var message = CreateDefaultMessage("Your Email Change", toEmail); diff --git a/src/Core/Services/Implementations/StripeAdapter.cs b/src/Core/Services/Implementations/StripeAdapter.cs index e5fee63b9..8d1833145 100644 --- a/src/Core/Services/Implementations/StripeAdapter.cs +++ b/src/Core/Services/Implementations/StripeAdapter.cs @@ -79,6 +79,20 @@ public class StripeAdapter : IStripeAdapter return _subscriptionService.GetAsync(id, options); } + public async Task ProviderSubscriptionGetAsync( + string id, + Guid providerId, + SubscriptionGetOptions options = null) + { + var subscription = await _subscriptionService.GetAsync(id, options); + if (subscription.Metadata.TryGetValue("providerId", out var value) && value == providerId.ToString()) + { + return subscription; + } + + throw new InvalidOperationException("Subscription does not belong to the provider."); + } + public Task SubscriptionUpdateAsync(string id, Stripe.SubscriptionUpdateOptions options = null) { diff --git a/src/Core/Services/Implementations/StripePaymentService.cs b/src/Core/Services/Implementations/StripePaymentService.cs index 1720447b4..7eb2b402b 100644 --- a/src/Core/Services/Implementations/StripePaymentService.cs +++ b/src/Core/Services/Implementations/StripePaymentService.cs @@ -792,19 +792,16 @@ public class StripePaymentService : IPaymentService var daysUntilDue = sub.DaysUntilDue; var chargeNow = collectionMethod == "charge_automatically"; var updatedItemOptions = subscriptionUpdate.UpgradeItemsOptions(sub); - var isPm5864DollarThresholdEnabled = _featureService.IsEnabled(FeatureFlagKeys.PM5864DollarThreshold); var isAnnualPlan = sub?.Items?.Data.FirstOrDefault()?.Plan?.Interval == "year"; var subUpdateOptions = new SubscriptionUpdateOptions { Items = updatedItemOptions, - ProrationBehavior = !isPm5864DollarThresholdEnabled || invoiceNow - ? Constants.AlwaysInvoice - : Constants.CreateProrations, + ProrationBehavior = invoiceNow ? Constants.AlwaysInvoice : Constants.CreateProrations, DaysUntilDue = daysUntilDue ?? 1, CollectionMethod = "send_invoice" }; - if (!invoiceNow && isAnnualPlan && isPm5864DollarThresholdEnabled && sub.Status.Trim() != "trialing") + if (!invoiceNow && isAnnualPlan && sub.Status.Trim() != "trialing") { subUpdateOptions.PendingInvoiceItemInterval = new SubscriptionPendingInvoiceItemIntervalOptions { Interval = "month" }; @@ -838,7 +835,7 @@ public class StripePaymentService : IPaymentService { try { - if (!isPm5864DollarThresholdEnabled && !invoiceNow) + if (invoiceNow) { if (chargeNow) { diff --git a/src/Core/Services/Implementations/UserService.cs b/src/Core/Services/Implementations/UserService.cs index f2e1d183d..2199d0a7a 100644 --- a/src/Core/Services/Implementations/UserService.cs +++ b/src/Core/Services/Implementations/UserService.cs @@ -297,6 +297,12 @@ public class UserService : UserManager, IUserService, IDisposable return; } + if (await IsManagedByAnyOrganizationAsync(user.Id)) + { + await _mailService.SendCannotDeleteManagedAccountEmailAsync(user.Email); + return; + } + var token = await base.GenerateUserTokenAsync(user, TokenOptions.DefaultProvider, "DeleteAccount"); await _mailService.SendVerifyDeleteEmailAsync(user.Email, user.Id, token); } diff --git a/src/Core/Services/NoopImplementations/NoopMailService.cs b/src/Core/Services/NoopImplementations/NoopMailService.cs index f637ae904..9b8a9abee 100644 --- a/src/Core/Services/NoopImplementations/NoopMailService.cs +++ b/src/Core/Services/NoopImplementations/NoopMailService.cs @@ -94,6 +94,11 @@ public class NoopMailService : IMailService return Task.FromResult(0); } + public Task SendCannotDeleteManagedAccountEmailAsync(string email) + { + return Task.FromResult(0); + } + public Task SendPasswordlessSignInAsync(string returnUrl, string token, string email) { return Task.FromResult(0); diff --git a/src/Identity/Controllers/AccountsController.cs b/src/Identity/Controllers/AccountsController.cs index 38316566c..40c926bda 100644 --- a/src/Identity/Controllers/AccountsController.cs +++ b/src/Identity/Controllers/AccountsController.cs @@ -1,4 +1,5 @@ -using Bit.Core; +using System.Diagnostics; +using Bit.Core; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Models.Api.Request.Accounts; using Bit.Core.Auth.Models.Api.Response.Accounts; @@ -149,40 +150,44 @@ public class AccountsController : Controller IdentityResult identityResult = null; var delaysEnabled = !_featureService.IsEnabled(FeatureFlagKeys.EmailVerificationDisableTimingDelays); - if (!string.IsNullOrEmpty(model.OrgInviteToken) && model.OrganizationUserId.HasValue) + switch (model.GetTokenType()) { - identityResult = await _registerUserCommand.RegisterUserViaOrganizationInviteToken(user, model.MasterPasswordHash, - model.OrgInviteToken, model.OrganizationUserId); + case RegisterFinishTokenType.EmailVerification: + identityResult = + await _registerUserCommand.RegisterUserViaEmailVerificationToken(user, model.MasterPasswordHash, + model.EmailVerificationToken); - return await ProcessRegistrationResult(identityResult, user, delaysEnabled); + return await ProcessRegistrationResult(identityResult, user, delaysEnabled); + break; + case RegisterFinishTokenType.OrganizationInvite: + identityResult = await _registerUserCommand.RegisterUserViaOrganizationInviteToken(user, model.MasterPasswordHash, + model.OrgInviteToken, model.OrganizationUserId); + + return await ProcessRegistrationResult(identityResult, user, delaysEnabled); + break; + case RegisterFinishTokenType.OrgSponsoredFreeFamilyPlan: + identityResult = await _registerUserCommand.RegisterUserViaOrganizationSponsoredFreeFamilyPlanInviteToken(user, model.MasterPasswordHash, model.OrgSponsoredFreeFamilyPlanToken); + + return await ProcessRegistrationResult(identityResult, user, delaysEnabled); + break; + case RegisterFinishTokenType.EmergencyAccessInvite: + Debug.Assert(model.AcceptEmergencyAccessId.HasValue); + identityResult = await _registerUserCommand.RegisterUserViaAcceptEmergencyAccessInviteToken(user, model.MasterPasswordHash, + model.AcceptEmergencyAccessInviteToken, model.AcceptEmergencyAccessId.Value); + + return await ProcessRegistrationResult(identityResult, user, delaysEnabled); + break; + case RegisterFinishTokenType.ProviderInvite: + Debug.Assert(model.ProviderUserId.HasValue); + identityResult = await _registerUserCommand.RegisterUserViaProviderInviteToken(user, model.MasterPasswordHash, + model.ProviderInviteToken, model.ProviderUserId.Value); + + return await ProcessRegistrationResult(identityResult, user, delaysEnabled); + break; + + default: + throw new BadRequestException("Invalid registration finish request"); } - - if (!string.IsNullOrEmpty(model.OrgSponsoredFreeFamilyPlanToken)) - { - identityResult = await _registerUserCommand.RegisterUserViaOrganizationSponsoredFreeFamilyPlanInviteToken(user, model.MasterPasswordHash, model.OrgSponsoredFreeFamilyPlanToken); - - return await ProcessRegistrationResult(identityResult, user, delaysEnabled); - } - - if (!string.IsNullOrEmpty(model.AcceptEmergencyAccessInviteToken) && model.AcceptEmergencyAccessId.HasValue) - { - identityResult = await _registerUserCommand.RegisterUserViaAcceptEmergencyAccessInviteToken(user, model.MasterPasswordHash, - model.AcceptEmergencyAccessInviteToken, model.AcceptEmergencyAccessId.Value); - - return await ProcessRegistrationResult(identityResult, user, delaysEnabled); - } - - if (string.IsNullOrEmpty(model.EmailVerificationToken)) - { - throw new BadRequestException("Invalid registration finish request"); - } - - identityResult = - await _registerUserCommand.RegisterUserViaEmailVerificationToken(user, model.MasterPasswordHash, - model.EmailVerificationToken); - - return await ProcessRegistrationResult(identityResult, user, delaysEnabled); - } private async Task ProcessRegistrationResult(IdentityResult result, User user, bool delaysEnabled) diff --git a/src/Identity/IdentityServer/ApiClient.cs b/src/Identity/IdentityServer/ApiClient.cs index 02fd3dd40..5d768ae80 100644 --- a/src/Identity/IdentityServer/ApiClient.cs +++ b/src/Identity/IdentityServer/ApiClient.cs @@ -1,4 +1,5 @@ using Bit.Core.Settings; +using Bit.Identity.IdentityServer.RequestValidators; using Duende.IdentityServer.Models; namespace Bit.Identity.IdentityServer; diff --git a/src/Identity/IdentityServer/BaseRequestValidator.cs b/src/Identity/IdentityServer/RequestValidators/BaseRequestValidator.cs similarity index 53% rename from src/Identity/IdentityServer/BaseRequestValidator.cs rename to src/Identity/IdentityServer/RequestValidators/BaseRequestValidator.cs index 8129a1a10..185d32a7f 100644 --- a/src/Identity/IdentityServer/BaseRequestValidator.cs +++ b/src/Identity/IdentityServer/RequestValidators/BaseRequestValidator.cs @@ -1,15 +1,10 @@ using System.Security.Claims; -using System.Text.Json; using Bit.Core; -using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Services; using Bit.Core.Auth.Entities; using Bit.Core.Auth.Enums; -using Bit.Core.Auth.Identity; -using Bit.Core.Auth.Models; using Bit.Core.Auth.Models.Api.Response; -using Bit.Core.Auth.Models.Business.Tokenables; using Bit.Core.Auth.Repositories; using Bit.Core.Context; using Bit.Core.Entities; @@ -17,32 +12,26 @@ using Bit.Core.Enums; using Bit.Core.Identity; using Bit.Core.Models.Api; using Bit.Core.Models.Api.Response; -using Bit.Core.Models.Data.Organizations; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; -using Bit.Core.Tokens; using Bit.Core.Utilities; using Duende.IdentityServer.Validation; using Microsoft.AspNetCore.Identity; -namespace Bit.Identity.IdentityServer; +namespace Bit.Identity.IdentityServer.RequestValidators; public abstract class BaseRequestValidator where T : class { private UserManager _userManager; private readonly IEventService _eventService; private readonly IDeviceValidator _deviceValidator; - private readonly IOrganizationDuoWebTokenProvider _organizationDuoWebTokenProvider; - private readonly ITemporaryDuoWebV4SDKService _duoWebV4SDKService; - private readonly IOrganizationRepository _organizationRepository; + private readonly ITwoFactorAuthenticationValidator _twoFactorAuthenticationValidator; private readonly IOrganizationUserRepository _organizationUserRepository; - private readonly IApplicationCacheService _applicationCacheService; private readonly IMailService _mailService; private readonly ILogger _logger; private readonly GlobalSettings _globalSettings; private readonly IUserRepository _userRepository; - private readonly IDataProtectorTokenFactory _tokenDataFactory; protected ICurrentContext CurrentContext { get; } protected IPolicyService PolicyService { get; } @@ -56,18 +45,14 @@ public abstract class BaseRequestValidator where T : class IUserService userService, IEventService eventService, IDeviceValidator deviceValidator, - IOrganizationDuoWebTokenProvider organizationDuoWebTokenProvider, - ITemporaryDuoWebV4SDKService duoWebV4SDKService, - IOrganizationRepository organizationRepository, + ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator, IOrganizationUserRepository organizationUserRepository, - IApplicationCacheService applicationCacheService, IMailService mailService, ILogger logger, ICurrentContext currentContext, GlobalSettings globalSettings, IUserRepository userRepository, IPolicyService policyService, - IDataProtectorTokenFactory tokenDataFactory, IFeatureService featureService, ISsoConfigRepository ssoConfigRepository, IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder) @@ -76,18 +61,14 @@ public abstract class BaseRequestValidator where T : class _userService = userService; _eventService = eventService; _deviceValidator = deviceValidator; - _organizationDuoWebTokenProvider = organizationDuoWebTokenProvider; - _duoWebV4SDKService = duoWebV4SDKService; - _organizationRepository = organizationRepository; + _twoFactorAuthenticationValidator = twoFactorAuthenticationValidator; _organizationUserRepository = organizationUserRepository; - _applicationCacheService = applicationCacheService; _mailService = mailService; _logger = logger; CurrentContext = currentContext; _globalSettings = globalSettings; PolicyService = policyService; _userRepository = userRepository; - _tokenDataFactory = tokenDataFactory; FeatureService = featureService; SsoConfigRepository = ssoConfigRepository; UserDecryptionOptionsBuilder = userDecryptionOptionsBuilder; @@ -104,12 +85,6 @@ public abstract class BaseRequestValidator where T : class request.UserName, validatorContext.CaptchaResponse.Score); } - var twoFactorToken = request.Raw["TwoFactorToken"]?.ToString(); - var twoFactorProvider = request.Raw["TwoFactorProvider"]?.ToString(); - var twoFactorRemember = request.Raw["TwoFactorRemember"]?.ToString() == "1"; - var twoFactorRequest = !string.IsNullOrWhiteSpace(twoFactorToken) && - !string.IsNullOrWhiteSpace(twoFactorProvider); - var valid = await ValidateContextAsync(context, validatorContext); var user = validatorContext.User; if (!valid) @@ -123,17 +98,37 @@ public abstract class BaseRequestValidator where T : class return; } - var (isTwoFactorRequired, twoFactorOrganization) = await RequiresTwoFactorAsync(user, request); + var (isTwoFactorRequired, twoFactorOrganization) = await _twoFactorAuthenticationValidator.RequiresTwoFactorAsync(user, request); + var twoFactorToken = request.Raw["TwoFactorToken"]?.ToString(); + var twoFactorProvider = request.Raw["TwoFactorProvider"]?.ToString(); + var twoFactorRemember = request.Raw["TwoFactorRemember"]?.ToString() == "1"; + var validTwoFactorRequest = !string.IsNullOrWhiteSpace(twoFactorToken) && + !string.IsNullOrWhiteSpace(twoFactorProvider); + if (isTwoFactorRequired) { - if (!twoFactorRequest || !Enum.TryParse(twoFactorProvider, out TwoFactorProviderType twoFactorProviderType)) + // 2FA required and not provided response + if (!validTwoFactorRequest || + !Enum.TryParse(twoFactorProvider, out TwoFactorProviderType twoFactorProviderType)) { - await BuildTwoFactorResultAsync(user, twoFactorOrganization, context); + var resultDict = await _twoFactorAuthenticationValidator + .BuildTwoFactorResultAsync(user, twoFactorOrganization); + if (resultDict == null) + { + await BuildErrorResultAsync("No two-step providers enabled.", false, context, user); + return; + } + + // Include Master Password Policy in 2FA response + resultDict.Add("MasterPasswordPolicy", await GetMasterPasswordPolicy(user)); + SetTwoFactorResult(context, resultDict); return; } - var verified = await VerifyTwoFactor(user, twoFactorOrganization, - twoFactorProviderType, twoFactorToken); + var verified = await _twoFactorAuthenticationValidator + .VerifyTwoFactor(user, twoFactorOrganization, twoFactorProviderType, twoFactorToken); + + // 2FA required but request not valid or remember token expired response if (!verified || isBot) { if (twoFactorProviderType != TwoFactorProviderType.Remember) @@ -143,16 +138,20 @@ public abstract class BaseRequestValidator where T : class } else if (twoFactorProviderType == TwoFactorProviderType.Remember) { - await BuildTwoFactorResultAsync(user, twoFactorOrganization, context); + var resultDict = await _twoFactorAuthenticationValidator + .BuildTwoFactorResultAsync(user, twoFactorOrganization); + + // Include Master Password Policy in 2FA response + resultDict.Add("MasterPasswordPolicy", await GetMasterPasswordPolicy(user)); + SetTwoFactorResult(context, resultDict); } return; } } else { - twoFactorRequest = false; + validTwoFactorRequest = false; twoFactorRemember = false; - twoFactorToken = null; } // Force legacy users to the web for migration @@ -165,7 +164,6 @@ public abstract class BaseRequestValidator where T : class } } - // Returns true if can finish validation process if (await IsValidAuthTypeAsync(user, request.GrantType)) { var device = await _deviceValidator.SaveDeviceAsync(user, request); @@ -174,8 +172,7 @@ public abstract class BaseRequestValidator where T : class await BuildErrorResultAsync("No device information provided.", false, context, user); return; } - - await BuildSuccessResultAsync(user, context, device, twoFactorRequest && twoFactorRemember); + await BuildSuccessResultAsync(user, context, device, validTwoFactorRequest && twoFactorRemember); } else { @@ -238,67 +235,6 @@ public abstract class BaseRequestValidator where T : class await SetSuccessResult(context, user, claims, customResponse); } - protected async Task BuildTwoFactorResultAsync(User user, Organization organization, T context) - { - var providerKeys = new List(); - var providers = new Dictionary>(); - - var enabledProviders = new List>(); - if (organization?.GetTwoFactorProviders() != null) - { - enabledProviders.AddRange(organization.GetTwoFactorProviders().Where( - p => organization.TwoFactorProviderIsEnabled(p.Key))); - } - - if (user.GetTwoFactorProviders() != null) - { - foreach (var p in user.GetTwoFactorProviders()) - { - if (await _userService.TwoFactorProviderIsEnabledAsync(p.Key, user)) - { - enabledProviders.Add(p); - } - } - } - - if (!enabledProviders.Any()) - { - await BuildErrorResultAsync("No two-step providers enabled.", false, context, user); - return; - } - - foreach (var provider in enabledProviders) - { - providerKeys.Add((byte)provider.Key); - var infoDict = await BuildTwoFactorParams(organization, user, provider.Key, provider.Value); - providers.Add(((byte)provider.Key).ToString(), infoDict); - } - - var twoFactorResultDict = new Dictionary - { - { "TwoFactorProviders", providers.Keys }, - { "TwoFactorProviders2", providers }, - { "MasterPasswordPolicy", await GetMasterPasswordPolicy(user) }, - }; - - // If we have email as a 2FA provider, we might need an SsoEmail2fa Session Token - if (enabledProviders.Any(p => p.Key == TwoFactorProviderType.Email)) - { - twoFactorResultDict.Add("SsoEmail2faSessionToken", - _tokenDataFactory.Protect(new SsoEmail2faSessionTokenable(user))); - - twoFactorResultDict.Add("Email", user.Email); - } - - SetTwoFactorResult(context, twoFactorResultDict); - - if (enabledProviders.Count() == 1 && enabledProviders.First().Key == TwoFactorProviderType.Email) - { - // Send email now if this is their only 2FA method - await _userService.SendTwoFactorEmailAsync(user); - } - } - protected async Task BuildErrorResultAsync(string message, bool twoFactorRequest, T context, User user) { if (user != null) @@ -329,35 +265,13 @@ public abstract class BaseRequestValidator where T : class protected abstract void SetErrorResult(T context, Dictionary customResponse); protected abstract ClaimsPrincipal GetSubject(T context); - protected virtual async Task> RequiresTwoFactorAsync(User user, ValidatedTokenRequest request) - { - if (request.GrantType == "client_credentials") - { - // Do not require MFA for api key logins - return new Tuple(false, null); - } - - var individualRequired = _userManager.SupportsUserTwoFactor && - await _userManager.GetTwoFactorEnabledAsync(user) && - (await _userManager.GetValidTwoFactorProvidersAsync(user)).Count > 0; - - Organization firstEnabledOrg = null; - var orgs = (await CurrentContext.OrganizationMembershipAsync(_organizationUserRepository, user.Id)).ToList(); - if (orgs.Count > 0) - { - var orgAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync(); - var twoFactorOrgs = orgs.Where(o => OrgUsing2fa(orgAbilities, o.Id)); - if (twoFactorOrgs.Any()) - { - var userOrgs = await _organizationRepository.GetManyByUserIdAsync(user.Id); - firstEnabledOrg = userOrgs.FirstOrDefault( - o => orgs.Any(om => om.Id == o.Id) && o.TwoFactorIsEnabled()); - } - } - - return new Tuple(individualRequired || firstEnabledOrg != null, firstEnabledOrg); - } - + /// + /// Check if the user is required to authenticate via SSO. If the user requires SSO, but they are + /// logging in using an API Key (client_credentials) then they are allowed to bypass the SSO requirement. + /// + /// user trying to login + /// magic string identifying the grant type requested + /// private async Task IsValidAuthTypeAsync(User user, string grantType) { if (grantType == "authorization_code" || grantType == "client_credentials") @@ -367,7 +281,6 @@ public abstract class BaseRequestValidator where T : class return true; } - // Check if user belongs to any organization with an active SSO policy var anySsoPoliciesApplicableToUser = await PolicyService.AnyPoliciesApplicableToUserAsync(user.Id, PolicyType.RequireSso, OrganizationUserStatusType.Confirmed); if (anySsoPoliciesApplicableToUser) @@ -379,134 +292,6 @@ public abstract class BaseRequestValidator where T : class return true; } - private bool OrgUsing2fa(IDictionary orgAbilities, Guid orgId) - { - return orgAbilities != null && orgAbilities.ContainsKey(orgId) && - orgAbilities[orgId].Enabled && orgAbilities[orgId].Using2fa; - } - - private async Task VerifyTwoFactor(User user, Organization organization, TwoFactorProviderType type, - string token) - { - switch (type) - { - case TwoFactorProviderType.Authenticator: - case TwoFactorProviderType.Email: - case TwoFactorProviderType.Duo: - case TwoFactorProviderType.YubiKey: - case TwoFactorProviderType.WebAuthn: - case TwoFactorProviderType.Remember: - if (type != TwoFactorProviderType.Remember && - !await _userService.TwoFactorProviderIsEnabledAsync(type, user)) - { - return false; - } - // DUO SDK v4 Update: try to validate the token - PM-5156 addresses tech debt - if (FeatureService.IsEnabled(FeatureFlagKeys.DuoRedirect)) - { - if (type == TwoFactorProviderType.Duo) - { - if (!token.Contains(':')) - { - // We have to send the provider to the DuoWebV4SDKService to create the DuoClient - var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Duo); - return await _duoWebV4SDKService.ValidateAsync(token, provider, user); - } - } - } - - return await _userManager.VerifyTwoFactorTokenAsync(user, - CoreHelpers.CustomProviderName(type), token); - case TwoFactorProviderType.OrganizationDuo: - if (!organization?.TwoFactorProviderIsEnabled(type) ?? true) - { - return false; - } - - // DUO SDK v4 Update: try to validate the token - PM-5156 addresses tech debt - if (FeatureService.IsEnabled(FeatureFlagKeys.DuoRedirect)) - { - if (type == TwoFactorProviderType.OrganizationDuo) - { - if (!token.Contains(':')) - { - // We have to send the provider to the DuoWebV4SDKService to create the DuoClient - var provider = organization.GetTwoFactorProvider(TwoFactorProviderType.OrganizationDuo); - return await _duoWebV4SDKService.ValidateAsync(token, provider, user); - } - } - } - - return await _organizationDuoWebTokenProvider.ValidateAsync(token, organization, user); - default: - return false; - } - } - - private async Task> BuildTwoFactorParams(Organization organization, User user, - TwoFactorProviderType type, TwoFactorProvider provider) - { - switch (type) - { - case TwoFactorProviderType.Duo: - case TwoFactorProviderType.WebAuthn: - case TwoFactorProviderType.Email: - case TwoFactorProviderType.YubiKey: - if (!await _userService.TwoFactorProviderIsEnabledAsync(type, user)) - { - return null; - } - - var token = await _userManager.GenerateTwoFactorTokenAsync(user, - CoreHelpers.CustomProviderName(type)); - if (type == TwoFactorProviderType.Duo) - { - var duoResponse = new Dictionary - { - ["Host"] = provider.MetaData["Host"], - ["AuthUrl"] = await _duoWebV4SDKService.GenerateAsync(provider, user), - }; - - return duoResponse; - } - else if (type == TwoFactorProviderType.WebAuthn) - { - if (token == null) - { - return null; - } - - return JsonSerializer.Deserialize>(token); - } - else if (type == TwoFactorProviderType.Email) - { - var twoFactorEmail = (string)provider.MetaData["Email"]; - var redactedEmail = CoreHelpers.RedactEmailAddress(twoFactorEmail); - return new Dictionary { ["Email"] = redactedEmail }; - } - else if (type == TwoFactorProviderType.YubiKey) - { - return new Dictionary { ["Nfc"] = (bool)provider.MetaData["Nfc"] }; - } - - return null; - case TwoFactorProviderType.OrganizationDuo: - if (await _organizationDuoWebTokenProvider.CanGenerateTwoFactorTokenAsync(organization)) - { - var duoResponse = new Dictionary - { - ["Host"] = provider.MetaData["Host"], - ["AuthUrl"] = await _duoWebV4SDKService.GenerateAsync(provider, user), - }; - - return duoResponse; - } - return null; - default: - return null; - } - } - private async Task ResetFailedAuthDetailsAsync(User user) { // Early escape if db hit not necessary @@ -546,7 +331,7 @@ public abstract class BaseRequestValidator where T : class } /// - /// checks to see if a user is trying to log into a new device + /// checks to see if a user is trying to log into a new device /// and has reached the maximum number of failed login attempts. /// /// boolean diff --git a/src/Identity/IdentityServer/CustomTokenRequestValidator.cs b/src/Identity/IdentityServer/RequestValidators/CustomTokenRequestValidator.cs similarity index 88% rename from src/Identity/IdentityServer/CustomTokenRequestValidator.cs rename to src/Identity/IdentityServer/RequestValidators/CustomTokenRequestValidator.cs index 0d7a92c8a..c826243f8 100644 --- a/src/Identity/IdentityServer/CustomTokenRequestValidator.cs +++ b/src/Identity/IdentityServer/RequestValidators/CustomTokenRequestValidator.cs @@ -1,9 +1,7 @@ using System.Diagnostics; using System.Security.Claims; using Bit.Core.AdminConsole.Services; -using Bit.Core.Auth.Identity; using Bit.Core.Auth.Models.Api.Response; -using Bit.Core.Auth.Models.Business.Tokenables; using Bit.Core.Auth.Repositories; using Bit.Core.Context; using Bit.Core.Entities; @@ -11,7 +9,6 @@ using Bit.Core.IdentityServer; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; -using Bit.Core.Tokens; using Duende.IdentityServer.Extensions; using Duende.IdentityServer.Validation; using HandlebarsDotNet; @@ -20,7 +17,7 @@ using Microsoft.AspNetCore.Identity; #nullable enable -namespace Bit.Identity.IdentityServer; +namespace Bit.Identity.IdentityServer.RequestValidators; public class CustomTokenRequestValidator : BaseRequestValidator, ICustomTokenRequestValidator @@ -29,28 +26,36 @@ public class CustomTokenRequestValidator : BaseRequestValidator userManager, - IDeviceValidator deviceValidator, IUserService userService, IEventService eventService, - IOrganizationDuoWebTokenProvider organizationDuoWebTokenProvider, - ITemporaryDuoWebV4SDKService duoWebV4SDKService, - IOrganizationRepository organizationRepository, + IDeviceValidator deviceValidator, + ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator, IOrganizationUserRepository organizationUserRepository, - IApplicationCacheService applicationCacheService, IMailService mailService, ILogger logger, ICurrentContext currentContext, GlobalSettings globalSettings, - ISsoConfigRepository ssoConfigRepository, IUserRepository userRepository, IPolicyService policyService, - IDataProtectorTokenFactory tokenDataFactory, IFeatureService featureService, - IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder) - : base(userManager, userService, eventService, deviceValidator, - organizationDuoWebTokenProvider, duoWebV4SDKService, organizationRepository, organizationUserRepository, - applicationCacheService, mailService, logger, currentContext, globalSettings, - userRepository, policyService, tokenDataFactory, featureService, ssoConfigRepository, + ISsoConfigRepository ssoConfigRepository, + IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder + ) + : base( + userManager, + userService, + eventService, + deviceValidator, + twoFactorAuthenticationValidator, + organizationUserRepository, + mailService, + logger, + currentContext, + globalSettings, + userRepository, + policyService, + featureService, + ssoConfigRepository, userDecryptionOptionsBuilder) { _userManager = userManager; @@ -70,7 +75,7 @@ public class CustomTokenRequestValidator : BaseRequestValidator + /// Save a device to the database. If the device is already known, it will be returned. + /// + /// The user is assumed NOT null, still going to check though + /// Duende Validated Request that contains the data to create the device object + /// Returns null if user or device is malformed; The existing device if already in DB; a new device login public async Task SaveDeviceAsync(User user, ValidatedTokenRequest request) { var device = GetDeviceFromRequest(request); diff --git a/src/Identity/IdentityServer/ResourceOwnerPasswordValidator.cs b/src/Identity/IdentityServer/RequestValidators/ResourceOwnerPasswordValidator.cs similarity index 89% rename from src/Identity/IdentityServer/ResourceOwnerPasswordValidator.cs rename to src/Identity/IdentityServer/RequestValidators/ResourceOwnerPasswordValidator.cs index 08560e240..f072a6417 100644 --- a/src/Identity/IdentityServer/ResourceOwnerPasswordValidator.cs +++ b/src/Identity/IdentityServer/RequestValidators/ResourceOwnerPasswordValidator.cs @@ -1,8 +1,6 @@ using System.Security.Claims; using Bit.Core; using Bit.Core.AdminConsole.Services; -using Bit.Core.Auth.Identity; -using Bit.Core.Auth.Models.Business.Tokenables; using Bit.Core.Auth.Repositories; using Bit.Core.Auth.Services; using Bit.Core.Context; @@ -10,13 +8,12 @@ using Bit.Core.Entities; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; -using Bit.Core.Tokens; using Bit.Core.Utilities; using Duende.IdentityServer.Models; using Duende.IdentityServer.Validation; using Microsoft.AspNetCore.Identity; -namespace Bit.Identity.IdentityServer; +namespace Bit.Identity.IdentityServer.RequestValidators; public class ResourceOwnerPasswordValidator : BaseRequestValidator, IResourceOwnerPasswordValidator @@ -31,11 +28,8 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator logger, ICurrentContext currentContext, @@ -44,14 +38,25 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator tokenDataFactory, IFeatureService featureService, ISsoConfigRepository ssoConfigRepository, IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder) - : base(userManager, userService, eventService, deviceValidator, - organizationDuoWebTokenProvider, duoWebV4SDKService, organizationRepository, organizationUserRepository, - applicationCacheService, mailService, logger, currentContext, globalSettings, userRepository, policyService, - tokenDataFactory, featureService, ssoConfigRepository, userDecryptionOptionsBuilder) + : base( + userManager, + userService, + eventService, + deviceValidator, + twoFactorAuthenticationValidator, + organizationUserRepository, + mailService, + logger, + currentContext, + globalSettings, + userRepository, + policyService, + featureService, + ssoConfigRepository, + userDecryptionOptionsBuilder) { _userManager = userManager; _currentContext = currentContext; diff --git a/src/Identity/IdentityServer/RequestValidators/TwoFactorAuthenticationValidator.cs b/src/Identity/IdentityServer/RequestValidators/TwoFactorAuthenticationValidator.cs new file mode 100644 index 000000000..323d09c0e --- /dev/null +++ b/src/Identity/IdentityServer/RequestValidators/TwoFactorAuthenticationValidator.cs @@ -0,0 +1,297 @@ + +using System.Text.Json; +using Bit.Core; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Auth.Enums; +using Bit.Core.Auth.Identity; +using Bit.Core.Auth.Models; +using Bit.Core.Auth.Models.Business.Tokenables; +using Bit.Core.Context; +using Bit.Core.Entities; +using Bit.Core.Models.Data.Organizations; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Bit.Core.Tokens; +using Bit.Core.Utilities; +using Duende.IdentityServer.Validation; +using Microsoft.AspNetCore.Identity; + +namespace Bit.Identity.IdentityServer.RequestValidators; + +public interface ITwoFactorAuthenticationValidator +{ + /// + /// Check if the user is required to use two-factor authentication to login. This is based on the user's + /// enabled two-factor providers, the user's organizations enabled two-factor providers, and the grant type. + /// Client credentials and webauthn grant types do not require two-factor authentication. + /// + /// the active user for the request + /// the request that contains the grant types + /// boolean + Task> RequiresTwoFactorAsync(User user, ValidatedTokenRequest request); + /// + /// Builds the two-factor authentication result for the user based on the available two-factor providers + /// from either their user account or Organization. + /// + /// user trying to login + /// organization associated with the user; Can be null + /// Dictionary with the TwoFactorProviderType as the Key and the Provider Metadata as the Value + Task> BuildTwoFactorResultAsync(User user, Organization organization); + /// + /// Uses the built in userManager methods to verify the two-factor token for the user. If the organization uses + /// organization duo, it will use the organization duo token provider to verify the token. + /// + /// the active User + /// organization of user; can be null + /// Two Factor Provider to use to verify the token + /// secret passed from the user and consumed by the two-factor provider's verify method + /// boolean + Task VerifyTwoFactor(User user, Organization organization, TwoFactorProviderType twoFactorProviderType, string token); +} + +public class TwoFactorAuthenticationValidator( + IUserService userService, + UserManager userManager, + IOrganizationDuoWebTokenProvider organizationDuoWebTokenProvider, + ITemporaryDuoWebV4SDKService duoWebV4SDKService, + IFeatureService featureService, + IApplicationCacheService applicationCacheService, + IOrganizationUserRepository organizationUserRepository, + IOrganizationRepository organizationRepository, + IDataProtectorTokenFactory ssoEmail2faSessionTokeFactory, + ICurrentContext currentContext) : ITwoFactorAuthenticationValidator +{ + private readonly IUserService _userService = userService; + private readonly UserManager _userManager = userManager; + private readonly IOrganizationDuoWebTokenProvider _organizationDuoWebTokenProvider = organizationDuoWebTokenProvider; + private readonly ITemporaryDuoWebV4SDKService _duoWebV4SDKService = duoWebV4SDKService; + private readonly IFeatureService _featureService = featureService; + private readonly IApplicationCacheService _applicationCacheService = applicationCacheService; + private readonly IOrganizationUserRepository _organizationUserRepository = organizationUserRepository; + private readonly IOrganizationRepository _organizationRepository = organizationRepository; + private readonly IDataProtectorTokenFactory _ssoEmail2faSessionTokeFactory = ssoEmail2faSessionTokeFactory; + private readonly ICurrentContext _currentContext = currentContext; + + public async Task> RequiresTwoFactorAsync(User user, ValidatedTokenRequest request) + { + if (request.GrantType == "client_credentials" || request.GrantType == "webauthn") + { + /* + Do not require MFA for api key logins. + We consider Fido2 userVerification a second factor, so we don't require a second factor here. + */ + return new Tuple(false, null); + } + + var individualRequired = _userManager.SupportsUserTwoFactor && + await _userManager.GetTwoFactorEnabledAsync(user) && + (await _userManager.GetValidTwoFactorProvidersAsync(user)).Count > 0; + + Organization firstEnabledOrg = null; + var orgs = (await _currentContext.OrganizationMembershipAsync(_organizationUserRepository, user.Id)).ToList(); + if (orgs.Count > 0) + { + var orgAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync(); + var twoFactorOrgs = orgs.Where(o => OrgUsing2fa(orgAbilities, o.Id)); + if (twoFactorOrgs.Any()) + { + var userOrgs = await _organizationRepository.GetManyByUserIdAsync(user.Id); + firstEnabledOrg = userOrgs.FirstOrDefault( + o => orgs.Any(om => om.Id == o.Id) && o.TwoFactorIsEnabled()); + } + } + + return new Tuple(individualRequired || firstEnabledOrg != null, firstEnabledOrg); + } + + public async Task> BuildTwoFactorResultAsync(User user, Organization organization) + { + var enabledProviders = await GetEnabledTwoFactorProvidersAsync(user, organization); + if (enabledProviders.Count == 0) + { + return null; + } + + var providers = new Dictionary>(); + foreach (var provider in enabledProviders) + { + var twoFactorParams = await BuildTwoFactorParams(organization, user, provider.Key, provider.Value); + providers.Add(((byte)provider.Key).ToString(), twoFactorParams); + } + + var twoFactorResultDict = new Dictionary + { + { "TwoFactorProviders", null }, + { "TwoFactorProviders2", providers }, // backwards compatibility + }; + + // If we have email as a 2FA provider, we might need an SsoEmail2fa Session Token + if (enabledProviders.Any(p => p.Key == TwoFactorProviderType.Email)) + { + twoFactorResultDict.Add("SsoEmail2faSessionToken", + _ssoEmail2faSessionTokeFactory.Protect(new SsoEmail2faSessionTokenable(user))); + + twoFactorResultDict.Add("Email", user.Email); + } + + if (enabledProviders.Count == 1 && enabledProviders.First().Key == TwoFactorProviderType.Email) + { + // Send email now if this is their only 2FA method + await _userService.SendTwoFactorEmailAsync(user); + } + + return twoFactorResultDict; + } + + public async Task VerifyTwoFactor( + User user, + Organization organization, + TwoFactorProviderType type, + string token) + { + if (organization != null && type == TwoFactorProviderType.OrganizationDuo) + { + if (organization.TwoFactorProviderIsEnabled(type)) + { + // DUO SDK v4 Update: try to validate the token - PM-5156 addresses tech debt + if (_featureService.IsEnabled(FeatureFlagKeys.DuoRedirect)) + { + if (!token.Contains(':')) + { + // We have to send the provider to the DuoWebV4SDKService to create the DuoClient + var provider = organization.GetTwoFactorProvider(TwoFactorProviderType.OrganizationDuo); + return await _duoWebV4SDKService.ValidateAsync(token, provider, user); + } + } + return await _organizationDuoWebTokenProvider.ValidateAsync(token, organization, user); + } + return false; + } + + switch (type) + { + case TwoFactorProviderType.Authenticator: + case TwoFactorProviderType.Email: + case TwoFactorProviderType.Duo: + case TwoFactorProviderType.YubiKey: + case TwoFactorProviderType.WebAuthn: + case TwoFactorProviderType.Remember: + if (type != TwoFactorProviderType.Remember && + !await _userService.TwoFactorProviderIsEnabledAsync(type, user)) + { + return false; + } + // DUO SDK v4 Update: try to validate the token - PM-5156 addresses tech debt + if (_featureService.IsEnabled(FeatureFlagKeys.DuoRedirect)) + { + if (type == TwoFactorProviderType.Duo) + { + if (!token.Contains(':')) + { + // We have to send the provider to the DuoWebV4SDKService to create the DuoClient + var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Duo); + return await _duoWebV4SDKService.ValidateAsync(token, provider, user); + } + } + } + return await _userManager.VerifyTwoFactorTokenAsync(user, + CoreHelpers.CustomProviderName(type), token); + default: + return false; + } + } + + private async Task>> GetEnabledTwoFactorProvidersAsync( + User user, Organization organization) + { + var enabledProviders = new List>(); + var organizationTwoFactorProviders = organization?.GetTwoFactorProviders(); + if (organizationTwoFactorProviders != null) + { + enabledProviders.AddRange( + organizationTwoFactorProviders.Where( + p => (p.Value?.Enabled ?? false) && organization.Use2fa)); + } + + var userTwoFactorProviders = user.GetTwoFactorProviders(); + var userCanAccessPremium = await _userService.CanAccessPremium(user); + if (userTwoFactorProviders != null) + { + enabledProviders.AddRange( + userTwoFactorProviders.Where(p => + // Providers that do not require premium + (p.Value.Enabled && !TwoFactorProvider.RequiresPremium(p.Key)) || + // Providers that require premium and the User has Premium + (p.Value.Enabled && TwoFactorProvider.RequiresPremium(p.Key) && userCanAccessPremium))); + } + + return enabledProviders; + } + + /// + /// Builds the parameters for the two-factor authentication + /// + /// We need the organization for Organization Duo Provider type + /// The user for which the token is being generated + /// Provider Type + /// Raw data that is used to create the response + /// a dictionary with the correct provider configuration or null if the provider is not configured properly + private async Task> BuildTwoFactorParams(Organization organization, User user, + TwoFactorProviderType type, TwoFactorProvider provider) + { + // We will always return this dictionary. If none of the criteria is met then it will return null. + var twoFactorParams = new Dictionary(); + + // OrganizationDuo is odd since it doesn't use the UserManager built-in TwoFactor flows + /* + Note: Duo is in the midst of being updated to use the UserManager built-in TwoFactor class + in the future the `AuthUrl` will be the generated "token" - PM-8107 + */ + if (type == TwoFactorProviderType.OrganizationDuo && + await _organizationDuoWebTokenProvider.CanGenerateTwoFactorTokenAsync(organization)) + { + twoFactorParams.Add("Host", provider.MetaData["Host"]); + twoFactorParams.Add("AuthUrl", await _duoWebV4SDKService.GenerateAsync(provider, user)); + + return twoFactorParams; + } + + // Individual 2FA providers use the UserManager built-in TwoFactor flow so we can generate the token before building the params + var token = await _userManager.GenerateTwoFactorTokenAsync(user, + CoreHelpers.CustomProviderName(type)); + switch (type) + { + /* + Note: Duo is in the midst of being updated to use the UserManager built-in TwoFactor class + in the future the `AuthUrl` will be the generated "token" - PM-8107 + */ + case TwoFactorProviderType.Duo: + twoFactorParams.Add("Host", provider.MetaData["Host"]); + twoFactorParams.Add("AuthUrl", await _duoWebV4SDKService.GenerateAsync(provider, user)); + break; + case TwoFactorProviderType.WebAuthn: + if (token != null) + { + twoFactorParams = JsonSerializer.Deserialize>(token); + } + break; + case TwoFactorProviderType.Email: + var twoFactorEmail = (string)provider.MetaData["Email"]; + var redactedEmail = CoreHelpers.RedactEmailAddress(twoFactorEmail); + twoFactorParams.Add("Email", redactedEmail); + break; + case TwoFactorProviderType.YubiKey: + twoFactorParams.Add("Nfc", (bool)provider.MetaData["Nfc"]); + break; + } + + // return null if the dictionary is empty + return twoFactorParams.Count > 0 ? twoFactorParams : null; + } + + private bool OrgUsing2fa(IDictionary orgAbilities, Guid orgId) + { + return orgAbilities != null && orgAbilities.ContainsKey(orgId) && + orgAbilities[orgId].Enabled && orgAbilities[orgId].Using2fa; + } +} diff --git a/src/Identity/IdentityServer/WebAuthnGrantValidator.cs b/src/Identity/IdentityServer/RequestValidators/WebAuthnGrantValidator.cs similarity index 82% rename from src/Identity/IdentityServer/WebAuthnGrantValidator.cs rename to src/Identity/IdentityServer/RequestValidators/WebAuthnGrantValidator.cs index 7bf90c756..515dca782 100644 --- a/src/Identity/IdentityServer/WebAuthnGrantValidator.cs +++ b/src/Identity/IdentityServer/RequestValidators/WebAuthnGrantValidator.cs @@ -1,10 +1,8 @@ using System.Security.Claims; using System.Text.Json; using Bit.Core; -using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Services; using Bit.Core.Auth.Enums; -using Bit.Core.Auth.Identity; using Bit.Core.Auth.Models.Business.Tokenables; using Bit.Core.Auth.Repositories; using Bit.Core.Auth.UserFeatures.WebAuthnLogin; @@ -19,7 +17,7 @@ using Duende.IdentityServer.Validation; using Fido2NetLib; using Microsoft.AspNetCore.Identity; -namespace Bit.Identity.IdentityServer; +namespace Bit.Identity.IdentityServer.RequestValidators; public class WebAuthnGrantValidator : BaseRequestValidator, IExtensionGrantValidator { @@ -34,11 +32,8 @@ public class WebAuthnGrantValidator : BaseRequestValidator logger, ICurrentContext currentContext, @@ -46,16 +41,27 @@ public class WebAuthnGrantValidator : BaseRequestValidator tokenDataFactory, IDataProtectorTokenFactory assertionOptionsDataProtector, IFeatureService featureService, IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder, IAssertWebAuthnLoginCredentialCommand assertWebAuthnLoginCredentialCommand ) - : base(userManager, userService, eventService, deviceValidator, - organizationDuoWebTokenProvider, duoWebV4SDKService, organizationRepository, organizationUserRepository, - applicationCacheService, mailService, logger, currentContext, globalSettings, - userRepository, policyService, tokenDataFactory, featureService, ssoConfigRepository, userDecryptionOptionsBuilder) + : base( + userManager, + userService, + eventService, + deviceValidator, + twoFactorAuthenticationValidator, + organizationUserRepository, + mailService, + logger, + currentContext, + globalSettings, + userRepository, + policyService, + featureService, + ssoConfigRepository, + userDecryptionOptionsBuilder) { _assertionOptionsDataProtector = assertionOptionsDataProtector; _assertWebAuthnLoginCredentialCommand = assertWebAuthnLoginCredentialCommand; @@ -122,12 +128,6 @@ public class WebAuthnGrantValidator : BaseRequestValidator> RequiresTwoFactorAsync(User user, ValidatedTokenRequest request) - { - // We consider Fido2 userVerification a second factor, so we don't require a second factor here. - return Task.FromResult(new Tuple(false, null)); - } - protected override void SetTwoFactorResult(ExtensionGrantValidationContext context, Dictionary customResponse) { diff --git a/src/Identity/Utilities/ServiceCollectionExtensions.cs b/src/Identity/Utilities/ServiceCollectionExtensions.cs index 43532cb3f..36c38615a 100644 --- a/src/Identity/Utilities/ServiceCollectionExtensions.cs +++ b/src/Identity/Utilities/ServiceCollectionExtensions.cs @@ -3,6 +3,7 @@ using Bit.Core.IdentityServer; using Bit.Core.Settings; using Bit.Core.Utilities; using Bit.Identity.IdentityServer; +using Bit.Identity.IdentityServer.RequestValidators; using Bit.SharedWeb.Utilities; using Duende.IdentityServer.ResponseHandling; using Duende.IdentityServer.Services; @@ -21,6 +22,7 @@ public static class ServiceCollectionExtensions services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); var issuerUri = new Uri(globalSettings.BaseServiceUri.InternalIdentity); var identityServerBuilder = services diff --git a/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationUserRepository.cs b/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationUserRepository.cs index 6da2f581f..361b1f058 100644 --- a/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationUserRepository.cs +++ b/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationUserRepository.cs @@ -196,8 +196,7 @@ public class OrganizationUserRepository : Repository, IO return results.SingleOrDefault(); } } - public async Task>> - GetDetailsByIdWithCollectionsAsync(Guid id) + public async Task<(OrganizationUserUserDetails? OrganizationUser, ICollection Collections)> GetDetailsByIdWithCollectionsAsync(Guid id) { using (var connection = new SqlConnection(ConnectionString)) { @@ -206,9 +205,9 @@ public class OrganizationUserRepository : Repository, IO new { Id = id }, commandType: CommandType.StoredProcedure); - var user = (await results.ReadAsync()).SingleOrDefault(); + var organizationUserUserDetails = (await results.ReadAsync()).SingleOrDefault(); var collections = (await results.ReadAsync()).ToList(); - return new Tuple>(user, collections); + return (organizationUserUserDetails, collections); } } diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs index 089a0a5c5..0c9f1d0b9 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs +++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs @@ -248,7 +248,7 @@ public class OrganizationUserRepository : Repository>> GetDetailsByIdWithCollectionsAsync(Guid id) + public async Task<(OrganizationUserUserDetails? OrganizationUser, ICollection Collections)> GetDetailsByIdWithCollectionsAsync(Guid id) { var organizationUserUserDetails = await GetDetailsByIdAsync(id); using (var scope = ServiceScopeFactory.CreateScope()) @@ -265,7 +265,7 @@ public class OrganizationUserRepository : Repository>(organizationUserUserDetails, collections); + return (organizationUserUserDetails, collections); } } diff --git a/src/Infrastructure.EntityFramework/Configurations/DeviceEntityTypeConfiguration.cs b/src/Infrastructure.EntityFramework/Configurations/DeviceEntityTypeConfiguration.cs index 53cf98bd9..cf6afce7c 100644 --- a/src/Infrastructure.EntityFramework/Configurations/DeviceEntityTypeConfiguration.cs +++ b/src/Infrastructure.EntityFramework/Configurations/DeviceEntityTypeConfiguration.cs @@ -21,6 +21,10 @@ public class DeviceEntityTypeConfiguration : IEntityTypeConfiguration .HasIndex(d => d.Identifier) .IsClustered(false); + builder.Property(c => c.Active) + .ValueGeneratedNever() + .HasDefaultValue(true); + builder.ToTable(nameof(Device)); } } diff --git a/src/SharedWeb/SharedWeb.csproj b/src/SharedWeb/SharedWeb.csproj index 40915cb7e..8d1097eee 100644 --- a/src/SharedWeb/SharedWeb.csproj +++ b/src/SharedWeb/SharedWeb.csproj @@ -7,7 +7,7 @@
- + diff --git a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs index b0a2c42ea..5a5585952 100644 --- a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs +++ b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs @@ -274,6 +274,11 @@ public static class ServiceCollectionExtensions services.AddKeyedSingleton("implementation"); services.AddSingleton(); } + else + { + services.AddSingleton(); + } + if (CoreHelpers.SettingHasValue(globalSettings.InternalIdentityKey) && CoreHelpers.SettingHasValue(globalSettings.BaseServiceUri.InternalNotifications)) { @@ -290,10 +295,6 @@ public static class ServiceCollectionExtensions services.AddKeyedSingleton("implementation"); } } - else - { - services.AddSingleton(); - } if (!globalSettings.SelfHosted && CoreHelpers.SettingHasValue(globalSettings.Mail.ConnectionString)) { diff --git a/src/Sql/dbo/Stored Procedures/Device_Create.sql b/src/Sql/dbo/Stored Procedures/Device_Create.sql index 6e9159c52..11df68060 100644 --- a/src/Sql/dbo/Stored Procedures/Device_Create.sql +++ b/src/Sql/dbo/Stored Procedures/Device_Create.sql @@ -9,7 +9,8 @@ @RevisionDate DATETIME2(7), @EncryptedUserKey VARCHAR(MAX) = NULL, @EncryptedPublicKey VARCHAR(MAX) = NULL, - @EncryptedPrivateKey VARCHAR(MAX) = NULL + @EncryptedPrivateKey VARCHAR(MAX) = NULL, + @Active BIT = 1 AS BEGIN SET NOCOUNT ON @@ -26,7 +27,8 @@ BEGIN [RevisionDate], [EncryptedUserKey], [EncryptedPublicKey], - [EncryptedPrivateKey] + [EncryptedPrivateKey], + [Active] ) VALUES ( @@ -40,6 +42,7 @@ BEGIN @RevisionDate, @EncryptedUserKey, @EncryptedPublicKey, - @EncryptedPrivateKey + @EncryptedPrivateKey, + @Active ) END diff --git a/src/Sql/dbo/Stored Procedures/Device_DeleteById.sql b/src/Sql/dbo/Stored Procedures/Device_DeleteById.sql deleted file mode 100644 index ab1996ceb..000000000 --- a/src/Sql/dbo/Stored Procedures/Device_DeleteById.sql +++ /dev/null @@ -1,12 +0,0 @@ -CREATE PROCEDURE [dbo].[Device_DeleteById] - @Id UNIQUEIDENTIFIER -AS -BEGIN - SET NOCOUNT ON - - DELETE - FROM - [dbo].[Device] - WHERE - [Id] = @Id -END \ No newline at end of file diff --git a/src/Sql/dbo/Stored Procedures/Device_Update.sql b/src/Sql/dbo/Stored Procedures/Device_Update.sql index dd3bf4ba2..297523159 100644 --- a/src/Sql/dbo/Stored Procedures/Device_Update.sql +++ b/src/Sql/dbo/Stored Procedures/Device_Update.sql @@ -9,7 +9,8 @@ @RevisionDate DATETIME2(7), @EncryptedUserKey VARCHAR(MAX) = NULL, @EncryptedPublicKey VARCHAR(MAX) = NULL, - @EncryptedPrivateKey VARCHAR(MAX) = NULL + @EncryptedPrivateKey VARCHAR(MAX) = NULL, + @Active BIT = 1 AS BEGIN SET NOCOUNT ON @@ -26,7 +27,8 @@ BEGIN [RevisionDate] = @RevisionDate, [EncryptedUserKey] = @EncryptedUserKey, [EncryptedPublicKey] = @EncryptedPublicKey, - [EncryptedPrivateKey] = @EncryptedPrivateKey + [EncryptedPrivateKey] = @EncryptedPrivateKey, + [Active] = @Active WHERE [Id] = @Id END diff --git a/src/Sql/dbo/Tables/Device.sql b/src/Sql/dbo/Tables/Device.sql index 75ba218ff..66328afe5 100644 --- a/src/Sql/dbo/Tables/Device.sql +++ b/src/Sql/dbo/Tables/Device.sql @@ -1,25 +1,24 @@ CREATE TABLE [dbo].[Device] ( - [Id] UNIQUEIDENTIFIER NOT NULL, - [UserId] UNIQUEIDENTIFIER NOT NULL, - [Name] NVARCHAR (50) NOT NULL, - [Type] SMALLINT NOT NULL, - [Identifier] NVARCHAR (50) NOT NULL, - [PushToken] NVARCHAR (255) NULL, - [CreationDate] DATETIME2 (7) NOT NULL, - [RevisionDate] DATETIME2 (7) NOT NULL, - [EncryptedUserKey] VARCHAR (MAX) NULL, - [EncryptedPublicKey] VARCHAR (MAX) NULL, - [EncryptedPrivateKey] VARCHAR (MAX) NULL, + [Id] UNIQUEIDENTIFIER NOT NULL, + [UserId] UNIQUEIDENTIFIER NOT NULL, + [Name] NVARCHAR (50) NOT NULL, + [Type] SMALLINT NOT NULL, + [Identifier] NVARCHAR (50) NOT NULL, + [PushToken] NVARCHAR (255) NULL, + [CreationDate] DATETIME2 (7) NOT NULL, + [RevisionDate] DATETIME2 (7) NOT NULL, + [EncryptedUserKey] VARCHAR (MAX) NULL, + [EncryptedPublicKey] VARCHAR (MAX) NULL, + [EncryptedPrivateKey] VARCHAR (MAX) NULL, + [Active] BIT NOT NULL CONSTRAINT [DF_Device_Active] DEFAULT (1), CONSTRAINT [PK_Device] PRIMARY KEY CLUSTERED ([Id] ASC), CONSTRAINT [FK_Device_User] FOREIGN KEY ([UserId]) REFERENCES [dbo].[User] ([Id]) ); - GO CREATE UNIQUE NONCLUSTERED INDEX [UX_Device_UserId_Identifier] ON [dbo].[Device]([UserId] ASC, [Identifier] ASC); - GO CREATE NONCLUSTERED INDEX [IX_Device_Identifier] ON [dbo].[Device]([Identifier] ASC); diff --git a/test/Admin.Test/AdminConsole/Controllers/ProvidersControllerTests.cs b/test/Admin.Test/AdminConsole/Controllers/ProvidersControllerTests.cs new file mode 100644 index 000000000..be9883ba0 --- /dev/null +++ b/test/Admin.Test/AdminConsole/Controllers/ProvidersControllerTests.cs @@ -0,0 +1,251 @@ +using Bit.Admin.AdminConsole.Controllers; +using Bit.Admin.AdminConsole.Models; +using Bit.Core; +using Bit.Core.AdminConsole.Entities.Provider; +using Bit.Core.AdminConsole.Enums.Provider; +using Bit.Core.AdminConsole.Providers.Interfaces; +using Bit.Core.Billing.Enums; +using Bit.Core.Services; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Microsoft.AspNetCore.Mvc; +using NSubstitute; +using NSubstitute.ReceivedExtensions; + +namespace Admin.Test.AdminConsole.Controllers; + +[ControllerCustomize(typeof(ProvidersController))] +[SutProviderCustomize] +public class ProvidersControllerTests +{ + #region CreateMspAsync + [BitAutoData] + [SutProviderCustomize] + [Theory] + public async Task CreateMspAsync_WithValidModel_CreatesProvider( + CreateMspProviderModel model, + SutProvider sutProvider) + { + // Arrange + + // Act + var actual = await sutProvider.Sut.CreateMsp(model); + + // Assert + Assert.NotNull(actual); + await sutProvider.GetDependency() + .Received(Quantity.Exactly(1)) + .CreateMspAsync( + Arg.Is(x => x.Type == ProviderType.Msp), + model.OwnerEmail, + model.TeamsMonthlySeatMinimum, + model.EnterpriseMonthlySeatMinimum); + } + + [BitAutoData] + [SutProviderCustomize] + [Theory] + public async Task CreateMspAsync_RedirectsToExpectedPage_AfterCreatingProvider( + CreateMspProviderModel model, + Guid expectedProviderId, + SutProvider sutProvider) + { + // Arrange + sutProvider.GetDependency() + .When(x => + x.CreateMspAsync( + Arg.Is(y => y.Type == ProviderType.Msp), + model.OwnerEmail, + model.TeamsMonthlySeatMinimum, + model.EnterpriseMonthlySeatMinimum)) + .Do(callInfo => + { + var providerArgument = callInfo.ArgAt(0); + providerArgument.Id = expectedProviderId; + }); + + // Act + var actual = await sutProvider.Sut.CreateMsp(model); + + // Assert + Assert.NotNull(actual); + Assert.IsType(actual); + var actualResult = (RedirectToActionResult)actual; + Assert.Equal("Edit", actualResult.ActionName); + Assert.Null(actualResult.ControllerName); + Assert.Equal(expectedProviderId, actualResult.RouteValues["Id"]); + } + #endregion + + #region CreateMultiOrganizationEnterpriseAsync + [BitAutoData] + [SutProviderCustomize] + [Theory] + public async Task CreateMultiOrganizationEnterpriseAsync_WithValidModel_CreatesProvider( + CreateMultiOrganizationEnterpriseProviderModel model, + SutProvider sutProvider) + { + // Arrange + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.PM12275_MultiOrganizationEnterprises) + .Returns(true); + + // Act + var actual = await sutProvider.Sut.CreateMultiOrganizationEnterprise(model); + + // Assert + Assert.NotNull(actual); + await sutProvider.GetDependency() + .Received(Quantity.Exactly(1)) + .CreateMultiOrganizationEnterpriseAsync( + Arg.Is(x => x.Type == ProviderType.MultiOrganizationEnterprise), + model.OwnerEmail, + Arg.Is(y => y == model.Plan), + model.EnterpriseSeatMinimum); + sutProvider.GetDependency() + .Received(Quantity.Exactly(1)) + .IsEnabled(FeatureFlagKeys.PM12275_MultiOrganizationEnterprises); + } + + [BitAutoData] + [SutProviderCustomize] + [Theory] + public async Task CreateMultiOrganizationEnterpriseAsync_RedirectsToExpectedPage_AfterCreatingProvider( + CreateMultiOrganizationEnterpriseProviderModel model, + Guid expectedProviderId, + SutProvider sutProvider) + { + // Arrange + sutProvider.GetDependency() + .When(x => + x.CreateMultiOrganizationEnterpriseAsync( + Arg.Is(y => y.Type == ProviderType.MultiOrganizationEnterprise), + model.OwnerEmail, + Arg.Is(y => y == model.Plan), + model.EnterpriseSeatMinimum)) + .Do(callInfo => + { + var providerArgument = callInfo.ArgAt(0); + providerArgument.Id = expectedProviderId; + }); + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.PM12275_MultiOrganizationEnterprises) + .Returns(true); + + // Act + var actual = await sutProvider.Sut.CreateMultiOrganizationEnterprise(model); + + // Assert + Assert.NotNull(actual); + Assert.IsType(actual); + var actualResult = (RedirectToActionResult)actual; + Assert.Equal("Edit", actualResult.ActionName); + Assert.Null(actualResult.ControllerName); + Assert.Equal(expectedProviderId, actualResult.RouteValues["Id"]); + } + + [BitAutoData] + [SutProviderCustomize] + [Theory] + public async Task CreateMultiOrganizationEnterpriseAsync_ChecksFeatureFlag( + CreateMultiOrganizationEnterpriseProviderModel model, + SutProvider sutProvider) + { + // Arrange + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.PM12275_MultiOrganizationEnterprises) + .Returns(true); + + // Act + await sutProvider.Sut.CreateMultiOrganizationEnterprise(model); + + // Assert + sutProvider.GetDependency() + .Received(Quantity.Exactly(1)) + .IsEnabled(FeatureFlagKeys.PM12275_MultiOrganizationEnterprises); + } + + [BitAutoData] + [SutProviderCustomize] + [Theory] + public async Task CreateMultiOrganizationEnterpriseAsync_RedirectsToProviderTypeSelectionPage_WhenFeatureFlagIsDisabled( + CreateMultiOrganizationEnterpriseProviderModel model, + SutProvider sutProvider) + { + // Arrange + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.PM12275_MultiOrganizationEnterprises) + .Returns(false); + + // Act + var actual = await sutProvider.Sut.CreateMultiOrganizationEnterprise(model); + + // Assert + sutProvider.GetDependency() + .Received(Quantity.Exactly(1)) + .IsEnabled(FeatureFlagKeys.PM12275_MultiOrganizationEnterprises); + + Assert.IsType(actual); + var actualResult = (RedirectToActionResult)actual; + Assert.Equal("Create", actualResult.ActionName); + Assert.Null(actualResult.ControllerName); + } + #endregion + + #region CreateResellerAsync + [BitAutoData] + [SutProviderCustomize] + [Theory] + public async Task CreateResellerAsync_WithValidModel_CreatesProvider( + CreateResellerProviderModel model, + SutProvider sutProvider) + { + // Arrange + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.PM12275_MultiOrganizationEnterprises) + .Returns(true); + + // Act + var actual = await sutProvider.Sut.CreateReseller(model); + + // Assert + Assert.NotNull(actual); + await sutProvider.GetDependency() + .Received(Quantity.Exactly(1)) + .CreateResellerAsync( + Arg.Is(x => x.Type == ProviderType.Reseller)); + } + + [BitAutoData] + [SutProviderCustomize] + [Theory] + public async Task CreateResellerAsync_RedirectsToExpectedPage_AfterCreatingProvider( + CreateResellerProviderModel model, + Guid expectedProviderId, + SutProvider sutProvider) + { + // Arrange + sutProvider.GetDependency() + .When(x => + x.CreateResellerAsync( + Arg.Is(y => y.Type == ProviderType.Reseller))) + .Do(callInfo => + { + var providerArgument = callInfo.ArgAt(0); + providerArgument.Id = expectedProviderId; + }); + + // Act + var actual = await sutProvider.Sut.CreateReseller(model); + + // Assert + Assert.NotNull(actual); + Assert.IsType(actual); + var actualResult = (RedirectToActionResult)actual; + Assert.Equal("Edit", actualResult.ActionName); + Assert.Null(actualResult.ControllerName); + Assert.Equal(expectedProviderId, actualResult.RouteValues["Id"]); + } + #endregion +} diff --git a/test/Admin.Test/Models/UserViewModelTests.cs b/test/Admin.Test/Models/UserViewModelTests.cs index f7a76d80e..fac5d5f0e 100644 --- a/test/Admin.Test/Models/UserViewModelTests.cs +++ b/test/Admin.Test/Models/UserViewModelTests.cs @@ -2,6 +2,7 @@ using Bit.Admin.Models; using Bit.Core.Entities; +using Bit.Core.Vault.Entities; using Bit.Test.Common.AutoFixture.Attributes; namespace Admin.Test.Models; @@ -79,7 +80,7 @@ public class UserViewModelTests { var lookup = new List<(Guid, bool)> { (user.Id, true) }; - var actual = UserViewModel.MapViewModel(user, lookup); + var actual = UserViewModel.MapViewModel(user, lookup, false); Assert.True(actual.TwoFactorEnabled); } @@ -90,7 +91,7 @@ public class UserViewModelTests { var lookup = new List<(Guid, bool)> { (user.Id, false) }; - var actual = UserViewModel.MapViewModel(user, lookup); + var actual = UserViewModel.MapViewModel(user, lookup, false); Assert.False(actual.TwoFactorEnabled); } @@ -101,8 +102,44 @@ public class UserViewModelTests { var lookup = new List<(Guid, bool)> { (Guid.NewGuid(), true) }; - var actual = UserViewModel.MapViewModel(user, lookup); + var actual = UserViewModel.MapViewModel(user, lookup, false); Assert.False(actual.TwoFactorEnabled); } + + [Theory] + [BitAutoData] + public void MapUserViewModel_WithVerifiedDomain_ReturnsUserViewModel(User user) + { + + var verifiedDomain = true; + + var actual = UserViewModel.MapViewModel(user, true, Array.Empty(), verifiedDomain); + + Assert.True(actual.DomainVerified); + } + + [Theory] + [BitAutoData] + public void MapUserViewModel_WithoutVerifiedDomain_ReturnsUserViewModel(User user) + { + + var verifiedDomain = false; + + var actual = UserViewModel.MapViewModel(user, true, Array.Empty(), verifiedDomain); + + Assert.False(actual.DomainVerified); + } + + [Theory] + [BitAutoData] + public void MapUserViewModel_WithNullVerifiedDomain_ReturnsUserViewModel(User user) + { + + var actual = UserViewModel.MapViewModel(user, true, Array.Empty(), null); + + Assert.Null(actual.DomainVerified); + } + + } diff --git a/test/Api.IntegrationTest/Controllers/AccountsControllerTest.cs b/test/Api.IntegrationTest/Controllers/AccountsControllerTest.cs index b6a0ccbed..6dd7f42c6 100644 --- a/test/Api.IntegrationTest/Controllers/AccountsControllerTest.cs +++ b/test/Api.IntegrationTest/Controllers/AccountsControllerTest.cs @@ -1,6 +1,15 @@ -using System.Net.Http.Headers; +using System.Net; +using System.Net.Http.Headers; +using Bit.Api.Auth.Models.Request.Accounts; using Bit.Api.IntegrationTest.Factories; +using Bit.Api.IntegrationTest.Helpers; using Bit.Api.Models.Response; +using Bit.Core; +using Bit.Core.Billing.Enums; +using Bit.Core.Enums; +using Bit.Core.Models.Data; +using Bit.Core.Services; +using NSubstitute; using Xunit; namespace Bit.Api.IntegrationTest.Controllers; @@ -35,4 +44,82 @@ public class AccountsControllerTest : IClassFixture Assert.Null(content.PrivateKey); Assert.NotNull(content.SecurityStamp); } + + [Fact] + public async Task PostEmailToken_WhenAccountDeprovisioningEnabled_WithManagedAccount_ThrowsBadRequest() + { + var email = await SetupOrganizationManagedAccount(); + + var tokens = await _factory.LoginAsync(email); + var client = _factory.CreateClient(); + + var model = new EmailTokenRequestModel + { + NewEmail = $"{Guid.NewGuid()}@example.com", + MasterPasswordHash = "master_password_hash" + }; + + using var message = new HttpRequestMessage(HttpMethod.Post, "/accounts/email-token") + { + Content = JsonContent.Create(model) + }; + message.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tokens.Token); + var response = await client.SendAsync(message); + + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + var content = await response.Content.ReadAsStringAsync(); + Assert.Contains("Cannot change emails for accounts owned by an organization", content); + } + + [Fact] + public async Task PostEmail_WhenAccountDeprovisioningEnabled_WithManagedAccount_ThrowsBadRequest() + { + var email = await SetupOrganizationManagedAccount(); + + var tokens = await _factory.LoginAsync(email); + var client = _factory.CreateClient(); + + var model = new EmailRequestModel + { + NewEmail = $"{Guid.NewGuid()}@example.com", + MasterPasswordHash = "master_password_hash", + NewMasterPasswordHash = "master_password_hash", + Token = "validtoken", + Key = "key" + }; + + using var message = new HttpRequestMessage(HttpMethod.Post, "/accounts/email") + { + Content = JsonContent.Create(model) + }; + message.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tokens.Token); + var response = await client.SendAsync(message); + + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + var content = await response.Content.ReadAsStringAsync(); + Assert.Contains("Cannot change emails for accounts owned by an organization", content); + } + + private async Task SetupOrganizationManagedAccount() + { + _factory.SubstituteService(featureService => + featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning).Returns(true)); + + // Create the owner account + var ownerEmail = $"{Guid.NewGuid()}@bitwarden.com"; + await _factory.LoginWithNewAccount(ownerEmail); + + // Create the organization + var (_organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, plan: PlanType.EnterpriseAnnually2023, + ownerEmail: ownerEmail, passwordManagerSeats: 10, paymentMethod: PaymentMethodType.Card); + + // Create a new organization member + var (email, orgUser) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, _organization.Id, + OrganizationUserType.Custom, new Permissions { AccessReports = true, ManageScim = true }); + + // Add a verified domain + await OrganizationTestHelpers.CreateVerifiedDomainAsync(_factory, _organization.Id, "bitwarden.com"); + + return email; + } } diff --git a/test/Api.IntegrationTest/Helpers/OrganizationTestHelpers.cs b/test/Api.IntegrationTest/Helpers/OrganizationTestHelpers.cs index 83b345e78..64f719e82 100644 --- a/test/Api.IntegrationTest/Helpers/OrganizationTestHelpers.cs +++ b/test/Api.IntegrationTest/Helpers/OrganizationTestHelpers.cs @@ -105,4 +105,22 @@ public static class OrganizationTestHelpers return (email, organizationUser); } + + /// + /// Creates a VerifiedDomain for the specified organization. + /// + public static async Task CreateVerifiedDomainAsync(ApiApplicationFactory factory, Guid organizationId, string domain) + { + var organizationDomainRepository = factory.GetService(); + + var verifiedDomain = new OrganizationDomain + { + OrganizationId = organizationId, + DomainName = domain, + Txt = "btw+test18383838383" + }; + verifiedDomain.SetVerifiedDate(); + + await organizationDomainRepository.CreateAsync(verifiedDomain); + } } diff --git a/test/Api.Test/AdminConsole/Controllers/OrganizationUsersControllerTests.cs b/test/Api.Test/AdminConsole/Controllers/OrganizationUsersControllerTests.cs index 492112e5a..0ba8a101d 100644 --- a/test/Api.Test/AdminConsole/Controllers/OrganizationUsersControllerTests.cs +++ b/test/Api.Test/AdminConsole/Controllers/OrganizationUsersControllerTests.cs @@ -1,8 +1,8 @@ using System.Security.Claims; using Bit.Api.AdminConsole.Controllers; using Bit.Api.AdminConsole.Models.Request.Organizations; -using Bit.Api.Auth.Models.Request.Accounts; using Bit.Api.Vault.AuthorizationHandlers.Collections; +using Bit.Core; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; @@ -15,6 +15,7 @@ using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Models.Business; +using Bit.Core.Models.Data; using Bit.Core.Models.Data.Organizations; using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces; @@ -185,14 +186,46 @@ public class OrganizationUsersControllerTests await Assert.ThrowsAsync(() => sutProvider.Sut.Invite(organizationAbility.Id, model)); } + [Theory] + [BitAutoData(true)] + [BitAutoData(false)] + public async Task Get_ReturnsUser( + bool accountDeprovisioningEnabled, + OrganizationUserUserDetails organizationUser, ICollection collections, + SutProvider sutProvider) + { + organizationUser.Permissions = null; + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.AccountDeprovisioning) + .Returns(accountDeprovisioningEnabled); + + sutProvider.GetDependency() + .ManageUsers(organizationUser.OrganizationId) + .Returns(true); + + sutProvider.GetDependency() + .GetDetailsByIdWithCollectionsAsync(organizationUser.Id) + .Returns((organizationUser, collections)); + + sutProvider.GetDependency() + .GetUsersOrganizationManagementStatusAsync(organizationUser.OrganizationId, Arg.Is>(ids => ids.Contains(organizationUser.Id))) + .Returns(new Dictionary { { organizationUser.Id, true } }); + + var response = await sutProvider.Sut.Get(organizationUser.Id, false); + + Assert.Equal(organizationUser.Id, response.Id); + Assert.Equal(accountDeprovisioningEnabled, response.ManagedByOrganization); + } + [Theory] [BitAutoData] - public async Task Get_ReturnsUsers( + public async Task GetMany_ReturnsUsers( ICollection organizationUsers, OrganizationAbility organizationAbility, SutProvider sutProvider) { - Get_Setup(organizationAbility, organizationUsers, sutProvider); - var response = await sutProvider.Sut.Get(organizationAbility.Id); + GetMany_Setup(organizationAbility, organizationUsers, sutProvider); + var response = await sutProvider.Sut.Get(organizationAbility.Id, false, false); Assert.True(response.Data.All(r => organizationUsers.Any(ou => ou.Id == r.Id))); } @@ -239,17 +272,12 @@ public class OrganizationUsersControllerTests [Theory] [BitAutoData] public async Task DeleteAccount_WhenUserCanManageUsers_Success( - Guid orgId, - Guid id, - SecretVerificationRequestModel model, - User currentUser, - SutProvider sutProvider) + Guid orgId, Guid id, User currentUser, SutProvider sutProvider) { sutProvider.GetDependency().ManageUsers(orgId).Returns(true); sutProvider.GetDependency().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(currentUser); - sutProvider.GetDependency().VerifySecretAsync(currentUser, model.Secret).Returns(true); - await sutProvider.Sut.DeleteAccount(orgId, id, model); + await sutProvider.Sut.DeleteAccount(orgId, id); await sutProvider.GetDependency() .Received(1) @@ -259,60 +287,34 @@ public class OrganizationUsersControllerTests [Theory] [BitAutoData] public async Task DeleteAccount_WhenUserCannotManageUsers_ThrowsNotFoundException( - Guid orgId, - Guid id, - SecretVerificationRequestModel model, - SutProvider sutProvider) + Guid orgId, Guid id, SutProvider sutProvider) { sutProvider.GetDependency().ManageUsers(orgId).Returns(false); await Assert.ThrowsAsync(() => - sutProvider.Sut.DeleteAccount(orgId, id, model)); + sutProvider.Sut.DeleteAccount(orgId, id)); } [Theory] [BitAutoData] public async Task DeleteAccount_WhenCurrentUserNotFound_ThrowsUnauthorizedAccessException( - Guid orgId, - Guid id, - SecretVerificationRequestModel model, - SutProvider sutProvider) + Guid orgId, Guid id, SutProvider sutProvider) { sutProvider.GetDependency().ManageUsers(orgId).Returns(true); sutProvider.GetDependency().GetUserByPrincipalAsync(default).ReturnsForAnyArgs((User)null); await Assert.ThrowsAsync(() => - sutProvider.Sut.DeleteAccount(orgId, id, model)); - } - - [Theory] - [BitAutoData] - public async Task DeleteAccount_WhenSecretVerificationFails_ThrowsBadRequestException( - Guid orgId, - Guid id, - SecretVerificationRequestModel model, - User currentUser, - SutProvider sutProvider) - { - sutProvider.GetDependency().ManageUsers(orgId).Returns(true); - sutProvider.GetDependency().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(currentUser); - sutProvider.GetDependency().VerifySecretAsync(currentUser, model.Secret).Returns(false); - - await Assert.ThrowsAsync(() => sutProvider.Sut.DeleteAccount(orgId, id, model)); + sutProvider.Sut.DeleteAccount(orgId, id)); } [Theory] [BitAutoData] public async Task BulkDeleteAccount_WhenUserCanManageUsers_Success( - Guid orgId, - SecureOrganizationUserBulkRequestModel model, - User currentUser, - List<(Guid, string)> deleteResults, - SutProvider sutProvider) + Guid orgId, OrganizationUserBulkRequestModel model, User currentUser, + List<(Guid, string)> deleteResults, SutProvider sutProvider) { sutProvider.GetDependency().ManageUsers(orgId).Returns(true); sutProvider.GetDependency().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(currentUser); - sutProvider.GetDependency().VerifySecretAsync(currentUser, model.Secret).Returns(true); sutProvider.GetDependency() .DeleteManyUsersAsync(orgId, model.Ids, currentUser.Id) .Returns(deleteResults); @@ -329,9 +331,7 @@ public class OrganizationUsersControllerTests [Theory] [BitAutoData] public async Task BulkDeleteAccount_WhenUserCannotManageUsers_ThrowsNotFoundException( - Guid orgId, - SecureOrganizationUserBulkRequestModel model, - SutProvider sutProvider) + Guid orgId, OrganizationUserBulkRequestModel model, SutProvider sutProvider) { sutProvider.GetDependency().ManageUsers(orgId).Returns(false); @@ -342,9 +342,7 @@ public class OrganizationUsersControllerTests [Theory] [BitAutoData] public async Task BulkDeleteAccount_WhenCurrentUserNotFound_ThrowsUnauthorizedAccessException( - Guid orgId, - SecureOrganizationUserBulkRequestModel model, - SutProvider sutProvider) + Guid orgId, OrganizationUserBulkRequestModel model, SutProvider sutProvider) { sutProvider.GetDependency().ManageUsers(orgId).Returns(true); sutProvider.GetDependency().GetUserByPrincipalAsync(default).ReturnsForAnyArgs((User)null); @@ -353,22 +351,7 @@ public class OrganizationUsersControllerTests sutProvider.Sut.BulkDeleteAccount(orgId, model)); } - [Theory] - [BitAutoData] - public async Task BulkDeleteAccount_WhenSecretVerificationFails_ThrowsBadRequestException( - Guid orgId, - SecureOrganizationUserBulkRequestModel model, - User currentUser, - SutProvider sutProvider) - { - sutProvider.GetDependency().ManageUsers(orgId).Returns(true); - sutProvider.GetDependency().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(currentUser); - sutProvider.GetDependency().VerifySecretAsync(currentUser, model.Secret).Returns(false); - - await Assert.ThrowsAsync(() => sutProvider.Sut.BulkDeleteAccount(orgId, model)); - } - - private void Get_Setup(OrganizationAbility organizationAbility, + private void GetMany_Setup(OrganizationAbility organizationAbility, ICollection organizationUsers, SutProvider sutProvider) { diff --git a/test/Api.Test/Auth/Controllers/AccountsControllerTests.cs b/test/Api.Test/Auth/Controllers/AccountsControllerTests.cs index a16a9cb55..13c80f856 100644 --- a/test/Api.Test/Auth/Controllers/AccountsControllerTests.cs +++ b/test/Api.Test/Auth/Controllers/AccountsControllerTests.cs @@ -7,6 +7,7 @@ using Bit.Api.Auth.Models.Request.WebAuthn; using Bit.Api.Auth.Validators; using Bit.Api.Tools.Models.Request; using Bit.Api.Vault.Models.Request; +using Bit.Core; using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Services; using Bit.Core.Auth.Entities; @@ -143,6 +144,21 @@ public class AccountsControllerTests : IDisposable await _userService.Received(1).InitiateEmailChangeAsync(user, newEmail); } + [Fact] + public async Task PostEmailToken_WithAccountDeprovisioningEnabled_WhenUserIsNotManagedByAnOrganization_ShouldInitiateEmailChange() + { + var user = GenerateExampleUser(); + ConfigureUserServiceToReturnValidPrincipalFor(user); + ConfigureUserServiceToAcceptPasswordFor(user); + _featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning).Returns(true); + _userService.IsManagedByAnyOrganizationAsync(user.Id).Returns(false); + var newEmail = "example@user.com"; + + await _sut.PostEmailToken(new EmailTokenRequestModel { NewEmail = newEmail }); + + await _userService.Received(1).InitiateEmailChangeAsync(user, newEmail); + } + [Fact] public async Task PostEmailToken_WhenNotAuthorized_ShouldThrowUnauthorizedAccessException() { @@ -165,6 +181,22 @@ public class AccountsControllerTests : IDisposable ); } + [Fact] + public async Task PostEmailToken_WithAccountDeprovisioningEnabled_WhenUserIsManagedByAnOrganization_ShouldThrowBadRequestException() + { + var user = GenerateExampleUser(); + ConfigureUserServiceToReturnValidPrincipalFor(user); + ConfigureUserServiceToAcceptPasswordFor(user); + _featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning).Returns(true); + _userService.IsManagedByAnyOrganizationAsync(user.Id).Returns(true); + + var result = await Assert.ThrowsAsync( + () => _sut.PostEmailToken(new EmailTokenRequestModel()) + ); + + Assert.Equal("Cannot change emails for accounts owned by an organization. Contact your organization administrator for additional details.", result.Message); + } + [Fact] public async Task PostEmail_ShouldChangeUserEmail() { @@ -178,6 +210,21 @@ public class AccountsControllerTests : IDisposable await _userService.Received(1).ChangeEmailAsync(user, default, default, default, default, default); } + [Fact] + public async Task PostEmail_WithAccountDeprovisioningEnabled_WhenUserIsNotManagedByAnOrganization_ShouldChangeUserEmail() + { + var user = GenerateExampleUser(); + ConfigureUserServiceToReturnValidPrincipalFor(user); + _userService.ChangeEmailAsync(user, default, default, default, default, default) + .Returns(Task.FromResult(IdentityResult.Success)); + _featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning).Returns(true); + _userService.IsManagedByAnyOrganizationAsync(user.Id).Returns(false); + + await _sut.PostEmail(new EmailRequestModel()); + + await _userService.Received(1).ChangeEmailAsync(user, default, default, default, default, default); + } + [Fact] public async Task PostEmail_WhenNotAuthorized_ShouldThrownUnauthorizedAccessException() { @@ -201,6 +248,21 @@ public class AccountsControllerTests : IDisposable ); } + [Fact] + public async Task PostEmail_WithAccountDeprovisioningEnabled_WhenUserIsManagedByAnOrganization_ShouldThrowBadRequestException() + { + var user = GenerateExampleUser(); + ConfigureUserServiceToReturnValidPrincipalFor(user); + _featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning).Returns(true); + _userService.IsManagedByAnyOrganizationAsync(user.Id).Returns(true); + + var result = await Assert.ThrowsAsync( + () => _sut.PostEmail(new EmailRequestModel()) + ); + + Assert.Equal("Cannot change emails for accounts owned by an organization. Contact your organization administrator for additional details.", result.Message); + } + [Fact] public async Task PostVerifyEmail_ShouldSendEmailVerification() { @@ -472,6 +534,34 @@ public class AccountsControllerTests : IDisposable await Assert.ThrowsAsync(() => _sut.PostSetPasswordAsync(model)); } + [Fact] + public async Task Delete_WhenAccountDeprovisioningIsEnabled_WithUserManagedByAnOrganization_ThrowsBadRequestException() + { + var user = GenerateExampleUser(); + ConfigureUserServiceToReturnValidPrincipalFor(user); + ConfigureUserServiceToAcceptPasswordFor(user); + _featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning).Returns(true); + _userService.IsManagedByAnyOrganizationAsync(user.Id).Returns(true); + + var result = await Assert.ThrowsAsync(() => _sut.Delete(new SecretVerificationRequestModel())); + + Assert.Equal("Cannot delete accounts owned by an organization. Contact your organization administrator for additional details.", result.Message); + } + + [Fact] + public async Task Delete_WhenAccountDeprovisioningIsEnabled_WithUserNotManagedByAnOrganization_ShouldSucceed() + { + var user = GenerateExampleUser(); + ConfigureUserServiceToReturnValidPrincipalFor(user); + ConfigureUserServiceToAcceptPasswordFor(user); + _featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning).Returns(true); + _userService.IsManagedByAnyOrganizationAsync(user.Id).Returns(false); + _userService.DeleteAsync(user).Returns(IdentityResult.Success); + + await _sut.Delete(new SecretVerificationRequestModel()); + + await _userService.Received(1).DeleteAsync(user); + } // Below are helper functions that currently belong to this // test class, but ultimately may need to be split out into diff --git a/test/Api.Test/Billing/Controllers/OrganizationBillingControllerTests.cs b/test/Api.Test/Billing/Controllers/OrganizationBillingControllerTests.cs index b46fd307e..51e374fd5 100644 --- a/test/Api.Test/Billing/Controllers/OrganizationBillingControllerTests.cs +++ b/test/Api.Test/Billing/Controllers/OrganizationBillingControllerTests.cs @@ -37,7 +37,7 @@ public class OrganizationBillingControllerTests Guid organizationId, SutProvider sutProvider) { - sutProvider.GetDependency().AccessMembersTab(organizationId).Returns(true); + sutProvider.GetDependency().OrganizationUser(organizationId).Returns(true); sutProvider.GetDependency().GetMetadata(organizationId).Returns((OrganizationMetadata)null); var result = await sutProvider.Sut.GetMetadataAsync(organizationId); @@ -50,18 +50,20 @@ public class OrganizationBillingControllerTests Guid organizationId, SutProvider sutProvider) { - sutProvider.GetDependency().AccessMembersTab(organizationId).Returns(true); + sutProvider.GetDependency().OrganizationUser(organizationId).Returns(true); sutProvider.GetDependency().GetMetadata(organizationId) - .Returns(new OrganizationMetadata(true, true)); + .Returns(new OrganizationMetadata(true, true, true, true)); var result = await sutProvider.Sut.GetMetadataAsync(organizationId); Assert.IsType>(result); - var organizationMetadataResponse = ((Ok)result).Value; + var response = ((Ok)result).Value; - Assert.True(organizationMetadataResponse.IsEligibleForSelfHost); - Assert.True(organizationMetadataResponse.IsOnSecretsManagerStandalone); + Assert.True(response.IsEligibleForSelfHost); + Assert.True(response.IsManaged); + Assert.True(response.IsOnSecretsManagerStandalone); + Assert.True(response.IsSubscriptionUnpaid); } [Theory, BitAutoData] diff --git a/test/Api.Test/Controllers/PoliciesControllerTests.cs b/test/Api.Test/Controllers/PoliciesControllerTests.cs index ec69104e5..77cc5ea02 100644 --- a/test/Api.Test/Controllers/PoliciesControllerTests.cs +++ b/test/Api.Test/Controllers/PoliciesControllerTests.cs @@ -3,8 +3,10 @@ using System.Text.Json; using Bit.Api.AdminConsole.Controllers; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Models.Api.Response; using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Exceptions; using Bit.Core.Repositories; @@ -132,4 +134,71 @@ public class PoliciesControllerTests // Act & Assert await Assert.ThrowsAsync(() => sutProvider.Sut.GetMasterPasswordPolicy(orgId)); } + + [Theory] + [BitAutoData] + public async Task Get_WhenUserCanManagePolicies_WithExistingType_ReturnsExistingPolicy( + SutProvider sutProvider, Guid orgId, Policy policy, int type) + { + // Arrange + sutProvider.GetDependency() + .ManagePolicies(orgId) + .Returns(true); + + policy.Type = (PolicyType)type; + policy.Enabled = true; + policy.Data = null; + + sutProvider.GetDependency() + .GetByOrganizationIdTypeAsync(orgId, (PolicyType)type) + .Returns(policy); + + // Act + var result = await sutProvider.Sut.Get(orgId, type); + + // Assert + Assert.IsType(result); + Assert.Equal(policy.Id, result.Id); + Assert.Equal(policy.Type, result.Type); + Assert.Equal(policy.Enabled, result.Enabled); + Assert.Equal(policy.OrganizationId, result.OrganizationId); + } + + [Theory] + [BitAutoData] + public async Task Get_WhenUserCanManagePolicies_WithNonExistingType_ReturnsDefaultPolicy( + SutProvider sutProvider, Guid orgId, int type) + { + // Arrange + sutProvider.GetDependency() + .ManagePolicies(orgId) + .Returns(true); + + sutProvider.GetDependency() + .GetByOrganizationIdTypeAsync(orgId, (PolicyType)type) + .Returns((Policy)null); + + // Act + var result = await sutProvider.Sut.Get(orgId, type); + + // Assert + Assert.IsType(result); + Assert.Equal(result.Type, (PolicyType)type); + Assert.False(result.Enabled); + } + + [Theory] + [BitAutoData] + public async Task Get_WhenUserCannotManagePolicies_ThrowsNotFoundException( + SutProvider sutProvider, Guid orgId, int type) + { + // Arrange + sutProvider.GetDependency() + .ManagePolicies(orgId) + .Returns(false); + + // Act & Assert + await Assert.ThrowsAsync(() => sutProvider.Sut.Get(orgId, type)); + } + } diff --git a/test/Core.Test/AdminConsole/Authorization/OrganizationUserUserDetailsAuthorizationHandlerTests.cs b/test/Core.Test/AdminConsole/Authorization/OrganizationUserUserDetailsAuthorizationHandlerTests.cs index 4d9208a2b..4a3a7f647 100644 --- a/test/Core.Test/AdminConsole/Authorization/OrganizationUserUserDetailsAuthorizationHandlerTests.cs +++ b/test/Core.Test/AdminConsole/Authorization/OrganizationUserUserDetailsAuthorizationHandlerTests.cs @@ -2,7 +2,6 @@ using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Authorization; using Bit.Core.Context; using Bit.Core.Enums; -using Bit.Core.Services; using Bit.Core.Test.AdminConsole.AutoFixture; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; @@ -24,7 +23,6 @@ public class OrganizationUserUserDetailsAuthorizationHandlerTests CurrentContextOrganization organization, SutProvider sutProvider) { - EnableFeatureFlag(sutProvider); organization.Type = userType; sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); @@ -48,7 +46,6 @@ public class OrganizationUserUserDetailsAuthorizationHandlerTests CurrentContextOrganization organization, SutProvider sutProvider) { - EnableFeatureFlag(sutProvider); organization.Type = OrganizationUserType.User; sutProvider.GetDependency() .ProviderUserForOrgAsync(organization.Id) @@ -69,7 +66,6 @@ public class OrganizationUserUserDetailsAuthorizationHandlerTests CurrentContextOrganization organization, SutProvider sutProvider) { - EnableFeatureFlag(sutProvider); organization.Type = OrganizationUserType.User; sutProvider.GetDependency().GetOrganization(Arg.Any()).Returns(organization); sutProvider.GetDependency().ProviderUserForOrgAsync(Arg.Any()).Returns(false); @@ -88,78 +84,6 @@ public class OrganizationUserUserDetailsAuthorizationHandlerTests public async Task ReadAll_NotMember_NoSuccess( CurrentContextOrganization organization, SutProvider sutProvider) - { - EnableFeatureFlag(sutProvider); - var context = new AuthorizationHandlerContext( - new[] { OrganizationUserUserDetailsOperations.ReadAll }, - new ClaimsPrincipal(), - new OrganizationScope(organization.Id) - ); - - sutProvider.GetDependency().GetOrganization(Arg.Any()).Returns((CurrentContextOrganization)null); - sutProvider.GetDependency().ProviderUserForOrgAsync(Arg.Any()).Returns(false); - - await sutProvider.Sut.HandleAsync(context); - Assert.False(context.HasSucceeded); - } - - private void EnableFeatureFlag(SutProvider sutProvider) - { - sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.Pm3478RefactorOrganizationUserApi) - .Returns(true); - } - - // TESTS WITH FLAG DISABLED - TO BE DELETED IN FLAG CLEANUP - - [Theory, CurrentContextOrganizationCustomize] - [BitAutoData(OrganizationUserType.Admin)] - [BitAutoData(OrganizationUserType.Owner)] - [BitAutoData(OrganizationUserType.User)] - [BitAutoData(OrganizationUserType.Custom)] - public async Task FlagDisabled_ReadAll_AnyMemberOfOrg_Success( - OrganizationUserType userType, - Guid userId, SutProvider sutProvider, - CurrentContextOrganization organization) - { - organization.Type = userType; - - var context = new AuthorizationHandlerContext( - new[] { OrganizationUserUserDetailsOperations.ReadAll }, - new ClaimsPrincipal(), - new OrganizationScope(organization.Id)); - - sutProvider.GetDependency().UserId.Returns(userId); - sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); - - await sutProvider.Sut.HandleAsync(context); - - Assert.True(context.HasSucceeded); - } - - [Theory, BitAutoData, CurrentContextOrganizationCustomize] - public async Task FlagDisabled_ReadAll_ProviderUser_Success( - CurrentContextOrganization organization, - SutProvider sutProvider) - { - organization.Type = OrganizationUserType.User; - sutProvider.GetDependency() - .ProviderUserForOrgAsync(organization.Id) - .Returns(true); - - var context = new AuthorizationHandlerContext( - new[] { OrganizationUserUserDetailsOperations.ReadAll }, - new ClaimsPrincipal(), - new OrganizationScope(organization.Id)); - - await sutProvider.Sut.HandleAsync(context); - - Assert.True(context.HasSucceeded); - } - - [Theory, BitAutoData] - public async Task FlagDisabled_ReadAll_NotMember_NoSuccess( - CurrentContextOrganization organization, - SutProvider sutProvider) { var context = new AuthorizationHandlerContext( new[] { OrganizationUserUserDetailsOperations.ReadAll }, diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationDomains/OrganizationHasVerifiedDomainsQueryTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationDomains/OrganizationHasVerifiedDomainsQueryTests.cs new file mode 100644 index 000000000..f63f6e48b --- /dev/null +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationDomains/OrganizationHasVerifiedDomainsQueryTests.cs @@ -0,0 +1,57 @@ +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains; +using Bit.Core.Entities; +using Bit.Core.Repositories; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationDomains; + +[SutProviderCustomize] +public class OrganizationHasVerifiedDomainsQueryTests +{ + [Theory, BitAutoData] + public async Task HasVerifiedDomainsAsync_WithVerifiedDomain_ReturnsTrue( + OrganizationDomain organizationDomain, + SutProvider sutProvider) + { + organizationDomain.SetVerifiedDate(); // Set the verified date to make it verified + + sutProvider.GetDependency() + .GetDomainsByOrganizationIdAsync(organizationDomain.OrganizationId) + .Returns(new List { organizationDomain }); + + var result = await sutProvider.Sut.HasVerifiedDomainsAsync(organizationDomain.OrganizationId); + + Assert.True(result); + } + + [Theory, BitAutoData] + public async Task HasVerifiedDomainsAsync_WithoutVerifiedDomain_ReturnsFalse( + OrganizationDomain organizationDomain, + SutProvider sutProvider) + { + sutProvider.GetDependency() + .GetDomainsByOrganizationIdAsync(organizationDomain.OrganizationId) + .Returns(new List { organizationDomain }); + + var result = await sutProvider.Sut.HasVerifiedDomainsAsync(organizationDomain.OrganizationId); + + Assert.False(result); + } + + [Theory, BitAutoData] + public async Task HasVerifiedDomainsAsync_WithoutOrganizationDomains_ReturnsFalse( + Guid organizationId, + SutProvider sutProvider) + { + sutProvider.GetDependency() + .GetDomainsByOrganizationIdAsync(organizationId) + .Returns(new List()); + + var result = await sutProvider.Sut.HasVerifiedDomainsAsync(organizationId); + + Assert.False(result); + } +} diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationDomains/VerifyOrganizationDomainCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationDomains/VerifyOrganizationDomainCommandTests.cs index d61ded28b..2fcaf8134 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationDomains/VerifyOrganizationDomainCommandTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationDomains/VerifyOrganizationDomainCommandTests.cs @@ -1,4 +1,7 @@ -using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains; +using Bit.Core.AdminConsole.Services; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; @@ -15,7 +18,7 @@ namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationDomains; public class VerifyOrganizationDomainCommandTests { [Theory, BitAutoData] - public async Task UserVerifyOrganizationDomain_ShouldThrowConflict_WhenDomainHasBeenClaimed(Guid id, + public async Task UserVerifyOrganizationDomainAsync_ShouldThrowConflict_WhenDomainHasBeenClaimed(Guid id, SutProvider sutProvider) { var expected = new OrganizationDomain @@ -37,7 +40,7 @@ public class VerifyOrganizationDomainCommandTests } [Theory, BitAutoData] - public async Task UserVerifyOrganizationDomain_ShouldThrowConflict_WhenDomainHasBeenClaimedByAnotherOrganization(Guid id, + public async Task UserVerifyOrganizationDomainAsync_ShouldThrowConflict_WhenDomainHasBeenClaimedByAnotherOrganization(Guid id, SutProvider sutProvider) { var expected = new OrganizationDomain @@ -61,7 +64,7 @@ public class VerifyOrganizationDomainCommandTests } [Theory, BitAutoData] - public async Task UserVerifyOrganizationDomain_ShouldVerifyDomainUpdateAndLogEvent_WhenTxtRecordExists(Guid id, + public async Task UserVerifyOrganizationDomainAsync_ShouldVerifyDomainUpdateAndLogEvent_WhenTxtRecordExists(Guid id, SutProvider sutProvider) { var expected = new OrganizationDomain @@ -91,7 +94,7 @@ public class VerifyOrganizationDomainCommandTests } [Theory, BitAutoData] - public async Task UserVerifyOrganizationDomain_ShouldNotSetVerifiedDate_WhenTxtRecordDoesNotExist(Guid id, + public async Task UserVerifyOrganizationDomainAsync_ShouldNotSetVerifiedDate_WhenTxtRecordDoesNotExist(Guid id, SutProvider sutProvider) { var expected = new OrganizationDomain @@ -120,7 +123,7 @@ public class VerifyOrganizationDomainCommandTests [Theory, BitAutoData] - public async Task SystemVerifyOrganizationDomain_CallsEventServiceWithUpdatedJobRunCount(SutProvider sutProvider) + public async Task SystemVerifyOrganizationDomainAsync_CallsEventServiceWithUpdatedJobRunCount(SutProvider sutProvider) { var domain = new OrganizationDomain() { @@ -137,4 +140,97 @@ public class VerifyOrganizationDomainCommandTests .LogOrganizationDomainEventAsync(default, EventType.OrganizationDomain_NotVerified, EventSystemUser.DomainVerification); } + + [Theory, BitAutoData] + public async Task UserVerifyOrganizationDomainAsync_GivenOrganizationDomainWithAccountDeprovisioningEnabled_WhenDomainIsVerified_ThenSingleOrgPolicyShouldBeEnabled( + OrganizationDomain domain, SutProvider sutProvider) + { + sutProvider.GetDependency() + .GetClaimedDomainsByDomainNameAsync(domain.DomainName) + .Returns([]); + + sutProvider.GetDependency() + .ResolveAsync(domain.DomainName, domain.Txt) + .Returns(true); + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.AccountDeprovisioning) + .Returns(true); + + _ = await sutProvider.Sut.UserVerifyOrganizationDomainAsync(domain); + + await sutProvider.GetDependency() + .Received(1) + .SaveAsync(Arg.Is(x => x.Type == PolicyType.SingleOrg && x.OrganizationId == domain.OrganizationId && x.Enabled), null); + } + + [Theory, BitAutoData] + public async Task UserVerifyOrganizationDomainAsync_GivenOrganizationDomainWithAccountDeprovisioningDisabled_WhenDomainIsVerified_ThenSingleOrgPolicyShouldBeNotBeEnabled( + OrganizationDomain domain, SutProvider sutProvider) + { + sutProvider.GetDependency() + .GetClaimedDomainsByDomainNameAsync(domain.DomainName) + .Returns([]); + + sutProvider.GetDependency() + .ResolveAsync(domain.DomainName, domain.Txt) + .Returns(true); + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.AccountDeprovisioning) + .Returns(false); + + _ = await sutProvider.Sut.UserVerifyOrganizationDomainAsync(domain); + + await sutProvider.GetDependency() + .DidNotReceive() + .SaveAsync(Arg.Any(), null); + } + + [Theory, BitAutoData] + public async Task UserVerifyOrganizationDomainAsync_GivenOrganizationDomainWithAccountDeprovisioningEnabled_WhenDomainIsNotVerified_ThenSingleOrgPolicyShouldNotBeEnabled( + OrganizationDomain domain, SutProvider sutProvider) + { + sutProvider.GetDependency() + .GetClaimedDomainsByDomainNameAsync(domain.DomainName) + .Returns([]); + + sutProvider.GetDependency() + .ResolveAsync(domain.DomainName, domain.Txt) + .Returns(false); + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.AccountDeprovisioning) + .Returns(true); + + _ = await sutProvider.Sut.UserVerifyOrganizationDomainAsync(domain); + + await sutProvider.GetDependency() + .DidNotReceive() + .SaveAsync(Arg.Any(), null); + + } + + [Theory, BitAutoData] + public async Task UserVerifyOrganizationDomainAsync_GivenOrganizationDomainWithAccountDeprovisioningDisabled_WhenDomainIsNotVerified_ThenSingleOrgPolicyShouldBeNotBeEnabled( + OrganizationDomain domain, SutProvider sutProvider) + { + sutProvider.GetDependency() + .GetClaimedDomainsByDomainNameAsync(domain.DomainName) + .Returns([]); + + sutProvider.GetDependency() + .ResolveAsync(domain.DomainName, domain.Txt) + .Returns(false); + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.AccountDeprovisioning) + .Returns(true); + + _ = await sutProvider.Sut.UserVerifyOrganizationDomainAsync(domain); + + await sutProvider.GetDependency() + .DidNotReceive() + .SaveAsync(Arg.Any(), null); + } } diff --git a/test/Core.Test/AdminConsole/Services/OrganizationDomainServiceTests.cs b/test/Core.Test/AdminConsole/Services/OrganizationDomainServiceTests.cs index c779e3a1c..210726061 100644 --- a/test/Core.Test/AdminConsole/Services/OrganizationDomainServiceTests.cs +++ b/test/Core.Test/AdminConsole/Services/OrganizationDomainServiceTests.cs @@ -76,48 +76,4 @@ public class OrganizationDomainServiceTests await sutProvider.GetDependency().ReceivedWithAnyArgs(1) .DeleteExpiredAsync(7); } - - [Theory, BitAutoData] - public async Task HasVerifiedDomainsAsync_WithVerifiedDomain_ReturnsTrue( - OrganizationDomain organizationDomain, - SutProvider sutProvider) - { - organizationDomain.SetVerifiedDate(); // Set the verified date to make it verified - - sutProvider.GetDependency() - .GetDomainsByOrganizationIdAsync(organizationDomain.OrganizationId) - .Returns(new List { organizationDomain }); - - var result = await sutProvider.Sut.HasVerifiedDomainsAsync(organizationDomain.OrganizationId); - - Assert.True(result); - } - - [Theory, BitAutoData] - public async Task HasVerifiedDomainsAsync_WithoutVerifiedDomain_ReturnsFalse( - OrganizationDomain organizationDomain, - SutProvider sutProvider) - { - sutProvider.GetDependency() - .GetDomainsByOrganizationIdAsync(organizationDomain.OrganizationId) - .Returns(new List { organizationDomain }); - - var result = await sutProvider.Sut.HasVerifiedDomainsAsync(organizationDomain.OrganizationId); - - Assert.False(result); - } - - [Theory, BitAutoData] - public async Task HasVerifiedDomainsAsync_WithoutOrganizationDomains_ReturnsFalse( - Guid organizationId, - SutProvider sutProvider) - { - sutProvider.GetDependency() - .GetDomainsByOrganizationIdAsync(organizationId) - .Returns(new List()); - - var result = await sutProvider.Sut.HasVerifiedDomainsAsync(organizationId); - - Assert.False(result); - } } diff --git a/test/Core.Test/AdminConsole/Services/PolicyServiceTests.cs b/test/Core.Test/AdminConsole/Services/PolicyServiceTests.cs index f9bc49bbe..da3f2b267 100644 --- a/test/Core.Test/AdminConsole/Services/PolicyServiceTests.cs +++ b/test/Core.Test/AdminConsole/Services/PolicyServiceTests.cs @@ -1,6 +1,7 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Services.Implementations; @@ -815,4 +816,32 @@ public class PolicyServiceTests new() { OrganizationId = Guid.NewGuid(), PolicyType = PolicyType.DisableSend, PolicyEnabled = true, OrganizationUserType = OrganizationUserType.User, OrganizationUserStatus = OrganizationUserStatusType.Invited, IsProvider = true } }); } + + + [Theory, BitAutoData] + public async Task SaveAsync_GivenOrganizationUsingPoliciesAndHasVerifiedDomains_WhenSingleOrgPolicyIsDisabled_ThenAnErrorShouldBeThrownOrganizationHasVerifiedDomains( + [AdminConsoleFixtures.Policy(PolicyType.SingleOrg)] Policy policy, Organization org, SutProvider sutProvider) + { + org.Id = policy.OrganizationId; + org.UsePolicies = true; + + policy.Enabled = false; + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.AccountDeprovisioning) + .Returns(true); + + sutProvider.GetDependency() + .GetByIdAsync(policy.OrganizationId) + .Returns(org); + + sutProvider.GetDependency() + .HasVerifiedDomainsAsync(org.Id) + .Returns(true); + + var badRequestException = await Assert.ThrowsAsync( + () => sutProvider.Sut.SaveAsync(policy, null)); + + Assert.Equal("Organization has verified domains.", badRequestException.Message); + } } diff --git a/test/Core.Test/Auth/Models/Api/Request/Accounts/RegisterFinishRequestModelTests.cs b/test/Core.Test/Auth/Models/Api/Request/Accounts/RegisterFinishRequestModelTests.cs new file mode 100644 index 000000000..588ca878f --- /dev/null +++ b/test/Core.Test/Auth/Models/Api/Request/Accounts/RegisterFinishRequestModelTests.cs @@ -0,0 +1,173 @@ +using Bit.Core.Auth.Models.Api.Request.Accounts; +using Bit.Core.Enums; +using Bit.Test.Common.AutoFixture.Attributes; +using Xunit; + +namespace Bit.Core.Test.Auth.Models.Api.Request.Accounts; + +public class RegisterFinishRequestModelTests +{ + [Theory] + [BitAutoData] + public void GetTokenType_Returns_EmailVerification(string email, string masterPasswordHash, + string userSymmetricKey, KeysRequestModel userAsymmetricKeys, KdfType kdf, int kdfIterations, string emailVerificationToken) + { + // Arrange + var model = new RegisterFinishRequestModel + { + Email = email, + MasterPasswordHash = masterPasswordHash, + UserSymmetricKey = userSymmetricKey, + UserAsymmetricKeys = userAsymmetricKeys, + Kdf = kdf, + KdfIterations = kdfIterations, + EmailVerificationToken = emailVerificationToken + }; + + // Act + Assert.Equal(RegisterFinishTokenType.EmailVerification, model.GetTokenType()); + } + + [Theory] + [BitAutoData] + public void GetTokenType_Returns_OrganizationInvite(string email, string masterPasswordHash, + string userSymmetricKey, KeysRequestModel userAsymmetricKeys, KdfType kdf, int kdfIterations, string orgInviteToken, Guid organizationUserId) + { + // Arrange + var model = new RegisterFinishRequestModel + { + Email = email, + MasterPasswordHash = masterPasswordHash, + UserSymmetricKey = userSymmetricKey, + UserAsymmetricKeys = userAsymmetricKeys, + Kdf = kdf, + KdfIterations = kdfIterations, + OrgInviteToken = orgInviteToken, + OrganizationUserId = organizationUserId + }; + + // Act + Assert.Equal(RegisterFinishTokenType.OrganizationInvite, model.GetTokenType()); + } + + [Theory] + [BitAutoData] + public void GetTokenType_Returns_OrgSponsoredFreeFamilyPlan(string email, string masterPasswordHash, + string userSymmetricKey, KeysRequestModel userAsymmetricKeys, KdfType kdf, int kdfIterations, string orgSponsoredFreeFamilyPlanToken) + { + // Arrange + var model = new RegisterFinishRequestModel + { + Email = email, + MasterPasswordHash = masterPasswordHash, + UserSymmetricKey = userSymmetricKey, + UserAsymmetricKeys = userAsymmetricKeys, + Kdf = kdf, + KdfIterations = kdfIterations, + OrgSponsoredFreeFamilyPlanToken = orgSponsoredFreeFamilyPlanToken + }; + + // Act + Assert.Equal(RegisterFinishTokenType.OrgSponsoredFreeFamilyPlan, model.GetTokenType()); + } + + [Theory] + [BitAutoData] + public void GetTokenType_Returns_EmergencyAccessInvite(string email, string masterPasswordHash, + string userSymmetricKey, KeysRequestModel userAsymmetricKeys, KdfType kdf, int kdfIterations, string acceptEmergencyAccessInviteToken, Guid acceptEmergencyAccessId) + { + // Arrange + var model = new RegisterFinishRequestModel + { + Email = email, + MasterPasswordHash = masterPasswordHash, + UserSymmetricKey = userSymmetricKey, + UserAsymmetricKeys = userAsymmetricKeys, + Kdf = kdf, + KdfIterations = kdfIterations, + AcceptEmergencyAccessInviteToken = acceptEmergencyAccessInviteToken, + AcceptEmergencyAccessId = acceptEmergencyAccessId + }; + + // Act + Assert.Equal(RegisterFinishTokenType.EmergencyAccessInvite, model.GetTokenType()); + } + + [Theory] + [BitAutoData] + public void GetTokenType_Returns_ProviderInvite(string email, string masterPasswordHash, + string userSymmetricKey, KeysRequestModel userAsymmetricKeys, KdfType kdf, int kdfIterations, string providerInviteToken, Guid providerUserId) + { + // Arrange + var model = new RegisterFinishRequestModel + { + Email = email, + MasterPasswordHash = masterPasswordHash, + UserSymmetricKey = userSymmetricKey, + UserAsymmetricKeys = userAsymmetricKeys, + Kdf = kdf, + KdfIterations = kdfIterations, + ProviderInviteToken = providerInviteToken, + ProviderUserId = providerUserId + }; + + // Act + Assert.Equal(RegisterFinishTokenType.ProviderInvite, model.GetTokenType()); + } + + [Theory] + [BitAutoData] + public void GetTokenType_Returns_Invalid(string email, string masterPasswordHash, + string userSymmetricKey, KeysRequestModel userAsymmetricKeys, KdfType kdf, int kdfIterations) + { + // Arrange + var model = new RegisterFinishRequestModel + { + Email = email, + MasterPasswordHash = masterPasswordHash, + UserSymmetricKey = userSymmetricKey, + UserAsymmetricKeys = userAsymmetricKeys, + Kdf = kdf, + KdfIterations = kdfIterations + }; + + // Act + var result = Assert.Throws(() => model.GetTokenType()); + Assert.Equal("Invalid token type.", result.Message); + } + + [Theory] + [BitAutoData] + public void ToUser_Returns_User(string email, string masterPasswordHash, string masterPasswordHint, + string userSymmetricKey, KeysRequestModel userAsymmetricKeys, KdfType kdf, int kdfIterations, + int? kdfMemory, int? kdfParallelism) + { + // Arrange + var model = new RegisterFinishRequestModel + { + Email = email, + MasterPasswordHash = masterPasswordHash, + MasterPasswordHint = masterPasswordHint, + UserSymmetricKey = userSymmetricKey, + UserAsymmetricKeys = userAsymmetricKeys, + Kdf = kdf, + KdfIterations = kdfIterations, + KdfMemory = kdfMemory, + KdfParallelism = kdfParallelism + }; + + // Act + var result = model.ToUser(); + + // Assert + Assert.Equal(email, result.Email); + Assert.Equal(masterPasswordHint, result.MasterPasswordHint); + Assert.Equal(kdf, result.Kdf); + Assert.Equal(kdfIterations, result.KdfIterations); + Assert.Equal(kdfMemory, result.KdfMemory); + Assert.Equal(kdfParallelism, result.KdfParallelism); + Assert.Equal(userSymmetricKey, result.Key); + Assert.Equal(userAsymmetricKeys.PublicKey, result.PublicKey); + Assert.Equal(userAsymmetricKeys.EncryptedPrivateKey, result.PrivateKey); + } +} diff --git a/test/Core.Test/Auth/UserFeatures/Registration/RegisterUserCommandTests.cs b/test/Core.Test/Auth/UserFeatures/Registration/RegisterUserCommandTests.cs index e96e3553d..02ecb4ecd 100644 --- a/test/Core.Test/Auth/UserFeatures/Registration/RegisterUserCommandTests.cs +++ b/test/Core.Test/Auth/UserFeatures/Registration/RegisterUserCommandTests.cs @@ -1,4 +1,5 @@ -using Bit.Core.AdminConsole.Entities; +using System.Text; +using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Auth.Entities; @@ -19,7 +20,9 @@ using Bit.Core.Tools.Services; using Bit.Core.Utilities; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; +using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.WebUtilities; using NSubstitute; using Xunit; @@ -28,8 +31,10 @@ namespace Bit.Core.Test.Auth.UserFeatures.Registration; [SutProviderCustomize] public class RegisterUserCommandTests { - + // ----------------------------------------------------------------------------------------------- // RegisterUser tests + // ----------------------------------------------------------------------------------------------- + [Theory] [BitAutoData] public async Task RegisterUser_Succeeds(SutProvider sutProvider, User user) @@ -86,7 +91,10 @@ public class RegisterUserCommandTests .RaiseEventAsync(Arg.Any()); } + // ----------------------------------------------------------------------------------------------- // RegisterUserWithOrganizationInviteToken tests + // ----------------------------------------------------------------------------------------------- + // Simple happy path test [Theory] [BitAutoData] @@ -312,7 +320,10 @@ public class RegisterUserCommandTests Assert.Equal(expectedErrorMessage, exception.Message); } - // RegisterUserViaEmailVerificationToken + // ----------------------------------------------------------------------------------------------- + // RegisterUserViaEmailVerificationToken tests + // ----------------------------------------------------------------------------------------------- + [Theory] [BitAutoData] public async Task RegisterUserViaEmailVerificationToken_Succeeds(SutProvider sutProvider, User user, string masterPasswordHash, string emailVerificationToken, bool receiveMarketingMaterials) @@ -382,10 +393,9 @@ public class RegisterUserCommandTests } - - - // RegisterUserViaOrganizationSponsoredFreeFamilyPlanInviteToken - + // ----------------------------------------------------------------------------------------------- + // RegisterUserViaOrganizationSponsoredFreeFamilyPlanInviteToken tests + // ----------------------------------------------------------------------------------------------- [Theory] [BitAutoData] @@ -452,7 +462,9 @@ public class RegisterUserCommandTests Assert.Equal("Open registration has been disabled by the system administrator.", result.Message); } - // RegisterUserViaAcceptEmergencyAccessInviteToken + // ----------------------------------------------------------------------------------------------- + // RegisterUserViaAcceptEmergencyAccessInviteToken tests + // ----------------------------------------------------------------------------------------------- [Theory] [BitAutoData] @@ -495,8 +507,6 @@ public class RegisterUserCommandTests .RaiseEventAsync(Arg.Is(refEvent => refEvent.Type == ReferenceEventType.Signup)); } - - [Theory] [BitAutoData] public async Task RegisterUserViaAcceptEmergencyAccessInviteToken_InvalidToken_ThrowsBadRequestException(SutProvider sutProvider, User user, @@ -536,5 +546,140 @@ public class RegisterUserCommandTests Assert.Equal("Open registration has been disabled by the system administrator.", result.Message); } + // ----------------------------------------------------------------------------------------------- + // RegisterUserViaProviderInviteToken tests + // ----------------------------------------------------------------------------------------------- + + [Theory] + [BitAutoData] + public async Task RegisterUserViaProviderInviteToken_Succeeds(SutProvider sutProvider, + User user, string masterPasswordHash, Guid providerUserId) + { + // Arrange + // 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() + .CreateUserAsync(user, masterPasswordHash) + .Returns(IdentityResult.Success); + + // 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 + var result = await sutProvider.Sut.RegisterUserViaProviderInviteToken(user, masterPasswordHash, base64EncodedProviderInvToken, providerUserId); + + // Assert + Assert.True(result.Succeeded); + + await sutProvider.GetDependency() + .Received(1) + .CreateUserAsync(Arg.Is(u => u.Name == user.Name && u.EmailVerified == true && u.ApiKey != null), masterPasswordHash); + + await sutProvider.GetDependency() + .Received(1) + .SendWelcomeEmailAsync(user); + + await sutProvider.GetDependency() + .Received(1) + .RaiseEventAsync(Arg.Is(refEvent => refEvent.Type == ReferenceEventType.Signup)); + } + + [Theory] + [BitAutoData] + public async Task RegisterUserViaProviderInviteToken_InvalidToken_ThrowsBadRequestException(SutProvider sutProvider, + User user, string masterPasswordHash, Guid providerUserId) + { + // Arrange + // 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 + + // 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 result = await Assert.ThrowsAsync(() => + sutProvider.Sut.RegisterUserViaProviderInviteToken(user, masterPasswordHash, base64EncodedProviderInvToken, Guid.NewGuid())); + Assert.Equal("Invalid provider invite token.", result.Message); + } + + [Theory] + [BitAutoData] + public async Task RegisterUserViaProviderInviteToken_DisabledOpenRegistration_ThrowsBadRequestException(SutProvider sutProvider, + User user, string masterPasswordHash, Guid providerUserId) + { + // Arrange + // 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() + .DisableUserRegistration = 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 result = await Assert.ThrowsAsync(() => + sutProvider.Sut.RegisterUserViaProviderInviteToken(user, masterPasswordHash, base64EncodedProviderInvToken, providerUserId)); + Assert.Equal("Open registration has been disabled by the system administrator.", result.Message); + } + } diff --git a/test/Identity.IntegrationTest/Controllers/AccountsControllerTests.cs b/test/Identity.IntegrationTest/Controllers/AccountsControllerTests.cs index 50f7d70ab..3b8534ef3 100644 --- a/test/Identity.IntegrationTest/Controllers/AccountsControllerTests.cs +++ b/test/Identity.IntegrationTest/Controllers/AccountsControllerTests.cs @@ -1,4 +1,5 @@ using System.ComponentModel.DataAnnotations; +using System.Text; using Bit.Core; using Bit.Core.Auth.Entities; using Bit.Core.Auth.Models.Api.Request.Accounts; @@ -9,10 +10,12 @@ using Bit.Core.Models.Business.Tokenables; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Tokens; +using Bit.Core.Utilities; using Bit.Identity.Models.Request.Accounts; using Bit.IntegrationTestCommon.Factories; using Bit.Test.Common.AutoFixture.Attributes; - +using Microsoft.AspNetCore.DataProtection; +using Microsoft.AspNetCore.WebUtilities; using Microsoft.EntityFrameworkCore; using NSubstitute; using Xunit; @@ -470,6 +473,80 @@ public class AccountsControllerTests : IClassFixture Assert.Equal(kdfParallelism, user.KdfParallelism); } + [Theory, BitAutoData] + public async Task RegistrationWithEmailVerification_WithProviderInviteToken_Succeeds( + [StringLength(1000)] string masterPasswordHash, [StringLength(50)] string masterPasswordHint, string userSymmetricKey, + KeysRequestModel userAsymmetricKeys, int kdfMemory, int kdfParallelism) + { + + // Localize factory to just this test. + var localFactory = new IdentityApplicationFactory(); + + // Hardcoded, valid data + var email = "jsnider+local253@bitwarden.com"; + var providerUserId = new Guid("c6fdba35-2e52-43b4-8fb7-b211011d154a"); + var nowMillis = CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow); + var decryptedProviderInviteToken = $"ProviderUserInvite {providerUserId} {email} {nowMillis}"; + // var providerInviteToken = await GetValidProviderInviteToken(localFactory, email, providerUserId); + + // 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(); + mockDataProtector.Unprotect(Arg.Any()).Returns(decryptedProviderInviteTokenByteArray); + + localFactory.SubstituteService(dataProtectionProvider => + { + dataProtectionProvider.CreateProtector(Arg.Any()) + .Returns(mockDataProtector); + }); + + // As token contains now milliseconds for when it was created, create 1k year timespan for expiration + // to ensure token is valid for a good long while. + localFactory.UpdateConfiguration("globalSettings:OrganizationInviteExpirationHours", "8760000"); + + var registerFinishReqModel = new RegisterFinishRequestModel + { + Email = email, + MasterPasswordHash = masterPasswordHash, + MasterPasswordHint = masterPasswordHint, + ProviderInviteToken = base64EncodedProviderInvToken, + ProviderUserId = providerUserId, + Kdf = KdfType.PBKDF2_SHA256, + KdfIterations = AuthConstants.PBKDF2_ITERATIONS.Default, + UserSymmetricKey = userSymmetricKey, + UserAsymmetricKeys = userAsymmetricKeys, + KdfMemory = kdfMemory, + KdfParallelism = kdfParallelism + }; + + var postRegisterFinishHttpContext = await localFactory.PostRegisterFinishAsync(registerFinishReqModel); + + Assert.Equal(StatusCodes.Status200OK, postRegisterFinishHttpContext.Response.StatusCode); + + var database = localFactory.GetDatabaseContext(); + var user = await database.Users + .SingleAsync(u => u.Email == email); + + Assert.NotNull(user); + + // Assert user properties match the request model + Assert.Equal(email, user.Email); + Assert.NotEqual(masterPasswordHash, user.MasterPassword); // We execute server side hashing + Assert.NotNull(user.MasterPassword); + Assert.Equal(masterPasswordHint, user.MasterPasswordHint); + Assert.Equal(userSymmetricKey, user.Key); + Assert.Equal(userAsymmetricKeys.EncryptedPrivateKey, user.PrivateKey); + Assert.Equal(userAsymmetricKeys.PublicKey, user.PublicKey); + Assert.Equal(KdfType.PBKDF2_SHA256, user.Kdf); + Assert.Equal(AuthConstants.PBKDF2_ITERATIONS.Default, user.KdfIterations); + Assert.Equal(kdfMemory, user.KdfMemory); + Assert.Equal(kdfParallelism, user.KdfParallelism); + } + [Theory, BitAutoData] public async Task PostRegisterVerificationEmailClicked_Success( @@ -527,4 +604,5 @@ public class AccountsControllerTests : IClassFixture return user; } + } diff --git a/test/Identity.IntegrationTest/Identity.IntegrationTest.csproj b/test/Identity.IntegrationTest/Identity.IntegrationTest.csproj index 10240727c..d7a7bb9a0 100644 --- a/test/Identity.IntegrationTest/Identity.IntegrationTest.csproj +++ b/test/Identity.IntegrationTest/Identity.IntegrationTest.csproj @@ -10,7 +10,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all
- + diff --git a/test/Identity.IntegrationTest/RequestValidation/ResourceOwnerPasswordValidatorTests.cs b/test/Identity.IntegrationTest/RequestValidation/ResourceOwnerPasswordValidatorTests.cs index 91d0ee01f..703faed48 100644 --- a/test/Identity.IntegrationTest/RequestValidation/ResourceOwnerPasswordValidatorTests.cs +++ b/test/Identity.IntegrationTest/RequestValidation/ResourceOwnerPasswordValidatorTests.cs @@ -5,7 +5,7 @@ using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Repositories; using Bit.Core.Services; -using Bit.Identity.IdentityServer; +using Bit.Identity.IdentityServer.RequestValidators; using Bit.Identity.Models.Request.Accounts; using Bit.IntegrationTestCommon.Factories; using Bit.Test.Common.AutoFixture.Attributes; @@ -237,6 +237,11 @@ public class ResourceOwnerPasswordValidatorTests : IClassFixture>(); + await factory.RegisterAsync(new RegisterRequestModel + { + Email = DefaultUsername, + MasterPasswordHash = DefaultPassword + }); var user = await userManager.FindByEmailAsync(DefaultUsername); Assert.NotNull(user); diff --git a/test/Identity.Test/IdentityServer/BaseRequestValidatorTests.cs b/test/Identity.Test/IdentityServer/BaseRequestValidatorTests.cs index 39b7edf8d..d0372202a 100644 --- a/test/Identity.Test/IdentityServer/BaseRequestValidatorTests.cs +++ b/test/Identity.Test/IdentityServer/BaseRequestValidatorTests.cs @@ -1,8 +1,7 @@ using Bit.Core; +using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Services; -using Bit.Core.Auth.Identity; -using Bit.Core.Auth.Models.Business.Tokenables; using Bit.Core.Auth.Repositories; using Bit.Core.Context; using Bit.Core.Entities; @@ -11,8 +10,8 @@ using Bit.Core.Models.Api; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; -using Bit.Core.Tokens; using Bit.Identity.IdentityServer; +using Bit.Identity.IdentityServer.RequestValidators; using Bit.Identity.Test.Wrappers; using Bit.Test.Common.AutoFixture.Attributes; using Duende.IdentityServer.Validation; @@ -32,18 +31,14 @@ public class BaseRequestValidatorTests private readonly IUserService _userService; private readonly IEventService _eventService; private readonly IDeviceValidator _deviceValidator; - private readonly IOrganizationDuoWebTokenProvider _organizationDuoWebTokenProvider; - private readonly ITemporaryDuoWebV4SDKService _duoWebV4SDKService; - private readonly IOrganizationRepository _organizationRepository; + private readonly ITwoFactorAuthenticationValidator _twoFactorAuthenticationValidator; private readonly IOrganizationUserRepository _organizationUserRepository; - private readonly IApplicationCacheService _applicationCacheService; private readonly IMailService _mailService; private readonly ILogger _logger; private readonly ICurrentContext _currentContext; private readonly GlobalSettings _globalSettings; private readonly IUserRepository _userRepository; private readonly IPolicyService _policyService; - private readonly IDataProtectorTokenFactory _tokenDataFactory; private readonly IFeatureService _featureService; private readonly ISsoConfigRepository _ssoConfigRepository; private readonly IUserDecryptionOptionsBuilder _userDecryptionOptionsBuilder; @@ -52,43 +47,35 @@ public class BaseRequestValidatorTests public BaseRequestValidatorTests() { + _userManager = SubstituteUserManager(); _userService = Substitute.For(); _eventService = Substitute.For(); _deviceValidator = Substitute.For(); - _organizationDuoWebTokenProvider = Substitute.For(); - _duoWebV4SDKService = Substitute.For(); - _organizationRepository = Substitute.For(); + _twoFactorAuthenticationValidator = Substitute.For(); _organizationUserRepository = Substitute.For(); - _applicationCacheService = Substitute.For(); _mailService = Substitute.For(); _logger = Substitute.For>(); _currentContext = Substitute.For(); _globalSettings = Substitute.For(); _userRepository = Substitute.For(); _policyService = Substitute.For(); - _tokenDataFactory = Substitute.For>(); _featureService = Substitute.For(); _ssoConfigRepository = Substitute.For(); _userDecryptionOptionsBuilder = Substitute.For(); - _userManager = SubstituteUserManager(); _sut = new BaseRequestValidatorTestWrapper( _userManager, _userService, _eventService, _deviceValidator, - _organizationDuoWebTokenProvider, - _duoWebV4SDKService, - _organizationRepository, + _twoFactorAuthenticationValidator, _organizationUserRepository, - _applicationCacheService, _mailService, _logger, _currentContext, _globalSettings, _userRepository, _policyService, - _tokenDataFactory, _featureService, _ssoConfigRepository, _userDecryptionOptionsBuilder); @@ -116,7 +103,7 @@ public class BaseRequestValidatorTests var errorResponse = (ErrorResponseModel)context.GrantResult.CustomResponse["ErrorModel"]; - // Assert + // Assert await _eventService.Received(1) .LogUserEventAsync(context.CustomValidatorRequestContext.User.Id, Core.Enums.EventType.User_FailedLogIn); @@ -127,7 +114,7 @@ public class BaseRequestValidatorTests /* Logic path ValidateAsync -> UpdateFailedAuthDetailsAsync -> _mailService.SendFailedLoginAttemptsEmailAsync |-> BuildErrorResultAsync -> _eventService.LogUserEventAsync - (self hosted) |-> _logger.LogWarning() + (self hosted) |-> _logger.LogWarning() |-> SetErrorResult */ [Theory, BitAutoData] @@ -154,7 +141,7 @@ public class BaseRequestValidatorTests /* Logic path ValidateAsync -> UpdateFailedAuthDetailsAsync -> _mailService.SendFailedLoginAttemptsEmailAsync - |-> BuildErrorResultAsync -> _eventService.LogUserEventAsync + |-> BuildErrorResultAsync -> _eventService.LogUserEventAsync |-> SetErrorResult */ [Theory, BitAutoData] @@ -202,6 +189,9 @@ public class BaseRequestValidatorTests { // Arrange var context = CreateContext(tokenRequest, requestContext, grantResult); + _twoFactorAuthenticationValidator + .RequiresTwoFactorAsync(Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(new Tuple(false, default))); context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false; _sut.isValid = true; @@ -230,6 +220,9 @@ public class BaseRequestValidatorTests { // Arrange var context = CreateContext(tokenRequest, requestContext, grantResult); + _twoFactorAuthenticationValidator + .RequiresTwoFactorAsync(Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(new Tuple(false, null))); context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false; _sut.isValid = true; @@ -237,7 +230,7 @@ public class BaseRequestValidatorTests context.CustomValidatorRequestContext.User.CreationDate = DateTime.UtcNow - TimeSpan.FromDays(1); _globalSettings.DisableEmailNewDevice = false; - context.ValidatedTokenRequest.GrantType = "client_credentials"; // This || AuthCode will allow process to continue to get device + context.ValidatedTokenRequest.GrantType = "client_credentials"; // This || AuthCode will allow process to continue to get device _deviceValidator.SaveDeviceAsync(Arg.Any(), Arg.Any()) .Returns(device); @@ -267,10 +260,13 @@ public class BaseRequestValidatorTests context.CustomValidatorRequestContext.User.CreationDate = DateTime.UtcNow - TimeSpan.FromDays(1); _globalSettings.DisableEmailNewDevice = false; - context.ValidatedTokenRequest.GrantType = "client_credentials"; // This || AuthCode will allow process to continue to get device + context.ValidatedTokenRequest.GrantType = "client_credentials"; // This || AuthCode will allow process to continue to get device _deviceValidator.SaveDeviceAsync(Arg.Any(), Arg.Any()) .Returns(device); + _twoFactorAuthenticationValidator + .RequiresTwoFactorAsync(Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(new Tuple(false, null))); // Act await _sut.ValidateAsync(context); @@ -306,10 +302,13 @@ public class BaseRequestValidatorTests _policyService.AnyPoliciesApplicableToUserAsync( Arg.Any(), PolicyType.RequireSso, OrganizationUserStatusType.Confirmed) .Returns(Task.FromResult(true)); + _twoFactorAuthenticationValidator + .RequiresTwoFactorAsync(Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(new Tuple(false, null))); // Act await _sut.ValidateAsync(context); - // Assert + // Assert Assert.True(context.GrantResult.IsError); var errorResponse = (ErrorResponseModel)context.GrantResult.CustomResponse["ErrorModel"]; Assert.Equal("SSO authentication is required.", errorResponse.Message); @@ -330,6 +329,9 @@ public class BaseRequestValidatorTests context.ValidatedTokenRequest.ClientId = "Not Web"; _sut.isValid = true; _featureService.IsEnabled(FeatureFlagKeys.BlockLegacyUsers).Returns(true); + _twoFactorAuthenticationValidator + .RequiresTwoFactorAsync(Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(new Tuple(false, null))); // Act await _sut.ValidateAsync(context); @@ -341,28 +343,6 @@ public class BaseRequestValidatorTests , errorResponse.Message); } - [Theory, BitAutoData] - public async Task RequiresTwoFactorAsync_ClientCredentialsGrantType_ShouldReturnFalse( - [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest, - CustomValidatorRequestContext requestContext, - GrantValidationResult grantResult) - { - // Arrange - var context = CreateContext(tokenRequest, requestContext, grantResult); - - context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false; - context.ValidatedTokenRequest.GrantType = "client_credentials"; - - // Act - var result = await _sut.TestRequiresTwoFactorAsync( - context.CustomValidatorRequestContext.User, - context.ValidatedTokenRequest); - - // Assert - Assert.False(result.Item1); - Assert.Null(result.Item2); - } - private BaseRequestValidationContextFake CreateContext( ValidatedTokenRequest tokenRequest, CustomValidatorRequestContext requestContext, diff --git a/test/Identity.Test/IdentityServer/DeviceValidatorTests.cs b/test/Identity.Test/IdentityServer/DeviceValidatorTests.cs index 1f4d5a807..2db792c93 100644 --- a/test/Identity.Test/IdentityServer/DeviceValidatorTests.cs +++ b/test/Identity.Test/IdentityServer/DeviceValidatorTests.cs @@ -4,7 +4,7 @@ using Bit.Core.Enums; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; -using Bit.Identity.IdentityServer; +using Bit.Identity.IdentityServer.RequestValidators; using Bit.Test.Common.AutoFixture.Attributes; using Duende.IdentityServer.Validation; using NSubstitute; diff --git a/test/Identity.Test/IdentityServer/TwoFactorAuthenticationValidatorTests.cs b/test/Identity.Test/IdentityServer/TwoFactorAuthenticationValidatorTests.cs new file mode 100644 index 000000000..5783375ff --- /dev/null +++ b/test/Identity.Test/IdentityServer/TwoFactorAuthenticationValidatorTests.cs @@ -0,0 +1,575 @@ +using Bit.Core; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Auth.Enums; +using Bit.Core.Auth.Identity; +using Bit.Core.Auth.Models; +using Bit.Core.Auth.Models.Business.Tokenables; +using Bit.Core.Context; +using Bit.Core.Entities; +using Bit.Core.Models.Data.Organizations; +using Bit.Core.Models.Data.Organizations.OrganizationUsers; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Bit.Core.Tokens; +using Bit.Identity.IdentityServer.RequestValidators; +using Bit.Identity.Test.Wrappers; +using Bit.Test.Common.AutoFixture.Attributes; +using Duende.IdentityServer.Validation; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using NSubstitute; +using Xunit; +using AuthFixtures = Bit.Identity.Test.AutoFixture; + +namespace Bit.Identity.Test.IdentityServer; + +public class TwoFactorAuthenticationValidatorTests +{ + private readonly IUserService _userService; + private readonly UserManagerTestWrapper _userManager; + private readonly IOrganizationDuoWebTokenProvider _organizationDuoWebTokenProvider; + private readonly ITemporaryDuoWebV4SDKService _temporaryDuoWebV4SDKService; + private readonly IFeatureService _featureService; + private readonly IApplicationCacheService _applicationCacheService; + private readonly IOrganizationUserRepository _organizationUserRepository; + private readonly IOrganizationRepository _organizationRepository; + private readonly IDataProtectorTokenFactory _ssoEmail2faSessionTokenable; + private readonly ICurrentContext _currentContext; + private readonly TwoFactorAuthenticationValidator _sut; + + public TwoFactorAuthenticationValidatorTests() + { + _userService = Substitute.For(); + _userManager = SubstituteUserManager(); + _organizationDuoWebTokenProvider = Substitute.For(); + _temporaryDuoWebV4SDKService = Substitute.For(); + _featureService = Substitute.For(); + _applicationCacheService = Substitute.For(); + _organizationUserRepository = Substitute.For(); + _organizationRepository = Substitute.For(); + _ssoEmail2faSessionTokenable = Substitute.For>(); + _currentContext = Substitute.For(); + + _sut = new TwoFactorAuthenticationValidator( + _userService, + _userManager, + _organizationDuoWebTokenProvider, + _temporaryDuoWebV4SDKService, + _featureService, + _applicationCacheService, + _organizationUserRepository, + _organizationRepository, + _ssoEmail2faSessionTokenable, + _currentContext); + } + + [Theory] + [BitAutoData("password")] + [BitAutoData("authorization_code")] + public async void RequiresTwoFactorAsync_IndividualOnly_Required_ReturnTrue( + string grantType, + [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request, + User user) + { + // Arrange + request.GrantType = grantType; + // All three of these must be true for the two factor authentication to be required + _userManager.TWO_FACTOR_ENABLED = true; + _userManager.SUPPORTS_TWO_FACTOR = true; + // In order for the two factor authentication to be required, the user must have at least one two factor provider + _userManager.TWO_FACTOR_PROVIDERS = ["email"]; + + // Act + var result = await _sut.RequiresTwoFactorAsync(user, request); + + // Assert + Assert.True(result.Item1); + Assert.Null(result.Item2); + } + + [Theory] + [BitAutoData("client_credentials")] + [BitAutoData("webauthn")] + public async void RequiresTwoFactorAsync_NotRequired_ReturnFalse( + string grantType, + [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request, + User user) + { + // Arrange + request.GrantType = grantType; + + // Act + var result = await _sut.RequiresTwoFactorAsync(user, request); + + // Assert + Assert.False(result.Item1); + Assert.Null(result.Item2); + } + + [Theory] + [BitAutoData("password")] + [BitAutoData("authorization_code")] + public async void RequiresTwoFactorAsync_IndividualFalse_OrganizationRequired_ReturnTrue( + string grantType, + [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request, + User user, + OrganizationUserOrganizationDetails orgUser, + Organization organization, + ICollection organizationCollection) + { + // Arrange + request.GrantType = grantType; + // Link the orgUser to the User making the request + orgUser.UserId = user.Id; + // Link organization to the organization user + organization.Id = orgUser.OrganizationId; + + // Set Organization 2FA to required + organization.Use2fa = true; + organization.TwoFactorProviders = GetTwoFactorOrganizationDuoProviderJson(); + organization.Enabled = true; + + // Make sure organization list is not empty + organizationCollection.Clear(); + // Fix OrganizationUser Permissions field + orgUser.Permissions = "{}"; + organizationCollection.Add(new CurrentContextOrganization(orgUser)); + + _currentContext.OrganizationMembershipAsync(Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(organizationCollection)); + + _applicationCacheService.GetOrganizationAbilitiesAsync() + .Returns(new Dictionary() + { + { orgUser.OrganizationId, new OrganizationAbility(organization)} + }); + + _organizationRepository.GetManyByUserIdAsync(Arg.Any()).Returns([organization]); + + // Act + var result = await _sut.RequiresTwoFactorAsync(user, request); + + // Assert + Assert.True(result.Item1); + Assert.NotNull(result.Item2); + Assert.IsType(result.Item2); + } + + [Theory] + [BitAutoData] + public async void BuildTwoFactorResultAsync_NoProviders_ReturnsNull( + User user, + Organization organization) + { + // Arrange + organization.Use2fa = true; + organization.TwoFactorProviders = "{}"; + organization.Enabled = true; + + user.TwoFactorProviders = ""; + + // Act + var result = await _sut.BuildTwoFactorResultAsync(user, organization); + + // Assert + Assert.Null(result); + } + + [Theory] + [BitAutoData] + public async void BuildTwoFactorResultAsync_OrganizationProviders_NotEnabled_ReturnsNull( + User user, + Organization organization) + { + // Arrange + organization.Use2fa = true; + organization.TwoFactorProviders = GetTwoFactorOrganizationNotEnabledDuoProviderJson(); + organization.Enabled = true; + + user.TwoFactorProviders = null; + + // Act + var result = await _sut.BuildTwoFactorResultAsync(user, organization); + + // Assert + Assert.Null(result); + } + + [Theory] + [BitAutoData] + public async void BuildTwoFactorResultAsync_OrganizationProviders_ReturnsNotNull( + User user, + Organization organization) + { + // Arrange + organization.Use2fa = true; + organization.TwoFactorProviders = GetTwoFactorOrganizationDuoProviderJson(); + organization.Enabled = true; + + user.TwoFactorProviders = null; + + // Act + var result = await _sut.BuildTwoFactorResultAsync(user, organization); + + // Assert + Assert.NotNull(result); + Assert.IsType>(result); + Assert.NotEmpty(result); + Assert.True(result.ContainsKey("TwoFactorProviders2")); + } + + [Theory] + [BitAutoData] + public async void BuildTwoFactorResultAsync_IndividualProviders_NotEnabled_ReturnsNull( + User user) + { + // Arrange + user.TwoFactorProviders = GetTwoFactorIndividualNotEnabledProviderJson(TwoFactorProviderType.Email); + + // Act + var result = await _sut.BuildTwoFactorResultAsync(user, null); + + // Assert + Assert.Null(result); + } + + [Theory] + [BitAutoData] + public async void BuildTwoFactorResultAsync_IndividualProviders_ReturnsNotNull( + User user) + { + // Arrange + _userService.CanAccessPremium(user).Returns(true); + + user.TwoFactorProviders = GetTwoFactorIndividualProviderJson(TwoFactorProviderType.Duo); + + // Act + var result = await _sut.BuildTwoFactorResultAsync(user, null); + + // Assert + Assert.NotNull(result); + Assert.IsType>(result); + Assert.NotEmpty(result); + Assert.True(result.ContainsKey("TwoFactorProviders2")); + } + + [Theory] + [BitAutoData(TwoFactorProviderType.Email)] + public async void BuildTwoFactorResultAsync_IndividualEmailProvider_SendsEmail_SetsSsoToken_ReturnsNotNull( + TwoFactorProviderType providerType, + User user) + { + // Arrange + var providerTypeInt = (int)providerType; + user.TwoFactorProviders = GetTwoFactorIndividualProviderJson(providerType); + + _userManager.TWO_FACTOR_ENABLED = true; + _userManager.SUPPORTS_TWO_FACTOR = true; + _userManager.TWO_FACTOR_PROVIDERS = [providerType.ToString()]; + + _userService.TwoFactorProviderIsEnabledAsync(Arg.Any(), user) + .Returns(true); + + // Act + var result = await _sut.BuildTwoFactorResultAsync(user, null); + + // Assert + Assert.NotNull(result); + Assert.IsType>(result); + Assert.NotEmpty(result); + Assert.True(result.ContainsKey("TwoFactorProviders2")); + var providers = (Dictionary>)result["TwoFactorProviders2"]; + Assert.True(providers.ContainsKey(providerTypeInt.ToString())); + Assert.True(result.ContainsKey("SsoEmail2faSessionToken")); + Assert.True(result.ContainsKey("Email")); + + await _userService.Received(1).SendTwoFactorEmailAsync(Arg.Any()); + } + + [Theory] + [BitAutoData(TwoFactorProviderType.Duo)] + [BitAutoData(TwoFactorProviderType.WebAuthn)] + [BitAutoData(TwoFactorProviderType.Email)] + [BitAutoData(TwoFactorProviderType.YubiKey)] + [BitAutoData(TwoFactorProviderType.OrganizationDuo)] + public async void BuildTwoFactorResultAsync_IndividualProvider_ReturnMatchesType( + TwoFactorProviderType providerType, + User user) + { + // Arrange + var providerTypeInt = (int)providerType; + user.TwoFactorProviders = GetTwoFactorIndividualProviderJson(providerType); + + _userManager.TWO_FACTOR_ENABLED = true; + _userManager.SUPPORTS_TWO_FACTOR = true; + _userManager.TWO_FACTOR_PROVIDERS = [providerType.ToString()]; + _userManager.TWO_FACTOR_TOKEN = "{\"Key1\":\"WebauthnToken\"}"; + + _userService.CanAccessPremium(user).Returns(true); + + // Act + var result = await _sut.BuildTwoFactorResultAsync(user, null); + + // Assert + Assert.NotNull(result); + Assert.IsType>(result); + Assert.NotEmpty(result); + Assert.True(result.ContainsKey("TwoFactorProviders2")); + var providers = (Dictionary>)result["TwoFactorProviders2"]; + Assert.True(providers.ContainsKey(providerTypeInt.ToString())); + } + + [Theory] + [BitAutoData] + public async void VerifyTwoFactorAsync_Individual_TypeNull_ReturnsFalse( + User user, + string token) + { + // Arrange + _userService.TwoFactorProviderIsEnabledAsync( + TwoFactorProviderType.Email, user).Returns(true); + + _userManager.TWO_FACTOR_PROVIDERS = ["email"]; + + // Act + var result = await _sut.VerifyTwoFactor( + user, null, TwoFactorProviderType.U2f, token); + + // Assert + Assert.False(result); + } + + [Theory] + [BitAutoData] + public async void VerifyTwoFactorAsync_Individual_NotEnabled_ReturnsFalse( + User user, + string token) + { + // Arrange + _userService.TwoFactorProviderIsEnabledAsync( + TwoFactorProviderType.Email, user).Returns(false); + + _userManager.TWO_FACTOR_PROVIDERS = ["email"]; + + // Act + var result = await _sut.VerifyTwoFactor( + user, null, TwoFactorProviderType.Email, token); + + // Assert + Assert.False(result); + } + + [Theory] + [BitAutoData] + public async void VerifyTwoFactorAsync_Organization_NotEnabled_ReturnsFalse( + User user, + string token) + { + // Arrange + _userService.TwoFactorProviderIsEnabledAsync( + TwoFactorProviderType.OrganizationDuo, user).Returns(false); + + _userManager.TWO_FACTOR_PROVIDERS = ["OrganizationDuo"]; + + // Act + var result = await _sut.VerifyTwoFactor( + user, null, TwoFactorProviderType.OrganizationDuo, token); + + // Assert + Assert.False(result); + } + + [Theory] + [BitAutoData(TwoFactorProviderType.Duo)] + [BitAutoData(TwoFactorProviderType.WebAuthn)] + [BitAutoData(TwoFactorProviderType.Email)] + [BitAutoData(TwoFactorProviderType.YubiKey)] + [BitAutoData(TwoFactorProviderType.Remember)] + public async void VerifyTwoFactorAsync_Individual_ValidToken_ReturnsTrue( + TwoFactorProviderType providerType, + User user, + string token) + { + // Arrange + _userService.TwoFactorProviderIsEnabledAsync( + providerType, user).Returns(true); + + _userManager.TWO_FACTOR_ENABLED = true; + _userManager.TWO_FACTOR_TOKEN_VERIFIED = true; + + // Act + var result = await _sut.VerifyTwoFactor(user, null, providerType, token); + + // Assert + Assert.True(result); + } + + [Theory] + [BitAutoData(TwoFactorProviderType.Duo)] + [BitAutoData(TwoFactorProviderType.WebAuthn)] + [BitAutoData(TwoFactorProviderType.Email)] + [BitAutoData(TwoFactorProviderType.YubiKey)] + [BitAutoData(TwoFactorProviderType.Remember)] + public async void VerifyTwoFactorAsync_Individual_InvalidToken_ReturnsFalse( + TwoFactorProviderType providerType, + User user, + string token) + { + // Arrange + _userService.TwoFactorProviderIsEnabledAsync( + providerType, user).Returns(true); + + _userManager.TWO_FACTOR_ENABLED = true; + _userManager.TWO_FACTOR_TOKEN_VERIFIED = false; + + // Act + var result = await _sut.VerifyTwoFactor(user, null, providerType, token); + + // Assert + Assert.False(result); + } + + [Theory] + [BitAutoData(TwoFactorProviderType.OrganizationDuo)] + public async void VerifyTwoFactorAsync_Organization_ValidToken_ReturnsTrue( + TwoFactorProviderType providerType, + User user, + Organization organization, + string token) + { + // Arrange + _organizationDuoWebTokenProvider.ValidateAsync( + token, organization, user).Returns(true); + + _userManager.TWO_FACTOR_ENABLED = true; + _userManager.TWO_FACTOR_TOKEN_VERIFIED = true; + + organization.Use2fa = true; + organization.TwoFactorProviders = GetTwoFactorOrganizationDuoProviderJson(); + organization.Enabled = true; + + // Act + var result = await _sut.VerifyTwoFactor( + user, organization, providerType, token); + + // Assert + Assert.True(result); + } + + [Theory] + [BitAutoData(TwoFactorProviderType.Duo)] + [BitAutoData(TwoFactorProviderType.OrganizationDuo)] + public async void VerifyTwoFactorAsync_TemporaryDuoService_ValidToken_ReturnsTrue( + TwoFactorProviderType providerType, + User user, + Organization organization, + string token) + { + // Arrange + _featureService.IsEnabled(FeatureFlagKeys.DuoRedirect).Returns(true); + _userService.TwoFactorProviderIsEnabledAsync(providerType, user).Returns(true); + _temporaryDuoWebV4SDKService.ValidateAsync( + token, Arg.Any(), user).Returns(true); + + user.TwoFactorProviders = GetTwoFactorIndividualProviderJson(providerType); + organization.Use2fa = true; + organization.TwoFactorProviders = GetTwoFactorOrganizationDuoProviderJson(); + organization.Enabled = true; + + _userManager.TWO_FACTOR_ENABLED = true; + _userManager.TWO_FACTOR_TOKEN = token; + _userManager.TWO_FACTOR_TOKEN_VERIFIED = true; + + // Act + var result = await _sut.VerifyTwoFactor( + user, organization, providerType, token); + + // Assert + Assert.True(result); + } + + [Theory] + [BitAutoData(TwoFactorProviderType.Duo)] + [BitAutoData(TwoFactorProviderType.OrganizationDuo)] + public async void VerifyTwoFactorAsync_TemporaryDuoService_InvalidToken_ReturnsFalse( + TwoFactorProviderType providerType, + User user, + Organization organization, + string token) + { + // Arrange + _featureService.IsEnabled(FeatureFlagKeys.DuoRedirect).Returns(true); + _userService.TwoFactorProviderIsEnabledAsync(providerType, user).Returns(true); + _temporaryDuoWebV4SDKService.ValidateAsync( + token, Arg.Any(), user).Returns(true); + + user.TwoFactorProviders = GetTwoFactorIndividualProviderJson(providerType); + organization.Use2fa = true; + organization.TwoFactorProviders = GetTwoFactorOrganizationDuoProviderJson(); + organization.Enabled = true; + + _userManager.TWO_FACTOR_ENABLED = true; + _userManager.TWO_FACTOR_TOKEN = token; + _userManager.TWO_FACTOR_TOKEN_VERIFIED = false; + + // Act + var result = await _sut.VerifyTwoFactor( + user, organization, providerType, token); + + // Assert + Assert.True(result); + } + + private static UserManagerTestWrapper SubstituteUserManager() + { + return new UserManagerTestWrapper( + Substitute.For>(), + Substitute.For>(), + Substitute.For>(), + Enumerable.Empty>(), + Enumerable.Empty>(), + Substitute.For(), + Substitute.For(), + Substitute.For(), + Substitute.For>>()); + } + + private static string GetTwoFactorOrganizationDuoProviderJson(bool enabled = true) + { + return + "{\"6\":{\"Enabled\":true,\"MetaData\":{\"ClientSecret\":\"secretClientSecret\",\"ClientId\":\"clientId\",\"Host\":\"example.com\"}}}"; + } + + private static string GetTwoFactorOrganizationNotEnabledDuoProviderJson(bool enabled = true) + { + return + "{\"6\":{\"Enabled\":false,\"MetaData\":{\"ClientSecret\":\"secretClientSecret\",\"ClientId\":\"clientId\",\"Host\":\"example.com\"}}}"; + } + + private static string GetTwoFactorIndividualProviderJson(TwoFactorProviderType providerType) + { + return providerType switch + { + TwoFactorProviderType.Duo => "{\"2\":{\"Enabled\":true,\"MetaData\":{\"ClientSecret\":\"secretClientSecret\",\"ClientId\":\"clientId\",\"Host\":\"example.com\"}}}", + TwoFactorProviderType.Email => "{\"1\":{\"Enabled\":true,\"MetaData\":{\"Email\":\"user@test.dev\"}}}", + TwoFactorProviderType.WebAuthn => "{\"7\":{\"Enabled\":true,\"MetaData\":{\"Key1\":{\"Name\":\"key1\",\"Descriptor\":{\"Type\":0,\"Id\":\"keyId\",\"Transports\":null},\"PublicKey\":\"key\",\"UserHandle\":\"handle\",\"SignatureCounter\":0,\"CredType\":\"none\",\"RegDate\":\"2022-01-01T00:00:00Z\",\"AaGuid\":\"00000000-0000-0000-0000-000000000000\",\"Migrated\":false}}}}", + TwoFactorProviderType.YubiKey => "{\"3\":{\"Enabled\":true,\"MetaData\":{\"Id\":\"yubikeyId\",\"Nfc\":true}}}", + TwoFactorProviderType.OrganizationDuo => "{\"6\":{\"Enabled\":true,\"MetaData\":{\"ClientSecret\":\"secretClientSecret\",\"ClientId\":\"clientId\",\"Host\":\"example.com\"}}}", + _ => "{}", + }; + } + + private static string GetTwoFactorIndividualNotEnabledProviderJson(TwoFactorProviderType providerType) + { + return providerType switch + { + TwoFactorProviderType.Duo => "{\"2\":{\"Enabled\":false,\"MetaData\":{\"ClientSecret\":\"secretClientSecret\",\"ClientId\":\"clientId\",\"Host\":\"example.com\"}}}", + TwoFactorProviderType.Email => "{\"1\":{\"Enabled\":false,\"MetaData\":{\"Email\":\"user@test.dev\"}}}", + TwoFactorProviderType.WebAuthn => "{\"7\":{\"Enabled\":false,\"MetaData\":{\"Key1\":{\"Name\":\"key1\",\"Descriptor\":{\"Type\":0,\"Id\":\"keyId\",\"Transports\":null},\"PublicKey\":\"key\",\"UserHandle\":\"handle\",\"SignatureCounter\":0,\"CredType\":\"none\",\"RegDate\":\"2022-01-01T00:00:00Z\",\"AaGuid\":\"00000000-0000-0000-0000-000000000000\",\"Migrated\":false}}}}", + TwoFactorProviderType.YubiKey => "{\"3\":{\"Enabled\":false,\"MetaData\":{\"Id\":\"yubikeyId\",\"Nfc\":true}}}", + TwoFactorProviderType.OrganizationDuo => "{\"6\":{\"Enabled\":false,\"MetaData\":{\"ClientSecret\":\"secretClientSecret\",\"ClientId\":\"clientId\",\"Host\":\"example.com\"}}}", + _ => "{}", + }; + } +} diff --git a/test/Identity.Test/Wrappers/BaseRequestValidatorTestWrapper.cs b/test/Identity.Test/Wrappers/BaseRequestValidatorTestWrapper.cs index 26043fd59..f7cfd1d39 100644 --- a/test/Identity.Test/Wrappers/BaseRequestValidatorTestWrapper.cs +++ b/test/Identity.Test/Wrappers/BaseRequestValidatorTestWrapper.cs @@ -1,16 +1,13 @@ using System.Security.Claims; -using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Services; -using Bit.Core.Auth.Identity; -using Bit.Core.Auth.Models.Business.Tokenables; using Bit.Core.Auth.Repositories; using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; -using Bit.Core.Tokens; using Bit.Identity.IdentityServer; +using Bit.Identity.IdentityServer.RequestValidators; using Duende.IdentityServer.Models; using Duende.IdentityServer.Validation; using Microsoft.AspNetCore.Identity; @@ -54,38 +51,30 @@ IBaseRequestValidatorTestWrapper IUserService userService, IEventService eventService, IDeviceValidator deviceValidator, - IOrganizationDuoWebTokenProvider organizationDuoWebTokenProvider, - ITemporaryDuoWebV4SDKService duoWebV4SDKService, - IOrganizationRepository organizationRepository, + ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator, IOrganizationUserRepository organizationUserRepository, - IApplicationCacheService applicationCacheService, IMailService mailService, ILogger logger, ICurrentContext currentContext, GlobalSettings globalSettings, IUserRepository userRepository, IPolicyService policyService, - IDataProtectorTokenFactory tokenDataFactory, IFeatureService featureService, ISsoConfigRepository ssoConfigRepository, IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder) : - base( + base( userManager, userService, eventService, deviceValidator, - organizationDuoWebTokenProvider, - duoWebV4SDKService, - organizationRepository, + twoFactorAuthenticationValidator, organizationUserRepository, - applicationCacheService, mailService, logger, currentContext, globalSettings, userRepository, policyService, - tokenDataFactory, featureService, ssoConfigRepository, userDecryptionOptionsBuilder) @@ -98,13 +87,6 @@ IBaseRequestValidatorTestWrapper await ValidateAsync(context, context.ValidatedTokenRequest, context.CustomValidatorRequestContext); } - public async Task> TestRequiresTwoFactorAsync( - User user, - ValidatedTokenRequest context) - { - return await RequiresTwoFactorAsync(user, context); - } - protected override ClaimsPrincipal GetSubject( BaseRequestValidationContextFake context) { diff --git a/test/Identity.Test/Wrappers/UserManagerTestWrapper.cs b/test/Identity.Test/Wrappers/UserManagerTestWrapper.cs new file mode 100644 index 000000000..f1207a4b9 --- /dev/null +++ b/test/Identity.Test/Wrappers/UserManagerTestWrapper.cs @@ -0,0 +1,96 @@ + +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Bit.Identity.Test.Wrappers; + +public class UserManagerTestWrapper : UserManager where TUser : class +{ + /// + /// Modify this value to mock the responses from UserManager.GetTwoFactorEnabledAsync() + /// + public bool TWO_FACTOR_ENABLED { get; set; } = false; + /// + /// Modify this value to mock the responses from UserManager.GetValidTwoFactorProvidersAsync() + /// + public IList TWO_FACTOR_PROVIDERS { get; set; } = []; + /// + /// Modify this value to mock the responses from UserManager.GenerateTwoFactorTokenAsync() + /// + public string TWO_FACTOR_TOKEN { get; set; } = string.Empty; + /// + /// Modify this value to mock the responses from UserManager.VerifyTwoFactorTokenAsync() + /// + public bool TWO_FACTOR_TOKEN_VERIFIED { get; set; } = false; + + /// + /// Modify this value to mock the responses from UserManager.SupportsUserTwoFactor + /// + public bool SUPPORTS_TWO_FACTOR { get; set; } = false; + + public override bool SupportsUserTwoFactor + { + get + { + return SUPPORTS_TWO_FACTOR; + } + } + + public UserManagerTestWrapper( + IUserStore store, + IOptions optionsAccessor, + IPasswordHasher passwordHasher, + IEnumerable> userValidators, + IEnumerable> passwordValidators, + ILookupNormalizer keyNormalizer, + IdentityErrorDescriber errors, + IServiceProvider services, + ILogger> logger) + : base(store, optionsAccessor, passwordHasher, userValidators, passwordValidators, + keyNormalizer, errors, services, logger) + { } + + /// + /// return class variable TWO_FACTOR_ENABLED + /// + /// + /// + public override async Task GetTwoFactorEnabledAsync(TUser user) + { + return TWO_FACTOR_ENABLED; + } + + /// + /// return class variable TWO_FACTOR_PROVIDERS + /// + /// + /// + public override async Task> GetValidTwoFactorProvidersAsync(TUser user) + { + return TWO_FACTOR_PROVIDERS; + } + + /// + /// return class variable TWO_FACTOR_TOKEN + /// + /// + /// + /// + public override async Task GenerateTwoFactorTokenAsync(TUser user, string tokenProvider) + { + return TWO_FACTOR_TOKEN; + } + + /// + /// return class variable TWO_FACTOR_TOKEN_VERIFIED + /// + /// + /// + /// + /// + public override async Task VerifyTwoFactorTokenAsync(TUser user, string tokenProvider, string token) + { + return TWO_FACTOR_TOKEN_VERIFIED; + } +} diff --git a/test/Infrastructure.IntegrationTest/Infrastructure.IntegrationTest.csproj b/test/Infrastructure.IntegrationTest/Infrastructure.IntegrationTest.csproj index fd4c3be76..159572f38 100644 --- a/test/Infrastructure.IntegrationTest/Infrastructure.IntegrationTest.csproj +++ b/test/Infrastructure.IntegrationTest/Infrastructure.IntegrationTest.csproj @@ -1,4 +1,4 @@ - + enable @@ -8,10 +8,10 @@ - - - - + + + + diff --git a/test/IntegrationTestCommon/Factories/WebApplicationFactoryBase.cs b/test/IntegrationTestCommon/Factories/WebApplicationFactoryBase.cs index aafe86d56..3ce259970 100644 --- a/test/IntegrationTestCommon/Factories/WebApplicationFactoryBase.cs +++ b/test/IntegrationTestCommon/Factories/WebApplicationFactoryBase.cs @@ -57,6 +57,16 @@ public abstract class WebApplicationFactoryBase : WebApplicationFactory }); } + /// + /// Allows you to add your own services to the application as required. + /// + /// The service collection you want added to the test service collection. + /// This needs to be ran BEFORE making any calls through the factory to take effect. + public void ConfigureServices(Action configure) + { + _configureTestServices.Add(configure); + } + /// /// Add your own configuration provider to the application. /// diff --git a/test/IntegrationTestCommon/IntegrationTestCommon.csproj b/test/IntegrationTestCommon/IntegrationTestCommon.csproj index 664710560..3e8e55524 100644 --- a/test/IntegrationTestCommon/IntegrationTestCommon.csproj +++ b/test/IntegrationTestCommon/IntegrationTestCommon.csproj @@ -5,7 +5,7 @@ - + diff --git a/util/Migrator/DbScripts/2024-10-31-00_DeviceActivation.sql b/util/Migrator/DbScripts/2024-10-31-00_DeviceActivation.sql new file mode 100644 index 000000000..cf29f2823 --- /dev/null +++ b/util/Migrator/DbScripts/2024-10-31-00_DeviceActivation.sql @@ -0,0 +1,118 @@ +SET DEADLOCK_PRIORITY HIGH +GO + +-- add column +IF COL_LENGTH('[dbo].[Device]', 'Active') IS NULL + BEGIN + ALTER TABLE + [dbo].[Device] + ADD + [Active] BIT NOT NULL CONSTRAINT [DF_Device_Active] DEFAULT (1) +END +GO + +-- refresh view +CREATE OR ALTER VIEW [dbo].[DeviceView] +AS + SELECT + * + FROM + [dbo].[Device] +GO + +-- drop now-unused proc for deletion +IF OBJECT_ID('[dbo].[Device_DeleteById]') IS NOT NULL + BEGIN + DROP PROCEDURE [dbo].[Device_DeleteById] +END +GO + +-- refresh procs +CREATE OR ALTER PROCEDURE [dbo].[Device_Create] + @Id UNIQUEIDENTIFIER OUTPUT, + @UserId UNIQUEIDENTIFIER, + @Name NVARCHAR(50), + @Type TINYINT, + @Identifier NVARCHAR(50), + @PushToken NVARCHAR(255), + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7), + @EncryptedUserKey VARCHAR(MAX) = NULL, + @EncryptedPublicKey VARCHAR(MAX) = NULL, + @EncryptedPrivateKey VARCHAR(MAX) = NULL, + @Active BIT = 1 +AS +BEGIN + SET NOCOUNT ON + + INSERT INTO [dbo].[Device] + ( + [Id], + [UserId], + [Name], + [Type], + [Identifier], + [PushToken], + [CreationDate], + [RevisionDate], + [EncryptedUserKey], + [EncryptedPublicKey], + [EncryptedPrivateKey], + [Active] + ) + VALUES + ( + @Id, + @UserId, + @Name, + @Type, + @Identifier, + @PushToken, + @CreationDate, + @RevisionDate, + @EncryptedUserKey, + @EncryptedPublicKey, + @EncryptedPrivateKey, + @Active + ) +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[Device_Update] + @Id UNIQUEIDENTIFIER, + @UserId UNIQUEIDENTIFIER, + @Name NVARCHAR(50), + @Type TINYINT, + @Identifier NVARCHAR(50), + @PushToken NVARCHAR(255), + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7), + @EncryptedUserKey VARCHAR(MAX) = NULL, + @EncryptedPublicKey VARCHAR(MAX) = NULL, + @EncryptedPrivateKey VARCHAR(MAX) = NULL, + @Active BIT = 1 +AS +BEGIN + SET NOCOUNT ON + + UPDATE + [dbo].[Device] + SET + [UserId] = @UserId, + [Name] = @Name, + [Type] = @Type, + [Identifier] = @Identifier, + [PushToken] = @PushToken, + [CreationDate] = @CreationDate, + [RevisionDate] = @RevisionDate, + [EncryptedUserKey] = @EncryptedUserKey, + [EncryptedPublicKey] = @EncryptedPublicKey, + [EncryptedPrivateKey] = @EncryptedPrivateKey, + [Active] = @Active + WHERE + [Id] = @Id +END +GO + +SET DEADLOCK_PRIORITY NORMAL +GO diff --git a/util/Migrator/Migrator.csproj b/util/Migrator/Migrator.csproj index 7893a81c0..25f5f255a 100644 --- a/util/Migrator/Migrator.csproj +++ b/util/Migrator/Migrator.csproj @@ -1,4 +1,4 @@ - + @@ -7,7 +7,7 @@ - + diff --git a/util/MsSqlMigratorUtility/MsSqlMigratorUtility.csproj b/util/MsSqlMigratorUtility/MsSqlMigratorUtility.csproj index ebf0d05d8..d316e5616 100644 --- a/util/MsSqlMigratorUtility/MsSqlMigratorUtility.csproj +++ b/util/MsSqlMigratorUtility/MsSqlMigratorUtility.csproj @@ -1,18 +1,18 @@ - - - - Exe - true - - - - - - - - - - - - - + + + + Exe + true + + + + + + + + + + + + + diff --git a/util/MySqlMigrations/Migrations/20241031170511_DeviceActivation.Designer.cs b/util/MySqlMigrations/Migrations/20241031170511_DeviceActivation.Designer.cs new file mode 100644 index 000000000..b96f61b47 --- /dev/null +++ b/util/MySqlMigrations/Migrations/20241031170511_DeviceActivation.Designer.cs @@ -0,0 +1,2849 @@ +// +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("20241031170511_DeviceActivation")] + partial class DeviceActivation + { + /// + 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.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("LimitCollectionCreationDeletion") + .HasColumnType("tinyint(1)"); + + b.Property("LimitCollectionDeletion") + .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("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("TwoFactorProviders") + .HasColumnType("longtext"); + + b.Property("Use2fa") + .HasColumnType("tinyint(1)"); + + b.Property("UseApi") + .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("UsePasswordManager") + .HasColumnType("tinyint(1)"); + + b.Property("UsePolicies") + .HasColumnType("tinyint(1)"); + + b.Property("UseResetPassword") + .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.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("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("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(50) + .HasColumnType("varchar(50)"); + + 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.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.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("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.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("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("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"); + + b.HasIndex("Date", "OrganizationId", "ActingUserId", "CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + 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.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.HasKey("Id"); + + b.ToTable("Installation", (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("LastSyncDate") + .HasColumnType("datetime(6)"); + + 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("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("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("Storage") + .HasColumnType("bigint"); + + b.Property("TwoFactorProviders") + .HasColumnType("longtext"); + + b.Property("TwoFactorRecoveryCode") + .HasMaxLength(32) + .HasColumnType("varchar(32)"); + + b.Property("UsesKeyConnector") + .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.NotificationCenter.Models.Notification", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Body") + .HasColumnType("longtext"); + + 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("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("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.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.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("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("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.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.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.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.NotificationCenter.Models.Notification", 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.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.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("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("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/20241031170511_DeviceActivation.cs b/util/MySqlMigrations/Migrations/20241031170511_DeviceActivation.cs new file mode 100644 index 000000000..f6ca25552 --- /dev/null +++ b/util/MySqlMigrations/Migrations/20241031170511_DeviceActivation.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.MySqlMigrations.Migrations; + +/// +public partial class DeviceActivation : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Active", + table: "Device", + type: "tinyint(1)", + nullable: false, + defaultValue: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Active", + table: "Device"); + } +} diff --git a/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs index ef7212f17..be1369b02 100644 --- a/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -939,6 +939,10 @@ namespace Bit.MySqlMigrations.Migrations .ValueGeneratedOnAdd() .HasColumnType("char(36)"); + b.Property("Active") + .HasColumnType("tinyint(1)") + .HasDefaultValue(true); + b.Property("CreationDate") .HasColumnType("datetime(6)"); diff --git a/util/PostgresMigrations/Migrations/20241031170505_DeviceActivation.Designer.cs b/util/PostgresMigrations/Migrations/20241031170505_DeviceActivation.Designer.cs new file mode 100644 index 000000000..499df33e9 --- /dev/null +++ b/util/PostgresMigrations/Migrations/20241031170505_DeviceActivation.Designer.cs @@ -0,0 +1,2855 @@ +// +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("20241031170505_DeviceActivation")] + partial class DeviceActivation + { + /// + 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.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("LimitCollectionCreationDeletion") + .HasColumnType("boolean"); + + b.Property("LimitCollectionDeletion") + .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("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("TwoFactorProviders") + .HasColumnType("text"); + + b.Property("Use2fa") + .HasColumnType("boolean"); + + b.Property("UseApi") + .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("UsePasswordManager") + .HasColumnType("boolean"); + + b.Property("UsePolicies") + .HasColumnType("boolean"); + + b.Property("UseResetPassword") + .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.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("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("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(50) + .HasColumnType("character varying(50)") + .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.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.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("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.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("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("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"); + + b.HasIndex("Date", "OrganizationId", "ActingUserId", "CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + 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.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.HasKey("Id"); + + b.ToTable("Installation", (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("LastSyncDate") + .HasColumnType("timestamp with time zone"); + + 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("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("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("Storage") + .HasColumnType("bigint"); + + b.Property("TwoFactorProviders") + .HasColumnType("text"); + + b.Property("TwoFactorRecoveryCode") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("UsesKeyConnector") + .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.NotificationCenter.Models.Notification", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Body") + .HasColumnType("text"); + + 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("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("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.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.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("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("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.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.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.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.NotificationCenter.Models.Notification", 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.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.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("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("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/20241031170505_DeviceActivation.cs b/util/PostgresMigrations/Migrations/20241031170505_DeviceActivation.cs new file mode 100644 index 000000000..501b0f727 --- /dev/null +++ b/util/PostgresMigrations/Migrations/20241031170505_DeviceActivation.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.PostgresMigrations.Migrations; + +/// +public partial class DeviceActivation : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Active", + table: "Device", + type: "boolean", + nullable: false, + defaultValue: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Active", + table: "Device"); + } +} diff --git a/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs index a50b72568..659f42538 100644 --- a/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -944,6 +944,10 @@ namespace Bit.PostgresMigrations.Migrations .ValueGeneratedOnAdd() .HasColumnType("uuid"); + b.Property("Active") + .HasColumnType("boolean") + .HasDefaultValue(true); + b.Property("CreationDate") .HasColumnType("timestamp with time zone"); diff --git a/util/Setup/EnvironmentFileBuilder.cs b/util/Setup/EnvironmentFileBuilder.cs index a57013a6d..9c6471cb3 100644 --- a/util/Setup/EnvironmentFileBuilder.cs +++ b/util/Setup/EnvironmentFileBuilder.cs @@ -51,6 +51,12 @@ public class EnvironmentFileBuilder _globalOverrideValues.Remove("globalSettings__pushRelayBaseUri"); } + if (_globalOverrideValues.TryGetValue("globalSettings__baseServiceUri__vault", out var vaultUri) && vaultUri != _context.Config.Url) + { + _globalOverrideValues["globalSettings__baseServiceUri__vault"] = _context.Config.Url; + Helpers.WriteLine(_context, "Updated globalSettings__baseServiceUri__vault to match value in config.yml"); + } + Build(); } diff --git a/util/Setup/Setup.csproj b/util/Setup/Setup.csproj index 13897d637..6366d46d3 100644 --- a/util/Setup/Setup.csproj +++ b/util/Setup/Setup.csproj @@ -1,4 +1,4 @@ - + Exe @@ -11,7 +11,7 @@ - + diff --git a/util/SqliteMigrations/Migrations/20241031170500_DeviceActivation.Designer.cs b/util/SqliteMigrations/Migrations/20241031170500_DeviceActivation.Designer.cs new file mode 100644 index 000000000..a870dd901 --- /dev/null +++ b/util/SqliteMigrations/Migrations/20241031170500_DeviceActivation.Designer.cs @@ -0,0 +1,2838 @@ +// +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("20241031170500_DeviceActivation")] + partial class DeviceActivation + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.8"); + + 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("LimitCollectionCreationDeletion") + .HasColumnType("INTEGER"); + + b.Property("LimitCollectionDeletion") + .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("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("TwoFactorProviders") + .HasColumnType("TEXT"); + + b.Property("Use2fa") + .HasColumnType("INTEGER"); + + b.Property("UseApi") + .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("UsePasswordManager") + .HasColumnType("INTEGER"); + + b.Property("UsePolicies") + .HasColumnType("INTEGER"); + + b.Property("UseResetPassword") + .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.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("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("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(50) + .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.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.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("ExternalId") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + 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("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("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"); + + b.HasIndex("Date", "OrganizationId", "ActingUserId", "CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + 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.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.HasKey("Id"); + + b.ToTable("Installation", (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("LastSyncDate") + .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("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("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("Storage") + .HasColumnType("INTEGER"); + + b.Property("TwoFactorProviders") + .HasColumnType("TEXT"); + + b.Property("TwoFactorRecoveryCode") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("UsesKeyConnector") + .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.NotificationCenter.Models.Notification", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Body") + .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("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("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.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.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("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("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.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.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.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.NotificationCenter.Models.Notification", 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.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.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("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("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/20241031170500_DeviceActivation.cs b/util/SqliteMigrations/Migrations/20241031170500_DeviceActivation.cs new file mode 100644 index 000000000..93f7b929f --- /dev/null +++ b/util/SqliteMigrations/Migrations/20241031170500_DeviceActivation.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.SqliteMigrations.Migrations; + +/// +public partial class DeviceActivation : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Active", + table: "Device", + type: "INTEGER", + nullable: false, + defaultValue: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Active", + table: "Device"); + } +} diff --git a/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs index 997363135..a7eb51d68 100644 --- a/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -928,6 +928,10 @@ namespace Bit.SqliteMigrations.Migrations .ValueGeneratedOnAdd() .HasColumnType("TEXT"); + b.Property("Active") + .HasColumnType("INTEGER") + .HasDefaultValue(true); + b.Property("CreationDate") .HasColumnType("TEXT");