diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 1b76bccf2..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.0", + "version": "6.9.0", "commands": ["swagger"] }, "dotnet-ef": { diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 0ed3ee0f7..47d3525de 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -4,13 +4,22 @@ # # https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners -# DevOps for Actions and other workflow changes -.github/workflows @bitwarden/dept-devops +## Docker files have shared ownership ## +**/Dockerfile +**/*.Dockerfile +**/.dockerignore +**/entrypoint.sh -# DevOps for Docker changes -**/Dockerfile @bitwarden/dept-devops -**/*.Dockerfile @bitwarden/dept-devops -**/.dockerignore @bitwarden/dept-devops +## BRE team owns these workflows ## +.github/workflows/publish.yml @bitwarden/dept-bre + +## These are shared workflows ## +.github/workflows/_move_finalization_db_scripts.yml +.github/workflows/build.yml +.github/workflows/cleanup-after-pr.yml +.github/workflows/cleanup-rc-branch.yml +.github/workflows/release.yml +.github/workflows/repository-management.yml # Database Operations for database changes src/Sql/** @bitwarden/dept-dbops @@ -26,6 +35,9 @@ util/SqliteMigrations/** @bitwarden/dept-dbops bitwarden_license/src/Sso @bitwarden/team-auth-dev src/Identity @bitwarden/team-auth-dev +# Key Management team +**/KeyManagement @bitwarden/team-key-management-dev + **/SecretsManager @bitwarden/team-secrets-manager-dev **/Tools @bitwarden/team-tools-dev @@ -57,6 +69,6 @@ src/EventsProcessor @bitwarden/team-admin-console-dev src/Admin/Controllers/ToolsController.cs @bitwarden/team-billing-dev src/Admin/Views/Tools @bitwarden/team-billing-dev -# Multiple owners - DO NOT REMOVE (DevOps) +# Multiple owners - DO NOT REMOVE (BRE) **/packages.lock.json Directory.Build.props diff --git a/.github/workflows/_move_finalization_db_scripts.yml b/.github/workflows/_move_finalization_db_scripts.yml index c54e3abb2..d89787539 100644 --- a/.github/workflows/_move_finalization_db_scripts.yml +++ b/.github/workflows/_move_finalization_db_scripts.yml @@ -1,4 +1,3 @@ ---- name: _move_finalization_db_scripts run-name: Move finalization database scripts @@ -30,7 +29,7 @@ jobs: secrets: "github-pat-bitwarden-devops-bot-repo-scope" - name: Check out branch - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: token: ${{ steps.retrieve-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }} @@ -54,7 +53,7 @@ jobs: if: ${{ needs.setup.outputs.copy_finalization_scripts == 'true' }} steps: - name: Checkout - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 0 @@ -108,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/automatic-issue-responses.yml b/.github/workflows/automatic-issue-responses.yml index 0e6b9041b..b6a6a1ebf 100644 --- a/.github/workflows/automatic-issue-responses.yml +++ b/.github/workflows/automatic-issue-responses.yml @@ -1,4 +1,3 @@ ---- name: Automatic responses on: issues: diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index fe4063f44..17e3e999e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,4 +1,3 @@ ---- name: Build on: @@ -19,10 +18,10 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Check out repo - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + 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 @@ -68,13 +67,13 @@ jobs: node: true steps: - name: Check out repo - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + 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" @@ -110,7 +109,7 @@ jobs: ls -atlh ../../../ - name: Upload project artifact - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 with: name: ${{ matrix.project_name }}.zip path: ${{ matrix.base_path }}/${{ matrix.project_name }}/${{ matrix.project_name }}.zip @@ -173,7 +172,7 @@ jobs: dotnet: true steps: - name: Check out repo - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Check branch to publish env: @@ -263,7 +262,7 @@ jobs: -d ${{ matrix.base_path }}/${{ matrix.project_name }}/obj/build-output/publish - name: Build Docker image - uses: docker/build-push-action@32945a339266b759abcbdc89316275140b0fc960 # v6.8.0 + uses: docker/build-push-action@4f58ea79222b3b9dc2c8bbdd6debcef730109a75 # v6.9.0 with: context: ${{ matrix.base_path }}/${{ matrix.project_name }} file: ${{ matrix.base_path }}/${{ matrix.project_name }}/Dockerfile @@ -275,14 +274,14 @@ jobs: - name: Scan Docker image id: container-scan - uses: anchore/scan-action@64a33b277ea7a1215a3c142735a1091341939ff5 # v4.1.2 + 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@461ef6c76dfe95d5c364de2f431ddbd31a417628 # v3.26.9 + uses: github/codeql-action/upload-sarif@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0 with: sarif_file: ${{ steps.container-scan.outputs.sarif }} @@ -292,10 +291,10 @@ jobs: needs: build-docker steps: - name: Check out repo - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + 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 @@ -311,7 +310,7 @@ jobs: github.ref == 'refs/heads/hotfix-rc' run: | # Set proper setup image based on branch - case "${{ github.ref }}" in + case "$GITHUB_REF" in "refs/heads/main") SETUP_IMAGE="$_AZ_REGISTRY/setup:dev" ;; @@ -355,7 +354,7 @@ jobs: - name: Upload Docker stub US artifact if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc' - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 with: name: docker-stub-US.zip path: docker-stub-US.zip @@ -363,7 +362,7 @@ jobs: - name: Upload Docker stub EU artifact if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc' - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 with: name: docker-stub-EU.zip path: docker-stub-EU.zip @@ -371,7 +370,7 @@ jobs: - name: Upload Docker stub US checksum artifact if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc' - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 with: name: docker-stub-US-sha256.txt path: docker-stub-US-sha256.txt @@ -379,7 +378,7 @@ jobs: - name: Upload Docker stub EU checksum artifact if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc' - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 with: name: docker-stub-EU-sha256.txt path: docker-stub-EU-sha256.txt @@ -403,12 +402,12 @@ jobs: GLOBALSETTINGS__SQLSERVER__CONNECTIONSTRING: "placeholder" - name: Upload Public API Swagger artifact - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 with: name: swagger.json path: swagger.json if-no-files-found: error - + - name: Build Internal API Swagger run: | cd ./src/Api @@ -416,17 +415,17 @@ jobs: dotnet tool restore echo "Publish API" dotnet publish -c "Release" -o obj/build-output/publish - + dotnet swagger tofile --output ../../internal.json --host https://api.bitwarden.com \ ./obj/build-output/publish/Api.dll internal - + cd ../Identity - + echo "Restore Identity tools" dotnet tool restore echo "Publish Identity" dotnet publish -c "Release" -o obj/build-output/publish - + dotnet swagger tofile --output ../../identity.json --host https://identity.bitwarden.com \ ./obj/build-output/publish/Identity.dll v1 cd ../.. @@ -437,18 +436,18 @@ jobs: GLOBALSETTINGS__SQLSERVER__CONNECTIONSTRING: "placeholder" - name: Upload Internal API Swagger artifact - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 with: name: internal.json path: internal.json if-no-files-found: error - name: Upload Identity Swagger artifact - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 with: name: identity.json path: identity.json - if-no-files-found: error + if-no-files-found: error build-mssqlmigratorutility: name: Build MSSQL migrator utility @@ -467,10 +466,10 @@ jobs: - win-x64 steps: - name: Check out repo - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + 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: | @@ -486,7 +485,7 @@ jobs: - name: Upload project artifact for Windows if: ${{ contains(matrix.target, 'win') == true }} - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 with: name: MsSqlMigratorUtility-${{ matrix.target }} path: util/MsSqlMigratorUtility/obj/build-output/publish/MsSqlMigratorUtility.exe @@ -494,7 +493,7 @@ jobs: - name: Upload project artifact if: ${{ contains(matrix.target, 'win') == false }} - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 with: name: MsSqlMigratorUtility-${{ matrix.target }} path: util/MsSqlMigratorUtility/obj/build-output/publish/MsSqlMigratorUtility @@ -528,9 +527,9 @@ jobs: workflow_id: 'build-unified.yml', ref: 'main', inputs: { - server_branch: '${{ github.ref }}' + server_branch: process.env.GITHUB_REF } - }) + }); trigger-k8s-deploy: name: Trigger k8s deploy @@ -566,6 +565,39 @@ jobs: } }) + trigger-ee-updates: + name: Trigger Ephemeral Environment updates + if: github.ref != 'refs/heads/main' && contains(github.event.pull_request.labels.*.name, 'ephemeral-environment') + runs-on: ubuntu-24.04 + needs: build-docker + steps: + - name: Log in to Azure - CI subscription + uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 + with: + creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} + + - name: Retrieve GitHub PAT secrets + id: retrieve-secret-pat + uses: bitwarden/gh-actions/get-keyvault-secrets@main + with: + keyvault: "bitwarden-ci" + secrets: "github-pat-bitwarden-devops-bot-repo-scope" + + - name: Trigger Ephemeral Environment update + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + with: + github-token: ${{ steps.retrieve-secret-pat.outputs.github-pat-bitwarden-devops-bot-repo-scope }} + script: | + await github.rest.actions.createWorkflowDispatch({ + owner: 'bitwarden', + repo: 'devops', + workflow_id: '_update_ephemeral_tags.yml', + ref: 'main', + inputs: { + ephemeral_env_branch: process.env.GITHUB_HEAD_REF + } + }) + check-failures: name: Check for failures if: always() diff --git a/.github/workflows/cleanup-after-pr.yml b/.github/workflows/cleanup-after-pr.yml index 1bed3542d..c36dc4a03 100644 --- a/.github/workflows/cleanup-after-pr.yml +++ b/.github/workflows/cleanup-after-pr.yml @@ -1,4 +1,3 @@ ---- name: Container registry cleanup on: diff --git a/.github/workflows/cleanup-ephemeral-environment.yml b/.github/workflows/cleanup-ephemeral-environment.yml new file mode 100644 index 000000000..91e8ff083 --- /dev/null +++ b/.github/workflows/cleanup-ephemeral-environment.yml @@ -0,0 +1,59 @@ +name: Ephemeral environment cleanup + +on: + pull_request: + types: [unlabeled] + +jobs: + validate-pr: + name: Validate PR + runs-on: ubuntu-24.04 + outputs: + config-exists: ${{ steps.validate-config.outputs.config-exists }} + steps: + - name: Checkout PR + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Validate config exists in path + id: validate-config + run: | + if [[ -f "ephemeral-environments/$GITHUB_HEAD_REF.yaml" ]]; then + echo "Ephemeral environment config found in path, continuing." + echo "config-exists=true" >> $GITHUB_OUTPUT + fi + + + cleanup-config: + name: Cleanup ephemeral environment + runs-on: ubuntu-24.04 + needs: validate-pr + if: ${{ needs.validate-pr.outputs.config-exists }} + steps: + - name: Log in to Azure - CI subscription + uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 + with: + creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} + + - name: Retrieve GitHub PAT secrets + id: retrieve-secret-pat + uses: bitwarden/gh-actions/get-keyvault-secrets@main + with: + keyvault: "bitwarden-ci" + secrets: "github-pat-bitwarden-devops-bot-repo-scope" + + - name: Trigger Ephemeral Environment cleanup + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + with: + github-token: ${{ steps.retrieve-secret-pat.outputs.github-pat-bitwarden-devops-bot-repo-scope }} + script: | + await github.rest.actions.createWorkflowDispatch({ + owner: 'bitwarden', + repo: 'devops', + workflow_id: '_ephemeral_environment_pr_manager.yml', + ref: 'main', + inputs: { + ephemeral_env_branch: process.env.GITHUB_HEAD_REF, + cleanup_config: true, + project: 'server' + } + }) diff --git a/.github/workflows/cleanup-rc-branch.yml b/.github/workflows/cleanup-rc-branch.yml index 3b3c2d55d..1ea2eab08 100644 --- a/.github/workflows/cleanup-rc-branch.yml +++ b/.github/workflows/cleanup-rc-branch.yml @@ -1,4 +1,3 @@ ---- name: Cleanup RC Branch on: @@ -24,7 +23,7 @@ jobs: secrets: "github-pat-bitwarden-devops-bot-repo-scope" - name: Checkout main - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + 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 101e5730d..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@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Collect id: collect diff --git a/.github/workflows/container-registry-purge.yml b/.github/workflows/container-registry-purge.yml deleted file mode 100644 index 1fc4c511b..000000000 --- a/.github/workflows/container-registry-purge.yml +++ /dev/null @@ -1,102 +0,0 @@ ---- -name: Container registry purge - -on: - schedule: - - cron: "0 0 * * SUN" - workflow_dispatch: - inputs: {} - -jobs: - purge: - name: Purge old images - runs-on: ubuntu-22.04 - steps: - - name: Log in to Azure - uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 - with: - creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }} - - - name: Purge images - env: - REGISTRY: bitwardenprod - AGO_DUR_VER: "180d" - AGO_DUR: "30d" - run: | - REPO_LIST=$(az acr repository list -n $REGISTRY -o tsv) - for REPO in $REPO_LIST - do - - PURGE_LATEST="" - PURGE_VERSION="" - PURGE_ELSE="" - - TAG_LIST=$(az acr repository show-tags -n $REGISTRY --repository $REPO -o tsv) - for TAG in $TAG_LIST - do - if [ $TAG = "latest" ] || [ $TAG = "dev" ]; then - PURGE_LATEST+="--filter '$REPO:$TAG' " - elif [[ $TAG =~ [0-9]+\.[0-9]+\.[0-9]+ ]]; then - PURGE_VERSION+="--filter '$REPO:$TAG' " - else - PURGE_ELSE+="--filter '$REPO:$TAG' " - fi - done - - if [ ! -z "$PURGE_LATEST" ] - then - PURGE_LATEST_CMD="acr purge $PURGE_LATEST --ago $AGO_DUR_VER --untagged --keep 1" - az acr run --cmd "$PURGE_LATEST_CMD" --registry $REGISTRY /dev/null & - fi - - if [ ! -z "$PURGE_VERSION" ] - then - PURGE_VERSION_CMD="acr purge $PURGE_VERSION --ago $AGO_DUR_VER --untagged" - az acr run --cmd "$PURGE_VERSION_CMD" --registry $REGISTRY /dev/null & - fi - - if [ ! -z "$PURGE_ELSE" ] - then - PURGE_ELSE_CMD="acr purge $PURGE_ELSE --ago $AGO_DUR --untagged" - az acr run --cmd "$PURGE_ELSE_CMD" --registry $REGISTRY /dev/null & - fi - - wait - - done - - check-failures: - name: Check for failures - if: always() - runs-on: ubuntu-22.04 - needs: [purge] - steps: - - name: Check if any job failed - if: | - (github.ref == 'refs/heads/main' - || github.ref == 'refs/heads/rc' - || github.ref == 'refs/heads/hotfix-rc') - && contains(needs.*.result, 'failure') - run: exit 1 - - - name: Log in to Azure - CI subscription - uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 - if: failure() - with: - creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} - - - name: Retrieve secrets - id: retrieve-secrets - uses: bitwarden/gh-actions/get-keyvault-secrets@main - if: failure() - with: - keyvault: "bitwarden-ci" - secrets: "devops-alerts-slack-webhook-url" - - - name: Notify Slack on failure - uses: act10ns/slack@44541246747a30eb3102d87f7a4cc5471b0ffb7d # v2.1.0 - if: failure() - env: - SLACK_WEBHOOK_URL: ${{ steps.retrieve-secrets.outputs.devops-alerts-slack-webhook-url }} - with: - status: ${{ job.status }} diff --git a/.github/workflows/enforce-labels.yml b/.github/workflows/enforce-labels.yml index 160ee15b9..11d565493 100644 --- a/.github/workflows/enforce-labels.yml +++ b/.github/workflows/enforce-labels.yml @@ -1,4 +1,3 @@ ---- name: Enforce PR labels on: @@ -7,13 +6,13 @@ on: types: [labeled, unlabeled, opened, reopened, synchronize] jobs: enforce-label: - if: ${{ contains(github.event.*.labels.*.name, 'hold') || contains(github.event.*.labels.*.name, 'needs-qa') || contains(github.event.*.labels.*.name, 'DB-migrations-changed') }} + if: ${{ contains(github.event.*.labels.*.name, 'hold') || contains(github.event.*.labels.*.name, 'needs-qa') || contains(github.event.*.labels.*.name, 'DB-migrations-changed') || contains(github.event.*.labels.*.name, 'ephemeral-environment') }} name: Enforce label runs-on: ubuntu-22.04 steps: - name: Check for label run: | - echo "PRs with the hold or needs-qa labels cannot be merged" - echo "### :x: PRs with the hold or needs-qa labels cannot be merged" >> $GITHUB_STEP_SUMMARY + echo "PRs with the hold, needs-qa or ephemeral-environment labels cannot be merged" + echo "### :x: PRs with the hold, needs-qa or ephemeral-environment labels cannot be merged" >> $GITHUB_STEP_SUMMARY exit 1 diff --git a/.github/workflows/protect-files.yml b/.github/workflows/protect-files.yml index 3bbc7e74f..89d6d4c6d 100644 --- a/.github/workflows/protect-files.yml +++ b/.github/workflows/protect-files.yml @@ -1,7 +1,6 @@ # Runs if there are changes to the paths: list. # Starts a matrix job to check for modified files, then sets output based on the results. # The input decides if the label job is ran, adding a label to the PR. ---- name: Protect files on: @@ -29,7 +28,7 @@ jobs: label: "DB-migrations-changed" steps: - name: Check out repo - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 2 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 3c45f84b7..55220390c 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,4 +1,3 @@ ---- name: Publish run-name: Publish ${{ inputs.publish_type }} @@ -99,7 +98,7 @@ jobs: echo "Github Release Option: $RELEASE_OPTION" - name: Check out repo - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + 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 c63302cbc..0809ff833 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,4 +1,3 @@ ---- name: Release run-name: Release ${{ inputs.release_type }} @@ -37,7 +36,7 @@ jobs: fi - name: Check out repo - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Check release version id: version diff --git a/.github/workflows/version-bump.yml b/.github/workflows/repository-management.yml similarity index 58% rename from .github/workflows/version-bump.yml rename to .github/workflows/repository-management.yml index e1d96ee4d..8b0e3bcc0 100644 --- a/.github/workflows/version-bump.yml +++ b/.github/workflows/repository-management.yml @@ -1,88 +1,70 @@ ---- -name: Version Bump +name: Repository management on: workflow_dispatch: inputs: + branch_to_cut: + default: "rc" + description: "Branch to cut" + options: + - "rc" + - "hotfix-rc" + required: true + type: choice + target_ref: + default: "main" + description: "Branch/Tag to target for cut" + required: true + type: string version_number_override: description: "New version override (leave blank for automatic calculation, example: '2024.1.0')" required: false type: string - cut_rc_branch: - description: "Cut RC branch?" - default: true - type: boolean - enable_slack_notification: - description: "Enable Slack notifications for upcoming release?" - default: false - type: boolean jobs: + cut_branch: + name: Cut branch + runs-on: ubuntu-22.04 + steps: + - name: Check out target ref + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + ref: ${{ inputs.target_ref }} + + - name: Check if ${{ inputs.branch_to_cut }} branch exists + env: + BRANCH_NAME: ${{ inputs.branch_to_cut }} + run: | + if [[ $(git ls-remote --heads origin $BRANCH_NAME) ]]; then + echo "$BRANCH_NAME already exists! Please delete $BRANCH_NAME before running again." >> $GITHUB_STEP_SUMMARY + exit 1 + fi + + - name: Cut branch + env: + BRANCH_NAME: ${{ inputs.branch_to_cut }} + run: | + git switch --quiet --create $BRANCH_NAME + git push --quiet --set-upstream origin $BRANCH_NAME + + bump_version: name: Bump Version runs-on: ubuntu-22.04 + needs: cut_branch outputs: version: ${{ steps.set-final-version-output.outputs.version }} steps: - - name: Validate version input + - name: Validate version input format if: ${{ inputs.version_number_override != '' }} uses: bitwarden/gh-actions/version-check@main with: version: ${{ inputs.version_number_override }} - - name: Slack Notification Check - run: | - if [[ "${{ inputs.enable_slack_notification }}" == true ]]; then - echo "Slack notifications enabled." - else - echo "Slack notifications disabled." - fi - - name: Check out branch - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - - - name: Check if RC branch exists - if: ${{ inputs.cut_rc_branch == true }} - run: | - remote_rc_branch_check=$(git ls-remote --heads origin rc | wc -l) - if [[ "${remote_rc_branch_check}" -gt 0 ]]; then - echo "Remote RC branch exists." - echo "Please delete current RC branch before running again." - exit 1 - fi - - - name: Log in to Azure - CI subscription - uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: - creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} - - - name: Retrieve secrets - id: retrieve-secrets - uses: bitwarden/gh-actions/get-keyvault-secrets@main - with: - keyvault: "bitwarden-ci" - secrets: "github-gpg-private-key, - github-gpg-private-key-passphrase" - - - name: Import GPG key - uses: crazy-max/ghaction-import-gpg@01dd5d3ca463c7f10f7f4f7b4f177225ac661ee4 # v6.1.0 - with: - gpg_private_key: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key }} - passphrase: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key-passphrase }} - git_user_signingkey: true - git_commit_gpgsign: true - - - name: Set up Git - run: | - git config --local user.email "106330231+bitwarden-devops-bot@users.noreply.github.com" - git config --local user.name "bitwarden-devops-bot" - - - 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 + ref: main - name: Install xmllint run: | @@ -103,16 +85,16 @@ jobs: run: | # Error if version has not changed. if [[ "$NEW_VERSION" == "$CURRENT_VERSION" ]]; then - echo "Version has not changed." + echo "Specified override version is the same as the current version." >> $GITHUB_STEP_SUMMARY exit 1 fi # Check if version is newer. printf '%s\n' "${CURRENT_VERSION}" "${NEW_VERSION}" | sort -C -V if [ $? -eq 0 ]; then - echo "Version check successful." + echo "Version is newer than the current version." else - echo "Version check failed." + echo "Version is older than the current version." >> $GITHUB_STEP_SUMMARY exit 1 fi @@ -148,25 +130,23 @@ jobs: echo "version=${{ steps.calculate-next-version.outputs.version }}" >> $GITHUB_OUTPUT fi - - name: Check if version changed - id: version-changed + - name: Configure Git run: | - if [ -n "$(git status --porcelain)" ]; then - echo "changes_to_commit=TRUE" >> $GITHUB_OUTPUT - else - echo "changes_to_commit=FALSE" >> $GITHUB_OUTPUT - echo "No changes to commit!"; - fi + 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 - if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }} run: git commit -m "Bumped version to ${{ steps.set-final-version-output.outputs.version }}" -a - name: Push changes - if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }} - env: - PR_BRANCH: ${{ steps.create-branch.outputs.name }} - run: git push -u origin $PR_BRANCH + run: git push - name: Generate GH App token uses: actions/create-github-app-token@5d869da34e18e7287c1daad50e0b8ea0f506ce69 # v1.11.0 @@ -177,7 +157,6 @@ jobs: owner: ${{ github.repository_owner }} - name: Create version PR - if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }} id: create-pr env: GH_TOKEN: ${{ steps.app-token.outputs.token }} @@ -196,41 +175,30 @@ jobs: - [ ] 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 - if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }} env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} PR_NUMBER: ${{ steps.create-pr.outputs.pr_number }} run: gh pr review $PR_NUMBER --approve - name: Merge PR - if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }} 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 - - name: Report upcoming release version to Slack - if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' && inputs.enable_slack_notification == true }} - uses: bitwarden/gh-actions/report-upcoming-release-version@main - with: - version: ${{ steps.set-final-version-output.outputs.version }} - project: ${{ github.repository }} - AZURE_KV_CI_SERVICE_PRINCIPAL: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} - cut_rc: - name: Cut RC branch - if: ${{ inputs.cut_rc_branch == true }} - needs: bump_version + cherry_pick: + name: Cherry-Pick Commit(s) runs-on: ubuntu-22.04 + needs: bump_version steps: - - name: Check out branch - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + - name: Check out main branch + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: ref: main @@ -254,13 +222,76 @@ jobs: sleep 10 done - - name: Cut RC branch + - name: Get last version commit(s) + id: get-commits run: | - git switch --quiet --create rc - git push --quiet --set-upstream origin rc + git switch main + MAIN_COMMIT=$(git log --reverse --pretty=format:"%H" --max-count=1 Directory.Build.props) + echo "main_commit=$MAIN_COMMIT" >> $GITHUB_OUTPUT - move-future-db-scripts: + if [[ $(git ls-remote --heads origin rc) ]]; then + git switch rc + RC_COMMIT=$(git log --reverse --pretty=format:"%H" --max-count=1 Directory.Build.props) + echo "rc_commit=$RC_COMMIT" >> $GITHUB_OUTPUT + + 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" + git config --local user.name "Github Actions" + + - name: Perform cherry-pick(s) + env: + CUT_BRANCH: ${{ inputs.branch_to_cut }} + MAIN_COMMIT: ${{ steps.get-commits.outputs.main_commit }} + RC_COMMIT: ${{ steps.get-commits.outputs.rc_commit }} + RC_VERSION: ${{ steps.get-commits.outputs.rc_version }} + run: | + # If we are cutting 'hotfix-rc': + if [[ "$CUT_BRANCH" == "hotfix-rc" ]]; then + + # If the 'rc' branch exists: + if [[ $(git ls-remote --heads origin rc) ]]; then + + # Chery-pick from 'rc' into 'hotfix-rc' + git switch hotfix-rc + HOTFIX_RC_VERSION=$(xmllint -xpath "/Project/PropertyGroup/Version/text()" Directory.Build.props) + if [[ "$HOTFIX_RC_VERSION" != "$RC_VERSION" ]]; then + git cherry-pick --strategy-option=theirs -x $RC_COMMIT + git push -u origin hotfix-rc + fi + + # Cherry-pick from 'main' into 'rc' + git switch rc + git cherry-pick --strategy-option=theirs -x $MAIN_COMMIT + git push -u origin rc + + # If the 'rc' branch does not exist: + else + + # Cherry-pick from 'main' into 'hotfix-rc' + git switch hotfix-rc + git cherry-pick --strategy-option=theirs -x $MAIN_COMMIT + git push -u origin hotfix-rc + + fi + + # If we are cutting 'rc': + elif [[ "$CUT_BRANCH" == "rc" ]]; then + + # Cherry-pick from 'main' into 'rc' + git switch rc + git cherry-pick --strategy-option=theirs -x $MAIN_COMMIT + git push -u origin rc + + fi + + + move_future_db_scripts: name: Move finalization database scripts - needs: cut_rc + needs: cherry_pick uses: ./.github/workflows/_move_finalization_db_scripts.yml secrets: inherit diff --git a/.github/workflows/scan.yml b/.github/workflows/scan.yml index 0f4d060ba..f071cb4ec 100644 --- a/.github/workflows/scan.yml +++ b/.github/workflows/scan.yml @@ -26,12 +26,12 @@ jobs: steps: - name: Check out repo - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: ref: ${{ github.event.pull_request.head.sha }} - name: Scan with Checkmarx - uses: checkmarx/ast-github-action@9fda5a4a2c297608117a5a56af424502a9192e57 # 2.0.34 + uses: checkmarx/ast-github-action@f0869bd1a37fddc06499a096101e6c900e815d81 # 2.0.36 env: INCREMENTAL: "${{ contains(github.event_name, 'pull_request') && '--sast-incremental' || '' }}" with: @@ -46,7 +46,7 @@ jobs: --output-path . ${{ env.INCREMENTAL }} - name: Upload Checkmarx results to GitHub - uses: github/codeql-action/upload-sarif@461ef6c76dfe95d5c364de2f431ddbd31a417628 # v3.26.9 + 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@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + 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/stale-bot.yml b/.github/workflows/stale-bot.yml index d3dc92cd7..f8a25288f 100644 --- a/.github/workflows/stale-bot.yml +++ b/.github/workflows/stale-bot.yml @@ -1,4 +1,3 @@ ---- name: Staleness on: workflow_dispatch: diff --git a/.github/workflows/test-database.yml b/.github/workflows/test-database.yml index 325f10b94..e16c080bc 100644 --- a/.github/workflows/test-database.yml +++ b/.github/workflows/test-database.yml @@ -1,4 +1,3 @@ ---- name: Database testing on: @@ -36,10 +35,10 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Check out repo - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + 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 @@ -55,7 +54,7 @@ jobs: # I've seen the SQL Server container not be ready for commands right after starting up and just needing a bit longer to be ready - name: Sleep run: sleep 15s - + - name: Checking pending model changes (MySQL) working-directory: "util/MySqlMigrations" run: 'dotnet ef migrations has-pending-model-changes -- --GlobalSettings:MySql:ConnectionString="$CONN_STR"' @@ -114,7 +113,7 @@ jobs: BW_TEST_DATABASES__3__CONNECTIONSTRING: "Data Source=${{ runner.temp }}/test.db" run: dotnet test --logger "trx;LogFileName=infrastructure-test-results.trx" shell: pwsh - + - name: Print MySQL Logs if: failure() run: 'docker logs $(docker ps --quiet --filter "name=mysql")' @@ -147,10 +146,10 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Check out repo - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + 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: | @@ -164,7 +163,7 @@ jobs: shell: pwsh - name: Upload DACPAC - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 with: name: sql.dacpac path: Sql.dacpac @@ -190,7 +189,7 @@ jobs: shell: pwsh - name: Report validation results - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 with: name: report.xml path: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 216130a21..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@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + 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: | @@ -77,7 +77,7 @@ jobs: fail-on-error: true - name: Upload to codecov.io - uses: codecov/codecov-action@e28ff129e5465c2c0dcc6f003fc735cb6ae0c673 # v4.5.0 + uses: codecov/codecov-action@b9fd7d16f6d7d1b5d2bec1a2887e65ceed900238 # v4.6.0 if: ${{ needs.check-test-secrets.outputs.available == 'true' }} env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/Directory.Build.props b/Directory.Build.props index a7133709b..5cd12bfb7 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -3,7 +3,7 @@ net8.0 - 2024.9.2 + 2024.10.1 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/AdminConsole/Providers/RemoveOrganizationFromProviderCommand.cs b/bitwarden_license/src/Commercial.Core/AdminConsole/Providers/RemoveOrganizationFromProviderCommand.cs index 866d18f9a..045fd5059 100644 --- a/bitwarden_license/src/Commercial.Core/AdminConsole/Providers/RemoveOrganizationFromProviderCommand.cs +++ b/bitwarden_license/src/Commercial.Core/AdminConsole/Providers/RemoveOrganizationFromProviderCommand.cs @@ -2,6 +2,7 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Enums.Provider; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.Providers.Interfaces; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Billing.Constants; @@ -27,6 +28,7 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv private readonly IFeatureService _featureService; private readonly IProviderBillingService _providerBillingService; private readonly ISubscriberService _subscriberService; + private readonly IHasConfirmedOwnersExceptQuery _hasConfirmedOwnersExceptQuery; public RemoveOrganizationFromProviderCommand( IEventService eventService, @@ -37,7 +39,8 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv IStripeAdapter stripeAdapter, IFeatureService featureService, IProviderBillingService providerBillingService, - ISubscriberService subscriberService) + ISubscriberService subscriberService, + IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery) { _eventService = eventService; _mailService = mailService; @@ -48,6 +51,7 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv _featureService = featureService; _providerBillingService = providerBillingService; _subscriberService = subscriberService; + _hasConfirmedOwnersExceptQuery = hasConfirmedOwnersExceptQuery; } public async Task RemoveOrganizationFromProvider( @@ -63,7 +67,7 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv throw new BadRequestException("Failed to remove organization. Please contact support."); } - if (!await _organizationService.HasConfirmedOwnersExceptAsync( + if (!await _hasConfirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync( providerOrganization.OrganizationId, Array.Empty(), includeProvider: false)) diff --git a/bitwarden_license/src/Commercial.Core/Billing/ProviderBillingService.cs b/bitwarden_license/src/Commercial.Core/Billing/ProviderBillingService.cs index 787a11d1e..19991dab2 100644 --- a/bitwarden_license/src/Commercial.Core/Billing/ProviderBillingService.cs +++ b/bitwarden_license/src/Commercial.Core/Billing/ProviderBillingService.cs @@ -379,42 +379,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 diff --git a/bitwarden_license/src/Scim/Controllers/v2/UsersController.cs b/bitwarden_license/src/Scim/Controllers/v2/UsersController.cs index 1429fc387..1323205b9 100644 --- a/bitwarden_license/src/Scim/Controllers/v2/UsersController.cs +++ b/bitwarden_license/src/Scim/Controllers/v2/UsersController.cs @@ -17,7 +17,6 @@ namespace Bit.Scim.Controllers.v2; [ExceptionHandlerFilter] public class UsersController : Controller { - private readonly IUserService _userService; private readonly IOrganizationUserRepository _organizationUserRepository; private readonly IOrganizationService _organizationService; private readonly IGetUsersListQuery _getUsersListQuery; @@ -27,7 +26,6 @@ public class UsersController : Controller private readonly ILogger _logger; public UsersController( - IUserService userService, IOrganizationUserRepository organizationUserRepository, IOrganizationService organizationService, IGetUsersListQuery getUsersListQuery, @@ -36,7 +34,6 @@ public class UsersController : Controller IPostUserCommand postUserCommand, ILogger logger) { - _userService = userService; _organizationUserRepository = organizationUserRepository; _organizationService = organizationService; _getUsersListQuery = getUsersListQuery; @@ -60,17 +57,15 @@ public class UsersController : Controller [HttpGet("")] public async Task Get( Guid organizationId, - [FromQuery] string filter, - [FromQuery] int? count, - [FromQuery] int? startIndex) + [FromQuery] GetUsersQueryParamModel model) { - var usersListQueryResult = await _getUsersListQuery.GetUsersListAsync(organizationId, filter, count, startIndex); + var usersListQueryResult = await _getUsersListQuery.GetUsersListAsync(organizationId, model); var scimListResponseModel = new ScimListResponseModel { Resources = usersListQueryResult.userList.Select(u => new ScimUserResponseModel(u)).ToList(), - ItemsPerPage = count.GetValueOrDefault(usersListQueryResult.userList.Count()), + ItemsPerPage = model.Count, TotalResults = usersListQueryResult.totalResults, - StartIndex = startIndex.GetValueOrDefault(1), + StartIndex = model.StartIndex, }; return Ok(scimListResponseModel); } @@ -98,7 +93,7 @@ public class UsersController : Controller if (model.Active && orgUser.Status == OrganizationUserStatusType.Revoked) { - await _organizationService.RestoreUserAsync(orgUser, EventSystemUser.SCIM, _userService); + await _organizationService.RestoreUserAsync(orgUser, EventSystemUser.SCIM); } else if (!model.Active && orgUser.Status != OrganizationUserStatusType.Revoked) { diff --git a/bitwarden_license/src/Scim/Models/GetUserQueryParamModel.cs b/bitwarden_license/src/Scim/Models/GetUserQueryParamModel.cs new file mode 100644 index 000000000..27d7b6d9a --- /dev/null +++ b/bitwarden_license/src/Scim/Models/GetUserQueryParamModel.cs @@ -0,0 +1,12 @@ +using System.ComponentModel.DataAnnotations; + +public class GetUsersQueryParamModel +{ + public string Filter { get; init; } = string.Empty; + + [Range(1, int.MaxValue)] + public int Count { get; init; } = 50; + + [Range(1, int.MaxValue)] + public int StartIndex { get; init; } = 1; +} diff --git a/bitwarden_license/src/Scim/Startup.cs b/bitwarden_license/src/Scim/Startup.cs index 388ba5adc..3fac669ed 100644 --- a/bitwarden_license/src/Scim/Startup.cs +++ b/bitwarden_license/src/Scim/Startup.cs @@ -69,6 +69,7 @@ public class Startup // Services services.AddBaseServices(globalSettings); services.AddDefaultServices(globalSettings); + services.AddDistributedCache(globalSettings); services.AddBillingOperations(); services.TryAddSingleton(); diff --git a/bitwarden_license/src/Scim/Users/GetUsersListQuery.cs b/bitwarden_license/src/Scim/Users/GetUsersListQuery.cs index 1bea930f1..9bcbcbdaf 100644 --- a/bitwarden_license/src/Scim/Users/GetUsersListQuery.cs +++ b/bitwarden_license/src/Scim/Users/GetUsersListQuery.cs @@ -13,11 +13,16 @@ public class GetUsersListQuery : IGetUsersListQuery _organizationUserRepository = organizationUserRepository; } - public async Task<(IEnumerable userList, int totalResults)> GetUsersListAsync(Guid organizationId, string filter, int? count, int? startIndex) + public async Task<(IEnumerable userList, int totalResults)> GetUsersListAsync(Guid organizationId, GetUsersQueryParamModel userQueryParams) { string emailFilter = null; string usernameFilter = null; string externalIdFilter = null; + + int count = userQueryParams.Count; + int startIndex = userQueryParams.StartIndex; + string filter = userQueryParams.Filter; + if (!string.IsNullOrWhiteSpace(filter)) { var filterLower = filter.ToLowerInvariant(); @@ -56,11 +61,11 @@ public class GetUsersListQuery : IGetUsersListQuery } totalResults = userList.Count; } - else if (string.IsNullOrWhiteSpace(filter) && startIndex.HasValue && count.HasValue) + else if (string.IsNullOrWhiteSpace(filter)) { userList = orgUsers.OrderBy(ou => ou.Email) - .Skip(startIndex.Value - 1) - .Take(count.Value) + .Skip(startIndex - 1) + .Take(count) .ToList(); totalResults = orgUsers.Count; } diff --git a/bitwarden_license/src/Scim/Users/Interfaces/IGetUsersListQuery.cs b/bitwarden_license/src/Scim/Users/Interfaces/IGetUsersListQuery.cs index 265c6a8e7..f584cb8e7 100644 --- a/bitwarden_license/src/Scim/Users/Interfaces/IGetUsersListQuery.cs +++ b/bitwarden_license/src/Scim/Users/Interfaces/IGetUsersListQuery.cs @@ -4,5 +4,5 @@ namespace Bit.Scim.Users.Interfaces; public interface IGetUsersListQuery { - Task<(IEnumerable userList, int totalResults)> GetUsersListAsync(Guid organizationId, string filter, int? count, int? startIndex); + Task<(IEnumerable userList, int totalResults)> GetUsersListAsync(Guid organizationId, GetUsersQueryParamModel userQueryParams); } diff --git a/bitwarden_license/src/Scim/Users/PatchUserCommand.cs b/bitwarden_license/src/Scim/Users/PatchUserCommand.cs index 075807a58..f4445354c 100644 --- a/bitwarden_license/src/Scim/Users/PatchUserCommand.cs +++ b/bitwarden_license/src/Scim/Users/PatchUserCommand.cs @@ -9,18 +9,15 @@ namespace Bit.Scim.Users; public class PatchUserCommand : IPatchUserCommand { - private readonly IUserService _userService; private readonly IOrganizationUserRepository _organizationUserRepository; private readonly IOrganizationService _organizationService; private readonly ILogger _logger; public PatchUserCommand( - IUserService userService, IOrganizationUserRepository organizationUserRepository, IOrganizationService organizationService, ILogger logger) { - _userService = userService; _organizationUserRepository = organizationUserRepository; _organizationService = organizationService; _logger = logger; @@ -74,7 +71,7 @@ public class PatchUserCommand : IPatchUserCommand { if (active && orgUser.Status == OrganizationUserStatusType.Revoked) { - await _organizationService.RestoreUserAsync(orgUser, EventSystemUser.SCIM, _userService); + await _organizationService.RestoreUserAsync(orgUser, EventSystemUser.SCIM); return true; } else if (!active && orgUser.Status != OrganizationUserStatusType.Revoked) diff --git a/bitwarden_license/src/Sso/package-lock.json b/bitwarden_license/src/Sso/package-lock.json index 32a54b031..51da30772 100644 --- a/bitwarden_license/src/Sso/package-lock.json +++ b/bitwarden_license/src/Sso/package-lock.json @@ -9,7 +9,7 @@ "version": "0.0.0", "license": "-", "dependencies": { - "bootstrap": "5.3.3", + "bootstrap": "4.6.2", "font-awesome": "4.7.0", "jquery": "3.7.1", "popper.js": "1.16.1" @@ -18,9 +18,9 @@ "css-loader": "7.1.2", "expose-loader": "5.0.0", "mini-css-extract-plugin": "2.9.1", - "sass": "1.77.8", - "sass-loader": "16.0.1", - "webpack": "5.94.0", + "sass": "1.79.5", + "sass-loader": "16.0.2", + "webpack": "5.95.0", "webpack-cli": "5.1.4" } }, @@ -98,10 +98,296 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@parcel/watcher": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.4.1.tgz", + "integrity": "sha512-HNjmfLQEVRZmHRET336f20H/8kOozUGwk7yajvsonjNxbj2wBTK1WsQuHkD5yYh9RxFGL2EyDHryOihOwUoKDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^1.0.3", + "is-glob": "^4.0.3", + "micromatch": "^4.0.5", + "node-addon-api": "^7.0.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.4.1", + "@parcel/watcher-darwin-arm64": "2.4.1", + "@parcel/watcher-darwin-x64": "2.4.1", + "@parcel/watcher-freebsd-x64": "2.4.1", + "@parcel/watcher-linux-arm-glibc": "2.4.1", + "@parcel/watcher-linux-arm64-glibc": "2.4.1", + "@parcel/watcher-linux-arm64-musl": "2.4.1", + "@parcel/watcher-linux-x64-glibc": "2.4.1", + "@parcel/watcher-linux-x64-musl": "2.4.1", + "@parcel/watcher-win32-arm64": "2.4.1", + "@parcel/watcher-win32-ia32": "2.4.1", + "@parcel/watcher-win32-x64": "2.4.1" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.4.1.tgz", + "integrity": "sha512-LOi/WTbbh3aTn2RYddrO8pnapixAziFl6SMxHM69r3tvdSm94JtCenaKgk1GRg5FJ5wpMCpHeW+7yqPlvZv7kg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.4.1.tgz", + "integrity": "sha512-ln41eihm5YXIY043vBrrHfn94SIBlqOWmoROhsMVTSXGh0QahKGy77tfEywQ7v3NywyxBBkGIfrWRHm0hsKtzA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.4.1.tgz", + "integrity": "sha512-yrw81BRLjjtHyDu7J61oPuSoeYWR3lDElcPGJyOvIXmor6DEo7/G2u1o7I38cwlcoBHQFULqF6nesIX3tsEXMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.4.1.tgz", + "integrity": "sha512-TJa3Pex/gX3CWIx/Co8k+ykNdDCLx+TuZj3f3h7eOjgpdKM+Mnix37RYsYU4LHhiYJz3DK5nFCCra81p6g050w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.4.1.tgz", + "integrity": "sha512-4rVYDlsMEYfa537BRXxJ5UF4ddNwnr2/1O4MHM5PjI9cvV2qymvhwZSFgXqbS8YoTk5i/JR0L0JDs69BUn45YA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.4.1.tgz", + "integrity": "sha512-BJ7mH985OADVLpbrzCLgrJ3TOpiZggE9FMblfO65PlOCdG++xJpKUJ0Aol74ZUIYfb8WsRlUdgrZxKkz3zXWYA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.4.1.tgz", + "integrity": "sha512-p4Xb7JGq3MLgAfYhslU2SjoV9G0kI0Xry0kuxeG/41UfpjHGOhv7UoUDAz/jb1u2elbhazy4rRBL8PegPJFBhA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.4.1.tgz", + "integrity": "sha512-s9O3fByZ/2pyYDPoLM6zt92yu6P4E39a03zvO0qCHOTjxmt3GHRMLuRZEWhWLASTMSrrnVNWdVI/+pUElJBBBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.4.1.tgz", + "integrity": "sha512-L2nZTYR1myLNST0O632g0Dx9LyMNHrn6TOt76sYxWLdff3cB22/GZX2UPtJnaqQPdCRoszoY5rcOj4oMTtp5fQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.4.1.tgz", + "integrity": "sha512-Uq2BPp5GWhrq/lcuItCHoqxjULU1QYEcyjSO5jqqOK8RNFDBQnenMMx4gAl3v8GiWa59E9+uDM7yZ6LxwUIfRg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.4.1.tgz", + "integrity": "sha512-maNRit5QQV2kgHFSYwftmPBxiuK5u4DXjbXx7q6eKjq5dsLXZ4FJiVvlcw35QXzk0KrUecJmuVFbj4uV9oYrcw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.4.1.tgz", + "integrity": "sha512-+DvS92F9ezicfswqrvIRM2njcYJbd5mb9CUgtrHCHmvn7pPPa+nMDRu1o1bYYz/l5IB2NVGNJWiH7h1E58IF2A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/@types/estree": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", "dev": true, "license": "MIT" }, @@ -113,13 +399,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.1.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.1.0.tgz", - "integrity": "sha512-AOmuRF0R2/5j1knA3c6G3HOk523Ga+l+ZXltX8SF1+5oqcXijjfTd8fY3XRZqSihEu9XhtQnKYLmkFaoxgsJHw==", + "version": "22.7.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz", + "integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~6.13.0" + "undici-types": "~6.19.2" } }, "node_modules/@webassemblyjs/ast": { @@ -415,35 +701,8 @@ "ajv": "^8.8.2" } }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/bootstrap": { - "version": "5.3.3", + "version": "4.6.2", "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.6.2.tgz", "integrity": "sha512-51Bbp/Uxr9aTuy6ca/8FbFloBUJZLHwnhTcnjIeRn2suQWsWzcuJhGjKDB5eppVte/8oCdOL3VuwxvZDUggwGQ==", "funding": [ @@ -476,9 +735,9 @@ } }, "node_modules/browserslist": { - "version": "4.23.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz", - "integrity": "sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.0.tgz", + "integrity": "sha512-Rmb62sR1Zpjql25eSanFGEhAxcFwfA1K0GuQcLoaJBAcENegrQut3hYdhXFF1obQfiDyqIW/cLM5HSJ/9k884A==", "dev": true, "funding": [ { @@ -496,8 +755,8 @@ ], "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001646", - "electron-to-chromium": "^1.5.4", + "caniuse-lite": "^1.0.30001663", + "electron-to-chromium": "^1.5.28", "node-releases": "^2.0.18", "update-browserslist-db": "^1.1.0" }, @@ -516,9 +775,9 @@ "license": "MIT" }, "node_modules/caniuse-lite": { - "version": "1.0.30001651", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001651.tgz", - "integrity": "sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==", + "version": "1.0.30001668", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001668.tgz", + "integrity": "sha512-nWLrdxqCdblixUO+27JtGJJE/txpJlyUy5YN1u53wLZkP0emYCo5zgS6QYft7VUYR42LGgi/S5hdLZTrnyIddw==", "dev": true, "funding": [ { @@ -537,28 +796,19 @@ "license": "CC-BY-4.0" }, "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz", + "integrity": "sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==", "dev": true, "license": "MIT", "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" + "readdirp": "^4.0.1" }, "engines": { - "node": ">= 8.10.0" + "node": ">= 14.16.0" }, "funding": { "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" } }, "node_modules/chrome-trace-event": { @@ -664,10 +914,23 @@ "node": ">=4" } }, + "node_modules/detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "detect-libc": "bin/detect-libc.js" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/electron-to-chromium": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.5.tgz", - "integrity": "sha512-QR7/A7ZkMS8tZuoftC/jfqNkZLQO779SSW3YuZHP4eXpj3EffGLFcB/Xu9AAZQzLccTiCV+EmUo3ha4mQ9wnlA==", + "version": "1.5.36", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.36.tgz", + "integrity": "sha512-HYTX8tKge/VNp6FGO+f/uVDmUkq+cEfcxYhKf15Akc4M5yxt5YmorwlAitKWjWhWQnKcDRBAQKXkhqqXMqcrjw==", "dev": true, "license": "ISC" }, @@ -686,9 +949,9 @@ } }, "node_modules/envinfo": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.13.0.tgz", - "integrity": "sha512-cvcaMr7KqXVh4nyzGTVqTum+gAiL265x5jUWQIDLq//zOGbW+gSW/C+OWLleY/rs9Qole6AZLMXPbtIFQbqu+Q==", + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.14.0.tgz", + "integrity": "sha512-CO40UI41xDQzhLB1hWyqUKgFhs250pNcGbyGKe1l/e4FSaI/+YE4IMG76GDt0In67WLPACIITC+sOi08x4wIvg==", "dev": true, "license": "MIT", "bin": { @@ -706,9 +969,9 @@ "license": "MIT" }, "node_modules/escalade": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", - "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, "license": "MIT", "engines": { @@ -804,9 +1067,9 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.1.tgz", - "integrity": "sha512-MWipKbbYiYI0UC7cl8m/i/IWTqfC8YXsqjzybjddLsFjStroQzsHXkc73JutMvBiXmOvapk+axIl79ig5t55Bw==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.2.tgz", + "integrity": "sha512-GR6f0hD7XXyNJa25Tb9BuIdN0tdr+0BMi6/CJPH3wJO1JjNG3n/VsSw38AwRdKZABm8lGbPfakLRkYzx2V9row==", "dev": true, "license": "MIT" }, @@ -866,21 +1129,6 @@ "node": ">=0.10.3" } }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -891,19 +1139,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/glob-to-regexp": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", @@ -991,23 +1226,10 @@ "node": ">=10.13.0" } }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "license": "MIT", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/is-core-module": { - "version": "2.15.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.0.tgz", - "integrity": "sha512-Dd+Lb2/zvk9SKy1TGCt1wFJFo/MWBPMX5x7KcvLajWTGuomczdQX61PvY5yK6SVACwpoexWo81IfFyoKY2QnTA==", + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", + "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1158,6 +1380,20 @@ "dev": true, "license": "MIT" }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -1228,6 +1464,13 @@ "dev": true, "license": "MIT" }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, + "license": "MIT" + }, "node_modules/node-releases": { "version": "2.0.18", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", @@ -1235,16 +1478,6 @@ "dev": true, "license": "MIT" }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/p-limit": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", @@ -1312,9 +1545,9 @@ "license": "MIT" }, "node_modules/picocolors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", - "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", + "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", "dev": true, "license": "ISC" }, @@ -1356,9 +1589,9 @@ } }, "node_modules/postcss": { - "version": "8.4.41", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.41.tgz", - "integrity": "sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==", + "version": "8.4.47", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", + "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", "dev": true, "funding": [ { @@ -1377,8 +1610,8 @@ "license": "MIT", "dependencies": { "nanoid": "^3.3.7", - "picocolors": "^1.0.1", - "source-map-js": "^1.2.0" + "picocolors": "^1.1.0", + "source-map-js": "^1.2.1" }, "engines": { "node": "^10 || ^12 || >=14" @@ -1448,9 +1681,9 @@ } }, "node_modules/postcss-selector-parser": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.1.tgz", - "integrity": "sha512-b4dlw/9V8A71rLIDsSwVmak9z2DuBUB7CA1/wSdelNEzqsjoSPeADTWNO09lpH49Diy3/JIZ2bSPB1dI3LJCHg==", + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", "dev": true, "license": "MIT", "dependencies": { @@ -1489,16 +1722,17 @@ } }, "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz", + "integrity": "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==", "dev": true, "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, "engines": { - "node": ">=8.10.0" + "node": ">= 14.16.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" } }, "node_modules/rechoir": { @@ -1587,13 +1821,14 @@ "license": "MIT" }, "node_modules/sass": { - "version": "1.77.8", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.77.8.tgz", - "integrity": "sha512-4UHg6prsrycW20fqLGPShtEvo/WyHRVRHwOP4DzkUrObWoWI05QBSfzU71TVB7PFaL104TwNaHpjlWXAZbQiNQ==", + "version": "1.79.5", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.79.5.tgz", + "integrity": "sha512-W1h5kp6bdhqFh2tk3DsI771MoEJjvrSY/2ihJRJS4pjIyfJCw0nTsxqhnrUzaLMOJjFchj8rOvraI/YUVjtx5g==", "dev": true, "license": "MIT", "dependencies": { - "chokidar": ">=3.0.0 <4.0.0", + "@parcel/watcher": "^2.4.1", + "chokidar": "^4.0.0", "immutable": "^4.0.0", "source-map-js": ">=0.6.2 <2.0.0" }, @@ -1605,9 +1840,9 @@ } }, "node_modules/sass-loader": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.1.tgz", - "integrity": "sha512-xACl1ToTsKnL9Ce5yYpRxrLj9QUDCnwZNhzpC7tKiFyA8zXsd3Ap+HGVnbCgkdQcm43E+i6oKAWBsvGA6ZoiMw==", + "version": "16.0.2", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.2.tgz", + "integrity": "sha512-Ll6iXZ1EYwYT19SqW4mSBb76vSSi8JgzElmzIerhEGgzB5hRjDQIWsPmuk1UrAXkR16KJHqVY0eH+5/uw9Tmfw==", "dev": true, "license": "MIT", "dependencies": { @@ -1735,9 +1970,9 @@ } }, "node_modules/source-map-js": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", - "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -1795,9 +2030,9 @@ } }, "node_modules/terser": { - "version": "5.31.5", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.31.5.tgz", - "integrity": "sha512-YPmas0L0rE1UyLL/llTWA0SiDOqIcAQYLeUj7cJYzXHlRTAnMSg9pPe4VJ5PlKvTrPQsdVFuiRiwyeNlYgwh2Q==", + "version": "5.34.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.34.1.tgz", + "integrity": "sha512-FsJZ7iZLd/BXkz+4xrRTGJ26o/6VTjQytUk8b8OxkwcD2I+79VPJlz7qss1+zE7h8GNIScFqXcDyJ/KqBYZFVA==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -1915,16 +2150,16 @@ } }, "node_modules/undici-types": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.13.0.tgz", - "integrity": "sha512-xtFJHudx8S2DSoujjMd1WeWvn7KKWFRESZTMeL1RptAYERu29D6jphMjjY+vn96jvN3kVPDNxU/E13VTaXj6jg==", + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", "dev": true, "license": "MIT" }, "node_modules/update-browserslist-db": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", - "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", + "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", "dev": true, "funding": [ { @@ -1942,8 +2177,8 @@ ], "license": "MIT", "dependencies": { - "escalade": "^3.1.2", - "picocolors": "^1.0.1" + "escalade": "^3.2.0", + "picocolors": "^1.1.0" }, "bin": { "update-browserslist-db": "cli.js" @@ -1970,9 +2205,9 @@ "license": "MIT" }, "node_modules/watchpack": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.1.tgz", - "integrity": "sha512-8wrBCMtVhqcXP2Sup1ctSkga6uc2Bx0IIvKyT7yTFier5AXHooSI+QyQQAtTb7+E0IUCCKyTFmXqdqgum2XWGg==", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", + "integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==", "dev": true, "license": "MIT", "dependencies": { @@ -1984,9 +2219,9 @@ } }, "node_modules/webpack": { - "version": "5.94.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.94.0.tgz", - "integrity": "sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg==", + "version": "5.95.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.95.0.tgz", + "integrity": "sha512-2t3XstrKULz41MNMBF+cJ97TyHdyQ8HCt//pqErqDvNjU9YQBnZxIHa11VXsi7F3mb5/aO2tuDxdeTPdU7xu9Q==", "dev": true, "license": "MIT", "dependencies": { diff --git a/bitwarden_license/src/Sso/package.json b/bitwarden_license/src/Sso/package.json index 91dc513e1..fa9d37c1b 100644 --- a/bitwarden_license/src/Sso/package.json +++ b/bitwarden_license/src/Sso/package.json @@ -8,7 +8,7 @@ "build": "webpack" }, "dependencies": { - "bootstrap": "5.3.3", + "bootstrap": "4.6.2", "font-awesome": "4.7.0", "jquery": "3.7.1", "popper.js": "1.16.1" @@ -17,9 +17,9 @@ "css-loader": "7.1.2", "expose-loader": "5.0.0", "mini-css-extract-plugin": "2.9.1", - "sass": "1.77.8", - "sass-loader": "16.0.1", - "webpack": "5.94.0", + "sass": "1.79.5", + "sass-loader": "16.0.2", + "webpack": "5.95.0", "webpack-cli": "5.1.4" } } 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/AdminConsole/ProviderFeatures/RemoveOrganizationFromProviderCommandTests.cs b/bitwarden_license/test/Commercial.Core.Test/AdminConsole/ProviderFeatures/RemoveOrganizationFromProviderCommandTests.cs index 064dae26d..e984259e9 100644 --- a/bitwarden_license/test/Commercial.Core.Test/AdminConsole/ProviderFeatures/RemoveOrganizationFromProviderCommandTests.cs +++ b/bitwarden_license/test/Commercial.Core.Test/AdminConsole/ProviderFeatures/RemoveOrganizationFromProviderCommandTests.cs @@ -3,6 +3,7 @@ using Bit.Core; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Enums.Provider; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Billing.Constants; using Bit.Core.Billing.Enums; @@ -75,7 +76,7 @@ public class RemoveOrganizationFromProviderCommandTests { providerOrganization.ProviderId = provider.Id; - sutProvider.GetDependency().HasConfirmedOwnersExceptAsync( + sutProvider.GetDependency().HasConfirmedOwnersExceptAsync( providerOrganization.OrganizationId, [], includeProvider: false) @@ -98,7 +99,7 @@ public class RemoveOrganizationFromProviderCommandTests organization.GatewayCustomerId = null; organization.GatewaySubscriptionId = null; - sutProvider.GetDependency().HasConfirmedOwnersExceptAsync( + sutProvider.GetDependency().HasConfirmedOwnersExceptAsync( providerOrganization.OrganizationId, [], includeProvider: false) @@ -141,7 +142,7 @@ public class RemoveOrganizationFromProviderCommandTests { providerOrganization.ProviderId = provider.Id; - sutProvider.GetDependency().HasConfirmedOwnersExceptAsync( + sutProvider.GetDependency().HasConfirmedOwnersExceptAsync( providerOrganization.OrganizationId, [], includeProvider: false) @@ -208,7 +209,7 @@ public class RemoveOrganizationFromProviderCommandTests var teamsMonthlyPlan = StaticStore.GetPlan(PlanType.TeamsMonthly); - sutProvider.GetDependency().HasConfirmedOwnersExceptAsync( + sutProvider.GetDependency().HasConfirmedOwnersExceptAsync( providerOrganization.OrganizationId, [], includeProvider: false) diff --git a/bitwarden_license/test/Scim.IntegrationTest/Controllers/v2/UsersControllerTests.cs b/bitwarden_license/test/Scim.IntegrationTest/Controllers/v2/UsersControllerTests.cs index c0e4f3eb7..1c9b0c112 100644 --- a/bitwarden_license/test/Scim.IntegrationTest/Controllers/v2/UsersControllerTests.cs +++ b/bitwarden_license/test/Scim.IntegrationTest/Controllers/v2/UsersControllerTests.cs @@ -236,6 +236,46 @@ public class UsersControllerTests : IClassFixture, IAsyn AssertHelper.AssertPropertyEqual(expectedResponse, responseModel); } + [Fact] + public async Task GetList_SearchUserNameWithoutOptionalParameters_Success() + { + string filter = "userName eq user2@example.com"; + int? itemsPerPage = null; + int? startIndex = null; + var expectedResponse = new ScimListResponseModel + { + ItemsPerPage = 50, //default value + TotalResults = 1, + StartIndex = 1, //default value + Resources = new List + { + new ScimUserResponseModel + { + Id = ScimApplicationFactory.TestOrganizationUserId2, + DisplayName = "Test User 2", + ExternalId = "UB", + Active = true, + Emails = new List + { + new BaseScimUserModel.EmailModel { Primary = true, Type = "work", Value = "user2@example.com" } + }, + Groups = new List(), + Name = new BaseScimUserModel.NameModel("Test User 2"), + UserName = "user2@example.com", + Schemas = new List { ScimConstants.Scim2SchemaUser } + } + }, + Schemas = new List { ScimConstants.Scim2SchemaListResponse } + }; + + var context = await _factory.UsersGetListAsync(ScimApplicationFactory.TestOrganizationId1, filter, itemsPerPage, startIndex); + + Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode); + + var responseModel = JsonSerializer.Deserialize>(context.Response.Body, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); + AssertHelper.AssertPropertyEqual(expectedResponse, responseModel); + } + [Fact] public async Task Post_Success() { 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/bitwarden_license/test/Scim.Test/Users/GetUsersListQueryTests.cs b/bitwarden_license/test/Scim.Test/Users/GetUsersListQueryTests.cs index b7497e281..9352e5c20 100644 --- a/bitwarden_license/test/Scim.Test/Users/GetUsersListQueryTests.cs +++ b/bitwarden_license/test/Scim.Test/Users/GetUsersListQueryTests.cs @@ -24,7 +24,7 @@ public class GetUsersListQueryTests .GetManyDetailsByOrganizationAsync(organizationId) .Returns(organizationUserUserDetails); - var result = await sutProvider.Sut.GetUsersListAsync(organizationId, null, count, startIndex); + var result = await sutProvider.Sut.GetUsersListAsync(organizationId, new GetUsersQueryParamModel { Count = count, StartIndex = startIndex }); await sutProvider.GetDependency().Received(1).GetManyDetailsByOrganizationAsync(organizationId); @@ -49,7 +49,7 @@ public class GetUsersListQueryTests .GetManyDetailsByOrganizationAsync(organizationId) .Returns(organizationUserUserDetails); - var result = await sutProvider.Sut.GetUsersListAsync(organizationId, filter, null, null); + var result = await sutProvider.Sut.GetUsersListAsync(organizationId, new GetUsersQueryParamModel { Filter = filter }); await sutProvider.GetDependency().Received(1).GetManyDetailsByOrganizationAsync(organizationId); @@ -71,7 +71,7 @@ public class GetUsersListQueryTests .GetManyDetailsByOrganizationAsync(organizationId) .Returns(organizationUserUserDetails); - var result = await sutProvider.Sut.GetUsersListAsync(organizationId, filter, null, null); + var result = await sutProvider.Sut.GetUsersListAsync(organizationId, new GetUsersQueryParamModel { Filter = filter }); await sutProvider.GetDependency().Received(1).GetManyDetailsByOrganizationAsync(organizationId); @@ -96,7 +96,7 @@ public class GetUsersListQueryTests .GetManyDetailsByOrganizationAsync(organizationId) .Returns(organizationUserUserDetails); - var result = await sutProvider.Sut.GetUsersListAsync(organizationId, filter, null, null); + var result = await sutProvider.Sut.GetUsersListAsync(organizationId, new GetUsersQueryParamModel { Filter = filter }); await sutProvider.GetDependency().Received(1).GetManyDetailsByOrganizationAsync(organizationId); @@ -120,7 +120,7 @@ public class GetUsersListQueryTests .GetManyDetailsByOrganizationAsync(organizationId) .Returns(organizationUserUserDetails); - var result = await sutProvider.Sut.GetUsersListAsync(organizationId, filter, null, null); + var result = await sutProvider.Sut.GetUsersListAsync(organizationId, new GetUsersQueryParamModel { Filter = filter }); await sutProvider.GetDependency().Received(1).GetManyDetailsByOrganizationAsync(organizationId); diff --git a/bitwarden_license/test/Scim.Test/Users/PatchUserCommandTests.cs b/bitwarden_license/test/Scim.Test/Users/PatchUserCommandTests.cs index 977011b35..6e9c985b8 100644 --- a/bitwarden_license/test/Scim.Test/Users/PatchUserCommandTests.cs +++ b/bitwarden_license/test/Scim.Test/Users/PatchUserCommandTests.cs @@ -43,7 +43,7 @@ public class PatchUserCommandTests await sutProvider.Sut.PatchUserAsync(organizationUser.OrganizationId, organizationUser.Id, scimPatchModel); - await sutProvider.GetDependency().Received(1).RestoreUserAsync(organizationUser, EventSystemUser.SCIM, Arg.Any()); + await sutProvider.GetDependency().Received(1).RestoreUserAsync(organizationUser, EventSystemUser.SCIM); } [Theory] @@ -71,7 +71,7 @@ public class PatchUserCommandTests await sutProvider.Sut.PatchUserAsync(organizationUser.OrganizationId, organizationUser.Id, scimPatchModel); - await sutProvider.GetDependency().Received(1).RestoreUserAsync(organizationUser, EventSystemUser.SCIM, Arg.Any()); + await sutProvider.GetDependency().Received(1).RestoreUserAsync(organizationUser, EventSystemUser.SCIM); } [Theory] @@ -147,7 +147,7 @@ public class PatchUserCommandTests await sutProvider.Sut.PatchUserAsync(organizationUser.OrganizationId, organizationUser.Id, scimPatchModel); - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().RestoreUserAsync(default, EventSystemUser.SCIM, default); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().RestoreUserAsync(default, EventSystemUser.SCIM); await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().RevokeUserAsync(default, EventSystemUser.SCIM); } diff --git a/dev/.gitignore b/dev/.gitignore index 6134bc261..39b657f45 100644 --- a/dev/.gitignore +++ b/dev/.gitignore @@ -5,7 +5,6 @@ secrets.json # Docker container configurations .env authsources.php -directory.ldif # Development certificates identity_server_dev.crt diff --git a/dev/docker-compose.yml b/dev/docker-compose.yml index fd316f8ea..c02d3c872 100644 --- a/dev/docker-compose.yml +++ b/dev/docker-compose.yml @@ -59,7 +59,7 @@ services: container_name: bw-mysql ports: - "3306:3306" - command: + command: - --default-authentication-plugin=mysql_native_password - --innodb-print-all-deadlocks=ON environment: @@ -84,20 +84,6 @@ services: profiles: - idp - open-ldap: - image: osixia/openldap:1.5.0 - command: --copy-service - environment: - LDAP_ORGANISATION: "Bitwarden" - LDAP_DOMAIN: "bitwarden.com" - volumes: - - ./directory.ldif:/container/service/slapd/assets/config/bootstrap/ldif/output.ldif - ports: - - "389:389" - - "636:636" - profiles: - - ldap - reverse-proxy: image: nginx:alpine container_name: reverse-proxy diff --git a/src/Admin/AdminConsole/Controllers/OrganizationsController.cs b/src/Admin/AdminConsole/Controllers/OrganizationsController.cs index 4dc7ec56d..efab8620c 100644 --- a/src/Admin/AdminConsole/Controllers/OrganizationsController.cs +++ b/src/Admin/AdminConsole/Controllers/OrganizationsController.cs @@ -7,12 +7,10 @@ using Bit.Core; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Providers.Interfaces; using Bit.Core.AdminConsole.Repositories; -using Bit.Core.AdminConsole.Services; using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Services; using Bit.Core.Context; using Bit.Core.Enums; -using Bit.Core.Exceptions; using Bit.Core.Models.OrganizationConnectionConfigs; using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces; using Bit.Core.Repositories; @@ -57,7 +55,6 @@ public class OrganizationsController : Controller private readonly IRemoveOrganizationFromProviderCommand _removeOrganizationFromProviderCommand; private readonly IFeatureService _featureService; private readonly IProviderBillingService _providerBillingService; - private readonly IPolicyService _policyService; public OrganizationsController( IOrganizationService organizationService, @@ -84,8 +81,7 @@ public class OrganizationsController : Controller IProviderOrganizationRepository providerOrganizationRepository, IRemoveOrganizationFromProviderCommand removeOrganizationFromProviderCommand, IFeatureService featureService, - IProviderBillingService providerBillingService, - IPolicyService policyService) + IProviderBillingService providerBillingService) { _organizationService = organizationService; _organizationRepository = organizationRepository; @@ -112,7 +108,6 @@ public class OrganizationsController : Controller _removeOrganizationFromProviderCommand = removeOrganizationFromProviderCommand; _featureService = featureService; _providerBillingService = providerBillingService; - _policyService = policyService; } [RequirePermission(Permission.Org_List_View)] @@ -240,7 +235,8 @@ public class OrganizationsController : Controller if (organization.UseSecretsManager && !StaticStore.GetPlan(organization.PlanType).SupportsSecretsManager) { - throw new BadRequestException("Plan does not support Secrets Manager"); + TempData["Error"] = "Plan does not support Secrets Manager"; + return RedirectToAction("Edit", new { id }); } await _organizationRepository.ReplaceAsync(organization); @@ -440,13 +436,6 @@ public class OrganizationsController : Controller organization.MaxAutoscaleSmServiceAccounts = model.MaxAutoscaleSmServiceAccounts; } - var plan = StaticStore.GetPlan(organization.PlanType); - - if (!organization.UsePolicies || !plan.HasPolicies) - { - await DisableOrganizationPoliciesAsync(organization.Id); - } - if (_accessControlService.UserHasPermission(Permission.Org_Licensing_Edit)) { organization.LicenseKey = model.LicenseKey; @@ -463,18 +452,4 @@ public class OrganizationsController : Controller return organization; } - - private async Task DisableOrganizationPoliciesAsync(Guid organizationId) - { - var policies = await _policyRepository.GetManyByOrganizationIdAsync(organizationId); - - if (policies.Count != 0) - { - await Task.WhenAll(policies.Select(async policy => - { - policy.Enabled = false; - await _policyService.SaveAsync(policy, _userService, _organizationService, null); - })); - } - } } diff --git a/src/Admin/AdminConsole/Controllers/ProvidersController.cs b/src/Admin/AdminConsole/Controllers/ProvidersController.cs index 4adf0fce0..a7c49b214 100644 --- a/src/Admin/AdminConsole/Controllers/ProvidersController.cs +++ b/src/Admin/AdminConsole/Controllers/ProvidersController.cs @@ -107,9 +107,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 +123,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 +174,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 }); } @@ -367,7 +445,7 @@ public class ProvidersController : Controller return BadRequest("Provider does not exist"); } - if (!string.Equals(providerName.Trim(), provider.Name, StringComparison.OrdinalIgnoreCase)) + if (!string.Equals(providerName.Trim(), provider.DisplayName(), StringComparison.OrdinalIgnoreCase)) { return BadRequest("Invalid provider name"); } 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/OrganizationEditModel.cs b/src/Admin/AdminConsole/Models/OrganizationEditModel.cs index 04079138d..4ba22130f 100644 --- a/src/Admin/AdminConsole/Models/OrganizationEditModel.cs +++ b/src/Admin/AdminConsole/Models/OrganizationEditModel.cs @@ -181,7 +181,6 @@ public class OrganizationEditModel : OrganizationViewModel */ public object GetPlansHelper() => StaticStore.Plans - .Where(p => p.SupportsSecretsManager) .Select(p => { var plan = new diff --git a/src/Admin/AdminConsole/Views/Organizations/_ViewInformation.cshtml b/src/Admin/AdminConsole/Views/Organizations/_ViewInformation.cshtml index db2e2c601..f3853e16a 100644 --- a/src/Admin/AdminConsole/Views/Organizations/_ViewInformation.cshtml +++ b/src/Admin/AdminConsole/Views/Organizations/_ViewInformation.cshtml @@ -1,4 +1,6 @@ -@model OrganizationViewModel +@inject Bit.Core.Services.IFeatureService FeatureService +@model OrganizationViewModel +
Id
@Model.Organization.Id
@@ -53,8 +55,19 @@
Administrators manage all collections
@(Model.Organization.AllowAdminAccessToAllCollectionItems ? "On" : "Off")
-
Limit collection creation to administrators
-
@(Model.Organization.LimitCollectionCreationDeletion ? "On" : "Off")
+ @if (!FeatureService.IsEnabled(Bit.Core.FeatureFlagKeys.LimitCollectionCreationDeletionSplit)) + { +
Limit collection creation to administrators
+
@(Model.Organization.LimitCollectionCreationDeletion ? "On" : "Off")
+ } + else + { +
Limit collection creation to administrators
+
@(Model.Organization.LimitCollectionCreation ? "On" : "Off")
+ +
Limit collection deletion to administrators
+
@(Model.Organization.LimitCollectionDeletion ? "On" : "Off")
+ }

Secrets Manager

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 f4cf36f92..37cda8417 100644 --- a/src/Admin/AdminConsole/Views/Providers/Edit.cshtml +++ b/src/Admin/AdminConsole/Views/Providers/Edit.cshtml @@ -174,18 +174,15 @@
- @if (FeatureService.IsEnabled(FeatureFlagKeys.EnableDeleteProvider)) - { -
- - +
+ + - - + + - + -
- } +
} diff --git a/src/Admin/AdminConsole/Views/Providers/_ProviderScripts.cshtml b/src/Admin/AdminConsole/Views/Providers/_ProviderScripts.cshtml index 4fa1ed757..68af34ebd 100644 --- a/src/Admin/AdminConsole/Views/Providers/_ProviderScripts.cshtml +++ b/src/Admin/AdminConsole/Views/Providers/_ProviderScripts.cshtml @@ -20,9 +20,10 @@ function deleteProvider(id) { const providerName = $('#DeleteModal input#provider-name').val(); + const encodedProviderName = encodeURIComponent(providerName); $.ajax({ type: "POST", - url: `@Url.Action("Delete", "Providers")?id=${id}&providerName=${providerName}`, + url: `@Url.Action("Delete", "Providers")?id=${id}&providerName=${encodedProviderName}`, dataType: 'json', contentType: false, processData: false, diff --git a/src/Admin/Billing/Controllers/MigrateProvidersController.cs b/src/Admin/Billing/Controllers/MigrateProvidersController.cs new file mode 100644 index 000000000..d4ef105e3 --- /dev/null +++ b/src/Admin/Billing/Controllers/MigrateProvidersController.cs @@ -0,0 +1,83 @@ +using Bit.Admin.Billing.Models; +using Bit.Admin.Enums; +using Bit.Admin.Utilities; +using Bit.Core.Billing.Migration.Models; +using Bit.Core.Billing.Migration.Services; +using Bit.Core.Utilities; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Bit.Admin.Billing.Controllers; + +[Authorize] +[Route("migrate-providers")] +[SelfHosted(NotSelfHostedOnly = true)] +public class MigrateProvidersController( + IProviderMigrator providerMigrator) : Controller +{ + [HttpGet] + [RequirePermission(Permission.Tools_MigrateProviders)] + public IActionResult Index() + { + return View(new MigrateProvidersRequestModel()); + } + + [HttpPost] + [RequirePermission(Permission.Tools_MigrateProviders)] + [ValidateAntiForgeryToken] + public async Task PostAsync(MigrateProvidersRequestModel request) + { + var providerIds = GetProviderIdsFromInput(request.ProviderIds); + + if (providerIds.Count == 0) + { + return RedirectToAction("Index"); + } + + foreach (var providerId in providerIds) + { + await providerMigrator.Migrate(providerId); + } + + return RedirectToAction("Results", new { ProviderIds = string.Join("\r\n", providerIds) }); + } + + [HttpGet("results")] + [RequirePermission(Permission.Tools_MigrateProviders)] + public async Task ResultsAsync(MigrateProvidersRequestModel request) + { + var providerIds = GetProviderIdsFromInput(request.ProviderIds); + + if (providerIds.Count == 0) + { + return View(Array.Empty()); + } + + var results = await Task.WhenAll(providerIds.Select(providerMigrator.GetResult)); + + return View(results); + } + + [HttpGet("results/{providerId:guid}")] + [RequirePermission(Permission.Tools_MigrateProviders)] + public async Task DetailsAsync([FromRoute] Guid providerId) + { + var result = await providerMigrator.GetResult(providerId); + + if (result == null) + { + return RedirectToAction("Index"); + } + + return View(result); + } + + private static List GetProviderIdsFromInput(string text) => !string.IsNullOrEmpty(text) + ? text.Split( + ["\r\n", "\r", "\n"], + StringSplitOptions.TrimEntries + ) + .Select(id => new Guid(id)) + .ToList() + : []; +} diff --git a/src/Admin/Billing/Models/MigrateProvidersRequestModel.cs b/src/Admin/Billing/Models/MigrateProvidersRequestModel.cs new file mode 100644 index 000000000..fe1d88e22 --- /dev/null +++ b/src/Admin/Billing/Models/MigrateProvidersRequestModel.cs @@ -0,0 +1,10 @@ +using System.ComponentModel.DataAnnotations; + +namespace Bit.Admin.Billing.Models; + +public class MigrateProvidersRequestModel +{ + [Required] + [Display(Name = "Provider IDs")] + public string ProviderIds { get; set; } +} diff --git a/src/Admin/Billing/Views/MigrateProviders/Details.cshtml b/src/Admin/Billing/Views/MigrateProviders/Details.cshtml new file mode 100644 index 000000000..303e6d2e4 --- /dev/null +++ b/src/Admin/Billing/Views/MigrateProviders/Details.cshtml @@ -0,0 +1,39 @@ +@using System.Text.Json +@model Bit.Core.Billing.Migration.Models.ProviderMigrationResult +@{ + ViewData["Title"] = "Results"; +} + +

Migrate Providers

+

Migration Details: @Model.ProviderName

+
+
Id
+
@Model.ProviderId
+ +
Result
+
@Model.Result
+
+

Client Organizations

+
+ + + + + + + + + + + @foreach (var clientResult in Model.Clients) + { + + + + + + + } + +
IDNameResultPrevious State
@clientResult.OrganizationId@clientResult.OrganizationName@clientResult.Result
@Html.Raw(JsonSerializer.Serialize(clientResult.PreviousState))
+
diff --git a/src/Admin/Billing/Views/MigrateProviders/Index.cshtml b/src/Admin/Billing/Views/MigrateProviders/Index.cshtml new file mode 100644 index 000000000..f76996fe7 --- /dev/null +++ b/src/Admin/Billing/Views/MigrateProviders/Index.cshtml @@ -0,0 +1,46 @@ +@model Bit.Admin.Billing.Models.MigrateProvidersRequestModel; +@{ + ViewData["Title"] = "Migrate Providers"; +} + +

Migrate Providers

+

Bulk Consolidated Billing Migration Tool

+
+

+ This tool allows you to provide a list of IDs for Providers that you would like to migrate to Consolidated Billing. + Because of the expensive nature of the operation, you can only migrate 10 Providers at a time. +

+

+ Updates made through this tool are irreversible without manual intervention. +

+

Example Input (Please enter each Provider ID separated by a new line):

+
+
+
f513affc-2290-4336-879e-21ec3ecf3e78
+f7a5cb0d-4b74-445c-8d8c-232d1d32bbe2
+bf82d3cf-0e21-4f39-b81b-ef52b2fc6a3a
+174e82fc-70c3-448d-9fe7-00bad2a3ab00
+22a4bbbf-58e3-4e4c-a86a-a0d7caf4ff14
+
+
+
+
+
+ + +
+
+ +
+
+
+
+
+ + +
+
+ +
+
+
diff --git a/src/Admin/Billing/Views/MigrateProviders/Results.cshtml b/src/Admin/Billing/Views/MigrateProviders/Results.cshtml new file mode 100644 index 000000000..45611de80 --- /dev/null +++ b/src/Admin/Billing/Views/MigrateProviders/Results.cshtml @@ -0,0 +1,28 @@ +@model Bit.Core.Billing.Migration.Models.ProviderMigrationResult[] +@{ + ViewData["Title"] = "Results"; +} + +

Migrate Providers

+

Results

+
+ + + + + + + + + + @foreach (var result in Model) + { + + + + + + } + +
IDNameResult
@result.ProviderId@result.ProviderName@result.Result
+
diff --git a/src/Admin/Controllers/UsersController.cs b/src/Admin/Controllers/UsersController.cs index 842abaea6..54e43d8b4 100644 --- a/src/Admin/Controllers/UsersController.cs +++ b/src/Admin/Controllers/UsersController.cs @@ -25,8 +25,8 @@ public class UsersController : Controller private readonly GlobalSettings _globalSettings; private readonly IAccessControlService _accessControlService; private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery; - private readonly IFeatureService _featureService; private readonly IUserService _userService; + private readonly IFeatureService _featureService; public UsersController( IUserRepository userRepository, @@ -35,8 +35,8 @@ public class UsersController : Controller GlobalSettings globalSettings, IAccessControlService accessControlService, ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery, - IFeatureService featureService, - IUserService userService) + IUserService userService, + IFeatureService featureService) { _userRepository = userRepository; _cipherRepository = cipherRepository; @@ -44,8 +44,8 @@ public class UsersController : Controller _globalSettings = globalSettings; _accessControlService = accessControlService; _twoFactorIsEnabledQuery = twoFactorIsEnabledQuery; - _featureService = featureService; _userService = userService; + _featureService = featureService; } [RequirePermission(Permission.User_List_View)] @@ -64,22 +64,8 @@ public class UsersController : Controller var skip = (page - 1) * count; var users = await _userRepository.SearchAsync(email, skip, count); - var userModels = new List(); - - if (_featureService.IsEnabled(FeatureFlagKeys.MembersTwoFAQueryOptimization)) - { - var twoFactorAuthLookup = (await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(users.Select(u => u.Id))).ToList(); - - userModels = UserViewModel.MapViewModels(users, twoFactorAuthLookup).ToList(); - } - else - { - foreach (var user in users) - { - var isTwoFactorEnabled = await _userService.TwoFactorIsEnabledAsync(user); - userModels.Add(UserViewModel.MapViewModel(user, isTwoFactorEnabled)); - } - } + var twoFactorAuthLookup = (await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(users.Select(u => u.Id))).ToList(); + var userModels = UserViewModel.MapViewModels(users, twoFactorAuthLookup).ToList(); return View(new UsersModel { @@ -103,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)] @@ -120,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] @@ -174,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/Enums/Permissions.cs b/src/Admin/Enums/Permissions.cs index a8168b9e1..274db11cb 100644 --- a/src/Admin/Enums/Permissions.cs +++ b/src/Admin/Enums/Permissions.cs @@ -48,5 +48,6 @@ public enum Permission Tools_ManageTaxRates, Tools_ManageStripeSubscriptions, Tools_CreateEditTransaction, - Tools_ProcessStripeEvents + Tools_ProcessStripeEvents, + Tools_MigrateProviders } 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/Startup.cs b/src/Admin/Startup.cs index f25e5072d..11f9e7ce6 100644 --- a/src/Admin/Startup.cs +++ b/src/Admin/Startup.cs @@ -10,6 +10,7 @@ using Microsoft.AspNetCore.Mvc.Razor; using Microsoft.Extensions.DependencyInjection.Extensions; using Bit.Admin.Services; using Bit.Core.Billing.Extensions; +using Bit.Core.Billing.Migration; #if !OSS using Bit.Commercial.Core.Utilities; @@ -88,8 +89,10 @@ public class Startup services.AddBaseServices(globalSettings); services.AddDefaultServices(globalSettings); services.AddScoped(); + services.AddDistributedCache(globalSettings); services.AddBillingOperations(); services.AddHttpClient(); + services.AddProviderMigration(); #if OSS services.AddOosServices(); diff --git a/src/Admin/Utilities/RolePermissionMapping.cs b/src/Admin/Utilities/RolePermissionMapping.cs index cb4a0fe47..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, @@ -163,6 +164,7 @@ public static class RolePermissionMapping Permission.Tools_ManageStripeSubscriptions, Permission.Tools_CreateEditTransaction, Permission.Tools_ProcessStripeEvents, + Permission.Tools_MigrateProviders } }, { "sales", new List diff --git a/src/Admin/Views/Shared/_Layout.cshtml b/src/Admin/Views/Shared/_Layout.cshtml index d3bfc6313..485c09b7f 100644 --- a/src/Admin/Views/Shared/_Layout.cshtml +++ b/src/Admin/Views/Shared/_Layout.cshtml @@ -15,6 +15,7 @@ var canManageTaxRates = AccessControlService.UserHasPermission(Permission.Tools_ManageTaxRates); var canManageStripeSubscriptions = AccessControlService.UserHasPermission(Permission.Tools_ManageStripeSubscriptions); var canProcessStripeEvents = AccessControlService.UserHasPermission(Permission.Tools_ProcessStripeEvents); + var canMigrateProviders = AccessControlService.UserHasPermission(Permission.Tools_MigrateProviders); var canViewTools = canChargeBraintree || canCreateTransaction || canPromoteAdmin || canGenerateLicense || canManageTaxRates || canManageStripeSubscriptions; @@ -108,12 +109,18 @@ Manage Stripe Subscriptions } - @if (canProcessStripeEvents) + @if (canProcessStripeEvents) { Process Stripe Events } + @if (canMigrateProviders) + { + + Migrate Providers + + }
} 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/Admin/package-lock.json b/src/Admin/package-lock.json index 716c4d272..e6ed279ed 100644 --- a/src/Admin/package-lock.json +++ b/src/Admin/package-lock.json @@ -9,7 +9,7 @@ "version": "0.0.0", "license": "GPL-3.0", "dependencies": { - "bootstrap": "5.3.3", + "bootstrap": "4.6.2", "font-awesome": "4.7.0", "jquery": "3.7.1", "popper.js": "1.16.1", @@ -19,9 +19,9 @@ "css-loader": "7.1.2", "expose-loader": "5.0.0", "mini-css-extract-plugin": "2.9.1", - "sass": "1.77.8", - "sass-loader": "16.0.1", - "webpack": "5.94.0", + "sass": "1.79.5", + "sass-loader": "16.0.2", + "webpack": "5.95.0", "webpack-cli": "5.1.4" } }, @@ -99,10 +99,296 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@parcel/watcher": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.4.1.tgz", + "integrity": "sha512-HNjmfLQEVRZmHRET336f20H/8kOozUGwk7yajvsonjNxbj2wBTK1WsQuHkD5yYh9RxFGL2EyDHryOihOwUoKDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^1.0.3", + "is-glob": "^4.0.3", + "micromatch": "^4.0.5", + "node-addon-api": "^7.0.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.4.1", + "@parcel/watcher-darwin-arm64": "2.4.1", + "@parcel/watcher-darwin-x64": "2.4.1", + "@parcel/watcher-freebsd-x64": "2.4.1", + "@parcel/watcher-linux-arm-glibc": "2.4.1", + "@parcel/watcher-linux-arm64-glibc": "2.4.1", + "@parcel/watcher-linux-arm64-musl": "2.4.1", + "@parcel/watcher-linux-x64-glibc": "2.4.1", + "@parcel/watcher-linux-x64-musl": "2.4.1", + "@parcel/watcher-win32-arm64": "2.4.1", + "@parcel/watcher-win32-ia32": "2.4.1", + "@parcel/watcher-win32-x64": "2.4.1" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.4.1.tgz", + "integrity": "sha512-LOi/WTbbh3aTn2RYddrO8pnapixAziFl6SMxHM69r3tvdSm94JtCenaKgk1GRg5FJ5wpMCpHeW+7yqPlvZv7kg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.4.1.tgz", + "integrity": "sha512-ln41eihm5YXIY043vBrrHfn94SIBlqOWmoROhsMVTSXGh0QahKGy77tfEywQ7v3NywyxBBkGIfrWRHm0hsKtzA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.4.1.tgz", + "integrity": "sha512-yrw81BRLjjtHyDu7J61oPuSoeYWR3lDElcPGJyOvIXmor6DEo7/G2u1o7I38cwlcoBHQFULqF6nesIX3tsEXMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.4.1.tgz", + "integrity": "sha512-TJa3Pex/gX3CWIx/Co8k+ykNdDCLx+TuZj3f3h7eOjgpdKM+Mnix37RYsYU4LHhiYJz3DK5nFCCra81p6g050w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.4.1.tgz", + "integrity": "sha512-4rVYDlsMEYfa537BRXxJ5UF4ddNwnr2/1O4MHM5PjI9cvV2qymvhwZSFgXqbS8YoTk5i/JR0L0JDs69BUn45YA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.4.1.tgz", + "integrity": "sha512-BJ7mH985OADVLpbrzCLgrJ3TOpiZggE9FMblfO65PlOCdG++xJpKUJ0Aol74ZUIYfb8WsRlUdgrZxKkz3zXWYA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.4.1.tgz", + "integrity": "sha512-p4Xb7JGq3MLgAfYhslU2SjoV9G0kI0Xry0kuxeG/41UfpjHGOhv7UoUDAz/jb1u2elbhazy4rRBL8PegPJFBhA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.4.1.tgz", + "integrity": "sha512-s9O3fByZ/2pyYDPoLM6zt92yu6P4E39a03zvO0qCHOTjxmt3GHRMLuRZEWhWLASTMSrrnVNWdVI/+pUElJBBBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.4.1.tgz", + "integrity": "sha512-L2nZTYR1myLNST0O632g0Dx9LyMNHrn6TOt76sYxWLdff3cB22/GZX2UPtJnaqQPdCRoszoY5rcOj4oMTtp5fQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.4.1.tgz", + "integrity": "sha512-Uq2BPp5GWhrq/lcuItCHoqxjULU1QYEcyjSO5jqqOK8RNFDBQnenMMx4gAl3v8GiWa59E9+uDM7yZ6LxwUIfRg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.4.1.tgz", + "integrity": "sha512-maNRit5QQV2kgHFSYwftmPBxiuK5u4DXjbXx7q6eKjq5dsLXZ4FJiVvlcw35QXzk0KrUecJmuVFbj4uV9oYrcw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.4.1.tgz", + "integrity": "sha512-+DvS92F9ezicfswqrvIRM2njcYJbd5mb9CUgtrHCHmvn7pPPa+nMDRu1o1bYYz/l5IB2NVGNJWiH7h1E58IF2A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/@types/estree": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", "dev": true, "license": "MIT" }, @@ -114,13 +400,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.1.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.1.0.tgz", - "integrity": "sha512-AOmuRF0R2/5j1knA3c6G3HOk523Ga+l+ZXltX8SF1+5oqcXijjfTd8fY3XRZqSihEu9XhtQnKYLmkFaoxgsJHw==", + "version": "22.7.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz", + "integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~6.13.0" + "undici-types": "~6.19.2" } }, "node_modules/@webassemblyjs/ast": { @@ -416,35 +702,8 @@ "ajv": "^8.8.2" } }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/bootstrap": { - "version": "5.3.3", + "version": "4.6.2", "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.6.2.tgz", "integrity": "sha512-51Bbp/Uxr9aTuy6ca/8FbFloBUJZLHwnhTcnjIeRn2suQWsWzcuJhGjKDB5eppVte/8oCdOL3VuwxvZDUggwGQ==", "funding": [ @@ -477,9 +736,9 @@ } }, "node_modules/browserslist": { - "version": "4.23.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz", - "integrity": "sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.0.tgz", + "integrity": "sha512-Rmb62sR1Zpjql25eSanFGEhAxcFwfA1K0GuQcLoaJBAcENegrQut3hYdhXFF1obQfiDyqIW/cLM5HSJ/9k884A==", "dev": true, "funding": [ { @@ -497,8 +756,8 @@ ], "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001646", - "electron-to-chromium": "^1.5.4", + "caniuse-lite": "^1.0.30001663", + "electron-to-chromium": "^1.5.28", "node-releases": "^2.0.18", "update-browserslist-db": "^1.1.0" }, @@ -517,9 +776,9 @@ "license": "MIT" }, "node_modules/caniuse-lite": { - "version": "1.0.30001651", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001651.tgz", - "integrity": "sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==", + "version": "1.0.30001668", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001668.tgz", + "integrity": "sha512-nWLrdxqCdblixUO+27JtGJJE/txpJlyUy5YN1u53wLZkP0emYCo5zgS6QYft7VUYR42LGgi/S5hdLZTrnyIddw==", "dev": true, "funding": [ { @@ -538,28 +797,19 @@ "license": "CC-BY-4.0" }, "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz", + "integrity": "sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==", "dev": true, "license": "MIT", "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" + "readdirp": "^4.0.1" }, "engines": { - "node": ">= 8.10.0" + "node": ">= 14.16.0" }, "funding": { "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" } }, "node_modules/chrome-trace-event": { @@ -665,10 +915,23 @@ "node": ">=4" } }, + "node_modules/detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "detect-libc": "bin/detect-libc.js" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/electron-to-chromium": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.5.tgz", - "integrity": "sha512-QR7/A7ZkMS8tZuoftC/jfqNkZLQO779SSW3YuZHP4eXpj3EffGLFcB/Xu9AAZQzLccTiCV+EmUo3ha4mQ9wnlA==", + "version": "1.5.36", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.36.tgz", + "integrity": "sha512-HYTX8tKge/VNp6FGO+f/uVDmUkq+cEfcxYhKf15Akc4M5yxt5YmorwlAitKWjWhWQnKcDRBAQKXkhqqXMqcrjw==", "dev": true, "license": "ISC" }, @@ -687,9 +950,9 @@ } }, "node_modules/envinfo": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.13.0.tgz", - "integrity": "sha512-cvcaMr7KqXVh4nyzGTVqTum+gAiL265x5jUWQIDLq//zOGbW+gSW/C+OWLleY/rs9Qole6AZLMXPbtIFQbqu+Q==", + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.14.0.tgz", + "integrity": "sha512-CO40UI41xDQzhLB1hWyqUKgFhs250pNcGbyGKe1l/e4FSaI/+YE4IMG76GDt0In67WLPACIITC+sOi08x4wIvg==", "dev": true, "license": "MIT", "bin": { @@ -707,9 +970,9 @@ "license": "MIT" }, "node_modules/escalade": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", - "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, "license": "MIT", "engines": { @@ -805,9 +1068,9 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.1.tgz", - "integrity": "sha512-MWipKbbYiYI0UC7cl8m/i/IWTqfC8YXsqjzybjddLsFjStroQzsHXkc73JutMvBiXmOvapk+axIl79ig5t55Bw==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.2.tgz", + "integrity": "sha512-GR6f0hD7XXyNJa25Tb9BuIdN0tdr+0BMi6/CJPH3wJO1JjNG3n/VsSw38AwRdKZABm8lGbPfakLRkYzx2V9row==", "dev": true, "license": "MIT" }, @@ -867,21 +1130,6 @@ "node": ">=0.10.3" } }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -892,19 +1140,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/glob-to-regexp": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", @@ -992,23 +1227,10 @@ "node": ">=10.13.0" } }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "license": "MIT", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/is-core-module": { - "version": "2.15.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.0.tgz", - "integrity": "sha512-Dd+Lb2/zvk9SKy1TGCt1wFJFo/MWBPMX5x7KcvLajWTGuomczdQX61PvY5yK6SVACwpoexWo81IfFyoKY2QnTA==", + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", + "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1159,6 +1381,20 @@ "dev": true, "license": "MIT" }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -1229,6 +1465,13 @@ "dev": true, "license": "MIT" }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, + "license": "MIT" + }, "node_modules/node-releases": { "version": "2.0.18", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", @@ -1236,16 +1479,6 @@ "dev": true, "license": "MIT" }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/p-limit": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", @@ -1313,9 +1546,9 @@ "license": "MIT" }, "node_modules/picocolors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", - "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", + "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", "dev": true, "license": "ISC" }, @@ -1357,9 +1590,9 @@ } }, "node_modules/postcss": { - "version": "8.4.41", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.41.tgz", - "integrity": "sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==", + "version": "8.4.47", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", + "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", "dev": true, "funding": [ { @@ -1378,8 +1611,8 @@ "license": "MIT", "dependencies": { "nanoid": "^3.3.7", - "picocolors": "^1.0.1", - "source-map-js": "^1.2.0" + "picocolors": "^1.1.0", + "source-map-js": "^1.2.1" }, "engines": { "node": "^10 || ^12 || >=14" @@ -1449,9 +1682,9 @@ } }, "node_modules/postcss-selector-parser": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.1.tgz", - "integrity": "sha512-b4dlw/9V8A71rLIDsSwVmak9z2DuBUB7CA1/wSdelNEzqsjoSPeADTWNO09lpH49Diy3/JIZ2bSPB1dI3LJCHg==", + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", "dev": true, "license": "MIT", "dependencies": { @@ -1490,16 +1723,17 @@ } }, "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz", + "integrity": "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==", "dev": true, "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, "engines": { - "node": ">=8.10.0" + "node": ">= 14.16.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" } }, "node_modules/rechoir": { @@ -1588,13 +1822,14 @@ "license": "MIT" }, "node_modules/sass": { - "version": "1.77.8", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.77.8.tgz", - "integrity": "sha512-4UHg6prsrycW20fqLGPShtEvo/WyHRVRHwOP4DzkUrObWoWI05QBSfzU71TVB7PFaL104TwNaHpjlWXAZbQiNQ==", + "version": "1.79.5", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.79.5.tgz", + "integrity": "sha512-W1h5kp6bdhqFh2tk3DsI771MoEJjvrSY/2ihJRJS4pjIyfJCw0nTsxqhnrUzaLMOJjFchj8rOvraI/YUVjtx5g==", "dev": true, "license": "MIT", "dependencies": { - "chokidar": ">=3.0.0 <4.0.0", + "@parcel/watcher": "^2.4.1", + "chokidar": "^4.0.0", "immutable": "^4.0.0", "source-map-js": ">=0.6.2 <2.0.0" }, @@ -1606,9 +1841,9 @@ } }, "node_modules/sass-loader": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.1.tgz", - "integrity": "sha512-xACl1ToTsKnL9Ce5yYpRxrLj9QUDCnwZNhzpC7tKiFyA8zXsd3Ap+HGVnbCgkdQcm43E+i6oKAWBsvGA6ZoiMw==", + "version": "16.0.2", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.2.tgz", + "integrity": "sha512-Ll6iXZ1EYwYT19SqW4mSBb76vSSi8JgzElmzIerhEGgzB5hRjDQIWsPmuk1UrAXkR16KJHqVY0eH+5/uw9Tmfw==", "dev": true, "license": "MIT", "dependencies": { @@ -1736,9 +1971,9 @@ } }, "node_modules/source-map-js": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", - "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -1796,9 +2031,9 @@ } }, "node_modules/terser": { - "version": "5.31.5", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.31.5.tgz", - "integrity": "sha512-YPmas0L0rE1UyLL/llTWA0SiDOqIcAQYLeUj7cJYzXHlRTAnMSg9pPe4VJ5PlKvTrPQsdVFuiRiwyeNlYgwh2Q==", + "version": "5.34.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.34.1.tgz", + "integrity": "sha512-FsJZ7iZLd/BXkz+4xrRTGJ26o/6VTjQytUk8b8OxkwcD2I+79VPJlz7qss1+zE7h8GNIScFqXcDyJ/KqBYZFVA==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -1924,16 +2159,16 @@ } }, "node_modules/undici-types": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.13.0.tgz", - "integrity": "sha512-xtFJHudx8S2DSoujjMd1WeWvn7KKWFRESZTMeL1RptAYERu29D6jphMjjY+vn96jvN3kVPDNxU/E13VTaXj6jg==", + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", "dev": true, "license": "MIT" }, "node_modules/update-browserslist-db": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", - "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", + "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", "dev": true, "funding": [ { @@ -1951,8 +2186,8 @@ ], "license": "MIT", "dependencies": { - "escalade": "^3.1.2", - "picocolors": "^1.0.1" + "escalade": "^3.2.0", + "picocolors": "^1.1.0" }, "bin": { "update-browserslist-db": "cli.js" @@ -1979,9 +2214,9 @@ "license": "MIT" }, "node_modules/watchpack": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.1.tgz", - "integrity": "sha512-8wrBCMtVhqcXP2Sup1ctSkga6uc2Bx0IIvKyT7yTFier5AXHooSI+QyQQAtTb7+E0IUCCKyTFmXqdqgum2XWGg==", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", + "integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==", "dev": true, "license": "MIT", "dependencies": { @@ -1993,9 +2228,9 @@ } }, "node_modules/webpack": { - "version": "5.94.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.94.0.tgz", - "integrity": "sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg==", + "version": "5.95.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.95.0.tgz", + "integrity": "sha512-2t3XstrKULz41MNMBF+cJ97TyHdyQ8HCt//pqErqDvNjU9YQBnZxIHa11VXsi7F3mb5/aO2tuDxdeTPdU7xu9Q==", "dev": true, "license": "MIT", "dependencies": { diff --git a/src/Admin/package.json b/src/Admin/package.json index 1196b2453..ac78f8935 100644 --- a/src/Admin/package.json +++ b/src/Admin/package.json @@ -8,7 +8,7 @@ "build": "webpack" }, "dependencies": { - "bootstrap": "5.3.3", + "bootstrap": "4.6.2", "font-awesome": "4.7.0", "jquery": "3.7.1", "popper.js": "1.16.1", @@ -18,9 +18,9 @@ "css-loader": "7.1.2", "expose-loader": "5.0.0", "mini-css-extract-plugin": "2.9.1", - "sass": "1.77.8", - "sass-loader": "16.0.1", - "webpack": "5.94.0", + "sass": "1.79.5", + "sass-loader": "16.0.2", + "webpack": "5.95.0", "webpack-cli": "5.1.4" } } diff --git a/src/Api/AdminConsole/Controllers/OrganizationDomainController.cs b/src/Api/AdminConsole/Controllers/OrganizationDomainController.cs index 35c927d5a..b9afde272 100644 --- a/src/Api/AdminConsole/Controllers/OrganizationDomainController.cs +++ b/src/Api/AdminConsole/Controllers/OrganizationDomainController.cs @@ -2,11 +2,13 @@ using Bit.Api.AdminConsole.Models.Request.Organizations; using Bit.Api.AdminConsole.Models.Response.Organizations; using Bit.Api.Models.Response; +using Bit.Core; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces; using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Exceptions; using Bit.Core.Repositories; +using Bit.Core.Utilities; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -99,7 +101,7 @@ public class OrganizationDomainController : Controller throw new NotFoundException(); } - organizationDomain = await _verifyOrganizationDomainCommand.VerifyOrganizationDomainAsync(organizationDomain); + organizationDomain = await _verifyOrganizationDomainCommand.UserVerifyOrganizationDomainAsync(organizationDomain); return new OrganizationDomainResponseModel(organizationDomain); } @@ -133,6 +135,20 @@ public class OrganizationDomainController : Controller return new OrganizationDomainSsoDetailsResponseModel(ssoResult); } + [AllowAnonymous] + [HttpPost("domain/sso/verified")] + [RequireFeature(FeatureFlagKeys.VerifiedSsoDomainEndpoint)] + public async Task GetVerifiedOrgDomainSsoDetailsAsync( + [FromBody] OrganizationDomainSsoDetailsRequestModel model) + { + var ssoResults = (await _organizationDomainRepository + .GetVerifiedOrganizationDomainSsoDetailsAsync(model.Email)) + .ToList(); + + return new VerifiedOrganizationDomainSsoDetailsResponseModel( + ssoResults.Select(ssoResult => new VerifiedOrganizationDomainSsoDetailResponseModel(ssoResult))); + } + private async Task ValidateOrganizationAccessAsync(Guid orgIdGuid) { if (!await _currentContext.ManageSso(orgIdGuid)) diff --git a/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs b/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs index cd6bdd6fa..89a8627e9 100644 --- a/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs +++ b/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs @@ -48,12 +48,13 @@ public class OrganizationUsersController : Controller private readonly IAcceptOrgUserCommand _acceptOrgUserCommand; private readonly IAuthorizationService _authorizationService; private readonly IApplicationCacheService _applicationCacheService; - private readonly IFeatureService _featureService; private readonly ISsoConfigRepository _ssoConfigRepository; private readonly IOrganizationUserUserDetailsQuery _organizationUserUserDetailsQuery; private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery; + private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand; private readonly IDeleteManagedOrganizationUserAccountCommand _deleteManagedOrganizationUserAccountCommand; - + private readonly IGetOrganizationUsersManagementStatusQuery _getOrganizationUsersManagementStatusQuery; + private readonly IFeatureService _featureService; public OrganizationUsersController( IOrganizationRepository organizationRepository, @@ -70,11 +71,13 @@ public class OrganizationUsersController : Controller IAcceptOrgUserCommand acceptOrgUserCommand, IAuthorizationService authorizationService, IApplicationCacheService applicationCacheService, - IFeatureService featureService, ISsoConfigRepository ssoConfigRepository, IOrganizationUserUserDetailsQuery organizationUserUserDetailsQuery, ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery, - IDeleteManagedOrganizationUserAccountCommand deleteManagedOrganizationUserAccountCommand) + IRemoveOrganizationUserCommand removeOrganizationUserCommand, + IDeleteManagedOrganizationUserAccountCommand deleteManagedOrganizationUserAccountCommand, + IGetOrganizationUsersManagementStatusQuery getOrganizationUsersManagementStatusQuery, + IFeatureService featureService) { _organizationRepository = organizationRepository; _organizationUserRepository = organizationUserRepository; @@ -90,34 +93,39 @@ public class OrganizationUsersController : Controller _acceptOrgUserCommand = acceptOrgUserCommand; _authorizationService = authorizationService; _applicationCacheService = applicationCacheService; - _featureService = featureService; _ssoConfigRepository = ssoConfigRepository; _organizationUserUserDetailsQuery = organizationUserUserDetailsQuery; _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), @@ -142,11 +150,6 @@ public class OrganizationUsersController : Controller throw new NotFoundException(); } - if (_featureService.IsEnabled(FeatureFlagKeys.MembersTwoFAQueryOptimization)) - { - return await Get_vNext(orgId, includeGroups, includeCollections); - } - var organizationUsers = await _organizationUserUserDetailsQuery.GetOrganizationUserUserDetails( new OrganizationUserUserDetailsQueryRequest { @@ -155,17 +158,17 @@ public class OrganizationUsersController : Controller IncludeCollections = includeCollections } ); - - var responseTasks = organizationUsers - .Select(async o => + var organizationUsersTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(organizationUsers); + var organizationUsersManagementStatus = await GetManagedByOrganizationStatusAsync(orgId, organizationUsers.Select(o => o.Id)); + var responses = organizationUsers + .Select(o => { - var orgUser = new OrganizationUserUserDetailsResponseModel(o, - await _userService.TwoFactorIsEnabledAsync(o)); + var userTwoFactorEnabled = organizationUsersTwoFactorEnabled.FirstOrDefault(u => u.user.Id == o.Id).twoFactorIsEnabled; + var managedByOrganization = organizationUsersManagementStatus[o.Id]; + var orgUser = new OrganizationUserUserDetailsResponseModel(o, userTwoFactorEnabled, managedByOrganization); return orgUser; }); - var responses = await Task.WhenAll(responseTasks); - return new ListResponseModel(responses); } @@ -296,7 +299,7 @@ public class OrganizationUsersController : Controller await _organizationService.InitPendingOrganization(user.Id, orgId, organizationUserId, model.Keys.PublicKey, model.Keys.EncryptedPrivateKey, model.CollectionName); await _acceptOrgUserCommand.AcceptOrgUserByEmailTokenAsync(organizationUserId, user, model.Token, _userService); - await _organizationService.ConfirmUserAsync(orgId, organizationUserId, model.Key, user.Id, _userService); + await _organizationService.ConfirmUserAsync(orgId, organizationUserId, model.Key, user.Id); } [HttpPost("{organizationUserId}/accept")] @@ -335,8 +338,7 @@ public class OrganizationUsersController : Controller } var userId = _userService.GetProperUserId(User); - var result = await _organizationService.ConfirmUserAsync(orgGuidId, new Guid(id), model.Key, userId.Value, - _userService); + var result = await _organizationService.ConfirmUserAsync(orgGuidId, new Guid(id), model.Key, userId.Value); } [HttpPost("confirm")] @@ -350,10 +352,7 @@ public class OrganizationUsersController : Controller } var userId = _userService.GetProperUserId(User); - var results = _featureService.IsEnabled(FeatureFlagKeys.MembersTwoFAQueryOptimization) - ? await _organizationService.ConfirmUsersAsync_vNext(orgGuidId, model.ToDictionary(), userId.Value) - : await _organizationService.ConfirmUsersAsync(orgGuidId, model.ToDictionary(), userId.Value, - _userService); + var results = await _organizationService.ConfirmUsersAsync(orgGuidId, model.ToDictionary(), userId.Value); return new ListResponseModel(results.Select(r => new OrganizationUserBulkResponseModel(r.Item1.Id, r.Item2))); @@ -517,30 +516,28 @@ public class OrganizationUsersController : Controller [HttpDelete("{id}")] [HttpPost("{id}/remove")] - public async Task Remove(string orgId, string id) + public async Task Remove(Guid orgId, Guid id) { - var orgGuidId = new Guid(orgId); - if (!await _currentContext.ManageUsers(orgGuidId)) + if (!await _currentContext.ManageUsers(orgId)) { throw new NotFoundException(); } var userId = _userService.GetProperUserId(User); - await _organizationService.RemoveUserAsync(orgGuidId, new Guid(id), userId.Value); + await _removeOrganizationUserCommand.RemoveUserAsync(orgId, id, userId.Value); } [HttpDelete("")] [HttpPost("remove")] - public async Task> BulkRemove(string orgId, [FromBody] OrganizationUserBulkRequestModel model) + public async Task> BulkRemove(Guid orgId, [FromBody] OrganizationUserBulkRequestModel model) { - var orgGuidId = new Guid(orgId); - if (!await _currentContext.ManageUsers(orgGuidId)) + if (!await _currentContext.ManageUsers(orgId)) { throw new NotFoundException(); } var userId = _userService.GetProperUserId(User); - var result = await _organizationService.RemoveUsersAsync(orgGuidId, model.Ids, userId.Value); + var result = await _removeOrganizationUserCommand.RemoveUsersAsync(orgId, model.Ids, userId.Value); return new ListResponseModel(result.Select(r => new OrganizationUserBulkResponseModel(r.Item1.Id, r.Item2))); } @@ -616,7 +613,7 @@ public class OrganizationUsersController : Controller [HttpPut("{id}/restore")] public async Task RestoreAsync(Guid orgId, Guid id) { - await RestoreOrRevokeUserAsync(orgId, id, (orgUser, userId) => _organizationService.RestoreUserAsync(orgUser, userId, _userService)); + await RestoreOrRevokeUserAsync(orgId, id, (orgUser, userId) => _organizationService.RestoreUserAsync(orgUser, userId)); } [HttpPatch("restore")] @@ -697,26 +694,14 @@ public class OrganizationUsersController : Controller new OrganizationUserBulkResponseModel(r.Item1.Id, r.Item2))); } - private async Task> Get_vNext(Guid orgId, - bool includeGroups = false, bool includeCollections = false) + private async Task> GetManagedByOrganizationStatusAsync(Guid orgId, IEnumerable userIds) { - var organizationUsers = await _organizationUserUserDetailsQuery.GetOrganizationUserUserDetails( - new OrganizationUserUserDetailsQueryRequest - { - OrganizationId = orgId, - IncludeGroups = includeGroups, - IncludeCollections = includeCollections - } - ); - var organizationUsersTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(organizationUsers); - var responses = organizationUsers - .Select(o => - { - var userTwoFactorEnabled = organizationUsersTwoFactorEnabled.FirstOrDefault(u => u.user.Id == o.Id).twoFactorIsEnabled; - var orgUser = new OrganizationUserUserDetailsResponseModel(o, userTwoFactorEnabled); + if (!_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)) + { + return userIds.ToDictionary(kvp => kvp, kvp => false); + } - return orgUser; - }); - return new ListResponseModel(responses); + var usersOrganizationManagementStatus = await _getOrganizationUsersManagementStatusQuery.GetUsersOrganizationManagementStatusAsync(orgId, userIds); + return usersOrganizationManagementStatus; } } diff --git a/src/Api/AdminConsole/Controllers/OrganizationsController.cs b/src/Api/AdminConsole/Controllers/OrganizationsController.cs index e5dbcd10b..0b3811618 100644 --- a/src/Api/AdminConsole/Controllers/OrganizationsController.cs +++ b/src/Api/AdminConsole/Controllers/OrganizationsController.cs @@ -13,6 +13,7 @@ using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Models.Business.Tokenables; using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationApiKeys.Interfaces; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Repositories; @@ -55,6 +56,7 @@ public class OrganizationsController : Controller private readonly IProviderRepository _providerRepository; private readonly IProviderBillingService _providerBillingService; private readonly IDataProtectorTokenFactory _orgDeleteTokenDataFactory; + private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand; public OrganizationsController( IOrganizationRepository organizationRepository, @@ -74,7 +76,8 @@ public class OrganizationsController : Controller IPushNotificationService pushNotificationService, IProviderRepository providerRepository, IProviderBillingService providerBillingService, - IDataProtectorTokenFactory orgDeleteTokenDataFactory) + IDataProtectorTokenFactory orgDeleteTokenDataFactory, + IRemoveOrganizationUserCommand removeOrganizationUserCommand) { _organizationRepository = organizationRepository; _organizationUserRepository = organizationUserRepository; @@ -94,6 +97,7 @@ public class OrganizationsController : Controller _providerRepository = providerRepository; _providerBillingService = providerBillingService; _orgDeleteTokenDataFactory = orgDeleteTokenDataFactory; + _removeOrganizationUserCommand = removeOrganizationUserCommand; } [HttpGet("{id}")] @@ -120,7 +124,11 @@ public class OrganizationsController : Controller var userId = _userService.GetProperUserId(User).Value; var organizations = await _organizationUserRepository.GetManyDetailsByUserAsync(userId, OrganizationUserStatusType.Confirmed); - var responses = organizations.Select(o => new ProfileOrganizationResponseModel(o)); + + var organizationManagingActiveUser = await _userService.GetOrganizationsManagingUserAsync(userId); + var organizationIdsManagingActiveUser = organizationManagingActiveUser.Select(o => o.Id); + + var responses = organizations.Select(o => new ProfileOrganizationResponseModel(o, organizationIdsManagingActiveUser)); return new ListResponseModel(responses); } @@ -229,24 +237,22 @@ public class OrganizationsController : Controller } [HttpPost("{id}/leave")] - public async Task Leave(string id) + public async Task Leave(Guid id) { - var orgGuidId = new Guid(id); - if (!await _currentContext.OrganizationUser(orgGuidId)) + if (!await _currentContext.OrganizationUser(id)) { throw new NotFoundException(); } var user = await _userService.GetUserByPrincipalAsync(User); - var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(orgGuidId); + var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(id); if (ssoConfig?.GetData()?.MemberDecryptionType == MemberDecryptionType.KeyConnector && user.UsesKeyConnector) { throw new BadRequestException("Your organization's Single Sign-On settings prevent you from leaving."); } - - await _organizationService.RemoveUserAsync(orgGuidId, user.Id); + await _removeOrganizationUserCommand.RemoveUserAsync(id, user.Id); } [HttpDelete("{id}")] @@ -514,9 +520,16 @@ public class OrganizationsController : Controller } [HttpPut("{id}/collection-management")] - [SelfHosted(NotSelfHostedOnly = true)] public async Task PutCollectionManagement(Guid id, [FromBody] OrganizationCollectionManagementUpdateRequestModel model) { + if ( + _globalSettings.SelfHosted && + !_featureService.IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit) + ) + { + throw new BadRequestException("Only allowed when not self hosted."); + } + var organization = await _organizationRepository.GetByIdAsync(id); if (organization == null) { @@ -528,7 +541,7 @@ public class OrganizationsController : Controller throw new NotFoundException(); } - await _organizationService.UpdateAsync(model.ToOrganization(organization), eventType: EventType.Organization_CollectionManagement_Updated); + await _organizationService.UpdateAsync(model.ToOrganization(organization, _featureService), eventType: EventType.Organization_CollectionManagement_Updated); return new OrganizationResponseModel(organization); } } diff --git a/src/Api/AdminConsole/Controllers/PoliciesController.cs b/src/Api/AdminConsole/Controllers/PoliciesController.cs index 5668031d2..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; @@ -25,7 +26,6 @@ public class PoliciesController : Controller { private readonly IPolicyRepository _policyRepository; private readonly IPolicyService _policyService; - private readonly IOrganizationService _organizationService; private readonly IOrganizationUserRepository _organizationUserRepository; private readonly IUserService _userService; private readonly ICurrentContext _currentContext; @@ -36,7 +36,6 @@ public class PoliciesController : Controller public PoliciesController( IPolicyRepository policyRepository, IPolicyService policyService, - IOrganizationService organizationService, IOrganizationUserRepository organizationUserRepository, IUserService userService, ICurrentContext currentContext, @@ -46,7 +45,6 @@ public class PoliciesController : Controller { _policyRepository = policyRepository; _policyService = policyService; - _organizationService = organizationService; _organizationUserRepository = organizationUserRepository; _userService = userService; _currentContext = currentContext; @@ -58,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); @@ -185,7 +182,7 @@ public class PoliciesController : Controller } var userId = _userService.GetProperUserId(User); - await _policyService.SaveAsync(policy, _userService, _organizationService, userId); + await _policyService.SaveAsync(policy, userId); return new PolicyResponseModel(policy); } } diff --git a/src/Api/AdminConsole/Models/Response/Organizations/OrganizationResponseModel.cs b/src/Api/AdminConsole/Models/Response/Organizations/OrganizationResponseModel.cs index 08b4e4b06..7808b564a 100644 --- a/src/Api/AdminConsole/Models/Response/Organizations/OrganizationResponseModel.cs +++ b/src/Api/AdminConsole/Models/Response/Organizations/OrganizationResponseModel.cs @@ -55,6 +55,9 @@ public class OrganizationResponseModel : ResponseModel SmServiceAccounts = organization.SmServiceAccounts; MaxAutoscaleSmSeats = organization.MaxAutoscaleSmSeats; MaxAutoscaleSmServiceAccounts = organization.MaxAutoscaleSmServiceAccounts; + LimitCollectionCreation = organization.LimitCollectionCreation; + LimitCollectionDeletion = organization.LimitCollectionDeletion; + // Deprecated: https://bitwarden.atlassian.net/browse/PM-10863 LimitCollectionCreationDeletion = organization.LimitCollectionCreationDeletion; AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems; } @@ -98,6 +101,9 @@ public class OrganizationResponseModel : ResponseModel public int? SmServiceAccounts { get; set; } public int? MaxAutoscaleSmSeats { get; set; } public int? MaxAutoscaleSmServiceAccounts { get; set; } + public bool LimitCollectionCreation { get; set; } + public bool LimitCollectionDeletion { get; set; } + // Deperectated: https://bitwarden.atlassian.net/browse/PM-10863 public bool LimitCollectionCreationDeletion { get; set; } public bool AllowAdminAccessToAllCollectionItems { 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/Models/Response/Organizations/VerifiedOrganizationDomainSsoDetailResponseModel.cs b/src/Api/AdminConsole/Models/Response/Organizations/VerifiedOrganizationDomainSsoDetailResponseModel.cs new file mode 100644 index 000000000..be4d8865d --- /dev/null +++ b/src/Api/AdminConsole/Models/Response/Organizations/VerifiedOrganizationDomainSsoDetailResponseModel.cs @@ -0,0 +1,23 @@ +using Bit.Core.Models.Api; +using Bit.Core.Models.Data.Organizations; + +namespace Bit.Api.AdminConsole.Models.Response.Organizations; + +public class VerifiedOrganizationDomainSsoDetailResponseModel : ResponseModel +{ + public VerifiedOrganizationDomainSsoDetailResponseModel(VerifiedOrganizationDomainSsoDetail data) + : base("verifiedOrganizationDomainSsoDetails") + { + if (data is null) + { + throw new ArgumentNullException(nameof(data)); + } + + DomainName = data.DomainName; + OrganizationIdentifier = data.OrganizationIdentifier; + OrganizationName = data.OrganizationName; + } + public string DomainName { get; } + public string OrganizationIdentifier { get; } + public string OrganizationName { get; } +} diff --git a/src/Api/AdminConsole/Models/Response/Organizations/VerifiedOrganizationDomainSsoDetailsResponseModel.cs b/src/Api/AdminConsole/Models/Response/Organizations/VerifiedOrganizationDomainSsoDetailsResponseModel.cs new file mode 100644 index 000000000..3488eab2c --- /dev/null +++ b/src/Api/AdminConsole/Models/Response/Organizations/VerifiedOrganizationDomainSsoDetailsResponseModel.cs @@ -0,0 +1,8 @@ +using Bit.Api.Models.Response; + +namespace Bit.Api.AdminConsole.Models.Response.Organizations; + +public class VerifiedOrganizationDomainSsoDetailsResponseModel( + IEnumerable data, + string continuationToken = null) + : ListResponseModel(data, continuationToken); diff --git a/src/Api/AdminConsole/Models/Response/ProfileOrganizationResponseModel.cs b/src/Api/AdminConsole/Models/Response/ProfileOrganizationResponseModel.cs index 17ebfc095..1fcaba5f9 100644 --- a/src/Api/AdminConsole/Models/Response/ProfileOrganizationResponseModel.cs +++ b/src/Api/AdminConsole/Models/Response/ProfileOrganizationResponseModel.cs @@ -15,7 +15,10 @@ public class ProfileOrganizationResponseModel : ResponseModel { public ProfileOrganizationResponseModel(string str) : base(str) { } - public ProfileOrganizationResponseModel(OrganizationUserOrganizationDetails organization) : this("profileOrganization") + public ProfileOrganizationResponseModel( + OrganizationUserOrganizationDetails organization, + IEnumerable organizationIdsManagingUser) + : this("profileOrganization") { Id = organization.OrganizationId; Name = organization.Name; @@ -62,8 +65,12 @@ public class ProfileOrganizationResponseModel : ResponseModel FamilySponsorshipToDelete = organization.FamilySponsorshipToDelete; FamilySponsorshipValidUntil = organization.FamilySponsorshipValidUntil; AccessSecretsManager = organization.AccessSecretsManager; + LimitCollectionCreation = organization.LimitCollectionCreation; + LimitCollectionDeletion = organization.LimitCollectionDeletion; + // Deprecated: https://bitwarden.atlassian.net/browse/PM-10863 LimitCollectionCreationDeletion = organization.LimitCollectionCreationDeletion; AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems; + UserIsManagedByOrganization = organizationIdsManagingUser.Contains(organization.OrganizationId); if (organization.SsoConfig != null) { @@ -120,6 +127,20 @@ public class ProfileOrganizationResponseModel : ResponseModel public DateTime? FamilySponsorshipValidUntil { get; set; } public bool? FamilySponsorshipToDelete { get; set; } public bool AccessSecretsManager { get; set; } + public bool LimitCollectionCreation { get; set; } + public bool LimitCollectionDeletion { get; set; } + // Deprecated: https://bitwarden.atlassian.net/browse/PM-10863 public bool LimitCollectionCreationDeletion { get; set; } public bool AllowAdminAccessToAllCollectionItems { get; set; } + /// + /// Indicates if the organization manages the user. + /// + /// + /// An organization manages a user if the user's email domain is verified by the organization and the user is a member of it. + /// The organization must be enabled and able to have verified domains. + /// + /// + /// False if the Account Deprovisioning feature flag is disabled. + /// + public bool UserIsManagedByOrganization { get; set; } } diff --git a/src/Api/AdminConsole/Models/Response/ProfileProviderOrganizationResponseModel.cs b/src/Api/AdminConsole/Models/Response/ProfileProviderOrganizationResponseModel.cs index 46819f886..92498834d 100644 --- a/src/Api/AdminConsole/Models/Response/ProfileProviderOrganizationResponseModel.cs +++ b/src/Api/AdminConsole/Models/Response/ProfileProviderOrganizationResponseModel.cs @@ -44,6 +44,9 @@ public class ProfileProviderOrganizationResponseModel : ProfileOrganizationRespo ProviderId = organization.ProviderId; ProviderName = organization.ProviderName; ProductTierType = StaticStore.GetPlan(organization.PlanType).ProductTier; + LimitCollectionCreation = organization.LimitCollectionCreation; + LimitCollectionDeletion = organization.LimitCollectionDeletion; + // https://bitwarden.atlassian.net/browse/PM-10863 LimitCollectionCreationDeletion = organization.LimitCollectionCreationDeletion; AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems; } diff --git a/src/Api/AdminConsole/Public/Controllers/MembersController.cs b/src/Api/AdminConsole/Public/Controllers/MembersController.cs index a737f0b49..4e99353d4 100644 --- a/src/Api/AdminConsole/Public/Controllers/MembersController.cs +++ b/src/Api/AdminConsole/Public/Controllers/MembersController.cs @@ -2,12 +2,10 @@ using Bit.Api.AdminConsole.Public.Models.Request; using Bit.Api.AdminConsole.Public.Models.Response; using Bit.Api.Models.Public.Response; -using Bit.Core; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; using Bit.Core.Context; -using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Repositories; using Bit.Core.Services; using Microsoft.AspNetCore.Authorization; @@ -29,8 +27,8 @@ public class MembersController : Controller private readonly IApplicationCacheService _applicationCacheService; private readonly IPaymentService _paymentService; private readonly IOrganizationRepository _organizationRepository; - private readonly IFeatureService _featureService; private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery; + private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand; public MembersController( IOrganizationUserRepository organizationUserRepository, @@ -43,8 +41,8 @@ public class MembersController : Controller IApplicationCacheService applicationCacheService, IPaymentService paymentService, IOrganizationRepository organizationRepository, - IFeatureService featureService, - ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery) + ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery, + IRemoveOrganizationUserCommand removeOrganizationUserCommand) { _organizationUserRepository = organizationUserRepository; _groupRepository = groupRepository; @@ -56,8 +54,8 @@ public class MembersController : Controller _applicationCacheService = applicationCacheService; _paymentService = paymentService; _organizationRepository = organizationRepository; - _featureService = featureService; _twoFactorIsEnabledQuery = twoFactorIsEnabledQuery; + _removeOrganizationUserCommand = removeOrganizationUserCommand; } /// @@ -73,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); } @@ -120,16 +117,11 @@ public class MembersController : Controller var organizationUserUserDetails = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(_currentContext.OrganizationId.Value); // TODO: Get all CollectionUser associations for the organization and marry them up here for the response. - if (_featureService.IsEnabled(FeatureFlagKeys.MembersTwoFAQueryOptimization)) + var orgUsersTwoFactorIsEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(organizationUserUserDetails); + var memberResponses = organizationUserUserDetails.Select(u => { - return await List_vNext(organizationUserUserDetails); - } - - var memberResponsesTasks = organizationUserUserDetails.Select(async u => - { - return new MemberResponseModel(u, await _userService.TwoFactorIsEnabledAsync(u), null); + return new MemberResponseModel(u, orgUsersTwoFactorIsEnabled.FirstOrDefault(tuple => tuple.user == u).twoFactorIsEnabled, null); }); - var memberResponses = await Task.WhenAll(memberResponsesTasks); var response = new ListResponseModel(memberResponses); return new JsonResult(response); } @@ -243,7 +235,7 @@ public class MembersController : Controller { return new NotFoundResult(); } - await _organizationService.RemoveUserAsync(_currentContext.OrganizationId.Value, id, null); + await _removeOrganizationUserCommand.RemoveUserAsync(_currentContext.OrganizationId.Value, id, null); return new OkResult(); } @@ -268,15 +260,4 @@ public class MembersController : Controller await _organizationService.ResendInviteAsync(_currentContext.OrganizationId.Value, null, id); return new OkResult(); } - - private async Task List_vNext(ICollection organizationUserUserDetails) - { - var orgUsersTwoFactorIsEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(organizationUserUserDetails); - var memberResponses = organizationUserUserDetails.Select(u => - { - return new MemberResponseModel(u, orgUsersTwoFactorIsEnabled.FirstOrDefault(tuple => tuple.user == u).twoFactorIsEnabled, null); - }); - var response = new ListResponseModel(memberResponses); - return new JsonResult(response); - } } diff --git a/src/Api/AdminConsole/Public/Controllers/PoliciesController.cs b/src/Api/AdminConsole/Public/Controllers/PoliciesController.cs index 6af83e57d..71e03a547 100644 --- a/src/Api/AdminConsole/Public/Controllers/PoliciesController.cs +++ b/src/Api/AdminConsole/Public/Controllers/PoliciesController.cs @@ -6,7 +6,6 @@ using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Services; using Bit.Core.Context; -using Bit.Core.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -18,21 +17,15 @@ public class PoliciesController : Controller { private readonly IPolicyRepository _policyRepository; private readonly IPolicyService _policyService; - private readonly IUserService _userService; - private readonly IOrganizationService _organizationService; private readonly ICurrentContext _currentContext; public PoliciesController( IPolicyRepository policyRepository, IPolicyService policyService, - IUserService userService, - IOrganizationService organizationService, ICurrentContext currentContext) { _policyRepository = policyRepository; _policyService = policyService; - _userService = userService; - _organizationService = organizationService; _currentContext = currentContext; } @@ -99,7 +92,7 @@ public class PoliciesController : Controller { policy = model.ToPolicy(policy); } - await _policyService.SaveAsync(policy, _userService, _organizationService, null); + await _policyService.SaveAsync(policy, null); var response = new PolicyResponseModel(policy); return new JsonResult(response); } diff --git a/src/Api/Api.csproj b/src/Api/Api.csproj index 4ca1c8344..d6d055e90 100644 --- a/src/Api/Api.csproj +++ b/src/Api/Api.csproj @@ -32,10 +32,10 @@ - - + + - + diff --git a/src/Api/Auth/Controllers/AccountsController.cs b/src/Api/Auth/Controllers/AccountsController.cs index cf74460fc..d9dfbafc7 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) @@ -443,11 +457,11 @@ public class AccountsController : Controller var twoFactorEnabled = await _userService.TwoFactorIsEnabledAsync(user); var hasPremiumFromOrg = await _userService.HasPremiumFromOrganization(user); - var managedByOrganizationId = await GetManagedByOrganizationIdAsync(user); + var organizationIdsManagingActiveUser = await GetOrganizationIdsManagingUserAsync(user.Id); var response = new ProfileResponseModel(user, organizationUserDetails, providerUserDetails, providerUserOrganizationDetails, twoFactorEnabled, - hasPremiumFromOrg, managedByOrganizationId); + hasPremiumFromOrg, organizationIdsManagingActiveUser); return response; } @@ -457,7 +471,9 @@ public class AccountsController : Controller var userId = _userService.GetProperUserId(User); var organizationUserDetails = await _organizationUserRepository.GetManyDetailsByUserAsync(userId.Value, OrganizationUserStatusType.Confirmed); - var responseData = organizationUserDetails.Select(o => new ProfileOrganizationResponseModel(o)); + var organizationIdsManagingActiveUser = await GetOrganizationIdsManagingUserAsync(userId.Value); + + var responseData = organizationUserDetails.Select(o => new ProfileOrganizationResponseModel(o, organizationIdsManagingActiveUser)); return new ListResponseModel(responseData); } @@ -475,9 +491,9 @@ public class AccountsController : Controller var twoFactorEnabled = await _userService.TwoFactorIsEnabledAsync(user); var hasPremiumFromOrg = await _userService.HasPremiumFromOrganization(user); - var managedByOrganizationId = await GetManagedByOrganizationIdAsync(user); + var organizationIdsManagingActiveUser = await GetOrganizationIdsManagingUserAsync(user.Id); - var response = new ProfileResponseModel(user, null, null, null, twoFactorEnabled, hasPremiumFromOrg, managedByOrganizationId); + var response = new ProfileResponseModel(user, null, null, null, twoFactorEnabled, hasPremiumFromOrg, organizationIdsManagingActiveUser); return response; } @@ -494,9 +510,9 @@ public class AccountsController : Controller var userTwoFactorEnabled = await _userService.TwoFactorIsEnabledAsync(user); var userHasPremiumFromOrganization = await _userService.HasPremiumFromOrganization(user); - var managedByOrganizationId = await GetManagedByOrganizationIdAsync(user); + var organizationIdsManagingActiveUser = await GetOrganizationIdsManagingUserAsync(user.Id); - var response = new ProfileResponseModel(user, null, null, null, userTwoFactorEnabled, userHasPremiumFromOrganization, managedByOrganizationId); + var response = new ProfileResponseModel(user, null, null, null, userTwoFactorEnabled, userHasPremiumFromOrganization, organizationIdsManagingActiveUser); return response; } @@ -647,9 +663,9 @@ public class AccountsController : Controller var userTwoFactorEnabled = await _userService.TwoFactorIsEnabledAsync(user); var userHasPremiumFromOrganization = await _userService.HasPremiumFromOrganization(user); - var managedByOrganizationId = await GetManagedByOrganizationIdAsync(user); + var organizationIdsManagingActiveUser = await GetOrganizationIdsManagingUserAsync(user.Id); - var profile = new ProfileResponseModel(user, null, null, null, userTwoFactorEnabled, userHasPremiumFromOrganization, managedByOrganizationId); + var profile = new ProfileResponseModel(user, null, null, null, userTwoFactorEnabled, userHasPremiumFromOrganization, organizationIdsManagingActiveUser); return new PaymentResponseModel { UserProfile = profile, @@ -937,14 +953,9 @@ public class AccountsController : Controller } } - private async Task GetManagedByOrganizationIdAsync(User user) + private async Task> GetOrganizationIdsManagingUserAsync(Guid userId) { - if (!_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)) - { - return null; - } - - var organizationManagingUser = await _userService.GetOrganizationManagingUserAsync(user.Id); - return organizationManagingUser?.Id; + var organizationManagingUser = await _userService.GetOrganizationsManagingUserAsync(userId); + return organizationManagingUser.Select(o => o.Id); } } diff --git a/src/Api/Auth/Controllers/TwoFactorController.cs b/src/Api/Auth/Controllers/TwoFactorController.cs index 0a50f9bc2..f2578fbc6 100644 --- a/src/Api/Auth/Controllers/TwoFactorController.cs +++ b/src/Api/Auth/Controllers/TwoFactorController.cs @@ -3,7 +3,6 @@ using Bit.Api.Auth.Models.Request.Accounts; using Bit.Api.Auth.Models.Response.TwoFactor; using Bit.Api.Models.Request; using Bit.Api.Models.Response; -using Bit.Core; using Bit.Core.Auth.Enums; using Bit.Core.Auth.LoginFeatures.PasswordlessLogin.Interfaces; using Bit.Core.Auth.Models.Business.Tokenables; @@ -37,7 +36,6 @@ public class TwoFactorController : Controller private readonly IFeatureService _featureService; private readonly IDataProtectorTokenFactory _twoFactorAuthenticatorDataProtector; private readonly IDataProtectorTokenFactory _ssoEmailTwoFactorSessionDataProtector; - private readonly bool _TwoFactorAuthenticatorTokenFeatureFlagEnabled; public TwoFactorController( IUserService userService, @@ -61,7 +59,6 @@ public class TwoFactorController : Controller _featureService = featureService; _twoFactorAuthenticatorDataProtector = twoFactorAuthenticatorDataProtector; _ssoEmailTwoFactorSessionDataProtector = ssoEmailTwoFactorSessionDataProtector; - _TwoFactorAuthenticatorTokenFeatureFlagEnabled = _featureService.IsEnabled(FeatureFlagKeys.AuthenticatorTwoFactorToken); } [HttpGet("")] @@ -102,13 +99,10 @@ public class TwoFactorController : Controller public async Task GetAuthenticator( [FromBody] SecretVerificationRequestModel model) { - var user = _TwoFactorAuthenticatorTokenFeatureFlagEnabled ? await CheckAsync(model, false) : await CheckAsync(model, false, true); + var user = await CheckAsync(model, false); var response = new TwoFactorAuthenticatorResponseModel(user); - if (_TwoFactorAuthenticatorTokenFeatureFlagEnabled) - { - var tokenable = new TwoFactorAuthenticatorUserVerificationTokenable(user, response.Key); - response.UserVerificationToken = _twoFactorAuthenticatorDataProtector.Protect(tokenable); - } + var tokenable = new TwoFactorAuthenticatorUserVerificationTokenable(user, response.Key); + response.UserVerificationToken = _twoFactorAuthenticatorDataProtector.Protect(tokenable); return response; } @@ -117,20 +111,11 @@ public class TwoFactorController : Controller public async Task PutAuthenticator( [FromBody] UpdateTwoFactorAuthenticatorRequestModel model) { - User user; - if (_TwoFactorAuthenticatorTokenFeatureFlagEnabled) + var user = model.ToUser(await _userService.GetUserByPrincipalAsync(User)); + _twoFactorAuthenticatorDataProtector.TryUnprotect(model.UserVerificationToken, out var decryptedToken); + if (!decryptedToken.TokenIsValid(user, model.Key)) { - user = model.ToUser(await _userService.GetUserByPrincipalAsync(User)); - _twoFactorAuthenticatorDataProtector.TryUnprotect(model.UserVerificationToken, out var decryptedToken); - if (!decryptedToken.TokenIsValid(user, model.Key)) - { - throw new BadRequestException("UserVerificationToken", "User verification failed."); - } - } - else - { - user = await CheckAsync(model, false); - model.ToUser(user); // populates user obj with proper metadata for VerifyTwoFactorTokenAsync + throw new BadRequestException("UserVerificationToken", "User verification failed."); } if (!await _userManager.VerifyTwoFactorTokenAsync(user, @@ -145,7 +130,6 @@ public class TwoFactorController : Controller return response; } - [RequireFeature(FeatureFlagKeys.AuthenticatorTwoFactorToken)] [HttpDelete("authenticator")] public async Task DisableAuthenticator( [FromBody] TwoFactorAuthenticatorDisableRequestModel model) @@ -157,7 +141,7 @@ public class TwoFactorController : Controller throw new BadRequestException("UserVerificationToken", "User verification failed."); } - await _userService.DisableTwoFactorProviderAsync(user, model.Type.Value, _organizationService); + await _userService.DisableTwoFactorProviderAsync(user, model.Type.Value); return new TwoFactorProviderResponseModel(model.Type.Value, user); } @@ -412,7 +396,7 @@ public class TwoFactorController : Controller public async Task PutDisable([FromBody] TwoFactorProviderRequestModel model) { var user = await CheckAsync(model, false); - await _userService.DisableTwoFactorProviderAsync(user, model.Type.Value, _organizationService); + await _userService.DisableTwoFactorProviderAsync(user, model.Type.Value); var response = new TwoFactorProviderResponseModel(model.Type.Value, user); return response; } @@ -453,8 +437,7 @@ public class TwoFactorController : Controller [AllowAnonymous] public async Task PostRecover([FromBody] TwoFactorRecoveryRequestModel model) { - if (!await _userService.RecoverTwoFactorAsync(model.Email, model.MasterPasswordHash, model.RecoveryCode, - _organizationService)) + if (!await _userService.RecoverTwoFactorAsync(model.Email, model.MasterPasswordHash, model.RecoveryCode)) { await Task.Delay(2000); throw new BadRequestException(string.Empty, "Invalid information. Try again."); diff --git a/src/Api/Billing/Controllers/AccountsBillingController.cs b/src/Api/Billing/Controllers/AccountsBillingController.cs index a72d79673..574ac3e65 100644 --- a/src/Api/Billing/Controllers/AccountsBillingController.cs +++ b/src/Api/Billing/Controllers/AccountsBillingController.cs @@ -1,4 +1,5 @@ -using Bit.Api.Billing.Models.Responses; +#nullable enable +using Bit.Api.Billing.Models.Responses; using Bit.Core.Billing.Services; using Bit.Core.Services; using Bit.Core.Utilities; @@ -43,7 +44,7 @@ public class AccountsBillingController( } [HttpGet("invoices")] - public async Task GetInvoicesAsync([FromQuery] string startAfter = null) + public async Task GetInvoicesAsync([FromQuery] string? status = null, [FromQuery] string? startAfter = null) { var user = await userService.GetUserByPrincipalAsync(User); if (user == null) @@ -54,6 +55,7 @@ public class AccountsBillingController( var invoices = await paymentHistoryService.GetInvoiceHistoryAsync( user, 5, + status, startAfter); return TypedResults.Ok(invoices); diff --git a/src/Api/Billing/Controllers/OrganizationBillingController.cs b/src/Api/Billing/Controllers/OrganizationBillingController.cs index 8d926ec9f..f6ba87c71 100644 --- a/src/Api/Billing/Controllers/OrganizationBillingController.cs +++ b/src/Api/Billing/Controllers/OrganizationBillingController.cs @@ -1,4 +1,5 @@ -using Bit.Api.Billing.Models.Requests; +#nullable enable +using Bit.Api.Billing.Models.Requests; using Bit.Api.Billing.Models.Responses; using Bit.Core; using Bit.Core.Billing.Services; @@ -63,7 +64,7 @@ public class OrganizationBillingController( } [HttpGet("invoices")] - public async Task GetInvoicesAsync([FromRoute] Guid organizationId, [FromQuery] string startAfter = null) + public async Task GetInvoicesAsync([FromRoute] Guid organizationId, [FromQuery] string? status = null, [FromQuery] string? startAfter = null) { if (!await currentContext.ViewBillingHistory(organizationId)) { @@ -80,6 +81,7 @@ public class OrganizationBillingController( var invoices = await paymentHistoryService.GetInvoiceHistoryAsync( organization, 5, + status, startAfter); return TypedResults.Ok(invoices); diff --git a/src/Api/Billing/Controllers/OrganizationsController.cs b/src/Api/Billing/Controllers/OrganizationsController.cs index 5371186b1..75ae2fb89 100644 --- a/src/Api/Billing/Controllers/OrganizationsController.cs +++ b/src/Api/Billing/Controllers/OrganizationsController.cs @@ -201,7 +201,10 @@ public class OrganizationsController( var organizationDetails = await organizationUserRepository.GetDetailsByUserAsync(userId, organization.Id, OrganizationUserStatusType.Confirmed); - return new ProfileOrganizationResponseModel(organizationDetails); + var organizationManagingActiveUser = await userService.GetOrganizationsManagingUserAsync(userId); + var organizationIdsManagingActiveUser = organizationManagingActiveUser.Select(o => o.Id); + + return new ProfileOrganizationResponseModel(organizationDetails, organizationIdsManagingActiveUser); } [HttpPost("{id:guid}/seat")] diff --git a/src/Api/Billing/Models/Responses/OrganizationMetadataResponse.cs b/src/Api/Billing/Models/Responses/OrganizationMetadataResponse.cs index 624eab1fe..b5f9ab2f5 100644 --- a/src/Api/Billing/Models/Responses/OrganizationMetadataResponse.cs +++ b/src/Api/Billing/Models/Responses/OrganizationMetadataResponse.cs @@ -3,8 +3,11 @@ namespace Bit.Api.Billing.Models.Responses; public record OrganizationMetadataResponse( + bool IsEligibleForSelfHost, bool IsOnSecretsManagerStandalone) { public static OrganizationMetadataResponse From(OrganizationMetadata metadata) - => new(metadata.IsOnSecretsManagerStandalone); + => new( + metadata.IsEligibleForSelfHost, + metadata.IsOnSecretsManagerStandalone); } diff --git a/src/Api/Controllers/PushController.cs b/src/Api/Controllers/PushController.cs index c83eb200b..383980510 100644 --- a/src/Api/Controllers/PushController.cs +++ b/src/Api/Controllers/PushController.cs @@ -46,7 +46,7 @@ public class PushController : Controller public async Task PostDelete([FromBody] PushDeviceRequestModel model) { CheckUsage(); - await _pushRegistrationService.DeleteRegistrationAsync(Prefix(model.Id), model.Type); + await _pushRegistrationService.DeleteRegistrationAsync(Prefix(model.Id)); } [HttpPut("add-organization")] @@ -54,7 +54,7 @@ public class PushController : Controller { CheckUsage(); await _pushRegistrationService.AddUserRegistrationOrganizationAsync( - model.Devices.Select(d => new KeyValuePair(Prefix(d.Id), d.Type)), + model.Devices.Select(d => Prefix(d.Id)), Prefix(model.OrganizationId)); } @@ -63,7 +63,7 @@ public class PushController : Controller { CheckUsage(); await _pushRegistrationService.DeleteUserRegistrationOrganizationAsync( - model.Devices.Select(d => new KeyValuePair(Prefix(d.Id), d.Type)), + model.Devices.Select(d => Prefix(d.Id)), Prefix(model.OrganizationId)); } diff --git a/src/Api/Models/Request/Organizations/OrganizationCollectionManagementUpdateRequestModel.cs b/src/Api/Models/Request/Organizations/OrganizationCollectionManagementUpdateRequestModel.cs index 68e87b522..a5a6f1f74 100644 --- a/src/Api/Models/Request/Organizations/OrganizationCollectionManagementUpdateRequestModel.cs +++ b/src/Api/Models/Request/Organizations/OrganizationCollectionManagementUpdateRequestModel.cs @@ -1,15 +1,29 @@ -using Bit.Core.AdminConsole.Entities; +using Bit.Core; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Services; namespace Bit.Api.Models.Request.Organizations; public class OrganizationCollectionManagementUpdateRequestModel { + public bool LimitCollectionCreation { get; set; } + public bool LimitCollectionDeletion { get; set; } + // Deprecated: https://bitwarden.atlassian.net/browse/PM-10863 public bool LimitCreateDeleteOwnerAdmin { get; set; } public bool AllowAdminAccessToAllCollectionItems { get; set; } - public virtual Organization ToOrganization(Organization existingOrganization) + public virtual Organization ToOrganization(Organization existingOrganization, IFeatureService featureService) { - existingOrganization.LimitCollectionCreationDeletion = LimitCreateDeleteOwnerAdmin; + if (featureService.IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit)) + { + existingOrganization.LimitCollectionCreation = LimitCollectionCreation; + existingOrganization.LimitCollectionDeletion = LimitCollectionDeletion; + } + else + { + existingOrganization.LimitCollectionCreationDeletion = LimitCreateDeleteOwnerAdmin || LimitCollectionCreation || LimitCollectionDeletion; + } + existingOrganization.AllowAdminAccessToAllCollectionItems = AllowAdminAccessToAllCollectionItems; return existingOrganization; } diff --git a/src/Api/Models/Response/ProfileResponseModel.cs b/src/Api/Models/Response/ProfileResponseModel.cs index f5d0382e5..a6ed4ebfa 100644 --- a/src/Api/Models/Response/ProfileResponseModel.cs +++ b/src/Api/Models/Response/ProfileResponseModel.cs @@ -15,7 +15,7 @@ public class ProfileResponseModel : ResponseModel IEnumerable providerUserOrganizationDetails, bool twoFactorEnabled, bool premiumFromOrganization, - Guid? managedByOrganizationId) : base("profile") + IEnumerable organizationIdsManagingUser) : base("profile") { if (user == null) { @@ -37,11 +37,10 @@ public class ProfileResponseModel : ResponseModel UsesKeyConnector = user.UsesKeyConnector; AvatarColor = user.AvatarColor; CreationDate = user.CreationDate; - Organizations = organizationsUserDetails?.Select(o => new ProfileOrganizationResponseModel(o)); + Organizations = organizationsUserDetails?.Select(o => new ProfileOrganizationResponseModel(o, organizationIdsManagingUser)); Providers = providerUserDetails?.Select(p => new ProfileProviderResponseModel(p)); ProviderOrganizations = providerUserOrganizationDetails?.Select(po => new ProfileProviderOrganizationResponseModel(po)); - ManagedByOrganizationId = managedByOrganizationId; } public ProfileResponseModel() : base("profile") @@ -63,7 +62,6 @@ public class ProfileResponseModel : ResponseModel public bool UsesKeyConnector { get; set; } public string AvatarColor { get; set; } public DateTime CreationDate { get; set; } - public Guid? ManagedByOrganizationId { get; set; } public IEnumerable Organizations { get; set; } public IEnumerable Providers { get; set; } public IEnumerable ProviderOrganizations { get; set; } diff --git a/src/Api/Vault/AuthorizationHandlers/Collections/BulkCollectionAuthorizationHandler.cs b/src/Api/Vault/AuthorizationHandlers/Collections/BulkCollectionAuthorizationHandler.cs index 5d11b39ea..c26d5b595 100644 --- a/src/Api/Vault/AuthorizationHandlers/Collections/BulkCollectionAuthorizationHandler.cs +++ b/src/Api/Vault/AuthorizationHandlers/Collections/BulkCollectionAuthorizationHandler.cs @@ -1,5 +1,6 @@ #nullable enable using System.Diagnostics; +using Bit.Core; using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Enums; @@ -101,7 +102,7 @@ public class BulkCollectionAuthorizationHandler : BulkAuthorizationHandler o.Id); var response = new SyncResponseModel(_globalSettings, user, userTwoFactorEnabled, userHasPremiumFromOrganization, - managedByOrganizationId, organizationUserDetails, providerUserDetails, providerUserOrganizationDetails, + organizationIdsManagingActiveUser, organizationUserDetails, providerUserDetails, providerUserOrganizationDetails, folders, collections, ciphers, collectionCiphersGroupDict, excludeDomains, policies, sends); return response; } - - private async Task GetManagedByOrganizationIdAsync(User user, IEnumerable organizationUserDetails) - { - if (!_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning) || - !organizationUserDetails.Any(o => o.Enabled && o.UseSso)) - { - return null; - } - - var organizationManagingUser = await _userService.GetOrganizationManagingUserAsync(user.Id); - return organizationManagingUser?.Id; - } } diff --git a/src/Api/Vault/Models/Response/SyncResponseModel.cs b/src/Api/Vault/Models/Response/SyncResponseModel.cs index 2170a5232..ce5f4562d 100644 --- a/src/Api/Vault/Models/Response/SyncResponseModel.cs +++ b/src/Api/Vault/Models/Response/SyncResponseModel.cs @@ -21,7 +21,7 @@ public class SyncResponseModel : ResponseModel User user, bool userTwoFactorEnabled, bool userHasPremiumFromOrganization, - Guid? managedByOrganizationId, + IEnumerable organizationIdsManagingUser, IEnumerable organizationUserDetails, IEnumerable providerUserDetails, IEnumerable providerUserOrganizationDetails, @@ -35,7 +35,7 @@ public class SyncResponseModel : ResponseModel : base("sync") { Profile = new ProfileResponseModel(user, organizationUserDetails, providerUserDetails, - providerUserOrganizationDetails, userTwoFactorEnabled, userHasPremiumFromOrganization, managedByOrganizationId); + providerUserOrganizationDetails, userTwoFactorEnabled, userHasPremiumFromOrganization, organizationIdsManagingUser); Folders = folders.Select(f => new FolderResponseModel(f)); Ciphers = ciphers.Select(c => new CipherDetailsResponseModel(c, globalSettings, collectionCiphersDict)); Collections = collections?.Select( diff --git a/src/Billing/Services/Implementations/SubscriptionDeletedHandler.cs b/src/Billing/Services/Implementations/SubscriptionDeletedHandler.cs index c495e8dd7..06692ab01 100644 --- a/src/Billing/Services/Implementations/SubscriptionDeletedHandler.cs +++ b/src/Billing/Services/Implementations/SubscriptionDeletedHandler.cs @@ -32,12 +32,14 @@ public class SubscriptionDeletedHandler : ISubscriptionDeletedHandler var (organizationId, userId, providerId) = _stripeEventUtilityService.GetIdsFromMetadata(subscription.Metadata); var subCanceled = subscription.Status == StripeSubscriptionStatus.Canceled; + const string providerMigrationCancellationComment = "Cancelled as part of provider migration to Consolidated Billing"; + if (!subCanceled) { return; } - if (organizationId.HasValue) + if (organizationId.HasValue && subscription is not { CancellationDetails.Comment: providerMigrationCancellationComment }) { await _organizationService.DisableAsync(organizationId.Value, subscription.CurrentPeriodEnd); } diff --git a/src/Billing/Startup.cs b/src/Billing/Startup.cs index acb0f120e..7965cbe50 100644 --- a/src/Billing/Startup.cs +++ b/src/Billing/Startup.cs @@ -75,6 +75,7 @@ public class Startup // Services services.AddBaseServices(globalSettings); services.AddDefaultServices(globalSettings); + services.AddDistributedCache(globalSettings); services.AddBillingOperations(); services.TryAddSingleton(); diff --git a/src/Core/AdminConsole/Entities/Organization.cs b/src/Core/AdminConsole/Entities/Organization.cs index 1f60a554c..c556dfe60 100644 --- a/src/Core/AdminConsole/Entities/Organization.cs +++ b/src/Core/AdminConsole/Entities/Organization.cs @@ -7,6 +7,7 @@ using Bit.Core.Billing.Enums; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Models.Business; +using Bit.Core.Services; using Bit.Core.Tools.Entities; using Bit.Core.Utilities; @@ -93,7 +94,21 @@ public class Organization : ITableObject, IStorableSubscriber, IRevisable, /// If set to false, any organization member can create a collection, and any member can delete a collection that /// they have Can Manage permissions for. /// - public bool LimitCollectionCreationDeletion { get; set; } + public bool LimitCollectionCreation { get; set; } + public bool LimitCollectionDeletion { get; set; } + // Deprecated by https://bitwarden.atlassian.net/browse/PM-10863. This + // was replaced with `LimitCollectionCreation` and + // `LimitCollectionDeletion`. + public bool LimitCollectionCreationDeletion + { + get => LimitCollectionCreation || LimitCollectionDeletion; + set + { + LimitCollectionCreation = value; + LimitCollectionDeletion = value; + } + } + /// /// If set to true, admins, owners, and some custom users can read/write all collections and items in the Admin Console. /// If set to false, users generally need collection-level permissions to read/write a collection or its items. @@ -264,7 +279,7 @@ public class Organization : ITableObject, IStorableSubscriber, IRevisable, return providers[provider]; } - public void UpdateFromLicense(OrganizationLicense license) + public void UpdateFromLicense(OrganizationLicense license, IFeatureService featureService) { // The following properties are intentionally excluded from being updated: // - Id - self-hosted org will have its own unique Guid @@ -299,7 +314,11 @@ public class Organization : ITableObject, IStorableSubscriber, IRevisable, UseSecretsManager = license.UseSecretsManager; SmSeats = license.SmSeats; SmServiceAccounts = license.SmServiceAccounts; - LimitCollectionCreationDeletion = license.LimitCollectionCreationDeletion; - AllowAdminAccessToAllCollectionItems = license.AllowAdminAccessToAllCollectionItems; + + if (!featureService.IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit)) + { + LimitCollectionCreationDeletion = license.LimitCollectionCreationDeletion; + AllowAdminAccessToAllCollectionItems = license.AllowAdminAccessToAllCollectionItems; + } } } diff --git a/src/Core/AdminConsole/Enums/PolicyType.cs b/src/Core/AdminConsole/Enums/PolicyType.cs index 0e1786cf5..bdde3e424 100644 --- a/src/Core/AdminConsole/Enums/PolicyType.cs +++ b/src/Core/AdminConsole/Enums/PolicyType.cs @@ -16,3 +16,30 @@ public enum PolicyType : byte ActivateAutofill = 11, AutomaticAppLogIn = 12, } + +public static class PolicyTypeExtensions +{ + /// + /// Returns the name of the policy for display to the user. + /// Do not include the word "policy" in the return value. + /// + public static string GetName(this PolicyType type) + { + return type switch + { + PolicyType.TwoFactorAuthentication => "Require two-step login", + PolicyType.MasterPassword => "Master password requirements", + PolicyType.PasswordGenerator => "Password generator", + PolicyType.SingleOrg => "Single organization", + PolicyType.RequireSso => "Require single sign-on authentication", + PolicyType.PersonalOwnership => "Remove individual vault", + PolicyType.DisableSend => "Remove Send", + PolicyType.SendOptions => "Send options", + PolicyType.ResetPassword => "Account recovery administration", + PolicyType.MaximumVaultTimeout => "Vault timeout", + PolicyType.DisablePersonalVaultExport => "Remove individual vault export", + PolicyType.ActivateAutofill => "Active auto-fill", + PolicyType.AutomaticAppLogIn => "Automatically log in users for allowed applications", + }; + } +} 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/Models/Data/Organizations/OrganizationAbility.cs b/src/Core/AdminConsole/Models/Data/Organizations/OrganizationAbility.cs index 07db80d43..a91b96083 100644 --- a/src/Core/AdminConsole/Models/Data/Organizations/OrganizationAbility.cs +++ b/src/Core/AdminConsole/Models/Data/Organizations/OrganizationAbility.cs @@ -21,6 +21,9 @@ public class OrganizationAbility UseResetPassword = organization.UseResetPassword; UseCustomPermissions = organization.UseCustomPermissions; UsePolicies = organization.UsePolicies; + LimitCollectionCreation = organization.LimitCollectionCreation; + LimitCollectionDeletion = organization.LimitCollectionDeletion; + // Deprecated: https://bitwarden.atlassian.net/browse/PM-10863 LimitCollectionCreationDeletion = organization.LimitCollectionCreationDeletion; AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems; } @@ -37,6 +40,9 @@ public class OrganizationAbility public bool UseResetPassword { get; set; } public bool UseCustomPermissions { get; set; } public bool UsePolicies { get; set; } + public bool LimitCollectionCreation { get; set; } + public bool LimitCollectionDeletion { get; set; } + // Deprecated: https://bitwarden.atlassian.net/browse/PM-10863 public bool LimitCollectionCreationDeletion { get; set; } public bool AllowAdminAccessToAllCollectionItems { get; set; } } diff --git a/src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserOrganizationDetails.cs b/src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserOrganizationDetails.cs index cdd73cba7..435369e77 100644 --- a/src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserOrganizationDetails.cs +++ b/src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserOrganizationDetails.cs @@ -54,6 +54,9 @@ public class OrganizationUserOrganizationDetails public bool UsePasswordManager { get; set; } public int? SmSeats { get; set; } public int? SmServiceAccounts { get; set; } + public bool LimitCollectionCreation { get; set; } + public bool LimitCollectionDeletion { get; set; } + // Deprecated: https://bitwarden.atlassian.net/browse/PM-10863 public bool LimitCollectionCreationDeletion { get; set; } public bool AllowAdminAccessToAllCollectionItems { get; set; } } diff --git a/src/Core/AdminConsole/Models/Data/Organizations/SelfHostedOrganizationDetails.cs b/src/Core/AdminConsole/Models/Data/Organizations/SelfHostedOrganizationDetails.cs index d21ba9183..1fa547d98 100644 --- a/src/Core/AdminConsole/Models/Data/Organizations/SelfHostedOrganizationDetails.cs +++ b/src/Core/AdminConsole/Models/Data/Organizations/SelfHostedOrganizationDetails.cs @@ -144,6 +144,9 @@ public class SelfHostedOrganizationDetails : Organization RevisionDate = RevisionDate, MaxAutoscaleSeats = MaxAutoscaleSeats, OwnersNotifiedOfAutoscaling = OwnersNotifiedOfAutoscaling, + LimitCollectionCreation = LimitCollectionCreation, + LimitCollectionDeletion = LimitCollectionDeletion, + // Deprecated: https://bitwarden.atlassian.net/browse/PM-10863 LimitCollectionCreationDeletion = LimitCollectionCreationDeletion, AllowAdminAccessToAllCollectionItems = AllowAdminAccessToAllCollectionItems, Status = Status diff --git a/src/Core/AdminConsole/Models/Data/Provider/ProviderUserOrganizationDetails.cs b/src/Core/AdminConsole/Models/Data/Provider/ProviderUserOrganizationDetails.cs index d3d831f51..a2ac62253 100644 --- a/src/Core/AdminConsole/Models/Data/Provider/ProviderUserOrganizationDetails.cs +++ b/src/Core/AdminConsole/Models/Data/Provider/ProviderUserOrganizationDetails.cs @@ -40,6 +40,8 @@ public class ProviderUserOrganizationDetails [JsonConverter(typeof(HtmlEncodingStringConverter))] public string ProviderName { get; set; } public PlanType PlanType { get; set; } + public bool LimitCollectionCreation { get; set; } + public bool LimitCollectionDeletion { get; set; } public bool LimitCollectionCreationDeletion { get; set; } public bool AllowAdminAccessToAllCollectionItems { get; set; } } diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationDomains/CreateOrganizationDomainCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationDomains/CreateOrganizationDomainCommand.cs index be8ed0e64..192ab3b79 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationDomains/CreateOrganizationDomainCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationDomains/CreateOrganizationDomainCommand.cs @@ -6,7 +6,6 @@ using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; using Bit.Core.Utilities; -using Microsoft.Extensions.Logging; namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains; @@ -14,21 +13,15 @@ public class CreateOrganizationDomainCommand : ICreateOrganizationDomainCommand { private readonly IOrganizationDomainRepository _organizationDomainRepository; private readonly IEventService _eventService; - private readonly IDnsResolverService _dnsResolverService; - private readonly ILogger _logger; private readonly IGlobalSettings _globalSettings; public CreateOrganizationDomainCommand( IOrganizationDomainRepository organizationDomainRepository, IEventService eventService, - IDnsResolverService dnsResolverService, - ILogger logger, IGlobalSettings globalSettings) { _organizationDomainRepository = organizationDomainRepository; _eventService = eventService; - _dnsResolverService = dnsResolverService; - _logger = logger; _globalSettings = globalSettings; } 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/Interfaces/IVerifyOrganizationDomainCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationDomains/Interfaces/IVerifyOrganizationDomainCommand.cs index b62a9f932..6b5beb11f 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationDomains/Interfaces/IVerifyOrganizationDomainCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationDomains/Interfaces/IVerifyOrganizationDomainCommand.cs @@ -4,5 +4,6 @@ namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfa public interface IVerifyOrganizationDomainCommand { - Task VerifyOrganizationDomainAsync(OrganizationDomain organizationDomain); + Task UserVerifyOrganizationDomainAsync(OrganizationDomain organizationDomain); + Task SystemVerifyOrganizationDomainAsync(OrganizationDomain organizationDomain); } 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 5f9476db8..4a597a290 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationDomains/VerifyOrganizationDomainCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationDomains/VerifyOrganizationDomainCommand.cs @@ -1,9 +1,13 @@ -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; using Bit.Core.Repositories; using Bit.Core.Services; +using Bit.Core.Settings; using Microsoft.Extensions.Logging; namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains; @@ -13,34 +17,94 @@ public class VerifyOrganizationDomainCommand : IVerifyOrganizationDomainCommand private readonly IOrganizationDomainRepository _organizationDomainRepository; 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( IOrganizationDomainRepository organizationDomainRepository, 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; } - public async Task VerifyOrganizationDomainAsync(OrganizationDomain domain) + + public async Task UserVerifyOrganizationDomainAsync(OrganizationDomain organizationDomain) { + var domainVerificationResult = await VerifyOrganizationDomainAsync(organizationDomain); + + await _eventService.LogOrganizationDomainEventAsync(domainVerificationResult, + domainVerificationResult.VerifiedDate != null + ? EventType.OrganizationDomain_Verified + : EventType.OrganizationDomain_NotVerified); + + await _organizationDomainRepository.ReplaceAsync(domainVerificationResult); + + return domainVerificationResult; + } + + public async Task SystemVerifyOrganizationDomainAsync(OrganizationDomain organizationDomain) + { + organizationDomain.SetJobRunCount(); + + var domainVerificationResult = await VerifyOrganizationDomainAsync(organizationDomain); + + if (domainVerificationResult.VerifiedDate is not null) + { + _logger.LogInformation(Constants.BypassFiltersEventId, "Successfully validated domain"); + + await _eventService.LogOrganizationDomainEventAsync(domainVerificationResult, + EventType.OrganizationDomain_Verified, + EventSystemUser.DomainVerification); + } + else + { + domainVerificationResult.SetNextRunDate(_globalSettings.DomainVerification.VerificationInterval); + + await _eventService.LogOrganizationDomainEventAsync(domainVerificationResult, + EventType.OrganizationDomain_NotVerified, + EventSystemUser.DomainVerification); + + _logger.LogInformation(Constants.BypassFiltersEventId, + "Verification for organization {OrgId} with domain {Domain} failed", + domainVerificationResult.OrganizationId, domainVerificationResult.DomainName); + } + + await _organizationDomainRepository.ReplaceAsync(domainVerificationResult); + + return domainVerificationResult; + } + + private async Task VerifyOrganizationDomainAsync(OrganizationDomain domain) + { + domain.SetLastCheckedDate(); + if (domain.VerifiedDate is not null) { - domain.SetLastCheckedDate(); await _organizationDomainRepository.ReplaceAsync(domain); throw new ConflictException("Domain has already been verified."); } var claimedDomain = await _organizationDomainRepository.GetClaimedDomainsByDomainNameAsync(domain.DomainName); - if (claimedDomain.Any()) + + if (claimedDomain.Count > 0) { - domain.SetLastCheckedDate(); await _organizationDomainRepository.ReplaceAsync(domain); throw new ConflictException("The domain is not available to be claimed."); } @@ -50,6 +114,8 @@ public class VerifyOrganizationDomainCommand : IVerifyOrganizationDomainCommand if (await _dnsResolverService.ResolveAsync(domain.DomainName, domain.Txt)) { domain.SetVerifiedDate(); + + await EnableSingleOrganizationPolicyAsync(domain.OrganizationId); } } catch (Exception e) @@ -58,11 +124,15 @@ public class VerifyOrganizationDomainCommand : IVerifyOrganizationDomainCommand domain.DomainName, e.Message); } - domain.SetLastCheckedDate(); - await _organizationDomainRepository.ReplaceAsync(domain); - - await _eventService.LogOrganizationDomainEventAsync(domain, - domain.VerifiedDate != null ? EventType.OrganizationDomain_Verified : EventType.OrganizationDomain_NotVerified); 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/OrganizationFeatures/OrganizationUsers/DeleteManagedOrganizationUserAccountCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteManagedOrganizationUserAccountCommand.cs index d70d061c8..0bcd16cee 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteManagedOrganizationUserAccountCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteManagedOrganizationUserAccountCommand.cs @@ -18,7 +18,8 @@ public class DeleteManagedOrganizationUserAccountCommand : IDeleteManagedOrganiz private readonly IOrganizationUserRepository _organizationUserRepository; private readonly IUserRepository _userRepository; private readonly ICurrentContext _currentContext; - private readonly IOrganizationService _organizationService; + private readonly IHasConfirmedOwnersExceptQuery _hasConfirmedOwnersExceptQuery; + public DeleteManagedOrganizationUserAccountCommand( IUserService userService, IEventService eventService, @@ -26,7 +27,7 @@ public class DeleteManagedOrganizationUserAccountCommand : IDeleteManagedOrganiz IOrganizationUserRepository organizationUserRepository, IUserRepository userRepository, ICurrentContext currentContext, - IOrganizationService organizationService) + IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery) { _userService = userService; _eventService = eventService; @@ -34,7 +35,7 @@ public class DeleteManagedOrganizationUserAccountCommand : IDeleteManagedOrganiz _organizationUserRepository = organizationUserRepository; _userRepository = userRepository; _currentContext = currentContext; - _organizationService = organizationService; + _hasConfirmedOwnersExceptQuery = hasConfirmedOwnersExceptQuery; } public async Task DeleteUserAsync(Guid organizationId, Guid organizationUserId, Guid? deletingUserId) @@ -46,7 +47,7 @@ public class DeleteManagedOrganizationUserAccountCommand : IDeleteManagedOrganiz } var managementStatus = await _getOrganizationUsersManagementStatusQuery.GetUsersOrganizationManagementStatusAsync(organizationId, new[] { organizationUserId }); - var hasOtherConfirmedOwners = await _organizationService.HasConfirmedOwnersExceptAsync(organizationId, new[] { organizationUserId }, includeProvider: true); + var hasOtherConfirmedOwners = await _hasConfirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync(organizationId, new[] { organizationUserId }, includeProvider: true); await ValidateDeleteUserAsync(organizationId, organizationUser, deletingUserId, managementStatus, hasOtherConfirmedOwners); @@ -67,7 +68,7 @@ public class DeleteManagedOrganizationUserAccountCommand : IDeleteManagedOrganiz var users = await _userRepository.GetManyAsync(userIds); var managementStatus = await _getOrganizationUsersManagementStatusQuery.GetUsersOrganizationManagementStatusAsync(organizationId, orgUserIds); - var hasOtherConfirmedOwners = await _organizationService.HasConfirmedOwnersExceptAsync(organizationId, orgUserIds, includeProvider: true); + var hasOtherConfirmedOwners = await _hasConfirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync(organizationId, orgUserIds, includeProvider: true); var results = new List<(Guid OrganizationUserId, string? ErrorMessage)>(); foreach (var orgUserId in orgUserIds) diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/HasConfirmedOwnersExceptQuery.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/HasConfirmedOwnersExceptQuery.cs new file mode 100644 index 000000000..746042ea3 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/HasConfirmedOwnersExceptQuery.cs @@ -0,0 +1,41 @@ +using Bit.Core.AdminConsole.Enums.Provider; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; +using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Repositories; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers; + +public class HasConfirmedOwnersExceptQuery : IHasConfirmedOwnersExceptQuery +{ + private readonly IProviderUserRepository _providerUserRepository; + private readonly IOrganizationUserRepository _organizationUserRepository; + + public HasConfirmedOwnersExceptQuery( + IProviderUserRepository providerUserRepository, + IOrganizationUserRepository organizationUserRepository) + { + _providerUserRepository = providerUserRepository; + _organizationUserRepository = organizationUserRepository; + } + + public async Task HasConfirmedOwnersExceptAsync(Guid organizationId, IEnumerable organizationUsersId, bool includeProvider = true) + { + var confirmedOwners = await GetConfirmedOwnersAsync(organizationId); + var confirmedOwnersIds = confirmedOwners.Select(u => u.Id); + bool hasOtherOwner = confirmedOwnersIds.Except(organizationUsersId).Any(); + if (!hasOtherOwner && includeProvider) + { + return (await _providerUserRepository.GetManyByOrganizationAsync(organizationId, ProviderUserStatusType.Confirmed)).Any(); + } + return hasOtherOwner; + } + + private async Task> GetConfirmedOwnersAsync(Guid organizationId) + { + var owners = await _organizationUserRepository.GetManyByOrganizationAsync(organizationId, + OrganizationUserType.Owner); + return owners.Where(o => o.Status == OrganizationUserStatusType.Confirmed); + } +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IHasConfirmedOwnersExceptQuery.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IHasConfirmedOwnersExceptQuery.cs new file mode 100644 index 000000000..2964f7116 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IHasConfirmedOwnersExceptQuery.cs @@ -0,0 +1,12 @@ +namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; + +public interface IHasConfirmedOwnersExceptQuery +{ + /// + /// Checks if an organization has any confirmed owners except for the ones in the list. + /// + /// The organization ID. + /// The organization user IDs to exclude. + /// Whether to include the provider users in the count. + Task HasConfirmedOwnersExceptAsync(Guid organizationId, IEnumerable organizationUsersId, bool includeProvider = true); +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IRemoveOrganizationUserCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IRemoveOrganizationUserCommand.cs index 3213762ea..583645a89 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IRemoveOrganizationUserCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IRemoveOrganizationUserCommand.cs @@ -1,4 +1,5 @@ -using Bit.Core.Enums; +using Bit.Core.Entities; +using Bit.Core.Enums; namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; @@ -7,4 +8,7 @@ public interface IRemoveOrganizationUserCommand Task RemoveUserAsync(Guid organizationId, Guid organizationUserId, Guid? deletingUserId); Task RemoveUserAsync(Guid organizationId, Guid organizationUserId, EventSystemUser eventSystemUser); + Task RemoveUserAsync(Guid organizationId, Guid userId); + Task>> RemoveUsersAsync(Guid organizationId, + IEnumerable organizationUserIds, Guid? deletingUserId); } diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RemoveOrganizationUserCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RemoveOrganizationUserCommand.cs index e2aea0249..e6d56ea87 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RemoveOrganizationUserCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RemoveOrganizationUserCommand.cs @@ -1,4 +1,6 @@ using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; +using Bit.Core.Context; +using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Repositories; @@ -8,38 +10,171 @@ namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers; public class RemoveOrganizationUserCommand : IRemoveOrganizationUserCommand { + private readonly IDeviceRepository _deviceRepository; private readonly IOrganizationUserRepository _organizationUserRepository; - private readonly IOrganizationService _organizationService; + private readonly IEventService _eventService; + private readonly IPushNotificationService _pushNotificationService; + private readonly IPushRegistrationService _pushRegistrationService; + private readonly ICurrentContext _currentContext; + private readonly IHasConfirmedOwnersExceptQuery _hasConfirmedOwnersExceptQuery; public RemoveOrganizationUserCommand( + IDeviceRepository deviceRepository, IOrganizationUserRepository organizationUserRepository, - IOrganizationService organizationService - ) + IEventService eventService, + IPushNotificationService pushNotificationService, + IPushRegistrationService pushRegistrationService, + ICurrentContext currentContext, + IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery) { + _deviceRepository = deviceRepository; _organizationUserRepository = organizationUserRepository; - _organizationService = organizationService; + _eventService = eventService; + _pushNotificationService = pushNotificationService; + _pushRegistrationService = pushRegistrationService; + _currentContext = currentContext; + _hasConfirmedOwnersExceptQuery = hasConfirmedOwnersExceptQuery; } public async Task RemoveUserAsync(Guid organizationId, Guid organizationUserId, Guid? deletingUserId) { - await ValidateDeleteUserAsync(organizationId, organizationUserId); + var organizationUser = await _organizationUserRepository.GetByIdAsync(organizationUserId); + ValidateDeleteUser(organizationId, organizationUser); - await _organizationService.RemoveUserAsync(organizationId, organizationUserId, deletingUserId); + await RepositoryDeleteUserAsync(organizationUser, deletingUserId); + + await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Removed); } public async Task RemoveUserAsync(Guid organizationId, Guid organizationUserId, EventSystemUser eventSystemUser) { - await ValidateDeleteUserAsync(organizationId, organizationUserId); + var organizationUser = await _organizationUserRepository.GetByIdAsync(organizationUserId); + ValidateDeleteUser(organizationId, organizationUser); - await _organizationService.RemoveUserAsync(organizationId, organizationUserId, eventSystemUser); + await RepositoryDeleteUserAsync(organizationUser, null); + + await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Removed, eventSystemUser); } - private async Task ValidateDeleteUserAsync(Guid organizationId, Guid organizationUserId) + public async Task RemoveUserAsync(Guid organizationId, Guid userId) + { + var organizationUser = await _organizationUserRepository.GetByOrganizationAsync(organizationId, userId); + ValidateDeleteUser(organizationId, organizationUser); + + await RepositoryDeleteUserAsync(organizationUser, null); + + await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Removed); + } + + public async Task>> RemoveUsersAsync(Guid organizationId, + IEnumerable organizationUsersId, + Guid? deletingUserId) + { + var orgUsers = await _organizationUserRepository.GetManyAsync(organizationUsersId); + var filteredUsers = orgUsers.Where(u => u.OrganizationId == organizationId) + .ToList(); + + if (!filteredUsers.Any()) + { + throw new BadRequestException("Users invalid."); + } + + if (!await _hasConfirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync(organizationId, organizationUsersId)) + { + throw new BadRequestException("Organization must have at least one confirmed owner."); + } + + var deletingUserIsOwner = false; + if (deletingUserId.HasValue) + { + deletingUserIsOwner = await _currentContext.OrganizationOwner(organizationId); + } + + var result = new List>(); + var deletedUserIds = new List(); + foreach (var orgUser in filteredUsers) + { + try + { + if (deletingUserId.HasValue && orgUser.UserId == deletingUserId) + { + throw new BadRequestException("You cannot remove yourself."); + } + + if (orgUser.Type == OrganizationUserType.Owner && deletingUserId.HasValue && !deletingUserIsOwner) + { + throw new BadRequestException("Only owners can delete other owners."); + } + + await _eventService.LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_Removed); + + if (orgUser.UserId.HasValue) + { + await DeleteAndPushUserRegistrationAsync(organizationId, orgUser.UserId.Value); + } + result.Add(Tuple.Create(orgUser, "")); + deletedUserIds.Add(orgUser.Id); + } + catch (BadRequestException e) + { + result.Add(Tuple.Create(orgUser, e.Message)); + } + + await _organizationUserRepository.DeleteManyAsync(deletedUserIds); + } + + return result; + } + + private void ValidateDeleteUser(Guid organizationId, OrganizationUser orgUser) { - var orgUser = await _organizationUserRepository.GetByIdAsync(organizationUserId); if (orgUser == null || orgUser.OrganizationId != organizationId) { throw new NotFoundException("User not found."); } } + + private async Task RepositoryDeleteUserAsync(OrganizationUser orgUser, Guid? deletingUserId) + { + if (deletingUserId.HasValue && orgUser.UserId == deletingUserId.Value) + { + throw new BadRequestException("You cannot remove yourself."); + } + + if (orgUser.Type == OrganizationUserType.Owner) + { + if (deletingUserId.HasValue && !await _currentContext.OrganizationOwner(orgUser.OrganizationId)) + { + throw new BadRequestException("Only owners can delete other owners."); + } + + if (!await _hasConfirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync(orgUser.OrganizationId, new[] { orgUser.Id }, includeProvider: true)) + { + throw new BadRequestException("Organization must have at least one confirmed owner."); + } + } + + await _organizationUserRepository.DeleteAsync(orgUser); + + if (orgUser.UserId.HasValue) + { + await DeleteAndPushUserRegistrationAsync(orgUser.OrganizationId, orgUser.UserId.Value); + } + } + + private async Task> GetUserDeviceIdsAsync(Guid userId) + { + var devices = await _deviceRepository.GetManyByUserIdAsync(userId); + return devices + .Where(d => !string.IsNullOrWhiteSpace(d.PushToken)) + .Select(d => d.Id.ToString()); + } + + private async Task DeleteAndPushUserRegistrationAsync(Guid organizationId, Guid userId) + { + var devices = await GetUserDeviceIdsAsync(userId); + await _pushRegistrationService.DeleteUserRegistrationOrganizationAsync(devices, + organizationId.ToString()); + await _pushNotificationService.PushSyncOrgKeysAsync(userId); + } } diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/UpdateOrganizationUserCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/UpdateOrganizationUserCommand.cs index cf8068c5b..c5a4b3da1 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/UpdateOrganizationUserCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/UpdateOrganizationUserCommand.cs @@ -22,6 +22,7 @@ public class UpdateOrganizationUserCommand : IUpdateOrganizationUserCommand private readonly IUpdateSecretsManagerSubscriptionCommand _updateSecretsManagerSubscriptionCommand; private readonly ICollectionRepository _collectionRepository; private readonly IGroupRepository _groupRepository; + private readonly IHasConfirmedOwnersExceptQuery _hasConfirmedOwnersExceptQuery; public UpdateOrganizationUserCommand( IEventService eventService, @@ -31,7 +32,8 @@ public class UpdateOrganizationUserCommand : IUpdateOrganizationUserCommand ICountNewSmSeatsRequiredQuery countNewSmSeatsRequiredQuery, IUpdateSecretsManagerSubscriptionCommand updateSecretsManagerSubscriptionCommand, ICollectionRepository collectionRepository, - IGroupRepository groupRepository) + IGroupRepository groupRepository, + IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery) { _eventService = eventService; _organizationService = organizationService; @@ -41,6 +43,7 @@ public class UpdateOrganizationUserCommand : IUpdateOrganizationUserCommand _updateSecretsManagerSubscriptionCommand = updateSecretsManagerSubscriptionCommand; _collectionRepository = collectionRepository; _groupRepository = groupRepository; + _hasConfirmedOwnersExceptQuery = hasConfirmedOwnersExceptQuery; } /// @@ -87,7 +90,7 @@ public class UpdateOrganizationUserCommand : IUpdateOrganizationUserCommand await _organizationService.ValidateOrganizationCustomPermissionsEnabledAsync(user.OrganizationId, user.Type); if (user.Type != OrganizationUserType.Owner && - !await _organizationService.HasConfirmedOwnersExceptAsync(user.OrganizationId, new[] { user.Id })) + !await _hasConfirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync(user.OrganizationId, new[] { user.Id })) { throw new BadRequestException("Organization must have at least one confirmed owner."); } diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/IPolicyValidator.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/IPolicyValidator.cs new file mode 100644 index 000000000..6aef9f248 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/IPolicyValidator.cs @@ -0,0 +1,43 @@ +#nullable enable + +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies; + +/// +/// Defines behavior and functionality for a given PolicyType. +/// +public interface IPolicyValidator +{ + /// + /// The PolicyType that this definition relates to. + /// + public PolicyType Type { get; } + + /// + /// PolicyTypes that must be enabled before this policy can be enabled, if any. + /// These dependencies will be checked when this policy is enabled and when any required policy is disabled. + /// + public IEnumerable RequiredPolicies { get; } + + /// + /// Validates a policy before saving it. + /// Do not use this for simple dependencies between different policies - see instead. + /// Implementation is optional; by default it will not perform any validation. + /// + /// The policy update request + /// The current policy, if any + /// A validation error if validation was unsuccessful, otherwise an empty string + public Task ValidateAsync(PolicyUpdate policyUpdate, Policy? currentPolicy); + + /// + /// Performs side effects after a policy is validated but before it is saved. + /// For example, this can be used to remove non-compliant users from the organization. + /// Implementation is optional; by default it will not perform any side effects. + /// + /// The policy update request + /// The current policy, if any + public Task OnSaveSideEffectsAsync(PolicyUpdate policyUpdate, Policy? currentPolicy); +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/ISavePolicyCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/ISavePolicyCommand.cs new file mode 100644 index 000000000..5bfdfc6aa --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/ISavePolicyCommand.cs @@ -0,0 +1,8 @@ +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies; + +public interface ISavePolicyCommand +{ + Task SaveAsync(PolicyUpdate policy); +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/SavePolicyCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/SavePolicyCommand.cs new file mode 100644 index 000000000..01ffce2cc --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/SavePolicyCommand.cs @@ -0,0 +1,129 @@ +#nullable enable + +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models; +using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.Services; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.Implementations; + +public class SavePolicyCommand : ISavePolicyCommand +{ + private readonly IApplicationCacheService _applicationCacheService; + private readonly IEventService _eventService; + private readonly IPolicyRepository _policyRepository; + private readonly IReadOnlyDictionary _policyValidators; + private readonly TimeProvider _timeProvider; + + public SavePolicyCommand( + IApplicationCacheService applicationCacheService, + IEventService eventService, + IPolicyRepository policyRepository, + IEnumerable policyValidators, + TimeProvider timeProvider) + { + _applicationCacheService = applicationCacheService; + _eventService = eventService; + _policyRepository = policyRepository; + _timeProvider = timeProvider; + + var policyValidatorsDict = new Dictionary(); + foreach (var policyValidator in policyValidators) + { + if (!policyValidatorsDict.TryAdd(policyValidator.Type, policyValidator)) + { + throw new Exception($"Duplicate PolicyValidator for {policyValidator.Type} policy."); + } + } + + _policyValidators = policyValidatorsDict; + } + + public async Task SaveAsync(PolicyUpdate policyUpdate) + { + var org = await _applicationCacheService.GetOrganizationAbilityAsync(policyUpdate.OrganizationId); + if (org == null) + { + throw new BadRequestException("Organization not found"); + } + + if (!org.UsePolicies) + { + throw new BadRequestException("This organization cannot use policies."); + } + + if (_policyValidators.TryGetValue(policyUpdate.Type, out var validator)) + { + await RunValidatorAsync(validator, policyUpdate); + } + + var policy = await _policyRepository.GetByOrganizationIdTypeAsync(policyUpdate.OrganizationId, policyUpdate.Type) + ?? new Policy + { + OrganizationId = policyUpdate.OrganizationId, + Type = policyUpdate.Type, + CreationDate = _timeProvider.GetUtcNow().UtcDateTime + }; + + policy.Enabled = policyUpdate.Enabled; + policy.Data = policyUpdate.Data; + policy.RevisionDate = _timeProvider.GetUtcNow().UtcDateTime; + + await _policyRepository.UpsertAsync(policy); + await _eventService.LogPolicyEventAsync(policy, EventType.Policy_Updated); + } + + private async Task RunValidatorAsync(IPolicyValidator validator, PolicyUpdate policyUpdate) + { + var savedPolicies = await _policyRepository.GetManyByOrganizationIdAsync(policyUpdate.OrganizationId); + // Note: policies may be missing from this dict if they have never been enabled + var savedPoliciesDict = savedPolicies.ToDictionary(p => p.Type); + var currentPolicy = savedPoliciesDict.GetValueOrDefault(policyUpdate.Type); + + // If enabling this policy - check that all policy requirements are satisfied + if (currentPolicy is not { Enabled: true } && policyUpdate.Enabled) + { + var missingRequiredPolicyTypes = validator.RequiredPolicies + .Where(requiredPolicyType => + savedPoliciesDict.GetValueOrDefault(requiredPolicyType) is not { Enabled: true }) + .ToList(); + + if (missingRequiredPolicyTypes.Count != 0) + { + throw new BadRequestException($"Turn on the {missingRequiredPolicyTypes.First().GetName()} policy because it is required for the {validator.Type.GetName()} policy."); + } + } + + // If disabling this policy - ensure it's not required by any other policy + if (currentPolicy is { Enabled: true } && !policyUpdate.Enabled) + { + var dependentPolicyTypes = _policyValidators.Values + .Where(otherValidator => otherValidator.RequiredPolicies.Contains(policyUpdate.Type)) + .Select(otherValidator => otherValidator.Type) + .Where(otherPolicyType => savedPoliciesDict.ContainsKey(otherPolicyType) && + savedPoliciesDict[otherPolicyType].Enabled) + .ToList(); + + switch (dependentPolicyTypes) + { + case { Count: 1 }: + throw new BadRequestException($"Turn off the {dependentPolicyTypes.First().GetName()} policy because it requires the {validator.Type.GetName()} policy."); + case { Count: > 1 }: + throw new BadRequestException($"Turn off all of the policies that require the {validator.Type.GetName()} policy."); + } + } + + // Run other validation + var validationError = await validator.ValidateAsync(policyUpdate, currentPolicy); + if (!string.IsNullOrEmpty(validationError)) + { + throw new BadRequestException(validationError); + } + + // Run side effects + await validator.OnSaveSideEffectsAsync(policyUpdate, currentPolicy); + } +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/Models/PolicyUpdate.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/Models/PolicyUpdate.cs new file mode 100644 index 000000000..117a7ec73 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/Models/PolicyUpdate.cs @@ -0,0 +1,28 @@ +#nullable enable + +using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; +using Bit.Core.Utilities; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models; + +/// +/// A request for SavePolicyCommand to update a policy +/// +public record PolicyUpdate +{ + public Guid OrganizationId { get; set; } + public PolicyType Type { get; set; } + public string? Data { get; set; } + public bool Enabled { get; set; } + + public T GetDataModel() where T : IPolicyDataModel, new() + { + return CoreHelpers.LoadClassFromJsonData(Data); + } + + public void SetDataModel(T dataModel) where T : IPolicyDataModel, new() + { + Data = CoreHelpers.ClassToJsonData(dataModel); + } +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs new file mode 100644 index 000000000..81096ef60 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs @@ -0,0 +1,22 @@ +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Implementations; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators; +using Bit.Core.AdminConsole.Services; +using Bit.Core.AdminConsole.Services.Implementations; +using Microsoft.Extensions.DependencyInjection; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies; + +public static class PolicyServiceCollectionExtensions +{ + public static void AddPolicyServices(this IServiceCollection services) + { + services.AddScoped(); + services.AddScoped(); + + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + } +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/MaximumVaultTimeoutPolicyValidator.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/MaximumVaultTimeoutPolicyValidator.cs new file mode 100644 index 000000000..bfd4dcfe0 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/MaximumVaultTimeoutPolicyValidator.cs @@ -0,0 +1,15 @@ +#nullable enable + +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators; + +public class MaximumVaultTimeoutPolicyValidator : IPolicyValidator +{ + public PolicyType Type => PolicyType.MaximumVaultTimeout; + public IEnumerable RequiredPolicies => [PolicyType.SingleOrg]; + public Task ValidateAsync(PolicyUpdate policyUpdate, Policy? currentPolicy) => Task.FromResult(""); + public Task OnSaveSideEffectsAsync(PolicyUpdate policyUpdate, Policy? currentPolicy) => Task.FromResult(0); +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/PolicyValidatorHelpers.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/PolicyValidatorHelpers.cs new file mode 100644 index 000000000..1bbaf1aa1 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/PolicyValidatorHelpers.cs @@ -0,0 +1,33 @@ +#nullable enable + +using Bit.Core.Auth.Entities; +using Bit.Core.Auth.Enums; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators; + +public static class PolicyValidatorHelpers +{ + /// + /// Validate that given Member Decryption Options are not enabled. + /// Used for validation when disabling a policy that is required by certain Member Decryption Options. + /// + /// The Member Decryption Options that require the policy to be enabled. + /// A validation error if validation was unsuccessful, otherwise an empty string + public static string ValidateDecryptionOptionsNotEnabled(this SsoConfig? ssoConfig, + MemberDecryptionType[] decryptionOptions) + { + if (ssoConfig is not { Enabled: true }) + { + return ""; + } + + return ssoConfig.GetData().MemberDecryptionType switch + { + MemberDecryptionType.KeyConnector when decryptionOptions.Contains(MemberDecryptionType.KeyConnector) + => "Key Connector is enabled and requires this policy.", + MemberDecryptionType.TrustedDeviceEncryption when decryptionOptions.Contains(MemberDecryptionType + .TrustedDeviceEncryption) => "Trusted device encryption is on and requires this policy.", + _ => "" + }; + } +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/RequireSsoPolicyValidator.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/RequireSsoPolicyValidator.cs new file mode 100644 index 000000000..2082d4305 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/RequireSsoPolicyValidator.cs @@ -0,0 +1,38 @@ +#nullable enable + +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models; +using Bit.Core.Auth.Enums; +using Bit.Core.Auth.Repositories; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators; + +public class RequireSsoPolicyValidator : IPolicyValidator +{ + private readonly ISsoConfigRepository _ssoConfigRepository; + + public RequireSsoPolicyValidator(ISsoConfigRepository ssoConfigRepository) + { + _ssoConfigRepository = ssoConfigRepository; + } + + public PolicyType Type => PolicyType.RequireSso; + public IEnumerable RequiredPolicies => [PolicyType.SingleOrg]; + + public async Task ValidateAsync(PolicyUpdate policyUpdate, Policy? currentPolicy) + { + if (policyUpdate is not { Enabled: true }) + { + var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(policyUpdate.OrganizationId); + return ssoConfig.ValidateDecryptionOptionsNotEnabled([ + MemberDecryptionType.KeyConnector, + MemberDecryptionType.TrustedDeviceEncryption + ]); + } + + return ""; + } + + public Task OnSaveSideEffectsAsync(PolicyUpdate policyUpdate, Policy? currentPolicy) => Task.FromResult(0); +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/ResetPasswordPolicyValidator.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/ResetPasswordPolicyValidator.cs new file mode 100644 index 000000000..1126c4b92 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/ResetPasswordPolicyValidator.cs @@ -0,0 +1,36 @@ +#nullable enable + +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models; +using Bit.Core.Auth.Enums; +using Bit.Core.Auth.Repositories; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators; + +public class ResetPasswordPolicyValidator : IPolicyValidator +{ + private readonly ISsoConfigRepository _ssoConfigRepository; + public PolicyType Type => PolicyType.ResetPassword; + public IEnumerable RequiredPolicies => [PolicyType.SingleOrg]; + + public ResetPasswordPolicyValidator(ISsoConfigRepository ssoConfigRepository) + { + _ssoConfigRepository = ssoConfigRepository; + } + + public async Task ValidateAsync(PolicyUpdate policyUpdate, Policy? currentPolicy) + { + if (policyUpdate is not { Enabled: true } || + policyUpdate.GetDataModel().AutoEnrollEnabled == false) + { + var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(policyUpdate.OrganizationId); + return ssoConfig.ValidateDecryptionOptionsNotEnabled([MemberDecryptionType.TrustedDeviceEncryption]); + } + + return ""; + } + + public Task OnSaveSideEffectsAsync(PolicyUpdate policyUpdate, Policy? currentPolicy) => Task.FromResult(0); +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SingleOrgPolicyValidator.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SingleOrgPolicyValidator.cs new file mode 100644 index 000000000..3e1f8d26c --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SingleOrgPolicyValidator.cs @@ -0,0 +1,101 @@ +#nullable enable + +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models; +using Bit.Core.Auth.Enums; +using Bit.Core.Auth.Repositories; +using Bit.Core.Context; +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.Repositories; +using Bit.Core.Services; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators; + +public class SingleOrgPolicyValidator : IPolicyValidator +{ + public PolicyType Type => PolicyType.SingleOrg; + + private readonly IOrganizationUserRepository _organizationUserRepository; + private readonly IMailService _mailService; + private readonly IOrganizationRepository _organizationRepository; + private readonly ISsoConfigRepository _ssoConfigRepository; + private readonly ICurrentContext _currentContext; + private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand; + + public SingleOrgPolicyValidator( + IOrganizationUserRepository organizationUserRepository, + IMailService mailService, + IOrganizationRepository organizationRepository, + ISsoConfigRepository ssoConfigRepository, + ICurrentContext currentContext, + IRemoveOrganizationUserCommand removeOrganizationUserCommand) + { + _organizationUserRepository = organizationUserRepository; + _mailService = mailService; + _organizationRepository = organizationRepository; + _ssoConfigRepository = ssoConfigRepository; + _currentContext = currentContext; + _removeOrganizationUserCommand = removeOrganizationUserCommand; + } + + public IEnumerable RequiredPolicies => []; + + public async Task OnSaveSideEffectsAsync(PolicyUpdate policyUpdate, Policy? currentPolicy) + { + if (currentPolicy is not { Enabled: true } && policyUpdate is { Enabled: true }) + { + await RemoveNonCompliantUsersAsync(policyUpdate.OrganizationId); + } + } + + private async Task RemoveNonCompliantUsersAsync(Guid organizationId) + { + // Remove non-compliant users + var savingUserId = _currentContext.UserId; + // Note: must get OrganizationUserUserDetails so that Email is always populated from the User object + var orgUsers = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(organizationId); + var org = await _organizationRepository.GetByIdAsync(organizationId); + if (org == null) + { + throw new NotFoundException("Organization not found."); + } + + var removableOrgUsers = orgUsers.Where(ou => + ou.Status != OrganizationUserStatusType.Invited && + ou.Status != OrganizationUserStatusType.Revoked && + ou.Type != OrganizationUserType.Owner && + ou.Type != OrganizationUserType.Admin && + ou.UserId != savingUserId + ).ToList(); + + var userOrgs = await _organizationUserRepository.GetManyByManyUsersAsync( + removableOrgUsers.Select(ou => ou.UserId!.Value)); + foreach (var orgUser in removableOrgUsers) + { + if (userOrgs.Any(ou => ou.UserId == orgUser.UserId + && ou.OrganizationId != org.Id + && ou.Status != OrganizationUserStatusType.Invited)) + { + await _removeOrganizationUserCommand.RemoveUserAsync(organizationId, orgUser.Id, + savingUserId); + + await _mailService.SendOrganizationUserRemovedForPolicySingleOrgEmailAsync( + org.DisplayName(), orgUser.Email); + } + } + } + + public async Task ValidateAsync(PolicyUpdate policyUpdate, Policy? currentPolicy) + { + if (policyUpdate is not { Enabled: true }) + { + var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(policyUpdate.OrganizationId); + return ssoConfig.ValidateDecryptionOptionsNotEnabled([MemberDecryptionType.KeyConnector]); + } + + return ""; + } +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/TwoFactorAuthenticationPolicyValidator.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/TwoFactorAuthenticationPolicyValidator.cs new file mode 100644 index 000000000..ef896bbb9 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/TwoFactorAuthenticationPolicyValidator.cs @@ -0,0 +1,87 @@ +#nullable enable + +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models; +using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; +using Bit.Core.Context; +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.Repositories; +using Bit.Core.Services; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators; + +public class TwoFactorAuthenticationPolicyValidator : IPolicyValidator +{ + private readonly IOrganizationUserRepository _organizationUserRepository; + private readonly IMailService _mailService; + private readonly IOrganizationRepository _organizationRepository; + private readonly ICurrentContext _currentContext; + private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery; + private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand; + + public PolicyType Type => PolicyType.TwoFactorAuthentication; + public IEnumerable RequiredPolicies => []; + + public TwoFactorAuthenticationPolicyValidator( + IOrganizationUserRepository organizationUserRepository, + IMailService mailService, + IOrganizationRepository organizationRepository, + ICurrentContext currentContext, + ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery, + IRemoveOrganizationUserCommand removeOrganizationUserCommand) + { + _organizationUserRepository = organizationUserRepository; + _mailService = mailService; + _organizationRepository = organizationRepository; + _currentContext = currentContext; + _twoFactorIsEnabledQuery = twoFactorIsEnabledQuery; + _removeOrganizationUserCommand = removeOrganizationUserCommand; + } + + public async Task OnSaveSideEffectsAsync(PolicyUpdate policyUpdate, Policy? currentPolicy) + { + if (currentPolicy is not { Enabled: true } && policyUpdate is { Enabled: true }) + { + await RemoveNonCompliantUsersAsync(policyUpdate.OrganizationId); + } + } + + private async Task RemoveNonCompliantUsersAsync(Guid organizationId) + { + var org = await _organizationRepository.GetByIdAsync(organizationId); + var savingUserId = _currentContext.UserId; + + var orgUsers = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(organizationId); + var organizationUsersTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(orgUsers); + var removableOrgUsers = orgUsers.Where(ou => + ou.Status != OrganizationUserStatusType.Invited && ou.Status != OrganizationUserStatusType.Revoked && + ou.Type != OrganizationUserType.Owner && ou.Type != OrganizationUserType.Admin && + ou.UserId != savingUserId); + + // Reorder by HasMasterPassword to prioritize checking users without a master if they have 2FA enabled + foreach (var orgUser in removableOrgUsers.OrderBy(ou => ou.HasMasterPassword)) + { + var userTwoFactorEnabled = organizationUsersTwoFactorEnabled.FirstOrDefault(u => u.user.Id == orgUser.Id) + .twoFactorIsEnabled; + if (!userTwoFactorEnabled) + { + if (!orgUser.HasMasterPassword) + { + throw new BadRequestException( + "Policy could not be enabled. Non-compliant members will lose access to their accounts. Identify members without two-step login from the policies column in the members page."); + } + + await _removeOrganizationUserCommand.RemoveUserAsync(organizationId, orgUser.Id, + savingUserId); + + await _mailService.SendOrganizationUserRemovedForPolicyTwoStepEmailAsync( + org!.DisplayName(), orgUser.Email); + } + } + } + + public Task ValidateAsync(PolicyUpdate policyUpdate, Policy? currentPolicy) => Task.FromResult(""); +} 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/IOrganizationRepository.cs b/src/Core/AdminConsole/Repositories/IOrganizationRepository.cs index 9c14c4fbd..5b274d3f8 100644 --- a/src/Core/AdminConsole/Repositories/IOrganizationRepository.cs +++ b/src/Core/AdminConsole/Repositories/IOrganizationRepository.cs @@ -19,7 +19,7 @@ public interface IOrganizationRepository : IRepository Task> GetOwnerEmailAddressesById(Guid organizationId); /// - /// Gets the organization that has a claimed domain matching the user's email domain. + /// Gets the organizations that have a verified domain matching the user's email domain. /// - Task GetByClaimedUserDomainAsync(Guid userId); + Task> GetByVerifiedUserEmailDomainAsync(Guid userId); } 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/IOrganizationService.cs b/src/Core/AdminConsole/Services/IOrganizationService.cs index aaa2f86c8..646ae6616 100644 --- a/src/Core/AdminConsole/Services/IOrganizationService.cs +++ b/src/Core/AdminConsole/Services/IOrganizationService.cs @@ -49,32 +49,21 @@ public interface IOrganizationService IEnumerable<(OrganizationUserInvite invite, string externalId)> invites); Task>> ResendInvitesAsync(Guid organizationId, Guid? invitingUserId, IEnumerable organizationUsersId); Task ResendInviteAsync(Guid organizationId, Guid? invitingUserId, Guid organizationUserId, bool initOrganization = false); - Task ConfirmUserAsync(Guid organizationId, Guid organizationUserId, string key, - Guid confirmingUserId, IUserService userService); + Task ConfirmUserAsync(Guid organizationId, Guid organizationUserId, string key, Guid confirmingUserId); Task>> ConfirmUsersAsync(Guid organizationId, Dictionary keys, - Guid confirmingUserId, IUserService userService); - Task>> ConfirmUsersAsync_vNext(Guid organizationId, Dictionary keys, Guid confirmingUserId); - [Obsolete("IRemoveOrganizationUserCommand should be used instead. To be removed by EC-607.")] - Task RemoveUserAsync(Guid organizationId, Guid organizationUserId, Guid? deletingUserId); - [Obsolete("IRemoveOrganizationUserCommand should be used instead. To be removed by EC-607.")] - Task RemoveUserAsync(Guid organizationId, Guid organizationUserId, EventSystemUser systemUser); - Task RemoveUserAsync(Guid organizationId, Guid userId); - Task>> RemoveUsersAsync(Guid organizationId, - IEnumerable organizationUserIds, Guid? deletingUserId); Task UpdateUserResetPasswordEnrollmentAsync(Guid organizationId, Guid userId, string resetPasswordKey, Guid? callingUserId); Task ImportAsync(Guid organizationId, IEnumerable groups, IEnumerable newUsers, IEnumerable removeUserExternalIds, bool overwriteExisting, EventSystemUser eventSystemUser); Task DeleteSsoUserAsync(Guid userId, Guid? organizationId); Task UpdateOrganizationKeysAsync(Guid orgId, string publicKey, string privateKey); - Task HasConfirmedOwnersExceptAsync(Guid organizationId, IEnumerable organizationUsersId, bool includeProvider = true); Task RevokeUserAsync(OrganizationUser organizationUser, Guid? revokingUserId); Task RevokeUserAsync(OrganizationUser organizationUser, EventSystemUser systemUser); Task>> RevokeUsersAsync(Guid organizationId, IEnumerable organizationUserIds, Guid? revokingUserId); - Task RestoreUserAsync(OrganizationUser organizationUser, Guid? restoringUserId, IUserService userService); - Task RestoreUserAsync(OrganizationUser organizationUser, EventSystemUser systemUser, IUserService userService); + Task RestoreUserAsync(OrganizationUser organizationUser, Guid? restoringUserId); + Task RestoreUserAsync(OrganizationUser organizationUser, EventSystemUser systemUser); Task>> RestoreUsersAsync(Guid organizationId, IEnumerable organizationUserIds, Guid? restoringUserId, IUserService userService); Task CreatePendingOrganization(Organization organization, string ownerEmail, ClaimsPrincipal user, IUserService userService, bool salesAssistedTrialStarted); diff --git a/src/Core/AdminConsole/Services/IPolicyService.cs b/src/Core/AdminConsole/Services/IPolicyService.cs index e2f2fa794..16ff2f4fa 100644 --- a/src/Core/AdminConsole/Services/IPolicyService.cs +++ b/src/Core/AdminConsole/Services/IPolicyService.cs @@ -4,14 +4,12 @@ using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Models.Data.Organizations.OrganizationUsers; -using Bit.Core.Services; namespace Bit.Core.AdminConsole.Services; public interface IPolicyService { - Task SaveAsync(Policy policy, IUserService userService, IOrganizationService organizationService, - Guid? savingUserId); + Task SaveAsync(Policy policy, Guid? savingUserId); /// /// Get the combined master password policy options for the specified user. diff --git a/src/Core/AdminConsole/Services/Implementations/OrganizationDomainService.cs b/src/Core/AdminConsole/Services/Implementations/OrganizationDomainService.cs index b526f27d9..4ce33f3b5 100644 --- a/src/Core/AdminConsole/Services/Implementations/OrganizationDomainService.cs +++ b/src/Core/AdminConsole/Services/Implementations/OrganizationDomainService.cs @@ -1,4 +1,5 @@ -using Bit.Core.Enums; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces; +using Bit.Core.Enums; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; @@ -10,26 +11,29 @@ public class OrganizationDomainService : IOrganizationDomainService { private readonly IOrganizationDomainRepository _domainRepository; private readonly IOrganizationUserRepository _organizationUserRepository; - private readonly IDnsResolverService _dnsResolverService; private readonly IEventService _eventService; private readonly IMailService _mailService; + private readonly IVerifyOrganizationDomainCommand _verifyOrganizationDomainCommand; + private readonly TimeProvider _timeProvider; private readonly ILogger _logger; private readonly IGlobalSettings _globalSettings; public OrganizationDomainService( IOrganizationDomainRepository domainRepository, IOrganizationUserRepository organizationUserRepository, - IDnsResolverService dnsResolverService, IEventService eventService, IMailService mailService, + IVerifyOrganizationDomainCommand verifyOrganizationDomainCommand, + TimeProvider timeProvider, ILogger logger, IGlobalSettings globalSettings) { _domainRepository = domainRepository; _organizationUserRepository = organizationUserRepository; - _dnsResolverService = dnsResolverService; _eventService = eventService; _mailService = mailService; + _verifyOrganizationDomainCommand = verifyOrganizationDomainCommand; + _timeProvider = timeProvider; _logger = logger; _globalSettings = globalSettings; } @@ -37,7 +41,7 @@ public class OrganizationDomainService : IOrganizationDomainService public async Task ValidateOrganizationsDomainAsync() { //Date should be set 1 hour behind to ensure it selects all domains that should be verified - var runDate = DateTime.UtcNow.AddHours(-1); + var runDate = _timeProvider.GetUtcNow().UtcDateTime.AddHours(-1); var verifiableDomains = await _domainRepository.GetManyByNextRunDateAsync(runDate); @@ -45,43 +49,17 @@ public class OrganizationDomainService : IOrganizationDomainService foreach (var domain in verifiableDomains) { + _logger.LogInformation(Constants.BypassFiltersEventId, + "Attempting verification for organization {OrgId} with domain {Domain}", + domain.OrganizationId, + domain.DomainName); + try { - _logger.LogInformation(Constants.BypassFiltersEventId, "Attempting verification for organization {OrgId} with domain {Domain}", domain.OrganizationId, domain.DomainName); - - var status = await _dnsResolverService.ResolveAsync(domain.DomainName, domain.Txt); - if (status) - { - _logger.LogInformation(Constants.BypassFiltersEventId, "Successfully validated domain"); - - // Update entry on OrganizationDomain table - domain.SetLastCheckedDate(); - domain.SetVerifiedDate(); - domain.SetJobRunCount(); - await _domainRepository.ReplaceAsync(domain); - - await _eventService.LogOrganizationDomainEventAsync(domain, EventType.OrganizationDomain_Verified, - EventSystemUser.DomainVerification); - } - else - { - // Update entry on OrganizationDomain table - domain.SetLastCheckedDate(); - domain.SetJobRunCount(); - domain.SetNextRunDate(_globalSettings.DomainVerification.VerificationInterval); - await _domainRepository.ReplaceAsync(domain); - - await _eventService.LogOrganizationDomainEventAsync(domain, EventType.OrganizationDomain_NotVerified, - EventSystemUser.DomainVerification); - _logger.LogInformation(Constants.BypassFiltersEventId, "Verification for organization {OrgId} with domain {Domain} failed", - domain.OrganizationId, domain.DomainName); - } + _ = await _verifyOrganizationDomainCommand.SystemVerifyOrganizationDomainAsync(domain); } catch (Exception ex) { - // Update entry on OrganizationDomain table - domain.SetLastCheckedDate(); - domain.SetJobRunCount(); domain.SetNextRunDate(_globalSettings.DomainVerification.VerificationInterval); await _domainRepository.ReplaceAsync(domain); @@ -128,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/OrganizationService.cs b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs index 5c3f81cee..f44ce686f 100644 --- a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs +++ b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs @@ -73,6 +73,7 @@ public class OrganizationService : IOrganizationService private readonly IFeatureService _featureService; private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery; private readonly IOrganizationBillingService _organizationBillingService; + private readonly IHasConfirmedOwnersExceptQuery _hasConfirmedOwnersExceptQuery; public OrganizationService( IOrganizationRepository organizationRepository, @@ -107,7 +108,8 @@ public class OrganizationService : IOrganizationService IProviderRepository providerRepository, IFeatureService featureService, ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery, - IOrganizationBillingService organizationBillingService) + IOrganizationBillingService organizationBillingService, + IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery) { _organizationRepository = organizationRepository; _organizationUserRepository = organizationUserRepository; @@ -142,6 +144,7 @@ public class OrganizationService : IOrganizationService _featureService = featureService; _twoFactorIsEnabledQuery = twoFactorIsEnabledQuery; _organizationBillingService = organizationBillingService; + _hasConfirmedOwnersExceptQuery = hasConfirmedOwnersExceptQuery; } public async Task ReplacePaymentMethodAsync(Guid organizationId, string paymentToken, @@ -705,10 +708,16 @@ public class OrganizationService : IOrganizationService UseSecretsManager = license.UseSecretsManager, SmSeats = license.SmSeats, SmServiceAccounts = license.SmServiceAccounts, - LimitCollectionCreationDeletion = license.LimitCollectionCreationDeletion, - AllowAdminAccessToAllCollectionItems = license.AllowAdminAccessToAllCollectionItems, }; + // These fields are being removed from consideration when processing + // licenses. + if (!_featureService.IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit)) + { + organization.LimitCollectionCreationDeletion = license.LimitCollectionCreationDeletion; + organization.AllowAdminAccessToAllCollectionItems = license.AllowAdminAccessToAllCollectionItems; + } + var result = await SignUpAsync(organization, owner.Id, ownerKey, collectionName, false); var dir = $"{_globalSettings.LicenseDirectory}/organization"; @@ -1074,7 +1083,7 @@ public class OrganizationService : IOrganizationService } var invitedAreAllOwners = invites.All(i => i.invite.Type == OrganizationUserType.Owner); - if (!invitedAreAllOwners && !await HasConfirmedOwnersExceptAsync(organizationId, new Guid[] { }, includeProvider: true)) + if (!invitedAreAllOwners && !await _hasConfirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync(organizationId, new Guid[] { }, includeProvider: true)) { throw new BadRequestException("Organization must have at least one confirmed owner."); } @@ -1323,13 +1332,12 @@ public class OrganizationService : IOrganizationService } public async Task ConfirmUserAsync(Guid organizationId, Guid organizationUserId, string key, - Guid confirmingUserId, IUserService userService) + Guid confirmingUserId) { - var result = _featureService.IsEnabled(FeatureFlagKeys.MembersTwoFAQueryOptimization) - ? await ConfirmUsersAsync_vNext(organizationId, new Dictionary() { { organizationUserId, key } }, - confirmingUserId) - : await ConfirmUsersAsync(organizationId, new Dictionary() { { organizationUserId, key } }, - confirmingUserId, userService); + var result = await ConfirmUsersAsync( + organizationId, + new Dictionary() { { organizationUserId, key } }, + confirmingUserId); if (!result.Any()) { @@ -1345,75 +1353,6 @@ public class OrganizationService : IOrganizationService } public async Task>> ConfirmUsersAsync(Guid organizationId, Dictionary keys, - Guid confirmingUserId, IUserService userService) - { - var organizationUsers = await _organizationUserRepository.GetManyAsync(keys.Keys); - var validOrganizationUsers = organizationUsers - .Where(u => u.Status == OrganizationUserStatusType.Accepted && u.OrganizationId == organizationId && u.UserId != null) - .ToList(); - - if (!validOrganizationUsers.Any()) - { - return new List>(); - } - - var validOrganizationUserIds = validOrganizationUsers.Select(u => u.UserId.Value).ToList(); - - var organization = await GetOrgById(organizationId); - var usersOrgs = await _organizationUserRepository.GetManyByManyUsersAsync(validOrganizationUserIds); - var users = await _userRepository.GetManyAsync(validOrganizationUserIds); - - var keyedFilteredUsers = validOrganizationUsers.ToDictionary(u => u.UserId.Value, u => u); - var keyedOrganizationUsers = usersOrgs.GroupBy(u => u.UserId.Value) - .ToDictionary(u => u.Key, u => u.ToList()); - - var succeededUsers = new List(); - var result = new List>(); - - foreach (var user in users) - { - if (!keyedFilteredUsers.ContainsKey(user.Id)) - { - continue; - } - var orgUser = keyedFilteredUsers[user.Id]; - var orgUsers = keyedOrganizationUsers.GetValueOrDefault(user.Id, new List()); - try - { - if (organization.PlanType == PlanType.Free && (orgUser.Type == OrganizationUserType.Admin - || orgUser.Type == OrganizationUserType.Owner)) - { - // Since free organizations only supports a few users there is not much point in avoiding N+1 queries for this. - var adminCount = await _organizationUserRepository.GetCountByFreeOrganizationAdminUserAsync(user.Id); - if (adminCount > 0) - { - throw new BadRequestException("User can only be an admin of one free organization."); - } - } - - await CheckPolicies(organizationId, user, orgUsers, userService); - orgUser.Status = OrganizationUserStatusType.Confirmed; - orgUser.Key = keys[orgUser.Id]; - orgUser.Email = null; - - await _eventService.LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_Confirmed); - await _mailService.SendOrganizationConfirmedEmailAsync(organization.DisplayName(), user.Email, orgUser.AccessSecretsManager); - await DeleteAndPushUserRegistrationAsync(organizationId, user.Id); - succeededUsers.Add(orgUser); - result.Add(Tuple.Create(orgUser, "")); - } - catch (BadRequestException e) - { - result.Add(Tuple.Create(orgUser, e.Message)); - } - } - - await _organizationUserRepository.ReplaceManyAsync(succeededUsers); - - return result; - } - - public async Task>> ConfirmUsersAsync_vNext(Guid organizationId, Dictionary keys, Guid confirmingUserId) { var selectedOrganizationUsers = await _organizationUserRepository.GetManyAsync(keys.Keys); @@ -1430,7 +1369,7 @@ public class OrganizationService : IOrganizationService var organization = await GetOrgById(organizationId); var allUsersOrgs = await _organizationUserRepository.GetManyByManyUsersAsync(validSelectedUserIds); - var users = await _userRepository.GetManyWithCalculatedPremiumAsync(validSelectedUserIds); + var users = await _userRepository.GetManyAsync(validSelectedUserIds); var usersTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(validSelectedUserIds); var keyedFilteredUsers = validSelectedOrganizationUsers.ToDictionary(u => u.UserId.Value, u => u); @@ -1462,7 +1401,7 @@ public class OrganizationService : IOrganizationService } var twoFactorEnabled = usersTwoFactorEnabled.FirstOrDefault(tuple => tuple.userId == user.Id).twoFactorIsEnabled; - await CheckPolicies_vNext(organizationId, user, orgUsers, twoFactorEnabled); + await CheckPoliciesAsync(organizationId, user, orgUsers, twoFactorEnabled); orgUser.Status = OrganizationUserStatusType.Confirmed; orgUser.Key = keys[orgUser.Id]; orgUser.Email = null; @@ -1567,33 +1506,7 @@ public class OrganizationService : IOrganizationService } } - private async Task CheckPolicies(Guid organizationId, User user, - ICollection userOrgs, IUserService userService) - { - // Enforce Two Factor Authentication Policy for this organization - var orgRequiresTwoFactor = (await _policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication)).Any(p => p.OrganizationId == organizationId); - if (orgRequiresTwoFactor && !await userService.TwoFactorIsEnabledAsync(user)) - { - throw new BadRequestException("User does not have two-step login enabled."); - } - - var hasOtherOrgs = userOrgs.Any(ou => ou.OrganizationId != organizationId); - var singleOrgPolicies = await _policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.SingleOrg); - var otherSingleOrgPolicies = - singleOrgPolicies.Where(p => p.OrganizationId != organizationId); - // Enforce Single Organization Policy for this organization - if (hasOtherOrgs && singleOrgPolicies.Any(p => p.OrganizationId == organizationId)) - { - throw new BadRequestException("Cannot confirm this member to the organization until they leave or remove all other organizations."); - } - // Enforce Single Organization Policy of other organizations user is a member of - if (otherSingleOrgPolicies.Any()) - { - throw new BadRequestException("Cannot confirm this member to the organization because they are in another organization which forbids it."); - } - } - - private async Task CheckPolicies_vNext(Guid organizationId, UserWithCalculatedPremium user, + private async Task CheckPoliciesAsync(Guid organizationId, User user, ICollection userOrgs, bool twoFactorEnabled) { // Enforce Two Factor Authentication Policy for this organization @@ -1620,149 +1533,6 @@ public class OrganizationService : IOrganizationService } } - [Obsolete("IRemoveOrganizationUserCommand should be used instead. To be removed by EC-607.")] - public async Task RemoveUserAsync(Guid organizationId, Guid organizationUserId, Guid? deletingUserId) - { - var orgUser = await RepositoryDeleteUserAsync(organizationId, organizationUserId, deletingUserId); - await _eventService.LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_Removed); - } - - [Obsolete("IRemoveOrganizationUserCommand should be used instead. To be removed by EC-607.")] - public async Task RemoveUserAsync(Guid organizationId, Guid organizationUserId, - EventSystemUser systemUser) - { - var orgUser = await RepositoryDeleteUserAsync(organizationId, organizationUserId, null); - await _eventService.LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_Removed, systemUser); - } - - private async Task RepositoryDeleteUserAsync(Guid organizationId, Guid organizationUserId, Guid? deletingUserId) - { - var orgUser = await _organizationUserRepository.GetByIdAsync(organizationUserId); - if (orgUser == null || orgUser.OrganizationId != organizationId) - { - throw new BadRequestException("User not valid."); - } - - if (deletingUserId.HasValue && orgUser.UserId == deletingUserId.Value) - { - throw new BadRequestException("You cannot remove yourself."); - } - - if (orgUser.Type == OrganizationUserType.Owner && deletingUserId.HasValue && - !await _currentContext.OrganizationOwner(organizationId)) - { - throw new BadRequestException("Only owners can delete other owners."); - } - - if (!await HasConfirmedOwnersExceptAsync(organizationId, new[] { organizationUserId }, includeProvider: true)) - { - throw new BadRequestException("Organization must have at least one confirmed owner."); - } - - await _organizationUserRepository.DeleteAsync(orgUser); - - if (orgUser.UserId.HasValue) - { - await DeleteAndPushUserRegistrationAsync(organizationId, orgUser.UserId.Value); - } - - return orgUser; - } - - public async Task RemoveUserAsync(Guid organizationId, Guid userId) - { - var orgUser = await _organizationUserRepository.GetByOrganizationAsync(organizationId, userId); - if (orgUser == null) - { - throw new NotFoundException(); - } - - if (!await HasConfirmedOwnersExceptAsync(organizationId, new[] { orgUser.Id })) - { - throw new BadRequestException("Organization must have at least one confirmed owner."); - } - - await _organizationUserRepository.DeleteAsync(orgUser); - await _eventService.LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_Removed); - - if (orgUser.UserId.HasValue) - { - await DeleteAndPushUserRegistrationAsync(organizationId, orgUser.UserId.Value); - } - } - - public async Task>> RemoveUsersAsync(Guid organizationId, - IEnumerable organizationUsersId, - Guid? deletingUserId) - { - var orgUsers = await _organizationUserRepository.GetManyAsync(organizationUsersId); - var filteredUsers = orgUsers.Where(u => u.OrganizationId == organizationId) - .ToList(); - - if (!filteredUsers.Any()) - { - throw new BadRequestException("Users invalid."); - } - - if (!await HasConfirmedOwnersExceptAsync(organizationId, organizationUsersId)) - { - throw new BadRequestException("Organization must have at least one confirmed owner."); - } - - var deletingUserIsOwner = false; - if (deletingUserId.HasValue) - { - deletingUserIsOwner = await _currentContext.OrganizationOwner(organizationId); - } - - var result = new List>(); - var deletedUserIds = new List(); - foreach (var orgUser in filteredUsers) - { - try - { - if (deletingUserId.HasValue && orgUser.UserId == deletingUserId) - { - throw new BadRequestException("You cannot remove yourself."); - } - - if (orgUser.Type == OrganizationUserType.Owner && deletingUserId.HasValue && !deletingUserIsOwner) - { - throw new BadRequestException("Only owners can delete other owners."); - } - - await _eventService.LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_Removed); - - if (orgUser.UserId.HasValue) - { - await DeleteAndPushUserRegistrationAsync(organizationId, orgUser.UserId.Value); - } - result.Add(Tuple.Create(orgUser, "")); - deletedUserIds.Add(orgUser.Id); - } - catch (BadRequestException e) - { - result.Add(Tuple.Create(orgUser, e.Message)); - } - - await _organizationUserRepository.DeleteManyAsync(deletedUserIds); - } - - return result; - } - - public async Task HasConfirmedOwnersExceptAsync(Guid organizationId, IEnumerable organizationUsersId, bool includeProvider = true) - { - var confirmedOwners = await GetConfirmedOwnersAsync(organizationId); - var confirmedOwnersIds = confirmedOwners.Select(u => u.Id); - bool hasOtherOwner = confirmedOwnersIds.Except(organizationUsersId).Any(); - if (!hasOtherOwner && includeProvider) - { - return (await _providerUserRepository.GetManyByOrganizationAsync(organizationId, ProviderUserStatusType.Confirmed)).Any(); - } - return hasOtherOwner; - } - public async Task UpdateUserResetPasswordEnrollmentAsync(Guid organizationId, Guid userId, string resetPasswordKey, Guid? callingUserId) { // Org User must be the same as the calling user and the organization ID associated with the user must match passed org ID @@ -2059,13 +1829,6 @@ public class OrganizationService : IOrganizationService await _groupRepository.UpdateUsersAsync(group.Id, users); } - private async Task> GetConfirmedOwnersAsync(Guid organizationId) - { - var owners = await _organizationUserRepository.GetManyByOrganizationAsync(organizationId, - OrganizationUserType.Owner); - return owners.Where(o => o.Status == OrganizationUserStatusType.Confirmed); - } - private async Task DeleteAndPushUserRegistrationAsync(Guid organizationId, Guid userId) { var devices = await GetUserDeviceIdsAsync(userId); @@ -2075,12 +1838,12 @@ public class OrganizationService : IOrganizationService } - private async Task>> GetUserDeviceIdsAsync(Guid userId) + private async Task> GetUserDeviceIdsAsync(Guid userId) { var devices = await _deviceRepository.GetManyByUserIdAsync(userId); return devices .Where(d => !string.IsNullOrWhiteSpace(d.PushToken)) - .Select(d => new KeyValuePair(d.Id.ToString(), d.Type)); + .Select(d => d.Id.ToString()); } public async Task ReplaceAndUpdateCacheAsync(Organization org, EventType? orgEvent = null) @@ -2370,7 +2133,7 @@ public class OrganizationService : IOrganizationService throw new BadRequestException("Already revoked."); } - if (!await HasConfirmedOwnersExceptAsync(organizationUser.OrganizationId, new[] { organizationUser.Id }, includeProvider: true)) + if (!await _hasConfirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync(organizationUser.OrganizationId, new[] { organizationUser.Id }, includeProvider: true)) { throw new BadRequestException("Organization must have at least one confirmed owner."); } @@ -2391,7 +2154,7 @@ public class OrganizationService : IOrganizationService throw new BadRequestException("Users invalid."); } - if (!await HasConfirmedOwnersExceptAsync(organizationId, organizationUserIds)) + if (!await _hasConfirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync(organizationId, organizationUserIds)) { throw new BadRequestException("Organization must have at least one confirmed owner."); } @@ -2438,8 +2201,7 @@ public class OrganizationService : IOrganizationService return result; } - public async Task RestoreUserAsync(OrganizationUser organizationUser, Guid? restoringUserId, - IUserService userService) + public async Task RestoreUserAsync(OrganizationUser organizationUser, Guid? restoringUserId) { if (restoringUserId.HasValue && organizationUser.UserId == restoringUserId.Value) { @@ -2452,18 +2214,17 @@ public class OrganizationService : IOrganizationService throw new BadRequestException("Only owners can restore other owners."); } - await RepositoryRestoreUserAsync(organizationUser, userService); + await RepositoryRestoreUserAsync(organizationUser); await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Restored); } - public async Task RestoreUserAsync(OrganizationUser organizationUser, EventSystemUser systemUser, - IUserService userService) + public async Task RestoreUserAsync(OrganizationUser organizationUser, EventSystemUser systemUser) { - await RepositoryRestoreUserAsync(organizationUser, userService); + await RepositoryRestoreUserAsync(organizationUser); await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Restored, systemUser); } - private async Task RepositoryRestoreUserAsync(OrganizationUser organizationUser, IUserService userService) + private async Task RepositoryRestoreUserAsync(OrganizationUser organizationUser) { if (organizationUser.Status != OrganizationUserStatusType.Revoked) { @@ -2478,21 +2239,14 @@ public class OrganizationService : IOrganizationService await AutoAddSeatsAsync(organization, 1); } - if (_featureService.IsEnabled(FeatureFlagKeys.MembersTwoFAQueryOptimization)) + var userTwoFactorIsEnabled = false; + // Only check Two Factor Authentication status if the user is linked to a user account + if (organizationUser.UserId.HasValue) { - var userTwoFactorIsEnabled = false; - // Only check Two Factor Authentication status if the user is linked to a user account - if (organizationUser.UserId.HasValue) - { - userTwoFactorIsEnabled = (await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(new[] { organizationUser.UserId.Value })).FirstOrDefault().twoFactorIsEnabled; - } + userTwoFactorIsEnabled = (await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(new[] { organizationUser.UserId.Value })).FirstOrDefault().twoFactorIsEnabled; + } - await CheckPoliciesBeforeRestoreAsync_vNext(organizationUser, userTwoFactorIsEnabled); - } - else - { - await CheckPoliciesBeforeRestoreAsync(organizationUser, userService); - } + await CheckPoliciesBeforeRestoreAsync(organizationUser, userTwoFactorIsEnabled); var status = GetPriorActiveOrganizationUserStatusType(organizationUser); @@ -2526,11 +2280,7 @@ public class OrganizationService : IOrganizationService // Query Two Factor Authentication status for all users in the organization // This is an optimization to avoid querying the Two Factor Authentication status for each user individually - IEnumerable<(Guid userId, bool twoFactorIsEnabled)> organizationUsersTwoFactorEnabled = null; - if (_featureService.IsEnabled(FeatureFlagKeys.MembersTwoFAQueryOptimization)) - { - organizationUsersTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(filteredUsers.Select(ou => ou.UserId.Value)); - } + var organizationUsersTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(filteredUsers.Select(ou => ou.UserId.Value)); var result = new List>(); @@ -2553,15 +2303,8 @@ public class OrganizationService : IOrganizationService throw new BadRequestException("Only owners can restore other owners."); } - if (_featureService.IsEnabled(FeatureFlagKeys.MembersTwoFAQueryOptimization)) - { - var twoFactorIsEnabled = organizationUsersTwoFactorEnabled.FirstOrDefault(ou => ou.userId == organizationUser.UserId.Value).twoFactorIsEnabled; - await CheckPoliciesBeforeRestoreAsync_vNext(organizationUser, twoFactorIsEnabled); - } - else - { - await CheckPoliciesBeforeRestoreAsync(organizationUser, userService); - } + var twoFactorIsEnabled = organizationUsersTwoFactorEnabled.FirstOrDefault(ou => ou.userId == organizationUser.UserId.Value).twoFactorIsEnabled; + await CheckPoliciesBeforeRestoreAsync(organizationUser, twoFactorIsEnabled); var status = GetPriorActiveOrganizationUserStatusType(organizationUser); @@ -2580,54 +2323,7 @@ public class OrganizationService : IOrganizationService return result; } - private async Task CheckPoliciesBeforeRestoreAsync(OrganizationUser orgUser, IUserService userService) - { - // An invited OrganizationUser isn't linked with a user account yet, so these checks are irrelevant - // The user will be subject to the same checks when they try to accept the invite - if (GetPriorActiveOrganizationUserStatusType(orgUser) == OrganizationUserStatusType.Invited) - { - return; - } - - var userId = orgUser.UserId.Value; - - // Enforce Single Organization Policy of organization user is being restored to - var allOrgUsers = await _organizationUserRepository.GetManyByUserAsync(userId); - var hasOtherOrgs = allOrgUsers.Any(ou => ou.OrganizationId != orgUser.OrganizationId); - var singleOrgPoliciesApplyingToRevokedUsers = await _policyService.GetPoliciesApplicableToUserAsync(userId, - PolicyType.SingleOrg, OrganizationUserStatusType.Revoked); - var singleOrgPolicyApplies = singleOrgPoliciesApplyingToRevokedUsers.Any(p => p.OrganizationId == orgUser.OrganizationId); - - if (hasOtherOrgs && singleOrgPolicyApplies) - { - throw new BadRequestException("You cannot restore this user until " + - "they leave or remove all other organizations."); - } - - // Enforce Single Organization Policy of other organizations user is a member of - var anySingleOrgPolicies = await _policyService.AnyPoliciesApplicableToUserAsync(userId, - PolicyType.SingleOrg); - if (anySingleOrgPolicies) - { - throw new BadRequestException("You cannot restore this user because they are a member of " + - "another organization which forbids it"); - } - - // Enforce Two Factor Authentication Policy of organization user is trying to join - var user = await _userRepository.GetByIdAsync(userId); - if (!await userService.TwoFactorIsEnabledAsync(user)) - { - var invitedTwoFactorPolicies = await _policyService.GetPoliciesApplicableToUserAsync(userId, - PolicyType.TwoFactorAuthentication, OrganizationUserStatusType.Invited); - if (invitedTwoFactorPolicies.Any(p => p.OrganizationId == orgUser.OrganizationId)) - { - throw new BadRequestException("You cannot restore this user until they enable " + - "two-step login on their user account."); - } - } - } - - private async Task CheckPoliciesBeforeRestoreAsync_vNext(OrganizationUser orgUser, bool userHasTwoFactorEnabled) + private async Task CheckPoliciesBeforeRestoreAsync(OrganizationUser orgUser, bool userHasTwoFactorEnabled) { // An invited OrganizationUser isn't linked with a user account yet, so these checks are irrelevant // The user will be subject to the same checks when they try to accept the invite diff --git a/src/Core/AdminConsole/Services/Implementations/PolicyService.cs b/src/Core/AdminConsole/Services/Implementations/PolicyService.cs index 1ffa2a0e2..072aa8283 100644 --- a/src/Core/AdminConsole/Services/Implementations/PolicyService.cs +++ b/src/Core/AdminConsole/Services/Implementations/PolicyService.cs @@ -1,6 +1,10 @@ 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; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Repositories; @@ -25,8 +29,11 @@ public class PolicyService : IPolicyService private readonly ISsoConfigRepository _ssoConfigRepository; private readonly IMailService _mailService; private readonly GlobalSettings _globalSettings; - private readonly IFeatureService _featureService; private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery; + private readonly IFeatureService _featureService; + private readonly ISavePolicyCommand _savePolicyCommand; + private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand; + private readonly IOrganizationHasVerifiedDomainsQuery _organizationHasVerifiedDomainsQuery; public PolicyService( IApplicationCacheService applicationCacheService, @@ -37,8 +44,11 @@ public class PolicyService : IPolicyService ISsoConfigRepository ssoConfigRepository, IMailService mailService, GlobalSettings globalSettings, + ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery, IFeatureService featureService, - ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery) + ISavePolicyCommand savePolicyCommand, + IRemoveOrganizationUserCommand removeOrganizationUserCommand, + IOrganizationHasVerifiedDomainsQuery organizationHasVerifiedDomainsQuery) { _applicationCacheService = applicationCacheService; _eventService = eventService; @@ -48,13 +58,30 @@ public class PolicyService : IPolicyService _ssoConfigRepository = ssoConfigRepository; _mailService = mailService; _globalSettings = globalSettings; - _featureService = featureService; _twoFactorIsEnabledQuery = twoFactorIsEnabledQuery; + _featureService = featureService; + _savePolicyCommand = savePolicyCommand; + _removeOrganizationUserCommand = removeOrganizationUserCommand; + _organizationHasVerifiedDomainsQuery = organizationHasVerifiedDomainsQuery; } - public async Task SaveAsync(Policy policy, IUserService userService, IOrganizationService organizationService, - Guid? savingUserId) + public async Task SaveAsync(Policy policy, Guid? savingUserId) { + if (_featureService.IsEnabled(FeatureFlagKeys.Pm13322AddPolicyDefinitions)) + { + // Transitional mapping - this will be moved to callers once the feature flag is removed + var policyUpdate = new PolicyUpdate + { + OrganizationId = policy.OrganizationId, + Type = policy.Type, + Enabled = policy.Enabled, + Data = policy.Data + }; + + await _savePolicyCommand.SaveAsync(policyUpdate); + return; + } + var org = await _organizationRepository.GetByIdAsync(policy.OrganizationId); if (org == null) { @@ -88,14 +115,7 @@ public class PolicyService : IPolicyService return; } - if (_featureService.IsEnabled(FeatureFlagKeys.MembersTwoFAQueryOptimization)) - { - await EnablePolicy_vNext(policy, org, organizationService, savingUserId); - return; - } - - await EnablePolicy(policy, org, userService, organizationService, savingUserId); - return; + await EnablePolicyAsync(policy, org, savingUserId); } public async Task GetMasterPasswordPolicyForUserAsync(User user) @@ -223,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); @@ -263,68 +284,22 @@ 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); await _eventService.LogPolicyEventAsync(policy, EventType.Policy_Updated); } - private async Task EnablePolicy(Policy policy, Organization org, IUserService userService, IOrganizationService organizationService, Guid? savingUserId) - { - var currentPolicy = await _policyRepository.GetByIdAsync(policy.Id); - if (!currentPolicy?.Enabled ?? true) - { - var orgUsers = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(policy.OrganizationId); - var removableOrgUsers = orgUsers.Where(ou => - ou.Status != OrganizationUserStatusType.Invited && ou.Status != OrganizationUserStatusType.Revoked && - ou.Type != OrganizationUserType.Owner && ou.Type != OrganizationUserType.Admin && - ou.UserId != savingUserId); - switch (policy.Type) - { - case PolicyType.TwoFactorAuthentication: - // Reorder by HasMasterPassword to prioritize checking users without a master if they have 2FA enabled - foreach (var orgUser in removableOrgUsers.OrderBy(ou => ou.HasMasterPassword)) - { - if (!await userService.TwoFactorIsEnabledAsync(orgUser)) - { - if (!orgUser.HasMasterPassword) - { - throw new BadRequestException( - "Policy could not be enabled. Non-compliant members will lose access to their accounts. Identify members without two-step login from the policies column in the members page."); - } - - await organizationService.RemoveUserAsync(policy.OrganizationId, orgUser.Id, - savingUserId); - await _mailService.SendOrganizationUserRemovedForPolicyTwoStepEmailAsync( - org.DisplayName(), orgUser.Email); - } - } - break; - case PolicyType.SingleOrg: - var userOrgs = await _organizationUserRepository.GetManyByManyUsersAsync( - removableOrgUsers.Select(ou => ou.UserId.Value)); - foreach (var orgUser in removableOrgUsers) - { - if (userOrgs.Any(ou => ou.UserId == orgUser.UserId - && ou.OrganizationId != org.Id - && ou.Status != OrganizationUserStatusType.Invited)) - { - await organizationService.RemoveUserAsync(policy.OrganizationId, orgUser.Id, - savingUserId); - await _mailService.SendOrganizationUserRemovedForPolicySingleOrgEmailAsync( - org.DisplayName(), orgUser.Email); - } - } - break; - default: - break; - } - } - - await SetPolicyConfiguration(policy); - } - - private async Task EnablePolicy_vNext(Policy policy, Organization org, IOrganizationService organizationService, Guid? savingUserId) + private async Task EnablePolicyAsync(Policy policy, Organization org, Guid? savingUserId) { var currentPolicy = await _policyRepository.GetByIdAsync(policy.Id); if (!currentPolicy?.Enabled ?? true) @@ -350,7 +325,7 @@ public class PolicyService : IPolicyService "Policy could not be enabled. Non-compliant members will lose access to their accounts. Identify members without two-step login from the policies column in the members page."); } - await organizationService.RemoveUserAsync(policy.OrganizationId, orgUser.Id, + await _removeOrganizationUserCommand.RemoveUserAsync(policy.OrganizationId, orgUser.Id, savingUserId); await _mailService.SendOrganizationUserRemovedForPolicyTwoStepEmailAsync( org.DisplayName(), orgUser.Email); @@ -366,7 +341,7 @@ public class PolicyService : IPolicyService && ou.OrganizationId != org.Id && ou.Status != OrganizationUserStatusType.Invited)) { - await organizationService.RemoveUserAsync(policy.OrganizationId, orgUser.Id, + await _removeOrganizationUserCommand.RemoveUserAsync(policy.OrganizationId, orgUser.Id, savingUserId); await _mailService.SendOrganizationUserRemovedForPolicySingleOrgEmailAsync( org.DisplayName(), orgUser.Email); 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/Services/Implementations/EmergencyAccessService.cs b/src/Core/Auth/Services/Implementations/EmergencyAccessService.cs index dffa9f65c..dda16e29f 100644 --- a/src/Core/Auth/Services/Implementations/EmergencyAccessService.cs +++ b/src/Core/Auth/Services/Implementations/EmergencyAccessService.cs @@ -1,4 +1,5 @@ using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Auth.Entities; using Bit.Core.Auth.Enums; @@ -33,6 +34,7 @@ public class EmergencyAccessService : IEmergencyAccessService private readonly IPasswordHasher _passwordHasher; private readonly IOrganizationService _organizationService; private readonly IDataProtectorTokenFactory _dataProtectorTokenizer; + private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand; public EmergencyAccessService( IEmergencyAccessRepository emergencyAccessRepository, @@ -46,7 +48,8 @@ public class EmergencyAccessService : IEmergencyAccessService IPasswordHasher passwordHasher, GlobalSettings globalSettings, IOrganizationService organizationService, - IDataProtectorTokenFactory dataProtectorTokenizer) + IDataProtectorTokenFactory dataProtectorTokenizer, + IRemoveOrganizationUserCommand removeOrganizationUserCommand) { _emergencyAccessRepository = emergencyAccessRepository; _organizationUserRepository = organizationUserRepository; @@ -60,6 +63,7 @@ public class EmergencyAccessService : IEmergencyAccessService _globalSettings = globalSettings; _organizationService = organizationService; _dataProtectorTokenizer = dataProtectorTokenizer; + _removeOrganizationUserCommand = removeOrganizationUserCommand; } public async Task InviteAsync(User invitingUser, string email, EmergencyAccessType type, int waitTime) @@ -341,7 +345,7 @@ public class EmergencyAccessService : IEmergencyAccessService { if (o.Type != OrganizationUserType.Owner) { - await _organizationService.RemoveUserAsync(o.OrganizationId, grantor.Id); + await _removeOrganizationUserCommand.RemoveUserAsync(o.OrganizationId, grantor.Id); } } } diff --git a/src/Core/Auth/Services/Implementations/SsoConfigService.cs b/src/Core/Auth/Services/Implementations/SsoConfigService.cs index 62c828495..532f00039 100644 --- a/src/Core/Auth/Services/Implementations/SsoConfigService.cs +++ b/src/Core/Auth/Services/Implementations/SsoConfigService.cs @@ -20,8 +20,6 @@ public class SsoConfigService : ISsoConfigService private readonly IPolicyService _policyService; private readonly IOrganizationRepository _organizationRepository; private readonly IOrganizationUserRepository _organizationUserRepository; - private readonly IUserService _userService; - private readonly IOrganizationService _organizationService; private readonly IEventService _eventService; public SsoConfigService( @@ -30,8 +28,6 @@ public class SsoConfigService : ISsoConfigService IPolicyService policyService, IOrganizationRepository organizationRepository, IOrganizationUserRepository organizationUserRepository, - IUserService userService, - IOrganizationService organizationService, IEventService eventService) { _ssoConfigRepository = ssoConfigRepository; @@ -39,8 +35,6 @@ public class SsoConfigService : ISsoConfigService _policyService = policyService; _organizationRepository = organizationRepository; _organizationUserRepository = organizationUserRepository; - _userService = userService; - _organizationService = organizationService; _eventService = eventService; } @@ -74,20 +68,20 @@ public class SsoConfigService : ISsoConfigService singleOrgPolicy.Enabled = true; - await _policyService.SaveAsync(singleOrgPolicy, _userService, _organizationService, null); + await _policyService.SaveAsync(singleOrgPolicy, null); var resetPolicy = await _policyRepository.GetByOrganizationIdTypeAsync(config.OrganizationId, PolicyType.ResetPassword) ?? new Policy { OrganizationId = config.OrganizationId, Type = PolicyType.ResetPassword, }; resetPolicy.Enabled = true; resetPolicy.SetDataModel(new ResetPasswordDataModel { AutoEnrollEnabled = true }); - await _policyService.SaveAsync(resetPolicy, _userService, _organizationService, null); + await _policyService.SaveAsync(resetPolicy, null); var ssoRequiredPolicy = await _policyRepository.GetByOrganizationIdTypeAsync(config.OrganizationId, PolicyType.RequireSso) ?? new Policy { OrganizationId = config.OrganizationId, Type = PolicyType.RequireSso, }; ssoRequiredPolicy.Enabled = true; - await _policyService.SaveAsync(ssoRequiredPolicy, _userService, _organizationService, null); + await _policyService.SaveAsync(ssoRequiredPolicy, null); } await LogEventsAsync(config, oldConfig); 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/Entities/ClientOrganizationMigrationRecord.cs b/src/Core/Billing/Entities/ClientOrganizationMigrationRecord.cs new file mode 100644 index 000000000..1e719b3ce --- /dev/null +++ b/src/Core/Billing/Entities/ClientOrganizationMigrationRecord.cs @@ -0,0 +1,32 @@ +using System.ComponentModel.DataAnnotations; +using Bit.Core.Billing.Enums; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Utilities; + +#nullable enable + +namespace Bit.Core.Billing.Entities; + +public class ClientOrganizationMigrationRecord : ITableObject +{ + public Guid Id { get; set; } + public Guid OrganizationId { get; set; } + public Guid ProviderId { get; set; } + public PlanType PlanType { get; set; } + public int Seats { get; set; } + public short? MaxStorageGb { get; set; } + [MaxLength(50)] public string GatewayCustomerId { get; set; } = null!; + [MaxLength(50)] public string GatewaySubscriptionId { get; set; } = null!; + public DateTime? ExpirationDate { get; set; } + public int? MaxAutoscaleSeats { get; set; } + public OrganizationStatusType Status { get; set; } + + public void SetNewId() + { + if (Id == default) + { + Id = CoreHelpers.GenerateComb(); + } + } +} diff --git a/src/Core/Billing/Migration/Models/ClientMigrationTracker.cs b/src/Core/Billing/Migration/Models/ClientMigrationTracker.cs new file mode 100644 index 000000000..69398004f --- /dev/null +++ b/src/Core/Billing/Migration/Models/ClientMigrationTracker.cs @@ -0,0 +1,23 @@ +namespace Bit.Core.Billing.Migration.Models; + +public enum ClientMigrationProgress +{ + Started = 1, + MigrationRecordCreated = 2, + SubscriptionEnded = 3, + Completed = 4, + + Reversing = 5, + ResetOrganization = 6, + RecreatedSubscription = 7, + RemovedMigrationRecord = 8, + Reversed = 9 +} + +public class ClientMigrationTracker +{ + public Guid ProviderId { get; set; } + public Guid OrganizationId { get; set; } + public string OrganizationName { get; set; } + public ClientMigrationProgress Progress { get; set; } = ClientMigrationProgress.Started; +} diff --git a/src/Core/Billing/Migration/Models/ProviderMigrationResult.cs b/src/Core/Billing/Migration/Models/ProviderMigrationResult.cs new file mode 100644 index 000000000..137ba8bd0 --- /dev/null +++ b/src/Core/Billing/Migration/Models/ProviderMigrationResult.cs @@ -0,0 +1,45 @@ +using Bit.Core.Billing.Entities; + +namespace Bit.Core.Billing.Migration.Models; + +public class ProviderMigrationResult +{ + public Guid ProviderId { get; set; } + public string ProviderName { get; set; } + public string Result { get; set; } + public List Clients { get; set; } +} + +public class ClientMigrationResult +{ + public Guid OrganizationId { get; set; } + public string OrganizationName { get; set; } + public string Result { get; set; } + public ClientPreviousState PreviousState { get; set; } +} + +public class ClientPreviousState +{ + public ClientPreviousState() { } + + public ClientPreviousState(ClientOrganizationMigrationRecord migrationRecord) + { + PlanType = migrationRecord.PlanType.ToString(); + Seats = migrationRecord.Seats; + MaxStorageGb = migrationRecord.MaxStorageGb; + GatewayCustomerId = migrationRecord.GatewayCustomerId; + GatewaySubscriptionId = migrationRecord.GatewaySubscriptionId; + ExpirationDate = migrationRecord.ExpirationDate; + MaxAutoscaleSeats = migrationRecord.MaxAutoscaleSeats; + Status = migrationRecord.Status.ToString(); + } + + public string PlanType { get; set; } + public int Seats { get; set; } + public short? MaxStorageGb { get; set; } + public string GatewayCustomerId { get; set; } = null!; + public string GatewaySubscriptionId { get; set; } = null!; + public DateTime? ExpirationDate { get; set; } + public int? MaxAutoscaleSeats { get; set; } + public string Status { get; set; } +} diff --git a/src/Core/Billing/Migration/Models/ProviderMigrationTracker.cs b/src/Core/Billing/Migration/Models/ProviderMigrationTracker.cs new file mode 100644 index 000000000..7bfef8a93 --- /dev/null +++ b/src/Core/Billing/Migration/Models/ProviderMigrationTracker.cs @@ -0,0 +1,22 @@ +namespace Bit.Core.Billing.Migration.Models; + +public enum ProviderMigrationProgress +{ + Started = 1, + NoClients = 2, + ClientsMigrated = 3, + TeamsPlanConfigured = 4, + EnterprisePlanConfigured = 5, + CustomerSetup = 6, + SubscriptionSetup = 7, + CreditApplied = 8, + Completed = 9, +} + +public class ProviderMigrationTracker +{ + public Guid ProviderId { get; set; } + public string ProviderName { get; set; } + public List OrganizationIds { get; set; } + public ProviderMigrationProgress Progress { get; set; } = ProviderMigrationProgress.Started; +} diff --git a/src/Core/Billing/Migration/ServiceCollectionExtensions.cs b/src/Core/Billing/Migration/ServiceCollectionExtensions.cs new file mode 100644 index 000000000..109259d59 --- /dev/null +++ b/src/Core/Billing/Migration/ServiceCollectionExtensions.cs @@ -0,0 +1,15 @@ +using Bit.Core.Billing.Migration.Services; +using Bit.Core.Billing.Migration.Services.Implementations; +using Microsoft.Extensions.DependencyInjection; + +namespace Bit.Core.Billing.Migration; + +public static class ServiceCollectionExtensions +{ + public static void AddProviderMigration(this IServiceCollection services) + { + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + } +} diff --git a/src/Core/Billing/Migration/Services/IMigrationTrackerCache.cs b/src/Core/Billing/Migration/Services/IMigrationTrackerCache.cs new file mode 100644 index 000000000..6734c6956 --- /dev/null +++ b/src/Core/Billing/Migration/Services/IMigrationTrackerCache.cs @@ -0,0 +1,17 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Entities.Provider; +using Bit.Core.Billing.Migration.Models; + +namespace Bit.Core.Billing.Migration.Services; + +public interface IMigrationTrackerCache +{ + Task StartTracker(Provider provider); + Task SetOrganizationIds(Guid providerId, IEnumerable organizationIds); + Task GetTracker(Guid providerId); + Task UpdateTrackingStatus(Guid providerId, ProviderMigrationProgress status); + + Task StartTracker(Guid providerId, Organization organization); + Task GetTracker(Guid providerId, Guid organizationId); + Task UpdateTrackingStatus(Guid providerId, Guid organizationId, ClientMigrationProgress status); +} diff --git a/src/Core/Billing/Migration/Services/IOrganizationMigrator.cs b/src/Core/Billing/Migration/Services/IOrganizationMigrator.cs new file mode 100644 index 000000000..7bc944371 --- /dev/null +++ b/src/Core/Billing/Migration/Services/IOrganizationMigrator.cs @@ -0,0 +1,8 @@ +using Bit.Core.AdminConsole.Entities; + +namespace Bit.Core.Billing.Migration.Services; + +public interface IOrganizationMigrator +{ + Task Migrate(Guid providerId, Organization organization); +} diff --git a/src/Core/Billing/Migration/Services/IProviderMigrator.cs b/src/Core/Billing/Migration/Services/IProviderMigrator.cs new file mode 100644 index 000000000..9ca14e7fd --- /dev/null +++ b/src/Core/Billing/Migration/Services/IProviderMigrator.cs @@ -0,0 +1,10 @@ +using Bit.Core.Billing.Migration.Models; + +namespace Bit.Core.Billing.Migration.Services; + +public interface IProviderMigrator +{ + Task Migrate(Guid providerId); + + Task GetResult(Guid providerId); +} diff --git a/src/Core/Billing/Migration/Services/Implementations/MigrationTrackerDistributedCache.cs b/src/Core/Billing/Migration/Services/Implementations/MigrationTrackerDistributedCache.cs new file mode 100644 index 000000000..920bc5539 --- /dev/null +++ b/src/Core/Billing/Migration/Services/Implementations/MigrationTrackerDistributedCache.cs @@ -0,0 +1,107 @@ +using System.Text.Json; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Entities.Provider; +using Bit.Core.Billing.Migration.Models; +using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.DependencyInjection; + +namespace Bit.Core.Billing.Migration.Services.Implementations; + +public class MigrationTrackerDistributedCache( + [FromKeyedServices("persistent")] + IDistributedCache distributedCache) : IMigrationTrackerCache +{ + public async Task StartTracker(Provider provider) => + await SetAsync(new ProviderMigrationTracker + { + ProviderId = provider.Id, + ProviderName = provider.Name + }); + + public async Task SetOrganizationIds(Guid providerId, IEnumerable organizationIds) + { + var tracker = await GetAsync(providerId); + + tracker.OrganizationIds = organizationIds.ToList(); + + await SetAsync(tracker); + } + + public Task GetTracker(Guid providerId) => GetAsync(providerId); + + public async Task UpdateTrackingStatus(Guid providerId, ProviderMigrationProgress status) + { + var tracker = await GetAsync(providerId); + + tracker.Progress = status; + + await SetAsync(tracker); + } + + public async Task StartTracker(Guid providerId, Organization organization) => + await SetAsync(new ClientMigrationTracker + { + ProviderId = providerId, + OrganizationId = organization.Id, + OrganizationName = organization.Name + }); + + public Task GetTracker(Guid providerId, Guid organizationId) => + GetAsync(providerId, organizationId); + + public async Task UpdateTrackingStatus(Guid providerId, Guid organizationId, ClientMigrationProgress status) + { + var tracker = await GetAsync(providerId, organizationId); + + tracker.Progress = status; + + await SetAsync(tracker); + } + + private static string GetProviderCacheKey(Guid providerId) => $"provider_{providerId}_migration"; + + private static string GetClientCacheKey(Guid providerId, Guid clientId) => + $"provider_{providerId}_client_{clientId}_migration"; + + private async Task GetAsync(Guid providerId) + { + var cacheKey = GetProviderCacheKey(providerId); + + var json = await distributedCache.GetStringAsync(cacheKey); + + return string.IsNullOrEmpty(json) ? null : JsonSerializer.Deserialize(json); + } + + private async Task GetAsync(Guid providerId, Guid organizationId) + { + var cacheKey = GetClientCacheKey(providerId, organizationId); + + var json = await distributedCache.GetStringAsync(cacheKey); + + return string.IsNullOrEmpty(json) ? null : JsonSerializer.Deserialize(json); + } + + private async Task SetAsync(ProviderMigrationTracker tracker) + { + var cacheKey = GetProviderCacheKey(tracker.ProviderId); + + var json = JsonSerializer.Serialize(tracker); + + await distributedCache.SetStringAsync(cacheKey, json, new DistributedCacheEntryOptions + { + SlidingExpiration = TimeSpan.FromMinutes(30) + }); + } + + private async Task SetAsync(ClientMigrationTracker tracker) + { + var cacheKey = GetClientCacheKey(tracker.ProviderId, tracker.OrganizationId); + + var json = JsonSerializer.Serialize(tracker); + + await distributedCache.SetStringAsync(cacheKey, json, new DistributedCacheEntryOptions + { + SlidingExpiration = TimeSpan.FromMinutes(30) + }); + } +} diff --git a/src/Core/Billing/Migration/Services/Implementations/OrganizationMigrator.cs b/src/Core/Billing/Migration/Services/Implementations/OrganizationMigrator.cs new file mode 100644 index 000000000..a24193f13 --- /dev/null +++ b/src/Core/Billing/Migration/Services/Implementations/OrganizationMigrator.cs @@ -0,0 +1,326 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Billing.Constants; +using Bit.Core.Billing.Entities; +using Bit.Core.Billing.Enums; +using Bit.Core.Billing.Migration.Models; +using Bit.Core.Billing.Repositories; +using Bit.Core.Enums; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Bit.Core.Utilities; +using Microsoft.Extensions.Logging; +using Stripe; +using Plan = Bit.Core.Models.StaticStore.Plan; + +namespace Bit.Core.Billing.Migration.Services.Implementations; + +public class OrganizationMigrator( + IClientOrganizationMigrationRecordRepository clientOrganizationMigrationRecordRepository, + ILogger logger, + IMigrationTrackerCache migrationTrackerCache, + IOrganizationRepository organizationRepository, + IStripeAdapter stripeAdapter) : IOrganizationMigrator +{ + private const string _cancellationComment = "Cancelled as part of provider migration to Consolidated Billing"; + + public async Task Migrate(Guid providerId, Organization organization) + { + logger.LogInformation("CB: Starting migration for organization ({OrganizationID})", organization.Id); + + await migrationTrackerCache.StartTracker(providerId, organization); + + await CreateMigrationRecordAsync(providerId, organization); + + await CancelSubscriptionAsync(providerId, organization); + + await UpdateOrganizationAsync(providerId, organization); + } + + #region Steps + + private async Task CreateMigrationRecordAsync(Guid providerId, Organization organization) + { + logger.LogInformation("CB: Creating ClientOrganizationMigrationRecord for organization ({OrganizationID})", organization.Id); + + var migrationRecord = await clientOrganizationMigrationRecordRepository.GetByOrganizationId(organization.Id); + + if (migrationRecord != null) + { + logger.LogInformation( + "CB: ClientOrganizationMigrationRecord already exists for organization ({OrganizationID}), deleting record", + organization.Id); + + await clientOrganizationMigrationRecordRepository.DeleteAsync(migrationRecord); + } + + await clientOrganizationMigrationRecordRepository.CreateAsync(new ClientOrganizationMigrationRecord + { + OrganizationId = organization.Id, + ProviderId = providerId, + PlanType = organization.PlanType, + Seats = organization.Seats ?? 0, + MaxStorageGb = organization.MaxStorageGb, + GatewayCustomerId = organization.GatewayCustomerId!, + GatewaySubscriptionId = organization.GatewaySubscriptionId!, + ExpirationDate = organization.ExpirationDate, + MaxAutoscaleSeats = organization.MaxAutoscaleSeats, + Status = organization.Status + }); + + logger.LogInformation("CB: Created migration record for organization ({OrganizationID})", organization.Id); + + await migrationTrackerCache.UpdateTrackingStatus(providerId, organization.Id, + ClientMigrationProgress.MigrationRecordCreated); + } + + private async Task CancelSubscriptionAsync(Guid providerId, Organization organization) + { + logger.LogInformation("CB: Cancelling subscription for organization ({OrganizationID})", organization.Id); + + var subscription = await stripeAdapter.SubscriptionGetAsync(organization.GatewaySubscriptionId); + + if (subscription is + { + Status: + StripeConstants.SubscriptionStatus.Active or + StripeConstants.SubscriptionStatus.PastDue or + StripeConstants.SubscriptionStatus.Trialing + }) + { + await stripeAdapter.SubscriptionUpdateAsync(organization.GatewaySubscriptionId, + new SubscriptionUpdateOptions { CancelAtPeriodEnd = false }); + + subscription = await stripeAdapter.SubscriptionCancelAsync(organization.GatewaySubscriptionId, + new SubscriptionCancelOptions + { + CancellationDetails = new SubscriptionCancellationDetailsOptions + { + Comment = _cancellationComment + }, + InvoiceNow = true, + Prorate = true, + Expand = ["latest_invoice", "test_clock"] + }); + + logger.LogInformation("CB: Cancelled subscription for organization ({OrganizationID})", organization.Id); + + var now = subscription.TestClock?.FrozenTime ?? DateTime.UtcNow; + + var trialing = subscription.TrialEnd.HasValue && subscription.TrialEnd.Value > now; + + if (!trialing && subscription is { Status: StripeConstants.SubscriptionStatus.Canceled, CancellationDetails.Comment: _cancellationComment }) + { + var latestInvoice = subscription.LatestInvoice; + + if (latestInvoice.Status == "draft") + { + await stripeAdapter.InvoiceFinalizeInvoiceAsync(latestInvoice.Id, + new InvoiceFinalizeOptions { AutoAdvance = true }); + + logger.LogInformation("CB: Finalized prorated invoice for organization ({OrganizationID})", organization.Id); + } + } + } + else + { + logger.LogInformation( + "CB: Did not need to cancel subscription for organization ({OrganizationID}) as it was inactive", + organization.Id); + } + + await migrationTrackerCache.UpdateTrackingStatus(providerId, organization.Id, + ClientMigrationProgress.SubscriptionEnded); + } + + private async Task UpdateOrganizationAsync(Guid providerId, Organization organization) + { + logger.LogInformation("CB: Bringing organization ({OrganizationID}) under provider management", + organization.Id); + + var plan = StaticStore.GetPlan(organization.Plan.Contains("Teams") ? PlanType.TeamsMonthly : PlanType.EnterpriseMonthly); + + ResetOrganizationPlan(organization, plan); + organization.MaxStorageGb = plan.PasswordManager.BaseStorageGb; + organization.GatewaySubscriptionId = null; + organization.ExpirationDate = null; + organization.MaxAutoscaleSeats = null; + organization.Status = OrganizationStatusType.Managed; + + await organizationRepository.ReplaceAsync(organization); + + logger.LogInformation("CB: Brought organization ({OrganizationID}) under provider management", + organization.Id); + + await migrationTrackerCache.UpdateTrackingStatus(providerId, organization.Id, + ClientMigrationProgress.Completed); + } + + #endregion + + #region Reverse + + private async Task RemoveMigrationRecordAsync(Guid providerId, Organization organization) + { + logger.LogInformation("CB: Removing migration record for organization ({OrganizationID})", organization.Id); + + var migrationRecord = await clientOrganizationMigrationRecordRepository.GetByOrganizationId(organization.Id); + + if (migrationRecord != null) + { + await clientOrganizationMigrationRecordRepository.DeleteAsync(migrationRecord); + + logger.LogInformation( + "CB: Removed migration record for organization ({OrganizationID})", + organization.Id); + } + else + { + logger.LogInformation("CB: Did not remove migration record for organization ({OrganizationID}) as it does not exist", organization.Id); + } + + await migrationTrackerCache.UpdateTrackingStatus(providerId, organization.Id, ClientMigrationProgress.Reversed); + } + + private async Task RecreateSubscriptionAsync(Guid providerId, Organization organization) + { + logger.LogInformation("CB: Recreating subscription for organization ({OrganizationID})", organization.Id); + + if (!string.IsNullOrEmpty(organization.GatewaySubscriptionId)) + { + if (string.IsNullOrEmpty(organization.GatewayCustomerId)) + { + logger.LogError( + "CB: Cannot recreate subscription for organization ({OrganizationID}) as it does not have a Stripe customer", + organization.Id); + + throw new Exception(); + } + + var customer = await stripeAdapter.CustomerGetAsync(organization.GatewayCustomerId, + new CustomerGetOptions { Expand = ["default_source", "invoice_settings.default_payment_method"] }); + + var collectionMethod = + customer.DefaultSource != null || + customer.InvoiceSettings?.DefaultPaymentMethod != null || + customer.Metadata.ContainsKey(Utilities.BraintreeCustomerIdKey) + ? StripeConstants.CollectionMethod.ChargeAutomatically + : StripeConstants.CollectionMethod.SendInvoice; + + var plan = StaticStore.GetPlan(organization.PlanType); + + var items = new List + { + new () + { + Price = plan.PasswordManager.StripeSeatPlanId, + Quantity = organization.Seats + } + }; + + if (organization.MaxStorageGb.HasValue && plan.PasswordManager.BaseStorageGb.HasValue && organization.MaxStorageGb.Value > plan.PasswordManager.BaseStorageGb.Value) + { + var additionalStorage = organization.MaxStorageGb.Value - plan.PasswordManager.BaseStorageGb.Value; + + items.Add(new SubscriptionItemOptions + { + Price = plan.PasswordManager.StripeStoragePlanId, + Quantity = additionalStorage + }); + } + + var subscriptionCreateOptions = new SubscriptionCreateOptions + { + AutomaticTax = new SubscriptionAutomaticTaxOptions + { + Enabled = true + }, + Customer = customer.Id, + CollectionMethod = collectionMethod, + DaysUntilDue = collectionMethod == StripeConstants.CollectionMethod.SendInvoice ? 30 : null, + Items = items, + Metadata = new Dictionary + { + [organization.GatewayIdField()] = organization.Id.ToString() + }, + OffSession = true, + ProrationBehavior = StripeConstants.ProrationBehavior.CreateProrations, + TrialPeriodDays = plan.TrialPeriodDays + }; + + var subscription = await stripeAdapter.SubscriptionCreateAsync(subscriptionCreateOptions); + + organization.GatewaySubscriptionId = subscription.Id; + + await organizationRepository.ReplaceAsync(organization); + + logger.LogInformation("CB: Recreated subscription for organization ({OrganizationID})", organization.Id); + } + else + { + logger.LogInformation( + "CB: Did not recreate subscription for organization ({OrganizationID}) as it already exists", + organization.Id); + } + + await migrationTrackerCache.UpdateTrackingStatus(providerId, organization.Id, + ClientMigrationProgress.RecreatedSubscription); + } + + private async Task ReverseOrganizationUpdateAsync(Guid providerId, Organization organization) + { + var migrationRecord = await clientOrganizationMigrationRecordRepository.GetByOrganizationId(organization.Id); + + if (migrationRecord == null) + { + logger.LogError( + "CB: Cannot reverse migration for organization ({OrganizationID}) as it does not have a migration record", + organization.Id); + + throw new Exception(); + } + + var plan = StaticStore.GetPlan(migrationRecord.PlanType); + + ResetOrganizationPlan(organization, plan); + organization.MaxStorageGb = migrationRecord.MaxStorageGb; + organization.ExpirationDate = migrationRecord.ExpirationDate; + organization.MaxAutoscaleSeats = migrationRecord.MaxAutoscaleSeats; + organization.Status = migrationRecord.Status; + + await organizationRepository.ReplaceAsync(organization); + + logger.LogInformation("CB: Reversed organization ({OrganizationID}) updates", + organization.Id); + + await migrationTrackerCache.UpdateTrackingStatus(providerId, organization.Id, + ClientMigrationProgress.ResetOrganization); + } + + #endregion + + #region Shared + + private static void ResetOrganizationPlan(Organization organization, Plan plan) + { + organization.Plan = plan.Name; + organization.PlanType = plan.Type; + organization.MaxCollections = plan.PasswordManager.MaxCollections; + organization.MaxStorageGb = plan.PasswordManager.BaseStorageGb; + organization.UsePolicies = plan.HasPolicies; + organization.UseSso = plan.HasSso; + organization.UseGroups = plan.HasGroups; + organization.UseEvents = plan.HasEvents; + organization.UseDirectory = plan.HasDirectory; + organization.UseTotp = plan.HasTotp; + organization.Use2fa = plan.Has2fa; + organization.UseApi = plan.HasApi; + organization.UseResetPassword = plan.HasResetPassword; + organization.SelfHost = plan.HasSelfHost; + organization.UsersGetPremium = plan.UsersGetPremium; + organization.UseCustomPermissions = plan.HasCustomPermissions; + organization.UseScim = plan.HasScim; + organization.UseKeyConnector = plan.HasKeyConnector; + } + + #endregion +} diff --git a/src/Core/Billing/Migration/Services/Implementations/ProviderMigrator.cs b/src/Core/Billing/Migration/Services/Implementations/ProviderMigrator.cs new file mode 100644 index 000000000..9ca515a26 --- /dev/null +++ b/src/Core/Billing/Migration/Services/Implementations/ProviderMigrator.cs @@ -0,0 +1,418 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Entities.Provider; +using Bit.Core.AdminConsole.Enums.Provider; +using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Billing.Constants; +using Bit.Core.Billing.Entities; +using Bit.Core.Billing.Enums; +using Bit.Core.Billing.Migration.Models; +using Bit.Core.Billing.Repositories; +using Bit.Core.Billing.Services; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Microsoft.Extensions.Logging; +using Stripe; + +namespace Bit.Core.Billing.Migration.Services.Implementations; + +public class ProviderMigrator( + IClientOrganizationMigrationRecordRepository clientOrganizationMigrationRecordRepository, + IOrganizationMigrator organizationMigrator, + ILogger logger, + IMigrationTrackerCache migrationTrackerCache, + IOrganizationRepository organizationRepository, + IPaymentService paymentService, + IProviderBillingService providerBillingService, + IProviderOrganizationRepository providerOrganizationRepository, + IProviderRepository providerRepository, + IProviderPlanRepository providerPlanRepository, + IStripeAdapter stripeAdapter) : IProviderMigrator +{ + public async Task Migrate(Guid providerId) + { + var provider = await GetProviderAsync(providerId); + + if (provider == null) + { + return; + } + + logger.LogInformation("CB: Starting migration for provider ({ProviderID})", providerId); + + await migrationTrackerCache.StartTracker(provider); + + var organizations = await GetClientsAsync(provider.Id); + + if (organizations.Count == 0) + { + logger.LogInformation("CB: Skipping migration for provider ({ProviderID}) with no clients", providerId); + + await migrationTrackerCache.UpdateTrackingStatus(providerId, ProviderMigrationProgress.NoClients); + + return; + } + + await MigrateClientsAsync(providerId, organizations); + + await ConfigureTeamsPlanAsync(providerId); + + await ConfigureEnterprisePlanAsync(providerId); + + await SetupCustomerAsync(provider); + + await SetupSubscriptionAsync(provider); + + await ApplyCreditAsync(provider); + + await UpdateProviderAsync(provider); + } + + public async Task GetResult(Guid providerId) + { + var providerTracker = await migrationTrackerCache.GetTracker(providerId); + + if (providerTracker == null) + { + return null; + } + + if (providerTracker.Progress == ProviderMigrationProgress.NoClients) + { + return new ProviderMigrationResult + { + ProviderId = providerTracker.ProviderId, + ProviderName = providerTracker.ProviderName, + Result = providerTracker.Progress.ToString() + }; + } + + var clientTrackers = await Task.WhenAll(providerTracker.OrganizationIds.Select(organizationId => + migrationTrackerCache.GetTracker(providerId, organizationId))); + + var migrationRecordLookup = new Dictionary(); + + foreach (var clientTracker in clientTrackers) + { + var migrationRecord = + await clientOrganizationMigrationRecordRepository.GetByOrganizationId(clientTracker.OrganizationId); + + migrationRecordLookup.Add(clientTracker.OrganizationId, migrationRecord); + } + + return new ProviderMigrationResult + { + ProviderId = providerTracker.ProviderId, + ProviderName = providerTracker.ProviderName, + Result = providerTracker.Progress.ToString(), + Clients = clientTrackers.Select(tracker => + { + var foundMigrationRecord = migrationRecordLookup.TryGetValue(tracker.OrganizationId, out var migrationRecord); + return new ClientMigrationResult + { + OrganizationId = tracker.OrganizationId, + OrganizationName = tracker.OrganizationName, + Result = tracker.Progress.ToString(), + PreviousState = foundMigrationRecord ? new ClientPreviousState(migrationRecord) : null + }; + }).ToList(), + }; + } + + #region Steps + + private async Task MigrateClientsAsync(Guid providerId, List organizations) + { + logger.LogInformation("CB: Migrating clients for provider ({ProviderID})", providerId); + + var organizationIds = organizations.Select(organization => organization.Id); + + await migrationTrackerCache.SetOrganizationIds(providerId, organizationIds); + + foreach (var organization in organizations) + { + var tracker = await migrationTrackerCache.GetTracker(providerId, organization.Id); + + if (tracker is not { Progress: ClientMigrationProgress.Completed }) + { + await organizationMigrator.Migrate(providerId, organization); + } + } + + logger.LogInformation("CB: Migrated clients for provider ({ProviderID})", providerId); + + await migrationTrackerCache.UpdateTrackingStatus(providerId, + ProviderMigrationProgress.ClientsMigrated); + } + + private async Task ConfigureTeamsPlanAsync(Guid providerId) + { + logger.LogInformation("CB: Configuring Teams plan for provider ({ProviderID})", providerId); + + var organizations = await GetClientsAsync(providerId); + + var teamsSeats = organizations + .Where(IsTeams) + .Sum(client => client.Seats) ?? 0; + + var teamsProviderPlan = (await providerPlanRepository.GetByProviderId(providerId)) + .FirstOrDefault(providerPlan => providerPlan.PlanType == PlanType.TeamsMonthly); + + if (teamsProviderPlan == null) + { + await providerPlanRepository.CreateAsync(new ProviderPlan + { + ProviderId = providerId, + PlanType = PlanType.TeamsMonthly, + SeatMinimum = teamsSeats, + PurchasedSeats = 0, + AllocatedSeats = teamsSeats + }); + + logger.LogInformation("CB: Created Teams plan for provider ({ProviderID}) with a seat minimum of {Seats}", + providerId, teamsSeats); + } + else + { + logger.LogInformation("CB: Teams plan already exists for provider ({ProviderID}), updating seat minimum", providerId); + + teamsProviderPlan.SeatMinimum = teamsSeats; + teamsProviderPlan.AllocatedSeats = teamsSeats; + + await providerPlanRepository.ReplaceAsync(teamsProviderPlan); + + logger.LogInformation("CB: Updated Teams plan for provider ({ProviderID}) to seat minimum of {Seats}", + providerId, teamsProviderPlan.SeatMinimum); + } + + await migrationTrackerCache.UpdateTrackingStatus(providerId, ProviderMigrationProgress.TeamsPlanConfigured); + } + + private async Task ConfigureEnterprisePlanAsync(Guid providerId) + { + logger.LogInformation("CB: Configuring Enterprise plan for provider ({ProviderID})", providerId); + + var organizations = await GetClientsAsync(providerId); + + var enterpriseSeats = organizations + .Where(IsEnterprise) + .Sum(client => client.Seats) ?? 0; + + var enterpriseProviderPlan = (await providerPlanRepository.GetByProviderId(providerId)) + .FirstOrDefault(providerPlan => providerPlan.PlanType == PlanType.EnterpriseMonthly); + + if (enterpriseProviderPlan == null) + { + await providerPlanRepository.CreateAsync(new ProviderPlan + { + ProviderId = providerId, + PlanType = PlanType.EnterpriseMonthly, + SeatMinimum = enterpriseSeats, + PurchasedSeats = 0, + AllocatedSeats = enterpriseSeats + }); + + logger.LogInformation("CB: Created Enterprise plan for provider ({ProviderID}) with a seat minimum of {Seats}", + providerId, enterpriseSeats); + } + else + { + logger.LogInformation("CB: Enterprise plan already exists for provider ({ProviderID}), updating seat minimum", providerId); + + enterpriseProviderPlan.SeatMinimum = enterpriseSeats; + enterpriseProviderPlan.AllocatedSeats = enterpriseSeats; + + await providerPlanRepository.ReplaceAsync(enterpriseProviderPlan); + + logger.LogInformation("CB: Updated Enterprise plan for provider ({ProviderID}) to seat minimum of {Seats}", + providerId, enterpriseProviderPlan.SeatMinimum); + } + + await migrationTrackerCache.UpdateTrackingStatus(providerId, ProviderMigrationProgress.EnterprisePlanConfigured); + } + + private async Task SetupCustomerAsync(Provider provider) + { + if (string.IsNullOrEmpty(provider.GatewayCustomerId)) + { + var organizations = await GetClientsAsync(provider.Id); + + var sampleOrganization = organizations.FirstOrDefault(organization => !string.IsNullOrEmpty(organization.GatewayCustomerId)); + + if (sampleOrganization == null) + { + logger.LogInformation( + "CB: Could not find sample organization for provider ({ProviderID}) that has a Stripe customer", + provider.Id); + + return; + } + + var taxInfo = await paymentService.GetTaxInfoAsync(sampleOrganization); + + var customer = await providerBillingService.SetupCustomer(provider, taxInfo); + + await stripeAdapter.CustomerUpdateAsync(customer.Id, new CustomerUpdateOptions + { + Coupon = StripeConstants.CouponIDs.MSPDiscount35 + }); + + provider.GatewayCustomerId = customer.Id; + + await providerRepository.ReplaceAsync(provider); + + logger.LogInformation("CB: Setup Stripe customer for provider ({ProviderID})", provider.Id); + } + else + { + logger.LogInformation("CB: Stripe customer already exists for provider ({ProviderID})", provider.Id); + } + + await migrationTrackerCache.UpdateTrackingStatus(provider.Id, ProviderMigrationProgress.CustomerSetup); + } + + private async Task SetupSubscriptionAsync(Provider provider) + { + if (string.IsNullOrEmpty(provider.GatewaySubscriptionId)) + { + if (!string.IsNullOrEmpty(provider.GatewayCustomerId)) + { + var subscription = await providerBillingService.SetupSubscription(provider); + + provider.GatewaySubscriptionId = subscription.Id; + + await providerRepository.ReplaceAsync(provider); + + logger.LogInformation("CB: Setup Stripe subscription for provider ({ProviderID})", provider.Id); + } + else + { + logger.LogInformation( + "CB: Could not set up Stripe subscription for provider ({ProviderID}) with no Stripe customer", + provider.Id); + + return; + } + } + else + { + logger.LogInformation("CB: Stripe subscription already exists for provider ({ProviderID})", provider.Id); + + var providerPlans = await providerPlanRepository.GetByProviderId(provider.Id); + + var enterpriseSeatMinimum = providerPlans + .FirstOrDefault(providerPlan => providerPlan.PlanType == PlanType.EnterpriseMonthly)? + .SeatMinimum ?? 0; + + var teamsSeatMinimum = providerPlans + .FirstOrDefault(providerPlan => providerPlan.PlanType == PlanType.TeamsMonthly)? + .SeatMinimum ?? 0; + + await providerBillingService.UpdateSeatMinimums(provider, enterpriseSeatMinimum, teamsSeatMinimum); + + logger.LogInformation( + "CB: Updated Stripe subscription for provider ({ProviderID}) with current seat minimums", provider.Id); + } + + await migrationTrackerCache.UpdateTrackingStatus(provider.Id, ProviderMigrationProgress.SubscriptionSetup); + } + + private async Task ApplyCreditAsync(Provider provider) + { + var organizations = await GetClientsAsync(provider.Id); + + var organizationCustomers = + await Task.WhenAll(organizations.Select(organization => stripeAdapter.CustomerGetAsync(organization.GatewayCustomerId))); + + 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." + }); + + var migrationRecords = await Task.WhenAll(organizations.Select(organization => + clientOrganizationMigrationRecordRepository.GetByOrganizationId(organization.Id))); + + var legacyOrganizationMigrationRecords = migrationRecords.Where(migrationRecord => + migrationRecord.PlanType is + PlanType.EnterpriseAnnually2020 or + PlanType.TeamsAnnually2020); + + var legacyOrganizationCredit = legacyOrganizationMigrationRecords.Sum(migrationRecord => migrationRecord.Seats) * 12 * -100; + + if (legacyOrganizationCredit < 0) + { + await stripeAdapter.CustomerBalanceTransactionCreate(provider.GatewayCustomerId, + new CustomerBalanceTransactionCreateOptions + { + Amount = legacyOrganizationCredit, + Currency = "USD", + Description = "1 year rebate for legacy client organizations." + }); + } + + logger.LogInformation("CB: Applied {Credit} credit to provider ({ProviderID})", organizationCancellationCredit + legacyOrganizationCredit, provider.Id); + + await migrationTrackerCache.UpdateTrackingStatus(provider.Id, ProviderMigrationProgress.CreditApplied); + } + + private async Task UpdateProviderAsync(Provider provider) + { + provider.Status = ProviderStatusType.Billable; + + await providerRepository.ReplaceAsync(provider); + + logger.LogInformation("CB: Completed migration for provider ({ProviderID})", provider.Id); + + await migrationTrackerCache.UpdateTrackingStatus(provider.Id, ProviderMigrationProgress.Completed); + } + + #endregion + + #region Utilities + + private async Task> GetClientsAsync(Guid providerId) + { + var providerOrganizations = await providerOrganizationRepository.GetManyDetailsByProviderAsync(providerId); + + return (await Task.WhenAll(providerOrganizations.Select(providerOrganization => + organizationRepository.GetByIdAsync(providerOrganization.OrganizationId)))) + .ToList(); + } + + private async Task GetProviderAsync(Guid providerId) + { + var provider = await providerRepository.GetByIdAsync(providerId); + + if (provider == null) + { + logger.LogWarning("CB: Cannot migrate provider ({ProviderID}) as it does not exist", providerId); + + return null; + } + + if (provider.Type != ProviderType.Msp) + { + logger.LogWarning("CB: Cannot migrate provider ({ProviderID}) as it is not an MSP", providerId); + + return null; + } + + if (provider.Status == ProviderStatusType.Created) + { + return provider; + } + + logger.LogWarning("CB: Cannot migrate provider ({ProviderID}) as it is not in the 'Created' state", providerId); + + return null; + } + + private static bool IsEnterprise(Organization organization) => organization.Plan.Contains("Enterprise"); + private static bool IsTeams(Organization organization) => organization.Plan.Contains("Teams"); + + #endregion +} diff --git a/src/Core/Billing/Models/OrganizationMetadata.cs b/src/Core/Billing/Models/OrganizationMetadata.cs index decc35ffd..136964d7c 100644 --- a/src/Core/Billing/Models/OrganizationMetadata.cs +++ b/src/Core/Billing/Models/OrganizationMetadata.cs @@ -1,8 +1,10 @@ namespace Bit.Core.Billing.Models; public record OrganizationMetadata( + bool IsEligibleForSelfHost, bool IsOnSecretsManagerStandalone) { public static OrganizationMetadata Default() => new( - IsOnSecretsManagerStandalone: default); + IsEligibleForSelfHost: false, + IsOnSecretsManagerStandalone: false); } 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/Repositories/IClientOrganizationMigrationRecordRepository.cs b/src/Core/Billing/Repositories/IClientOrganizationMigrationRecordRepository.cs new file mode 100644 index 000000000..216598438 --- /dev/null +++ b/src/Core/Billing/Repositories/IClientOrganizationMigrationRecordRepository.cs @@ -0,0 +1,10 @@ +using Bit.Core.Billing.Entities; +using Bit.Core.Repositories; + +namespace Bit.Core.Billing.Repositories; + +public interface IClientOrganizationMigrationRecordRepository : IRepository +{ + Task GetByOrganizationId(Guid organizationId); + Task> GetByProviderId(Guid providerId); +} diff --git a/src/Core/Billing/Services/IPaymentHistoryService.cs b/src/Core/Billing/Services/IPaymentHistoryService.cs index e38659b94..c2e1a7df8 100644 --- a/src/Core/Billing/Services/IPaymentHistoryService.cs +++ b/src/Core/Billing/Services/IPaymentHistoryService.cs @@ -1,4 +1,5 @@ -using Bit.Core.Billing.Models; +#nullable enable +using Bit.Core.Billing.Models; using Bit.Core.Entities; namespace Bit.Core.Billing.Services; @@ -8,7 +9,8 @@ public interface IPaymentHistoryService Task> GetInvoiceHistoryAsync( ISubscriber subscriber, int pageSize = 5, - string startAfter = null); + string? status = null, + string? startAfter = null); Task> GetTransactionHistoryAsync( ISubscriber subscriber, diff --git a/src/Core/Billing/Services/Implementations/OrganizationBillingService.cs b/src/Core/Billing/Services/Implementations/OrganizationBillingService.cs index 3c5938cab..7db886203 100644 --- a/src/Core/Billing/Services/Implementations/OrganizationBillingService.cs +++ b/src/Core/Billing/Services/Implementations/OrganizationBillingService.cs @@ -1,4 +1,5 @@ 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; @@ -26,6 +27,7 @@ public class OrganizationBillingService( IGlobalSettings globalSettings, ILogger logger, IOrganizationRepository organizationRepository, + IProviderRepository providerRepository, ISetupIntentCache setupIntentCache, IStripeAdapter stripeAdapter, ISubscriberService subscriberService) : IOrganizationBillingService @@ -69,14 +71,11 @@ public class OrganizationBillingService( var subscription = await subscriberService.GetSubscription(organization); - if (customer == null || subscription == null) - { - return OrganizationMetadata.Default(); - } + var isEligibleForSelfHost = await IsEligibleForSelfHost(organization, subscription); var isOnSecretsManagerStandalone = IsOnSecretsManagerStandalone(organization, customer, subscription); - return new OrganizationMetadata(isOnSecretsManagerStandalone); + return new OrganizationMetadata(isEligibleForSelfHost, isOnSecretsManagerStandalone); } public async Task UpdatePaymentMethod( @@ -340,11 +339,38 @@ public class OrganizationBillingService( return await stripeAdapter.SubscriptionCreateAsync(subscriptionCreateOptions); } + private async Task IsEligibleForSelfHost( + Organization organization, + Subscription? organizationSubscription) + { + if (organization.Status != OrganizationStatusType.Managed) + { + return organization.Plan.Contains("Families") || + organization.Plan.Contains("Enterprise") && IsActive(organizationSubscription); + } + + 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; + } + private static bool IsOnSecretsManagerStandalone( Organization organization, - Customer customer, - Subscription subscription) + Customer? customer, + Subscription? subscription) { + if (customer == null || subscription == null) + { + return false; + } + var plan = StaticStore.GetPlan(organization.PlanType); if (!plan.SupportsSecretsManager) diff --git a/src/Core/Billing/Services/Implementations/PaymentHistoryService.cs b/src/Core/Billing/Services/Implementations/PaymentHistoryService.cs index 1e5e3ea0e..69e1a4cfb 100644 --- a/src/Core/Billing/Services/Implementations/PaymentHistoryService.cs +++ b/src/Core/Billing/Services/Implementations/PaymentHistoryService.cs @@ -1,4 +1,5 @@ -using Bit.Core.AdminConsole.Entities; +#nullable enable +using Bit.Core.AdminConsole.Entities; using Bit.Core.Billing.Models; using Bit.Core.Entities; using Bit.Core.Models.BitStripe; @@ -16,11 +17,12 @@ public class PaymentHistoryService( public async Task> GetInvoiceHistoryAsync( ISubscriber subscriber, int pageSize = 5, - string startAfter = null) + string? status = null, + string? startAfter = null) { if (subscriber is not { GatewayCustomerId: not null, GatewaySubscriptionId: not null }) { - return null; + return Array.Empty(); } var invoices = await stripeAdapter.InvoiceListAsync(new StripeInvoiceListOptions @@ -28,6 +30,7 @@ public class PaymentHistoryService( Customer = subscriber.GatewayCustomerId, Subscription = subscriber.GatewaySubscriptionId, Limit = pageSize, + Status = status, StartingAfter = startAfter }); @@ -48,6 +51,7 @@ public class PaymentHistoryService( }; return transactions?.OrderByDescending(i => i.CreationDate) - .Select(t => new BillingHistoryInfo.BillingTransaction(t)); + .Select(t => new BillingHistoryInfo.BillingTransaction(t)) + ?? Array.Empty(); } } diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 3f54f7d42..b64d46b5b 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -110,7 +110,6 @@ public static class FeatureFlagKeys 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"; - public const string EnableDeleteProvider = "AC-1218-delete-provider"; public const string EmailVerification = "email-verification"; public const string EmailVerificationDisableTimingDelays = "email-verification-disable-timing-delays"; public const string AnhFcmv1Migration = "anh-fcmv1-migration"; @@ -118,7 +117,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"; @@ -143,7 +141,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() { @@ -159,7 +164,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 735fe0758..8d8822b34 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -21,11 +21,11 @@ - - + + - + @@ -35,31 +35,31 @@ - - + + - + - + - + - + - - + + diff --git a/src/Core/Models/Api/Request/PushDeviceRequestModel.cs b/src/Core/Models/Api/Request/PushDeviceRequestModel.cs index e1866b6f2..8b97dcc36 100644 --- a/src/Core/Models/Api/Request/PushDeviceRequestModel.cs +++ b/src/Core/Models/Api/Request/PushDeviceRequestModel.cs @@ -1,5 +1,4 @@ using System.ComponentModel.DataAnnotations; -using Bit.Core.Enums; namespace Bit.Core.Models.Api; @@ -7,6 +6,4 @@ public class PushDeviceRequestModel { [Required] public string Id { get; set; } - [Required] - public DeviceType Type { get; set; } } diff --git a/src/Core/Models/Api/Request/PushUpdateRequestModel.cs b/src/Core/Models/Api/Request/PushUpdateRequestModel.cs index 9f7ed5f28..f8c2d296f 100644 --- a/src/Core/Models/Api/Request/PushUpdateRequestModel.cs +++ b/src/Core/Models/Api/Request/PushUpdateRequestModel.cs @@ -1,5 +1,4 @@ using System.ComponentModel.DataAnnotations; -using Bit.Core.Enums; namespace Bit.Core.Models.Api; @@ -8,9 +7,9 @@ public class PushUpdateRequestModel public PushUpdateRequestModel() { } - public PushUpdateRequestModel(IEnumerable> devices, string organizationId) + public PushUpdateRequestModel(IEnumerable deviceIds, string organizationId) { - Devices = devices.Select(d => new PushDeviceRequestModel { Id = d.Key, Type = d.Value }); + Devices = deviceIds.Select(d => new PushDeviceRequestModel { Id = d }); OrganizationId = organizationId; } diff --git a/src/Core/Models/Business/OrganizationLicense.cs b/src/Core/Models/Business/OrganizationLicense.cs index ebc1a083f..ea5127364 100644 --- a/src/Core/Models/Business/OrganizationLicense.cs +++ b/src/Core/Models/Business/OrganizationLicense.cs @@ -53,8 +53,11 @@ public class OrganizationLicense : ILicense UseSecretsManager = org.UseSecretsManager; SmSeats = org.SmSeats; SmServiceAccounts = org.SmServiceAccounts; + + // Deprecated. Left for backwards compatibility with old license versions. LimitCollectionCreationDeletion = org.LimitCollectionCreationDeletion; AllowAdminAccessToAllCollectionItems = org.AllowAdminAccessToAllCollectionItems; + // if (subscriptionInfo?.Subscription == null) { @@ -138,8 +141,12 @@ public class OrganizationLicense : ILicense public bool UseSecretsManager { get; set; } public int? SmSeats { get; set; } public int? SmServiceAccounts { get; set; } + + // Deprecated. Left for backwards compatibility with old license versions. public bool LimitCollectionCreationDeletion { get; set; } = true; public bool AllowAdminAccessToAllCollectionItems { get; set; } = true; + // + public bool Trial { get; set; } public LicenseType? LicenseType { get; set; } public string Hash { get; set; } @@ -150,7 +157,8 @@ public class OrganizationLicense : ILicense /// Represents the current version of the license format. Should be updated whenever new fields are added. /// /// Intentionally set one version behind to allow self hosted users some time to update before - /// getting out of date license errors + /// getting out of date license errors + /// public const int CurrentLicenseFileVersion = 14; private bool ValidLicenseVersion { @@ -368,10 +376,11 @@ public class OrganizationLicense : ILicense } /* - * Version 14 added LimitCollectionCreationDeletion and Version 15 added AllowAdminAccessToAllCollectionItems, - * however these are just user settings and it is not worth failing validation if they mismatch. - * They are intentionally excluded. - */ + * Version 14 added LimitCollectionCreationDeletion and Version + * 15 added AllowAdminAccessToAllCollectionItems, however they + * are no longer used and are intentionally excluded from + * validation. + */ return valid; } diff --git a/src/Core/Models/Data/InstallationDeviceEntity.cs b/src/Core/Models/Data/InstallationDeviceEntity.cs index 3186efc66..a3d960b24 100644 --- a/src/Core/Models/Data/InstallationDeviceEntity.cs +++ b/src/Core/Models/Data/InstallationDeviceEntity.cs @@ -37,4 +37,25 @@ public class InstallationDeviceEntity : ITableEntity { return deviceId != null && deviceId.Length == 73 && deviceId[36] == '_'; } + public static bool TryParse(string deviceId, out InstallationDeviceEntity installationDeviceEntity) + { + installationDeviceEntity = null; + var installationId = Guid.Empty; + var deviceIdGuid = Guid.Empty; + if (!IsInstallationDeviceId(deviceId)) + { + return false; + } + var parts = deviceId.Split("_"); + if (parts.Length < 2) + { + return false; + } + if (!Guid.TryParse(parts[0], out installationId) || !Guid.TryParse(parts[1], out deviceIdGuid)) + { + return false; + } + installationDeviceEntity = new InstallationDeviceEntity(installationId, deviceIdGuid); + return true; + } } diff --git a/src/Core/Models/Data/Organizations/VerifiedOrganizationDomainSsoDetail.cs b/src/Core/Models/Data/Organizations/VerifiedOrganizationDomainSsoDetail.cs new file mode 100644 index 000000000..0a07af66b --- /dev/null +++ b/src/Core/Models/Data/Organizations/VerifiedOrganizationDomainSsoDetail.cs @@ -0,0 +1,22 @@ +namespace Bit.Core.Models.Data.Organizations; + +public class VerifiedOrganizationDomainSsoDetail +{ + public VerifiedOrganizationDomainSsoDetail() + { + } + + public VerifiedOrganizationDomainSsoDetail(Guid organizationId, string organizationName, string domainName, + string organizationIdentifier) + { + OrganizationId = organizationId; + OrganizationName = organizationName; + DomainName = domainName; + OrganizationIdentifier = organizationIdentifier; + } + + public Guid OrganizationId { get; init; } + public string OrganizationName { get; init; } + public string DomainName { get; init; } + public string OrganizationIdentifier { get; init; } +} diff --git a/src/Core/NotificationCenter/Authorization/NotificationAuthorizationHandler.cs b/src/Core/NotificationCenter/Authorization/NotificationAuthorizationHandler.cs new file mode 100644 index 000000000..6e06c92db --- /dev/null +++ b/src/Core/NotificationCenter/Authorization/NotificationAuthorizationHandler.cs @@ -0,0 +1,68 @@ +#nullable enable +using Bit.Core.Context; +using Bit.Core.NotificationCenter.Entities; +using Microsoft.AspNetCore.Authorization; + +namespace Bit.Core.NotificationCenter.Authorization; + +public class NotificationAuthorizationHandler : AuthorizationHandler +{ + private readonly ICurrentContext _currentContext; + + public NotificationAuthorizationHandler(ICurrentContext currentContext) + { + _currentContext = currentContext; + } + + protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, + NotificationOperationsRequirement requirement, + Notification notification) + { + if (!_currentContext.UserId.HasValue) + { + return; + } + + var authorized = requirement switch + { + not null when requirement == NotificationOperations.Read => CanRead(notification), + not null when requirement == NotificationOperations.Create => await CanCreate(notification), + not null when requirement == NotificationOperations.Update => await CanUpdate(notification), + _ => throw new ArgumentException("Unsupported operation requirement type provided.", nameof(requirement)) + }; + + if (authorized) + { + context.Succeed(requirement); + } + } + + private bool CanRead(Notification notification) + { + var userMatching = !notification.UserId.HasValue || notification.UserId.Value == _currentContext.UserId!.Value; + var organizationMatching = !notification.OrganizationId.HasValue || + _currentContext.GetOrganization(notification.OrganizationId.Value) != null; + + return notification.Global || (userMatching && organizationMatching); + } + + private async Task CanCreate(Notification notification) + { + var organizationPermissionsMatching = !notification.OrganizationId.HasValue || + await _currentContext.AccessReports(notification.OrganizationId.Value); + var userNoOrganizationMatching = !notification.UserId.HasValue || notification.OrganizationId.HasValue || + notification.UserId.Value == _currentContext.UserId!.Value; + + return !notification.Global && organizationPermissionsMatching && userNoOrganizationMatching; + } + + private async Task CanUpdate(Notification notification) + { + var organizationPermissionsMatching = !notification.OrganizationId.HasValue || + await _currentContext.AccessReports(notification.OrganizationId.Value); + var userNoOrganizationMatching = !notification.UserId.HasValue || notification.OrganizationId.HasValue || + notification.UserId.Value == _currentContext.UserId!.Value; + + return !notification.Global && organizationPermissionsMatching && userNoOrganizationMatching; + } +} diff --git a/src/Core/NotificationCenter/Authorization/NotificationOperations.cs b/src/Core/NotificationCenter/Authorization/NotificationOperations.cs new file mode 100644 index 000000000..5a67805b4 --- /dev/null +++ b/src/Core/NotificationCenter/Authorization/NotificationOperations.cs @@ -0,0 +1,19 @@ +#nullable enable +using Microsoft.AspNetCore.Authorization.Infrastructure; + +namespace Bit.Core.NotificationCenter.Authorization; + +public class NotificationOperationsRequirement : OperationAuthorizationRequirement +{ + public NotificationOperationsRequirement(string name) + { + Name = name; + } +} + +public static class NotificationOperations +{ + public static readonly NotificationOperationsRequirement Read = new(nameof(Read)); + public static readonly NotificationOperationsRequirement Create = new(nameof(Create)); + public static readonly NotificationOperationsRequirement Update = new(nameof(Update)); +} diff --git a/src/Core/NotificationCenter/Authorization/NotificationStatusAuthorizationHandler.cs b/src/Core/NotificationCenter/Authorization/NotificationStatusAuthorizationHandler.cs new file mode 100644 index 000000000..df018c081 --- /dev/null +++ b/src/Core/NotificationCenter/Authorization/NotificationStatusAuthorizationHandler.cs @@ -0,0 +1,57 @@ +#nullable enable +using Bit.Core.Context; +using Bit.Core.NotificationCenter.Entities; +using Microsoft.AspNetCore.Authorization; + +namespace Bit.Core.NotificationCenter.Authorization; + +public class NotificationStatusAuthorizationHandler : AuthorizationHandler +{ + private readonly ICurrentContext _currentContext; + + public NotificationStatusAuthorizationHandler(ICurrentContext currentContext) + { + _currentContext = currentContext; + } + + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, + NotificationStatusOperationsRequirement requirement, + NotificationStatus notificationStatus) + { + if (!_currentContext.UserId.HasValue) + { + return Task.CompletedTask; + } + + var authorized = requirement switch + { + not null when requirement == NotificationStatusOperations.Read => CanRead(notificationStatus), + not null when requirement == NotificationStatusOperations.Create => CanCreate(notificationStatus), + not null when requirement == NotificationStatusOperations.Update => CanUpdate(notificationStatus), + _ => throw new ArgumentException("Unsupported operation requirement type provided.", nameof(requirement)) + }; + + if (authorized) + { + context.Succeed(requirement); + } + + return Task.CompletedTask; + } + + private bool CanRead(NotificationStatus notificationStatus) + { + return notificationStatus.UserId == _currentContext.UserId!.Value; + } + + private bool CanCreate(NotificationStatus notificationStatus) + { + return notificationStatus.UserId == _currentContext.UserId!.Value; + } + + private bool CanUpdate(NotificationStatus notificationStatus) + { + return notificationStatus.UserId == _currentContext.UserId!.Value; + } +} diff --git a/src/Core/NotificationCenter/Authorization/NotificationStatusOperations.cs b/src/Core/NotificationCenter/Authorization/NotificationStatusOperations.cs new file mode 100644 index 000000000..e097d67cb --- /dev/null +++ b/src/Core/NotificationCenter/Authorization/NotificationStatusOperations.cs @@ -0,0 +1,19 @@ +#nullable enable +using Microsoft.AspNetCore.Authorization.Infrastructure; + +namespace Bit.Core.NotificationCenter.Authorization; + +public class NotificationStatusOperationsRequirement : OperationAuthorizationRequirement +{ + public NotificationStatusOperationsRequirement(string name) + { + Name = name; + } +} + +public static class NotificationStatusOperations +{ + public static readonly NotificationStatusOperationsRequirement Read = new(nameof(Read)); + public static readonly NotificationStatusOperationsRequirement Create = new(nameof(Create)); + public static readonly NotificationStatusOperationsRequirement Update = new(nameof(Update)); +} diff --git a/src/Core/NotificationCenter/Commands/CreateNotificationCommand.cs b/src/Core/NotificationCenter/Commands/CreateNotificationCommand.cs new file mode 100644 index 000000000..4f76950a3 --- /dev/null +++ b/src/Core/NotificationCenter/Commands/CreateNotificationCommand.cs @@ -0,0 +1,36 @@ +#nullable enable +using Bit.Core.Context; +using Bit.Core.NotificationCenter.Authorization; +using Bit.Core.NotificationCenter.Commands.Interfaces; +using Bit.Core.NotificationCenter.Entities; +using Bit.Core.NotificationCenter.Repositories; +using Bit.Core.Utilities; +using Microsoft.AspNetCore.Authorization; + +namespace Bit.Core.NotificationCenter.Commands; + +public class CreateNotificationCommand : ICreateNotificationCommand +{ + private readonly ICurrentContext _currentContext; + private readonly IAuthorizationService _authorizationService; + private readonly INotificationRepository _notificationRepository; + + public CreateNotificationCommand(ICurrentContext currentContext, + IAuthorizationService authorizationService, + INotificationRepository notificationRepository) + { + _currentContext = currentContext; + _authorizationService = authorizationService; + _notificationRepository = notificationRepository; + } + + public async Task CreateAsync(Notification notification) + { + notification.CreationDate = notification.RevisionDate = DateTime.UtcNow; + + await _authorizationService.AuthorizeOrThrowAsync(_currentContext.HttpContext.User, notification, + NotificationOperations.Create); + + return await _notificationRepository.CreateAsync(notification); + } +} diff --git a/src/Core/NotificationCenter/Commands/CreateNotificationStatusCommand.cs b/src/Core/NotificationCenter/Commands/CreateNotificationStatusCommand.cs new file mode 100644 index 000000000..fcd61ceeb --- /dev/null +++ b/src/Core/NotificationCenter/Commands/CreateNotificationStatusCommand.cs @@ -0,0 +1,47 @@ +#nullable enable +using Bit.Core.Context; +using Bit.Core.Exceptions; +using Bit.Core.NotificationCenter.Authorization; +using Bit.Core.NotificationCenter.Commands.Interfaces; +using Bit.Core.NotificationCenter.Entities; +using Bit.Core.NotificationCenter.Repositories; +using Bit.Core.Utilities; +using Microsoft.AspNetCore.Authorization; + +namespace Bit.Core.NotificationCenter.Commands; + +public class CreateNotificationStatusCommand : ICreateNotificationStatusCommand +{ + private readonly ICurrentContext _currentContext; + private readonly IAuthorizationService _authorizationService; + private readonly INotificationRepository _notificationRepository; + private readonly INotificationStatusRepository _notificationStatusRepository; + + public CreateNotificationStatusCommand(ICurrentContext currentContext, + IAuthorizationService authorizationService, + INotificationRepository notificationRepository, + INotificationStatusRepository notificationStatusRepository) + { + _currentContext = currentContext; + _authorizationService = authorizationService; + _notificationRepository = notificationRepository; + _notificationStatusRepository = notificationStatusRepository; + } + + public async Task CreateAsync(NotificationStatus notificationStatus) + { + var notification = await _notificationRepository.GetByIdAsync(notificationStatus.NotificationId); + if (notification == null) + { + throw new NotFoundException(); + } + + await _authorizationService.AuthorizeOrThrowAsync(_currentContext.HttpContext.User, notification, + NotificationOperations.Read); + + await _authorizationService.AuthorizeOrThrowAsync(_currentContext.HttpContext.User, notificationStatus, + NotificationStatusOperations.Create); + + return await _notificationStatusRepository.CreateAsync(notificationStatus); + } +} diff --git a/src/Core/NotificationCenter/Commands/Interfaces/ICreateNotificationCommand.cs b/src/Core/NotificationCenter/Commands/Interfaces/ICreateNotificationCommand.cs new file mode 100644 index 000000000..a3b4d894e --- /dev/null +++ b/src/Core/NotificationCenter/Commands/Interfaces/ICreateNotificationCommand.cs @@ -0,0 +1,9 @@ +#nullable enable +using Bit.Core.NotificationCenter.Entities; + +namespace Bit.Core.NotificationCenter.Commands.Interfaces; + +public interface ICreateNotificationCommand +{ + Task CreateAsync(Notification notification); +} diff --git a/src/Core/NotificationCenter/Commands/Interfaces/ICreateNotificationStatusCommand.cs b/src/Core/NotificationCenter/Commands/Interfaces/ICreateNotificationStatusCommand.cs new file mode 100644 index 000000000..ea9695e2e --- /dev/null +++ b/src/Core/NotificationCenter/Commands/Interfaces/ICreateNotificationStatusCommand.cs @@ -0,0 +1,9 @@ +#nullable enable +using Bit.Core.NotificationCenter.Entities; + +namespace Bit.Core.NotificationCenter.Commands.Interfaces; + +public interface ICreateNotificationStatusCommand +{ + Task CreateAsync(NotificationStatus notificationStatus); +} diff --git a/src/Core/NotificationCenter/Commands/Interfaces/IMarkNotificationDeletedCommand.cs b/src/Core/NotificationCenter/Commands/Interfaces/IMarkNotificationDeletedCommand.cs new file mode 100644 index 000000000..39bc0735c --- /dev/null +++ b/src/Core/NotificationCenter/Commands/Interfaces/IMarkNotificationDeletedCommand.cs @@ -0,0 +1,7 @@ +#nullable enable +namespace Bit.Core.NotificationCenter.Commands.Interfaces; + +public interface IMarkNotificationDeletedCommand +{ + Task MarkDeletedAsync(Guid notificationId); +} diff --git a/src/Core/NotificationCenter/Commands/Interfaces/IMarkNotificationReadCommand.cs b/src/Core/NotificationCenter/Commands/Interfaces/IMarkNotificationReadCommand.cs new file mode 100644 index 000000000..91ce63def --- /dev/null +++ b/src/Core/NotificationCenter/Commands/Interfaces/IMarkNotificationReadCommand.cs @@ -0,0 +1,7 @@ +#nullable enable +namespace Bit.Core.NotificationCenter.Commands.Interfaces; + +public interface IMarkNotificationReadCommand +{ + Task MarkReadAsync(Guid notificationId); +} diff --git a/src/Core/NotificationCenter/Commands/Interfaces/IUpdateNotificationCommand.cs b/src/Core/NotificationCenter/Commands/Interfaces/IUpdateNotificationCommand.cs new file mode 100644 index 000000000..8db10e6f1 --- /dev/null +++ b/src/Core/NotificationCenter/Commands/Interfaces/IUpdateNotificationCommand.cs @@ -0,0 +1,9 @@ +#nullable enable +using Bit.Core.NotificationCenter.Entities; + +namespace Bit.Core.NotificationCenter.Commands.Interfaces; + +public interface IUpdateNotificationCommand +{ + Task UpdateAsync(Notification notification); +} diff --git a/src/Core/NotificationCenter/Commands/MarkNotificationDeletedCommand.cs b/src/Core/NotificationCenter/Commands/MarkNotificationDeletedCommand.cs new file mode 100644 index 000000000..2ca7aa905 --- /dev/null +++ b/src/Core/NotificationCenter/Commands/MarkNotificationDeletedCommand.cs @@ -0,0 +1,74 @@ +#nullable enable +using Bit.Core.Context; +using Bit.Core.Exceptions; +using Bit.Core.NotificationCenter.Authorization; +using Bit.Core.NotificationCenter.Commands.Interfaces; +using Bit.Core.NotificationCenter.Entities; +using Bit.Core.NotificationCenter.Repositories; +using Bit.Core.Utilities; +using Microsoft.AspNetCore.Authorization; + +namespace Bit.Core.NotificationCenter.Commands; + +public class MarkNotificationDeletedCommand : IMarkNotificationDeletedCommand +{ + private readonly ICurrentContext _currentContext; + private readonly IAuthorizationService _authorizationService; + private readonly INotificationRepository _notificationRepository; + private readonly INotificationStatusRepository _notificationStatusRepository; + + public MarkNotificationDeletedCommand(ICurrentContext currentContext, + IAuthorizationService authorizationService, + INotificationRepository notificationRepository, + INotificationStatusRepository notificationStatusRepository) + { + _currentContext = currentContext; + _authorizationService = authorizationService; + _notificationRepository = notificationRepository; + _notificationStatusRepository = notificationStatusRepository; + } + + public async Task MarkDeletedAsync(Guid notificationId) + { + if (!_currentContext.UserId.HasValue) + { + throw new NotFoundException(); + } + + var notification = await _notificationRepository.GetByIdAsync(notificationId); + if (notification == null) + { + throw new NotFoundException(); + } + + await _authorizationService.AuthorizeOrThrowAsync(_currentContext.HttpContext.User, notification, + NotificationOperations.Read); + + var notificationStatus = await _notificationStatusRepository.GetByNotificationIdAndUserIdAsync(notificationId, + _currentContext.UserId.Value); + + if (notificationStatus == null) + { + notificationStatus = new NotificationStatus + { + NotificationId = notificationId, + UserId = _currentContext.UserId.Value, + DeletedDate = DateTime.UtcNow + }; + + await _authorizationService.AuthorizeOrThrowAsync(_currentContext.HttpContext.User, notificationStatus, + NotificationStatusOperations.Create); + + await _notificationStatusRepository.CreateAsync(notificationStatus); + } + else + { + await _authorizationService.AuthorizeOrThrowAsync(_currentContext.HttpContext.User, notificationStatus, + NotificationStatusOperations.Update); + + notificationStatus.DeletedDate = DateTime.UtcNow; + + await _notificationStatusRepository.UpdateAsync(notificationStatus); + } + } +} diff --git a/src/Core/NotificationCenter/Commands/MarkNotificationReadCommand.cs b/src/Core/NotificationCenter/Commands/MarkNotificationReadCommand.cs new file mode 100644 index 000000000..400e44463 --- /dev/null +++ b/src/Core/NotificationCenter/Commands/MarkNotificationReadCommand.cs @@ -0,0 +1,74 @@ +#nullable enable +using Bit.Core.Context; +using Bit.Core.Exceptions; +using Bit.Core.NotificationCenter.Authorization; +using Bit.Core.NotificationCenter.Commands.Interfaces; +using Bit.Core.NotificationCenter.Entities; +using Bit.Core.NotificationCenter.Repositories; +using Bit.Core.Utilities; +using Microsoft.AspNetCore.Authorization; + +namespace Bit.Core.NotificationCenter.Commands; + +public class MarkNotificationReadCommand : IMarkNotificationReadCommand +{ + private readonly ICurrentContext _currentContext; + private readonly IAuthorizationService _authorizationService; + private readonly INotificationRepository _notificationRepository; + private readonly INotificationStatusRepository _notificationStatusRepository; + + public MarkNotificationReadCommand(ICurrentContext currentContext, + IAuthorizationService authorizationService, + INotificationRepository notificationRepository, + INotificationStatusRepository notificationStatusRepository) + { + _currentContext = currentContext; + _authorizationService = authorizationService; + _notificationRepository = notificationRepository; + _notificationStatusRepository = notificationStatusRepository; + } + + public async Task MarkReadAsync(Guid notificationId) + { + if (!_currentContext.UserId.HasValue) + { + throw new NotFoundException(); + } + + var notification = await _notificationRepository.GetByIdAsync(notificationId); + if (notification == null) + { + throw new NotFoundException(); + } + + await _authorizationService.AuthorizeOrThrowAsync(_currentContext.HttpContext.User, notification, + NotificationOperations.Read); + + var notificationStatus = await _notificationStatusRepository.GetByNotificationIdAndUserIdAsync(notificationId, + _currentContext.UserId.Value); + + if (notificationStatus == null) + { + notificationStatus = new NotificationStatus + { + NotificationId = notificationId, + UserId = _currentContext.UserId.Value, + ReadDate = DateTime.UtcNow + }; + + await _authorizationService.AuthorizeOrThrowAsync(_currentContext.HttpContext.User, notificationStatus, + NotificationStatusOperations.Create); + + await _notificationStatusRepository.CreateAsync(notificationStatus); + } + else + { + await _authorizationService.AuthorizeOrThrowAsync(_currentContext.HttpContext.User, + notificationStatus, NotificationStatusOperations.Update); + + notificationStatus.ReadDate = DateTime.UtcNow; + + await _notificationStatusRepository.UpdateAsync(notificationStatus); + } + } +} diff --git a/src/Core/NotificationCenter/Commands/UpdateNotificationCommand.cs b/src/Core/NotificationCenter/Commands/UpdateNotificationCommand.cs new file mode 100644 index 000000000..f04947817 --- /dev/null +++ b/src/Core/NotificationCenter/Commands/UpdateNotificationCommand.cs @@ -0,0 +1,47 @@ +#nullable enable +using Bit.Core.Context; +using Bit.Core.Exceptions; +using Bit.Core.NotificationCenter.Authorization; +using Bit.Core.NotificationCenter.Commands.Interfaces; +using Bit.Core.NotificationCenter.Entities; +using Bit.Core.NotificationCenter.Repositories; +using Bit.Core.Utilities; +using Microsoft.AspNetCore.Authorization; + +namespace Bit.Core.NotificationCenter.Commands; + +public class UpdateNotificationCommand : IUpdateNotificationCommand +{ + private readonly ICurrentContext _currentContext; + private readonly IAuthorizationService _authorizationService; + private readonly INotificationRepository _notificationRepository; + + public UpdateNotificationCommand(ICurrentContext currentContext, + IAuthorizationService authorizationService, + INotificationRepository notificationRepository) + { + _currentContext = currentContext; + _authorizationService = authorizationService; + _notificationRepository = notificationRepository; + } + + public async Task UpdateAsync(Notification notificationToUpdate) + { + var notification = await _notificationRepository.GetByIdAsync(notificationToUpdate.Id); + if (notification == null) + { + throw new NotFoundException(); + } + + await _authorizationService.AuthorizeOrThrowAsync(_currentContext.HttpContext.User, + notification, NotificationOperations.Update); + + notification.Priority = notificationToUpdate.Priority; + notification.ClientType = notificationToUpdate.ClientType; + notification.Title = notificationToUpdate.Title; + notification.Body = notificationToUpdate.Body; + notification.RevisionDate = DateTime.UtcNow; + + await _notificationRepository.ReplaceAsync(notification); + } +} diff --git a/src/Core/NotificationCenter/Models/Data/NotificationStatusDetails.cs b/src/Core/NotificationCenter/Models/Data/NotificationStatusDetails.cs new file mode 100644 index 000000000..d48985e72 --- /dev/null +++ b/src/Core/NotificationCenter/Models/Data/NotificationStatusDetails.cs @@ -0,0 +1,25 @@ +#nullable enable +using System.ComponentModel.DataAnnotations; +using Bit.Core.Enums; +using Bit.Core.NotificationCenter.Enums; + +namespace Bit.Core.NotificationCenter.Models.Data; + +public class NotificationStatusDetails +{ + // Notification fields + public Guid Id { get; set; } + public Priority Priority { get; set; } + public bool Global { get; set; } + public ClientType ClientType { get; set; } + public Guid? UserId { get; set; } + public Guid? OrganizationId { get; set; } + [MaxLength(256)] + public string? Title { get; set; } + public string? Body { get; set; } + public DateTime CreationDate { get; set; } + public DateTime RevisionDate { get; set; } + // Notification Status fields + public DateTime? ReadDate { get; set; } + public DateTime? DeletedDate { get; set; } +} diff --git a/src/Core/NotificationCenter/Queries/GetNotificationStatusDetailsForUserQuery.cs b/src/Core/NotificationCenter/Queries/GetNotificationStatusDetailsForUserQuery.cs new file mode 100644 index 000000000..0a783a59b --- /dev/null +++ b/src/Core/NotificationCenter/Queries/GetNotificationStatusDetailsForUserQuery.cs @@ -0,0 +1,38 @@ +#nullable enable +using Bit.Core.Context; +using Bit.Core.Exceptions; +using Bit.Core.NotificationCenter.Models.Data; +using Bit.Core.NotificationCenter.Models.Filter; +using Bit.Core.NotificationCenter.Queries.Interfaces; +using Bit.Core.NotificationCenter.Repositories; +using Bit.Core.Utilities; + +namespace Bit.Core.NotificationCenter.Queries; + +public class GetNotificationStatusDetailsForUserQuery : IGetNotificationStatusDetailsForUserQuery +{ + private readonly ICurrentContext _currentContext; + private readonly INotificationRepository _notificationRepository; + + public GetNotificationStatusDetailsForUserQuery(ICurrentContext currentContext, + INotificationRepository notificationRepository) + { + _currentContext = currentContext; + _notificationRepository = notificationRepository; + } + + public async Task> GetByUserIdStatusFilterAsync( + NotificationStatusFilter statusFilter) + { + if (!_currentContext.UserId.HasValue) + { + throw new NotFoundException(); + } + + var clientType = DeviceTypes.ToClientType(_currentContext.DeviceType); + + // Note: only returns the user's notifications - no authorization check needed + return await _notificationRepository.GetByUserIdAndStatusAsync(_currentContext.UserId.Value, clientType, + statusFilter); + } +} diff --git a/src/Core/NotificationCenter/Queries/GetNotificationStatusForUserQuery.cs b/src/Core/NotificationCenter/Queries/GetNotificationStatusForUserQuery.cs new file mode 100644 index 000000000..b28a0444a --- /dev/null +++ b/src/Core/NotificationCenter/Queries/GetNotificationStatusForUserQuery.cs @@ -0,0 +1,47 @@ +#nullable enable +using Bit.Core.Context; +using Bit.Core.Exceptions; +using Bit.Core.NotificationCenter.Authorization; +using Bit.Core.NotificationCenter.Entities; +using Bit.Core.NotificationCenter.Queries.Interfaces; +using Bit.Core.NotificationCenter.Repositories; +using Bit.Core.Utilities; +using Microsoft.AspNetCore.Authorization; + +namespace Bit.Core.NotificationCenter.Queries; + +public class GetNotificationStatusForUserQuery : IGetNotificationStatusForUserQuery +{ + private readonly ICurrentContext _currentContext; + private readonly IAuthorizationService _authorizationService; + private readonly INotificationStatusRepository _notificationStatusRepository; + + public GetNotificationStatusForUserQuery(ICurrentContext currentContext, + IAuthorizationService authorizationService, + INotificationStatusRepository notificationStatusRepository) + { + _currentContext = currentContext; + _authorizationService = authorizationService; + _notificationStatusRepository = notificationStatusRepository; + } + + public async Task GetByNotificationIdAndUserIdAsync(Guid notificationId) + { + if (!_currentContext.UserId.HasValue) + { + throw new NotFoundException(); + } + + var notificationStatus = await _notificationStatusRepository.GetByNotificationIdAndUserIdAsync(notificationId, + _currentContext.UserId.Value); + if (notificationStatus == null) + { + throw new NotFoundException(); + } + + await _authorizationService.AuthorizeOrThrowAsync(_currentContext.HttpContext.User, + notificationStatus, NotificationStatusOperations.Read); + + return notificationStatus; + } +} diff --git a/src/Core/NotificationCenter/Queries/Interfaces/IGetNotificationStatusDetailsForUserQuery.cs b/src/Core/NotificationCenter/Queries/Interfaces/IGetNotificationStatusDetailsForUserQuery.cs new file mode 100644 index 000000000..456a0e940 --- /dev/null +++ b/src/Core/NotificationCenter/Queries/Interfaces/IGetNotificationStatusDetailsForUserQuery.cs @@ -0,0 +1,10 @@ +#nullable enable +using Bit.Core.NotificationCenter.Models.Data; +using Bit.Core.NotificationCenter.Models.Filter; + +namespace Bit.Core.NotificationCenter.Queries.Interfaces; + +public interface IGetNotificationStatusDetailsForUserQuery +{ + Task> GetByUserIdStatusFilterAsync(NotificationStatusFilter statusFilter); +} diff --git a/src/Core/NotificationCenter/Queries/Interfaces/IGetNotificationStatusForUserQuery.cs b/src/Core/NotificationCenter/Queries/Interfaces/IGetNotificationStatusForUserQuery.cs new file mode 100644 index 000000000..a31956535 --- /dev/null +++ b/src/Core/NotificationCenter/Queries/Interfaces/IGetNotificationStatusForUserQuery.cs @@ -0,0 +1,9 @@ +#nullable enable +using Bit.Core.NotificationCenter.Entities; + +namespace Bit.Core.NotificationCenter.Queries.Interfaces; + +public interface IGetNotificationStatusForUserQuery +{ + Task GetByNotificationIdAndUserIdAsync(Guid notificationId); +} diff --git a/src/Core/NotificationCenter/Repositories/INotificationRepository.cs b/src/Core/NotificationCenter/Repositories/INotificationRepository.cs index 623e759df..2c3faed91 100644 --- a/src/Core/NotificationCenter/Repositories/INotificationRepository.cs +++ b/src/Core/NotificationCenter/Repositories/INotificationRepository.cs @@ -1,6 +1,7 @@ #nullable enable using Bit.Core.Enums; using Bit.Core.NotificationCenter.Entities; +using Bit.Core.NotificationCenter.Models.Data; using Bit.Core.NotificationCenter.Models.Filter; using Bit.Core.Repositories; @@ -23,7 +24,8 @@ public interface INotificationRepository : IRepository /// /// /// Ordered by priority (highest to lowest) and creation date (descending). + /// Includes all fields from and /// - Task> GetByUserIdAndStatusAsync(Guid userId, ClientType clientType, + Task> GetByUserIdAndStatusAsync(Guid userId, ClientType clientType, NotificationStatusFilter? statusFilter); } diff --git a/src/Core/NotificationHub/INotificationHubClientProxy.cs b/src/Core/NotificationHub/INotificationHubClientProxy.cs new file mode 100644 index 000000000..82b4d3959 --- /dev/null +++ b/src/Core/NotificationHub/INotificationHubClientProxy.cs @@ -0,0 +1,8 @@ +using Microsoft.Azure.NotificationHubs; + +namespace Bit.Core.NotificationHub; + +public interface INotificationHubProxy +{ + Task<(INotificationHubClient Client, NotificationOutcome Outcome)[]> SendTemplateNotificationAsync(IDictionary properties, string tagExpression); +} diff --git a/src/Core/NotificationHub/INotificationHubPool.cs b/src/Core/NotificationHub/INotificationHubPool.cs new file mode 100644 index 000000000..7c383d7b9 --- /dev/null +++ b/src/Core/NotificationHub/INotificationHubPool.cs @@ -0,0 +1,9 @@ +using Microsoft.Azure.NotificationHubs; + +namespace Bit.Core.NotificationHub; + +public interface INotificationHubPool +{ + NotificationHubClient ClientFor(Guid comb); + INotificationHubProxy AllClients { get; } +} diff --git a/src/Core/NotificationHub/NotificationHubClientProxy.cs b/src/Core/NotificationHub/NotificationHubClientProxy.cs new file mode 100644 index 000000000..815ac8836 --- /dev/null +++ b/src/Core/NotificationHub/NotificationHubClientProxy.cs @@ -0,0 +1,26 @@ +using Microsoft.Azure.NotificationHubs; + +namespace Bit.Core.NotificationHub; + +public class NotificationHubClientProxy : INotificationHubProxy +{ + private readonly IEnumerable _clients; + + public NotificationHubClientProxy(IEnumerable clients) + { + _clients = clients; + } + + private async Task<(INotificationHubClient, T)[]> ApplyToAllClientsAsync(Func> action) + { + var tasks = _clients.Select(async c => (c, await action(c))); + return await Task.WhenAll(tasks); + } + + // partial proxy of INotificationHubClient implementation + // Note: Any other methods that are needed can simply be delegated as done here. + public async Task<(INotificationHubClient Client, NotificationOutcome Outcome)[]> SendTemplateNotificationAsync(IDictionary properties, string tagExpression) + { + return await ApplyToAllClientsAsync(async c => await c.SendTemplateNotificationAsync(properties, tagExpression)); + } +} diff --git a/src/Core/NotificationHub/NotificationHubConnection.cs b/src/Core/NotificationHub/NotificationHubConnection.cs new file mode 100644 index 000000000..3a1437f70 --- /dev/null +++ b/src/Core/NotificationHub/NotificationHubConnection.cs @@ -0,0 +1,128 @@ +using Bit.Core.Settings; +using Bit.Core.Utilities; +using Microsoft.Azure.NotificationHubs; + +class NotificationHubConnection +{ + public string HubName { get; init; } + public string ConnectionString { get; init; } + public bool EnableSendTracing { get; init; } + private NotificationHubClient _hubClient; + /// + /// Gets the NotificationHubClient for this connection. + /// + /// If the client is null, it will be initialized. + /// + /// Exception if the connection is invalid. + /// + public NotificationHubClient HubClient + { + get + { + if (_hubClient == null) + { + if (!IsValid) + { + throw new Exception("Invalid notification hub settings"); + } + Init(); + } + return _hubClient; + } + private set + { + _hubClient = value; + } + } + /// + /// Gets the start date for registration. + /// + /// If null, registration is always disabled. + /// + public DateTime? RegistrationStartDate { get; init; } + /// + /// Gets the end date for registration. + /// + /// If null, registration has no end date. + /// + public DateTime? RegistrationEndDate { get; init; } + /// + /// Gets whether all data needed to generate a connection to Notification Hub is present. + /// + public bool IsValid + { + get + { + { + var invalid = string.IsNullOrWhiteSpace(HubName) || string.IsNullOrWhiteSpace(ConnectionString); + return !invalid; + } + } + } + + public string LogString + { + get + { + return $"HubName: {HubName}, EnableSendTracing: {EnableSendTracing}, RegistrationStartDate: {RegistrationStartDate}, RegistrationEndDate: {RegistrationEndDate}"; + } + } + + /// + /// Gets whether registration is enabled for the given comb ID. + /// This is based off of the generation time encoded in the comb ID. + /// + /// + /// + public bool RegistrationEnabled(Guid comb) + { + var combTime = CoreHelpers.DateFromComb(comb); + return RegistrationEnabled(combTime); + } + + /// + /// Gets whether registration is enabled for the given time. + /// + /// The time to check + /// + public bool RegistrationEnabled(DateTime queryTime) + { + if (queryTime >= RegistrationEndDate || RegistrationStartDate == null) + { + return false; + } + + return RegistrationStartDate < queryTime; + } + + private NotificationHubConnection() { } + + /// + /// Creates a new NotificationHubConnection from the given settings. + /// + /// + /// + public static NotificationHubConnection From(GlobalSettings.NotificationHubSettings settings) + { + return new() + { + HubName = settings.HubName, + ConnectionString = settings.ConnectionString, + EnableSendTracing = settings.EnableSendTracing, + // Comb time is not precise enough for millisecond accuracy + RegistrationStartDate = settings.RegistrationStartDate.HasValue ? Truncate(settings.RegistrationStartDate.Value, TimeSpan.FromMilliseconds(10)) : null, + RegistrationEndDate = settings.RegistrationEndDate + }; + } + + private NotificationHubConnection Init() + { + HubClient = NotificationHubClient.CreateClientFromConnectionString(ConnectionString, HubName, EnableSendTracing); + return this; + } + + private static DateTime Truncate(DateTime dateTime, TimeSpan resolution) + { + return dateTime.AddTicks(-(dateTime.Ticks % resolution.Ticks)); + } +} diff --git a/src/Core/NotificationHub/NotificationHubPool.cs b/src/Core/NotificationHub/NotificationHubPool.cs new file mode 100644 index 000000000..7448aad5b --- /dev/null +++ b/src/Core/NotificationHub/NotificationHubPool.cs @@ -0,0 +1,62 @@ +using Bit.Core.Settings; +using Bit.Core.Utilities; +using Microsoft.Azure.NotificationHubs; +using Microsoft.Extensions.Logging; + +namespace Bit.Core.NotificationHub; + +public class NotificationHubPool : INotificationHubPool +{ + private List _connections { get; } + private readonly IEnumerable _clients; + private readonly ILogger _logger; + public NotificationHubPool(ILogger logger, GlobalSettings globalSettings) + { + _logger = logger; + _connections = FilterInvalidHubs(globalSettings.NotificationHubPool.NotificationHubs); + _clients = _connections.GroupBy(c => c.ConnectionString).Select(g => g.First().HubClient); + } + + private List FilterInvalidHubs(IEnumerable hubs) + { + List result = new(); + _logger.LogDebug("Filtering {HubCount} notification hubs", hubs.Count()); + foreach (var hub in hubs) + { + var connection = NotificationHubConnection.From(hub); + if (!connection.IsValid) + { + _logger.LogWarning("Invalid notification hub settings: {HubName}", hub.HubName ?? "hub name missing"); + continue; + } + _logger.LogDebug("Adding notification hub: {ConnectionLogString}", connection.LogString); + result.Add(connection); + } + + return result; + } + + + /// + /// Gets the NotificationHubClient for the given comb ID. + /// + /// + /// + /// Thrown when no notification hub is found for a given comb. + public NotificationHubClient ClientFor(Guid comb) + { + var possibleConnections = _connections.Where(c => c.RegistrationEnabled(comb)).ToArray(); + if (possibleConnections.Length == 0) + { + throw new InvalidOperationException($"No valid notification hubs are available for the given comb ({comb}).\n" + + $"The comb's datetime is {CoreHelpers.DateFromComb(comb)}." + + $"Hub start and end times are configured as follows:\n" + + string.Join("\n", _connections.Select(c => $"Hub {c.HubName} - Start: {c.RegistrationStartDate}, End: {c.RegistrationEndDate}"))); + } + var resolvedConnection = possibleConnections[CoreHelpers.BinForComb(comb, possibleConnections.Length)]; + _logger.LogTrace("Resolved notification hub for comb {Comb} out of {HubCount} hubs.\n{ConnectionInfo}", comb, possibleConnections.Length, resolvedConnection.LogString); + return resolvedConnection.HubClient; + } + + public INotificationHubProxy AllClients { get { return new NotificationHubClientProxy(_clients); } } +} diff --git a/src/Core/Services/Implementations/NotificationHubPushNotificationService.cs b/src/Core/NotificationHub/NotificationHubPushNotificationService.cs similarity index 84% rename from src/Core/Services/Implementations/NotificationHubPushNotificationService.cs rename to src/Core/NotificationHub/NotificationHubPushNotificationService.cs index 480f0dfa9..6143676de 100644 --- a/src/Core/Services/Implementations/NotificationHubPushNotificationService.cs +++ b/src/Core/NotificationHub/NotificationHubPushNotificationService.cs @@ -6,45 +6,31 @@ using Bit.Core.Enums; using Bit.Core.Models; using Bit.Core.Models.Data; using Bit.Core.Repositories; -using Bit.Core.Settings; +using Bit.Core.Services; using Bit.Core.Tools.Entities; using Bit.Core.Vault.Entities; using Microsoft.AspNetCore.Http; -using Microsoft.Azure.NotificationHubs; using Microsoft.Extensions.Logging; -namespace Bit.Core.Services; +namespace Bit.Core.NotificationHub; public class NotificationHubPushNotificationService : IPushNotificationService { private readonly IInstallationDeviceRepository _installationDeviceRepository; - private readonly GlobalSettings _globalSettings; private readonly IHttpContextAccessor _httpContextAccessor; - private readonly List _clients = []; private readonly bool _enableTracing = false; + private readonly INotificationHubPool _notificationHubPool; private readonly ILogger _logger; public NotificationHubPushNotificationService( IInstallationDeviceRepository installationDeviceRepository, - GlobalSettings globalSettings, + INotificationHubPool notificationHubPool, IHttpContextAccessor httpContextAccessor, ILogger logger) { _installationDeviceRepository = installationDeviceRepository; - _globalSettings = globalSettings; _httpContextAccessor = httpContextAccessor; - - foreach (var hub in globalSettings.NotificationHubs) - { - var client = NotificationHubClient.CreateClientFromConnectionString( - hub.ConnectionString, - hub.HubName, - hub.EnableSendTracing); - _clients.Add(client); - - _enableTracing = _enableTracing || hub.EnableSendTracing; - } - + _notificationHubPool = notificationHubPool; _logger = logger; } @@ -264,30 +250,23 @@ public class NotificationHubPushNotificationService : IPushNotificationService private async Task SendPayloadAsync(string tag, PushType type, object payload) { - var tasks = new List>(); - foreach (var client in _clients) - { - var task = client.SendTemplateNotificationAsync( - new Dictionary - { - { "type", ((byte)type).ToString() }, - { "payload", JsonSerializer.Serialize(payload) } - }, tag); - tasks.Add(task); - } - - await Task.WhenAll(tasks); + var results = await _notificationHubPool.AllClients.SendTemplateNotificationAsync( + new Dictionary + { + { "type", ((byte)type).ToString() }, + { "payload", JsonSerializer.Serialize(payload) } + }, tag); if (_enableTracing) { - for (var i = 0; i < tasks.Count; i++) + foreach (var (client, outcome) in results) { - if (_clients[i].EnableTestSend) + if (!client.EnableTestSend) { - var outcome = await tasks[i]; - _logger.LogInformation("Azure Notification Hub Tracking ID: {id} | {type} push notification with {success} successes and {failure} failures with a payload of {@payload} and result of {@results}", - outcome.TrackingId, type, outcome.Success, outcome.Failure, payload, outcome.Results); + continue; } + _logger.LogInformation("Azure Notification Hub Tracking ID: {Id} | {Type} push notification with {Success} successes and {Failure} failures with a payload of {@Payload} and result of {@Results}", + outcome.TrackingId, type, outcome.Success, outcome.Failure, payload, outcome.Results); } } } diff --git a/src/Core/Services/Implementations/NotificationHubPushRegistrationService.cs b/src/Core/NotificationHub/NotificationHubPushRegistrationService.cs similarity index 64% rename from src/Core/Services/Implementations/NotificationHubPushRegistrationService.cs rename to src/Core/NotificationHub/NotificationHubPushRegistrationService.cs index 87df60e8e..ae32babf4 100644 --- a/src/Core/Services/Implementations/NotificationHubPushRegistrationService.cs +++ b/src/Core/NotificationHub/NotificationHubPushRegistrationService.cs @@ -1,50 +1,34 @@ using Bit.Core.Enums; using Bit.Core.Models.Data; using Bit.Core.Repositories; +using Bit.Core.Services; using Bit.Core.Settings; using Microsoft.Azure.NotificationHubs; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -namespace Bit.Core.Services; +namespace Bit.Core.NotificationHub; public class NotificationHubPushRegistrationService : IPushRegistrationService { private readonly IInstallationDeviceRepository _installationDeviceRepository; private readonly GlobalSettings _globalSettings; + private readonly INotificationHubPool _notificationHubPool; private readonly IServiceProvider _serviceProvider; private readonly ILogger _logger; - private Dictionary _clients = []; public NotificationHubPushRegistrationService( IInstallationDeviceRepository installationDeviceRepository, GlobalSettings globalSettings, + INotificationHubPool notificationHubPool, IServiceProvider serviceProvider, ILogger logger) { _installationDeviceRepository = installationDeviceRepository; _globalSettings = globalSettings; + _notificationHubPool = notificationHubPool; _serviceProvider = serviceProvider; _logger = logger; - - // Is this dirty to do in the ctor? - void addHub(NotificationHubType type) - { - var hubRegistration = globalSettings.NotificationHubs.FirstOrDefault( - h => h.HubType == type && h.EnableRegistration); - if (hubRegistration != null) - { - var client = NotificationHubClient.CreateClientFromConnectionString( - hubRegistration.ConnectionString, - hubRegistration.HubName, - hubRegistration.EnableSendTracing); - _clients.Add(type, client); - } - } - - addHub(NotificationHubType.General); - addHub(NotificationHubType.iOS); - addHub(NotificationHubType.Android); } public async Task CreateOrUpdateRegistrationAsync(string pushToken, string deviceId, string userId, @@ -117,7 +101,7 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService BuildInstallationTemplate(installation, "badgeMessage", badgeMessageTemplate ?? messageTemplate, userId, identifier); - await GetClient(type).CreateOrUpdateInstallationAsync(installation); + await ClientFor(GetComb(deviceId)).CreateOrUpdateInstallationAsync(installation); if (InstallationDeviceEntity.IsInstallationDeviceId(deviceId)) { await _installationDeviceRepository.UpsertAsync(new InstallationDeviceEntity(deviceId)); @@ -152,11 +136,11 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService installation.Templates.Add(fullTemplateId, template); } - public async Task DeleteRegistrationAsync(string deviceId, DeviceType deviceType) + public async Task DeleteRegistrationAsync(string deviceId) { try { - await GetClient(deviceType).DeleteInstallationAsync(deviceId); + await ClientFor(GetComb(deviceId)).DeleteInstallationAsync(deviceId); if (InstallationDeviceEntity.IsInstallationDeviceId(deviceId)) { await _installationDeviceRepository.DeleteAsync(new InstallationDeviceEntity(deviceId)); @@ -168,31 +152,31 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService } } - public async Task AddUserRegistrationOrganizationAsync(IEnumerable> devices, string organizationId) + public async Task AddUserRegistrationOrganizationAsync(IEnumerable deviceIds, string organizationId) { - await PatchTagsForUserDevicesAsync(devices, UpdateOperationType.Add, $"organizationId:{organizationId}"); - if (devices.Any() && InstallationDeviceEntity.IsInstallationDeviceId(devices.First().Key)) + await PatchTagsForUserDevicesAsync(deviceIds, UpdateOperationType.Add, $"organizationId:{organizationId}"); + if (deviceIds.Any() && InstallationDeviceEntity.IsInstallationDeviceId(deviceIds.First())) { - var entities = devices.Select(e => new InstallationDeviceEntity(e.Key)); + var entities = deviceIds.Select(e => new InstallationDeviceEntity(e)); await _installationDeviceRepository.UpsertManyAsync(entities.ToList()); } } - public async Task DeleteUserRegistrationOrganizationAsync(IEnumerable> devices, string organizationId) + public async Task DeleteUserRegistrationOrganizationAsync(IEnumerable deviceIds, string organizationId) { - await PatchTagsForUserDevicesAsync(devices, UpdateOperationType.Remove, + await PatchTagsForUserDevicesAsync(deviceIds, UpdateOperationType.Remove, $"organizationId:{organizationId}"); - if (devices.Any() && InstallationDeviceEntity.IsInstallationDeviceId(devices.First().Key)) + if (deviceIds.Any() && InstallationDeviceEntity.IsInstallationDeviceId(deviceIds.First())) { - var entities = devices.Select(e => new InstallationDeviceEntity(e.Key)); + var entities = deviceIds.Select(e => new InstallationDeviceEntity(e)); await _installationDeviceRepository.UpsertManyAsync(entities.ToList()); } } - private async Task PatchTagsForUserDevicesAsync(IEnumerable> devices, UpdateOperationType op, + private async Task PatchTagsForUserDevicesAsync(IEnumerable deviceIds, UpdateOperationType op, string tag) { - if (!devices.Any()) + if (!deviceIds.Any()) { return; } @@ -212,11 +196,11 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService operation.Path += $"/{tag}"; } - foreach (var device in devices) + foreach (var deviceId in deviceIds) { try { - await GetClient(device.Value).PatchInstallationAsync(device.Key, new List { operation }); + await ClientFor(GetComb(deviceId)).PatchInstallationAsync(deviceId, new List { operation }); } catch (Exception e) when (e.InnerException == null || !e.InnerException.Message.Contains("(404) Not Found")) { @@ -225,53 +209,29 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService } } - private NotificationHubClient GetClient(DeviceType deviceType) + private NotificationHubClient ClientFor(Guid deviceId) { - var hubType = NotificationHubType.General; - switch (deviceType) + return _notificationHubPool.ClientFor(deviceId); + } + + private Guid GetComb(string deviceId) + { + var deviceIdString = deviceId; + InstallationDeviceEntity installationDeviceEntity; + Guid deviceIdGuid; + if (InstallationDeviceEntity.TryParse(deviceIdString, out installationDeviceEntity)) { - case DeviceType.Android: - hubType = NotificationHubType.Android; - break; - case DeviceType.iOS: - hubType = NotificationHubType.iOS; - break; - case DeviceType.ChromeExtension: - case DeviceType.FirefoxExtension: - case DeviceType.OperaExtension: - case DeviceType.EdgeExtension: - case DeviceType.VivaldiExtension: - case DeviceType.SafariExtension: - hubType = NotificationHubType.GeneralBrowserExtension; - break; - case DeviceType.WindowsDesktop: - case DeviceType.MacOsDesktop: - case DeviceType.LinuxDesktop: - hubType = NotificationHubType.GeneralDesktop; - break; - case DeviceType.ChromeBrowser: - case DeviceType.FirefoxBrowser: - case DeviceType.OperaBrowser: - case DeviceType.EdgeBrowser: - case DeviceType.IEBrowser: - case DeviceType.UnknownBrowser: - case DeviceType.SafariBrowser: - case DeviceType.VivaldiBrowser: - hubType = NotificationHubType.GeneralWeb; - break; - default: - break; + // Strip off the installation id (PartitionId). RowKey is the ID in the Installation's table. + deviceIdString = installationDeviceEntity.RowKey; } - if (!_clients.ContainsKey(hubType)) + if (Guid.TryParse(deviceIdString, out deviceIdGuid)) { - _logger.LogWarning("No hub client for '{0}'. Using general hub instead.", hubType); - hubType = NotificationHubType.General; - if (!_clients.ContainsKey(hubType)) - { - throw new Exception("No general hub client found."); - } } - return _clients[hubType]; + else + { + throw new Exception($"Invalid device id {deviceId}."); + } + return deviceIdGuid; } } diff --git a/src/Core/OrganizationFeatures/OrganizationLicenses/UpdateOrganizationLicenseCommand.cs b/src/Core/OrganizationFeatures/OrganizationLicenses/UpdateOrganizationLicenseCommand.cs index 62c46460a..1f8c6604b 100644 --- a/src/Core/OrganizationFeatures/OrganizationLicenses/UpdateOrganizationLicenseCommand.cs +++ b/src/Core/OrganizationFeatures/OrganizationLicenses/UpdateOrganizationLicenseCommand.cs @@ -17,15 +17,18 @@ public class UpdateOrganizationLicenseCommand : IUpdateOrganizationLicenseComman private readonly ILicensingService _licensingService; private readonly IGlobalSettings _globalSettings; private readonly IOrganizationService _organizationService; + private readonly IFeatureService _featureService; public UpdateOrganizationLicenseCommand( ILicensingService licensingService, IGlobalSettings globalSettings, - IOrganizationService organizationService) + IOrganizationService organizationService, + IFeatureService featureService) { _licensingService = licensingService; _globalSettings = globalSettings; _organizationService = organizationService; + _featureService = featureService; } public async Task UpdateLicenseAsync(SelfHostedOrganizationDetails selfHostedOrganization, @@ -59,7 +62,8 @@ public class UpdateOrganizationLicenseCommand : IUpdateOrganizationLicenseComman private async Task UpdateOrganizationAsync(SelfHostedOrganizationDetails selfHostedOrganizationDetails, OrganizationLicense license) { var organization = selfHostedOrganizationDetails.ToOrganization(); - organization.UpdateFromLicense(license); + + organization.UpdateFromLicense(license, _featureService); await _organizationService.ReplaceAndUpdateCacheAsync(organization); } diff --git a/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs b/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs index 3e2946248..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) @@ -146,6 +147,7 @@ public static class OrganizationServiceCollectionExtensions services.AddScoped(); services.AddScoped(); + services.AddScoped(); } // TODO: move to OrganizationSubscriptionServiceCollectionExtensions when OrganizationUser methods are moved out of diff --git a/src/Core/Repositories/IOrganizationDomainRepository.cs b/src/Core/Repositories/IOrganizationDomainRepository.cs index 3fde08a54..f8b45574a 100644 --- a/src/Core/Repositories/IOrganizationDomainRepository.cs +++ b/src/Core/Repositories/IOrganizationDomainRepository.cs @@ -11,6 +11,7 @@ public interface IOrganizationDomainRepository : IRepository> GetDomainsByOrganizationIdAsync(Guid orgId); Task> GetManyByNextRunDateAsync(DateTime date); Task GetOrganizationDomainSsoDetailsAsync(string email); + Task> GetVerifiedOrganizationDomainSsoDetailsAsync(string email); Task GetDomainByIdOrganizationIdAsync(Guid id, Guid organizationId); Task GetDomainByOrgIdAndDomainNameAsync(Guid orgId, string domainName); Task> GetExpiredOrganizationDomainsAsync(); diff --git a/src/Core/Services/IPushRegistrationService.cs b/src/Core/Services/IPushRegistrationService.cs index 83bbed485..985246de0 100644 --- a/src/Core/Services/IPushRegistrationService.cs +++ b/src/Core/Services/IPushRegistrationService.cs @@ -6,7 +6,7 @@ public interface IPushRegistrationService { Task CreateOrUpdateRegistrationAsync(string pushToken, string deviceId, string userId, string identifier, DeviceType type); - Task DeleteRegistrationAsync(string deviceId, DeviceType type); - Task AddUserRegistrationOrganizationAsync(IEnumerable> devices, string organizationId); - Task DeleteUserRegistrationOrganizationAsync(IEnumerable> devices, string organizationId); + Task DeleteRegistrationAsync(string deviceId); + Task AddUserRegistrationOrganizationAsync(IEnumerable deviceIds, string organizationId); + Task DeleteUserRegistrationOrganizationAsync(IEnumerable deviceIds, string organizationId); } diff --git a/src/Core/Services/IStripeAdapter.cs b/src/Core/Services/IStripeAdapter.cs index bb57f1cd0..a288e1cbe 100644 --- a/src/Core/Services/IStripeAdapter.cs +++ b/src/Core/Services/IStripeAdapter.cs @@ -10,6 +10,8 @@ public interface IStripeAdapter Task CustomerUpdateAsync(string id, Stripe.CustomerUpdateOptions options = null); Task CustomerDeleteAsync(string id); Task> CustomerListPaymentMethods(string id, CustomerListPaymentMethodsOptions options = null); + Task CustomerBalanceTransactionCreate(string customerId, + CustomerBalanceTransactionCreateOptions options); Task SubscriptionCreateAsync(Stripe.SubscriptionCreateOptions subscriptionCreateOptions); Task SubscriptionGetAsync(string id, Stripe.SubscriptionGetOptions options = null); Task> SubscriptionListAsync(StripeSubscriptionListOptions subscriptionSearchOptions); diff --git a/src/Core/Services/IUserService.cs b/src/Core/Services/IUserService.cs index 0135b5f1b..65bec5ea9 100644 --- a/src/Core/Services/IUserService.cs +++ b/src/Core/Services/IUserService.cs @@ -40,10 +40,8 @@ public interface IUserService KdfType kdf, int kdfIterations, int? kdfMemory, int? kdfParallelism); Task RefreshSecurityStampAsync(User user, string masterPasswordHash); Task UpdateTwoFactorProviderAsync(User user, TwoFactorProviderType type, bool setEnabled = true, bool logEvent = true); - Task DisableTwoFactorProviderAsync(User user, TwoFactorProviderType type, - IOrganizationService organizationService); - Task RecoverTwoFactorAsync(string email, string masterPassword, string recoveryCode, - IOrganizationService organizationService); + Task DisableTwoFactorProviderAsync(User user, TwoFactorProviderType type); + Task RecoverTwoFactorAsync(string email, string masterPassword, string recoveryCode); Task GenerateUserTokenAsync(User user, string tokenProvider, string purpose); Task DeleteAsync(User user); Task DeleteAsync(User user, string token); @@ -92,14 +90,20 @@ public interface IUserService /// Indicates if the user is managed by any organization. /// /// - /// A managed user is a user whose email domain matches one of the Organization's verified domains. - /// The organization must be enabled and be on an Enterprise plan. + /// A user is considered managed by an organization if their email domain matches one of the verified domains of that organization, and the user is a member of it. + /// The organization must be enabled and able to have verified domains. /// + /// + /// False if the Account Deprovisioning feature flag is disabled. + /// Task IsManagedByAnyOrganizationAsync(Guid userId); /// - /// Gets the organization that manages the user. + /// Gets the organizations that manage the user. /// + /// + /// An empty collection if the Account Deprovisioning feature flag is disabled. + /// /// - Task GetOrganizationManagingUserAsync(Guid userId); + Task> GetOrganizationsManagingUserAsync(Guid userId); } diff --git a/src/Core/Services/Implementations/DeviceService.cs b/src/Core/Services/Implementations/DeviceService.cs index 9d8315f69..5b1e4b0f0 100644 --- a/src/Core/Services/Implementations/DeviceService.cs +++ b/src/Core/Services/Implementations/DeviceService.cs @@ -38,13 +38,13 @@ public class DeviceService : IDeviceService public async Task ClearTokenAsync(Device device) { await _deviceRepository.ClearPushTokenAsync(device.Id); - await _pushRegistrationService.DeleteRegistrationAsync(device.Id.ToString(), device.Type); + await _pushRegistrationService.DeleteRegistrationAsync(device.Id.ToString()); } public async Task DeleteAsync(Device device) { await _deviceRepository.DeleteAsync(device); - await _pushRegistrationService.DeleteRegistrationAsync(device.Id.ToString(), device.Type); + await _pushRegistrationService.DeleteRegistrationAsync(device.Id.ToString()); } public async Task UpdateDevicesTrustAsync(string currentDeviceIdentifier, diff --git a/src/Core/Services/Implementations/MultiServicePushNotificationService.cs b/src/Core/Services/Implementations/MultiServicePushNotificationService.cs index 92e29908f..00be72c98 100644 --- a/src/Core/Services/Implementations/MultiServicePushNotificationService.cs +++ b/src/Core/Services/Implementations/MultiServicePushNotificationService.cs @@ -1,61 +1,31 @@ using Bit.Core.Auth.Entities; using Bit.Core.Enums; -using Bit.Core.Repositories; using Bit.Core.Settings; using Bit.Core.Tools.Entities; -using Bit.Core.Utilities; using Bit.Core.Vault.Entities; -using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; namespace Bit.Core.Services; public class MultiServicePushNotificationService : IPushNotificationService { - private readonly List _services = new List(); + private readonly IEnumerable _services; private readonly ILogger _logger; public MultiServicePushNotificationService( - IHttpClientFactory httpFactory, - IDeviceRepository deviceRepository, - IInstallationDeviceRepository installationDeviceRepository, - GlobalSettings globalSettings, - IHttpContextAccessor httpContextAccessor, + [FromKeyedServices("implementation")] IEnumerable services, ILogger logger, - ILogger relayLogger, - ILogger hubLogger) + GlobalSettings globalSettings) { - if (globalSettings.SelfHosted) - { - if (CoreHelpers.SettingHasValue(globalSettings.PushRelayBaseUri) && - globalSettings.Installation?.Id != null && - CoreHelpers.SettingHasValue(globalSettings.Installation?.Key)) - { - _services.Add(new RelayPushNotificationService(httpFactory, deviceRepository, globalSettings, - httpContextAccessor, relayLogger)); - } - if (CoreHelpers.SettingHasValue(globalSettings.InternalIdentityKey) && - CoreHelpers.SettingHasValue(globalSettings.BaseServiceUri.InternalNotifications)) - { - _services.Add(new NotificationsApiPushNotificationService( - httpFactory, globalSettings, httpContextAccessor, hubLogger)); - } - } - else - { - var generalHub = globalSettings.NotificationHubs?.FirstOrDefault(h => h.HubType == NotificationHubType.General); - if (CoreHelpers.SettingHasValue(generalHub?.ConnectionString)) - { - _services.Add(new NotificationHubPushNotificationService(installationDeviceRepository, - globalSettings, httpContextAccessor, hubLogger)); - } - if (CoreHelpers.SettingHasValue(globalSettings.Notifications?.ConnectionString)) - { - _services.Add(new AzureQueuePushNotificationService(globalSettings, httpContextAccessor)); - } - } + _services = services; _logger = logger; + _logger.LogInformation("Hub services: {Services}", _services.Count()); + globalSettings?.NotificationHubPool?.NotificationHubs?.ForEach(hub => + { + _logger.LogInformation("HubName: {HubName}, EnableSendTracing: {EnableSendTracing}, RegistrationStartDate: {RegistrationStartDate}, RegistrationEndDate: {RegistrationEndDate}", hub.HubName, hub.EnableSendTracing, hub.RegistrationStartDate, hub.RegistrationEndDate); + }); } public Task PushSyncCipherCreateAsync(Cipher cipher, IEnumerable collectionIds) diff --git a/src/Core/Services/Implementations/RelayPushRegistrationService.cs b/src/Core/Services/Implementations/RelayPushRegistrationService.cs index d9df7d04d..d0f7736e9 100644 --- a/src/Core/Services/Implementations/RelayPushRegistrationService.cs +++ b/src/Core/Services/Implementations/RelayPushRegistrationService.cs @@ -38,37 +38,36 @@ public class RelayPushRegistrationService : BaseIdentityClientService, IPushRegi await SendAsync(HttpMethod.Post, "push/register", requestModel); } - public async Task DeleteRegistrationAsync(string deviceId, DeviceType type) + public async Task DeleteRegistrationAsync(string deviceId) { var requestModel = new PushDeviceRequestModel { Id = deviceId, - Type = type, }; await SendAsync(HttpMethod.Post, "push/delete", requestModel); } public async Task AddUserRegistrationOrganizationAsync( - IEnumerable> devices, string organizationId) + IEnumerable deviceIds, string organizationId) { - if (!devices.Any()) + if (!deviceIds.Any()) { return; } - var requestModel = new PushUpdateRequestModel(devices, organizationId); + var requestModel = new PushUpdateRequestModel(deviceIds, organizationId); await SendAsync(HttpMethod.Put, "push/add-organization", requestModel); } public async Task DeleteUserRegistrationOrganizationAsync( - IEnumerable> devices, string organizationId) + IEnumerable deviceIds, string organizationId) { - if (!devices.Any()) + if (!deviceIds.Any()) { return; } - var requestModel = new PushUpdateRequestModel(devices, organizationId); + var requestModel = new PushUpdateRequestModel(deviceIds, organizationId); await SendAsync(HttpMethod.Put, "push/delete-organization", requestModel); } } diff --git a/src/Core/Services/Implementations/StripeAdapter.cs b/src/Core/Services/Implementations/StripeAdapter.cs index 100a47f75..e5fee63b9 100644 --- a/src/Core/Services/Implementations/StripeAdapter.cs +++ b/src/Core/Services/Implementations/StripeAdapter.cs @@ -18,6 +18,7 @@ public class StripeAdapter : IStripeAdapter private readonly Stripe.PriceService _priceService; private readonly Stripe.SetupIntentService _setupIntentService; private readonly Stripe.TestHelpers.TestClockService _testClockService; + private readonly CustomerBalanceTransactionService _customerBalanceTransactionService; public StripeAdapter() { @@ -34,6 +35,7 @@ public class StripeAdapter : IStripeAdapter _priceService = new Stripe.PriceService(); _setupIntentService = new SetupIntentService(); _testClockService = new Stripe.TestHelpers.TestClockService(); + _customerBalanceTransactionService = new CustomerBalanceTransactionService(); } public Task CustomerCreateAsync(Stripe.CustomerCreateOptions options) @@ -63,6 +65,10 @@ public class StripeAdapter : IStripeAdapter return paymentMethods.Data; } + public async Task CustomerBalanceTransactionCreate(string customerId, + CustomerBalanceTransactionCreateOptions options) + => await _customerBalanceTransactionService.CreateAsync(customerId, options); + public Task SubscriptionCreateAsync(Stripe.SubscriptionCreateOptions options) { return _subscriptionService.CreateAsync(options); diff --git a/src/Core/Services/Implementations/UserService.cs b/src/Core/Services/Implementations/UserService.cs index fe04efa22..f2e1d183d 100644 --- a/src/Core/Services/Implementations/UserService.cs +++ b/src/Core/Services/Implementations/UserService.cs @@ -1,6 +1,7 @@ using System.Security.Claims; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Services; using Bit.Core.Auth.Enums; @@ -65,6 +66,7 @@ public class UserService : UserManager, IUserService, IDisposable private readonly IDataProtectorTokenFactory _orgUserInviteTokenDataFactory; private readonly IFeatureService _featureService; private readonly IPremiumUserBillingService _premiumUserBillingService; + private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand; public UserService( IUserRepository userRepository, @@ -98,7 +100,8 @@ public class UserService : UserManager, IUserService, IDisposable IStripeSyncService stripeSyncService, IDataProtectorTokenFactory orgUserInviteTokenDataFactory, IFeatureService featureService, - IPremiumUserBillingService premiumUserBillingService) + IPremiumUserBillingService premiumUserBillingService, + IRemoveOrganizationUserCommand removeOrganizationUserCommand) : base( store, optionsAccessor, @@ -138,6 +141,7 @@ public class UserService : UserManager, IUserService, IDisposable _orgUserInviteTokenDataFactory = orgUserInviteTokenDataFactory; _featureService = featureService; _premiumUserBillingService = premiumUserBillingService; + _removeOrganizationUserCommand = removeOrganizationUserCommand; } public Guid? GetProperUserId(ClaimsPrincipal principal) @@ -827,8 +831,7 @@ public class UserService : UserManager, IUserService, IDisposable } } - public async Task DisableTwoFactorProviderAsync(User user, TwoFactorProviderType type, - IOrganizationService organizationService) + public async Task DisableTwoFactorProviderAsync(User user, TwoFactorProviderType type) { var providers = user.GetTwoFactorProviders(); if (!providers?.ContainsKey(type) ?? true) @@ -843,12 +846,11 @@ public class UserService : UserManager, IUserService, IDisposable if (!await TwoFactorIsEnabledAsync(user)) { - await CheckPoliciesOnTwoFactorRemovalAsync(user, organizationService); + await CheckPoliciesOnTwoFactorRemovalAsync(user); } } - public async Task RecoverTwoFactorAsync(string email, string secret, string recoveryCode, - IOrganizationService organizationService) + public async Task RecoverTwoFactorAsync(string email, string secret, string recoveryCode) { var user = await _userRepository.GetByEmailAsync(email); if (user == null) @@ -872,7 +874,7 @@ public class UserService : UserManager, IUserService, IDisposable await SaveUserAsync(user); await _mailService.SendRecoverTwoFactorEmail(user.Email, DateTime.UtcNow, _currentContext.IpAddress); await _eventService.LogUserEventAsync(user.Id, EventType.User_Recovered2fa); - await CheckPoliciesOnTwoFactorRemovalAsync(user, organizationService); + await CheckPoliciesOnTwoFactorRemovalAsync(user); return true; } @@ -1265,18 +1267,24 @@ public class UserService : UserManager, IUserService, IDisposable public async Task IsManagedByAnyOrganizationAsync(Guid userId) { - var managingOrganization = await GetOrganizationManagingUserAsync(userId); - return managingOrganization != null; + var managingOrganizations = await GetOrganizationsManagingUserAsync(userId); + return managingOrganizations.Any(); } - public async Task GetOrganizationManagingUserAsync(Guid userId) + public async Task> GetOrganizationsManagingUserAsync(Guid userId) { - // Users can only be managed by an Organization that is enabled and can have organization domains - var organization = await _organizationRepository.GetByClaimedUserDomainAsync(userId); + if (!_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)) + { + return Enumerable.Empty(); + } + // Get all organizations that have verified the user's email domain. + var organizationsWithVerifiedUserEmailDomain = await _organizationRepository.GetByVerifiedUserEmailDomainAsync(userId); + + // Organizations must be enabled and able to have verified domains. // TODO: Replace "UseSso" with a new organization ability like "UseOrganizationDomains" (PM-11622). // Verified domains were tied to SSO, so we currently check the "UseSso" organization ability. - return (organization is { Enabled: true, UseSso: true }) ? organization : null; + return organizationsWithVerifiedUserEmailDomain.Where(organization => organization is { Enabled: true, UseSso: true }); } /// @@ -1327,13 +1335,13 @@ public class UserService : UserManager, IUserService, IDisposable } } - private async Task CheckPoliciesOnTwoFactorRemovalAsync(User user, IOrganizationService organizationService) + private async Task CheckPoliciesOnTwoFactorRemovalAsync(User user) { var twoFactorPolicies = await _policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication); var removeOrgUserTasks = twoFactorPolicies.Select(async p => { - await organizationService.RemoveUserAsync(p.OrganizationId, user.Id); + await _removeOrganizationUserCommand.RemoveUserAsync(p.OrganizationId, user.Id); var organization = await _organizationRepository.GetByIdAsync(p.OrganizationId); await _mailService.SendOrganizationUserRemovedForPolicyTwoStepEmailAsync( organization.DisplayName(), user.Email); diff --git a/src/Core/Services/NoopImplementations/NoopPushRegistrationService.cs b/src/Core/Services/NoopImplementations/NoopPushRegistrationService.cs index fcd088924..f6279c946 100644 --- a/src/Core/Services/NoopImplementations/NoopPushRegistrationService.cs +++ b/src/Core/Services/NoopImplementations/NoopPushRegistrationService.cs @@ -4,7 +4,7 @@ namespace Bit.Core.Services; public class NoopPushRegistrationService : IPushRegistrationService { - public Task AddUserRegistrationOrganizationAsync(IEnumerable> devices, string organizationId) + public Task AddUserRegistrationOrganizationAsync(IEnumerable deviceIds, string organizationId) { return Task.FromResult(0); } @@ -15,12 +15,12 @@ public class NoopPushRegistrationService : IPushRegistrationService return Task.FromResult(0); } - public Task DeleteRegistrationAsync(string deviceId, DeviceType deviceType) + public Task DeleteRegistrationAsync(string deviceId) { return Task.FromResult(0); } - public Task DeleteUserRegistrationOrganizationAsync(IEnumerable> devices, string organizationId) + public Task DeleteUserRegistrationOrganizationAsync(IEnumerable deviceIds, string organizationId) { return Task.FromResult(0); } diff --git a/src/Core/Settings/GlobalSettings.cs b/src/Core/Settings/GlobalSettings.cs index f99fb3b57..793b6ac1c 100644 --- a/src/Core/Settings/GlobalSettings.cs +++ b/src/Core/Settings/GlobalSettings.cs @@ -1,5 +1,4 @@ using Bit.Core.Auth.Settings; -using Bit.Core.Enums; using Bit.Core.Settings.LoggingSettings; namespace Bit.Core.Settings; @@ -65,7 +64,7 @@ public class GlobalSettings : IGlobalSettings public virtual SentrySettings Sentry { get; set; } = new SentrySettings(); public virtual SyslogSettings Syslog { get; set; } = new SyslogSettings(); public virtual ILogLevelSettings MinLogLevel { get; set; } = new LogLevelSettings(); - public virtual List NotificationHubs { get; set; } = new(); + public virtual NotificationHubPoolSettings NotificationHubPool { get; set; } = new(); public virtual YubicoSettings Yubico { get; set; } = new YubicoSettings(); public virtual DuoSettings Duo { get; set; } = new DuoSettings(); public virtual BraintreeSettings Braintree { get; set; } = new BraintreeSettings(); @@ -424,7 +423,7 @@ public class GlobalSettings : IGlobalSettings public string ConnectionString { get => _connectionString; - set => _connectionString = value.Trim('"'); + set => _connectionString = value?.Trim('"'); } public string HubName { get; set; } /// @@ -433,10 +432,32 @@ public class GlobalSettings : IGlobalSettings /// public bool EnableSendTracing { get; set; } = false; /// - /// At least one hub configuration should have registration enabled, preferably the General hub as a safety net. + /// The date and time at which registration will be enabled. + /// + /// **This value should not be updated once set, as it is used to determine installation location of devices.** + /// + /// If null, registration is disabled. + /// /// - public bool EnableRegistration { get; set; } - public NotificationHubType HubType { get; set; } + public DateTime? RegistrationStartDate { get; set; } + /// + /// The date and time at which registration will be disabled. + /// + /// **This value should not be updated once set, as it is used to determine installation location of devices.** + /// + /// If null, hub registration has no yet known expiry. + /// + public DateTime? RegistrationEndDate { get; set; } + } + + public class NotificationHubPoolSettings + { + /// + /// List of Notification Hub settings to use for sending push notifications. + /// + /// Note that hubs on the same namespace share active device limits, so multiple namespaces should be used to increase capacity. + /// + public List NotificationHubs { get; set; } = new(); } public class YubicoSettings diff --git a/src/Core/Utilities/CoreHelpers.cs b/src/Core/Utilities/CoreHelpers.cs index d900c82e2..af985914c 100644 --- a/src/Core/Utilities/CoreHelpers.cs +++ b/src/Core/Utilities/CoreHelpers.cs @@ -76,6 +76,39 @@ public static class CoreHelpers return new Guid(guidArray); } + internal static DateTime DateFromComb(Guid combGuid) + { + var guidArray = combGuid.ToByteArray(); + var daysArray = new byte[4]; + var msecsArray = new byte[4]; + + Array.Copy(guidArray, guidArray.Length - 6, daysArray, 2, 2); + Array.Copy(guidArray, guidArray.Length - 4, msecsArray, 0, 4); + + Array.Reverse(daysArray); + Array.Reverse(msecsArray); + + var days = BitConverter.ToInt32(daysArray, 0); + var msecs = BitConverter.ToInt32(msecsArray, 0); + + var time = TimeSpan.FromDays(days) + TimeSpan.FromMilliseconds(msecs * 3.333333); + return new DateTime(_baseDateTicks + time.Ticks, DateTimeKind.Utc); + } + + internal static long BinForComb(Guid combGuid, int binCount) + { + // From System.Web.Util.HashCodeCombiner + uint CombineHashCodes(uint h1, byte h2) + { + return (uint)(((h1 << 5) + h1) ^ h2); + } + var guidArray = combGuid.ToByteArray(); + var randomArray = new byte[10]; + Array.Copy(guidArray, 0, randomArray, 0, 10); + var hash = randomArray.Aggregate((uint)randomArray.Length, CombineHashCodes); + return hash % binCount; + } + public static string CleanCertificateThumbprint(string thumbprint) { // Clean possible garbage characters from thumbprint copy/paste diff --git a/src/Icons/Icons.csproj b/src/Icons/Icons.csproj index 2b188100f..1674e2f87 100644 --- a/src/Icons/Icons.csproj +++ b/src/Icons/Icons.csproj @@ -7,7 +7,7 @@ - + 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/Identity.csproj b/src/Identity/Identity.csproj index 584dd78d6..cb506d86e 100644 --- a/src/Identity/Identity.csproj +++ b/src/Identity/Identity.csproj @@ -12,8 +12,4 @@ - - - - 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/BaseRequestValidator.cs deleted file mode 100644 index 881ae4d49..000000000 --- a/src/Identity/IdentityServer/BaseRequestValidator.cs +++ /dev/null @@ -1,682 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.Reflection; -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; -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; - -public abstract class BaseRequestValidator where T : class -{ - private UserManager _userManager; - private readonly IDeviceRepository _deviceRepository; - private readonly IDeviceService _deviceService; - private readonly IEventService _eventService; - private readonly IOrganizationDuoWebTokenProvider _organizationDuoWebTokenProvider; - private readonly ITemporaryDuoWebV4SDKService _duoWebV4SDKService; - private readonly IOrganizationRepository _organizationRepository; - 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; } - protected IFeatureService FeatureService { get; } - protected ISsoConfigRepository SsoConfigRepository { get; } - protected IUserService _userService { get; } - protected IUserDecryptionOptionsBuilder UserDecryptionOptionsBuilder { get; } - - public BaseRequestValidator( - UserManager userManager, - IDeviceRepository deviceRepository, - IDeviceService deviceService, - IUserService userService, - IEventService eventService, - IOrganizationDuoWebTokenProvider organizationDuoWebTokenProvider, - ITemporaryDuoWebV4SDKService duoWebV4SDKService, - IOrganizationRepository organizationRepository, - IOrganizationUserRepository organizationUserRepository, - IApplicationCacheService applicationCacheService, - IMailService mailService, - ILogger logger, - ICurrentContext currentContext, - GlobalSettings globalSettings, - IUserRepository userRepository, - IPolicyService policyService, - IDataProtectorTokenFactory tokenDataFactory, - IFeatureService featureService, - ISsoConfigRepository ssoConfigRepository, - IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder) - { - _userManager = userManager; - _deviceRepository = deviceRepository; - _deviceService = deviceService; - _userService = userService; - _eventService = eventService; - _organizationDuoWebTokenProvider = organizationDuoWebTokenProvider; - _duoWebV4SDKService = duoWebV4SDKService; - _organizationRepository = organizationRepository; - _organizationUserRepository = organizationUserRepository; - _applicationCacheService = applicationCacheService; - _mailService = mailService; - _logger = logger; - CurrentContext = currentContext; - _globalSettings = globalSettings; - PolicyService = policyService; - _userRepository = userRepository; - _tokenDataFactory = tokenDataFactory; - FeatureService = featureService; - SsoConfigRepository = ssoConfigRepository; - UserDecryptionOptionsBuilder = userDecryptionOptionsBuilder; - } - - protected async Task ValidateAsync(T context, ValidatedTokenRequest request, - CustomValidatorRequestContext validatorContext) - { - var isBot = validatorContext.CaptchaResponse?.IsBot ?? false; - if (isBot) - { - _logger.LogInformation(Constants.BypassFiltersEventId, - "Login attempt for {0} detected as a captcha bot with score {1}.", - 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) - { - await UpdateFailedAuthDetailsAsync(user, false, !validatorContext.KnownDevice); - } - - if (!valid || isBot) - { - await BuildErrorResultAsync("Username or password is incorrect. Try again.", false, context, user); - return; - } - - var (isTwoFactorRequired, twoFactorOrganization) = await RequiresTwoFactorAsync(user, request); - if (isTwoFactorRequired) - { - // Just defaulting it - var twoFactorProviderType = TwoFactorProviderType.Authenticator; - if (!twoFactorRequest || !Enum.TryParse(twoFactorProvider, out twoFactorProviderType)) - { - await BuildTwoFactorResultAsync(user, twoFactorOrganization, context); - return; - } - - var verified = await VerifyTwoFactor(user, twoFactorOrganization, - twoFactorProviderType, twoFactorToken); - if (!verified || isBot) - { - if (twoFactorProviderType != TwoFactorProviderType.Remember) - { - await UpdateFailedAuthDetailsAsync(user, true, !validatorContext.KnownDevice); - await BuildErrorResultAsync("Two-step token is invalid. Try again.", true, context, user); - } - else if (twoFactorProviderType == TwoFactorProviderType.Remember) - { - await BuildTwoFactorResultAsync(user, twoFactorOrganization, context); - } - return; - } - } - else - { - twoFactorRequest = false; - twoFactorRemember = false; - twoFactorToken = null; - } - - - // Force legacy users to the web for migration - if (FeatureService.IsEnabled(FeatureFlagKeys.BlockLegacyUsers)) - { - if (UserService.IsLegacyUser(user) && request.ClientId != "web") - { - await FailAuthForLegacyUserAsync(user, context); - return; - } - } - - // Returns true if can finish validation process - if (await IsValidAuthTypeAsync(user, request.GrantType)) - { - var device = await SaveDeviceAsync(user, request); - if (device == null) - { - await BuildErrorResultAsync("No device information provided.", false, context, user); - return; - } - - await BuildSuccessResultAsync(user, context, device, twoFactorRequest && twoFactorRemember); - } - else - { - SetSsoResult(context, - new Dictionary - { - { "ErrorModel", new ErrorResponseModel("SSO authentication is required.") } - }); - } - } - - protected async Task FailAuthForLegacyUserAsync(User user, T context) - { - await BuildErrorResultAsync( - $"Encryption key migration is required. Please log in to the web vault at {_globalSettings.BaseServiceUri.VaultWithHash}", - false, context, user); - } - - protected abstract Task ValidateContextAsync(T context, CustomValidatorRequestContext validatorContext); - - protected async Task BuildSuccessResultAsync(User user, T context, Device device, bool sendRememberToken) - { - await _eventService.LogUserEventAsync(user.Id, EventType.User_LoggedIn); - - var claims = new List(); - - if (device != null) - { - claims.Add(new Claim(Claims.Device, device.Identifier)); - } - - var customResponse = new Dictionary(); - if (!string.IsNullOrWhiteSpace(user.PrivateKey)) - { - customResponse.Add("PrivateKey", user.PrivateKey); - } - - if (!string.IsNullOrWhiteSpace(user.Key)) - { - customResponse.Add("Key", user.Key); - } - - customResponse.Add("MasterPasswordPolicy", await GetMasterPasswordPolicy(user)); - customResponse.Add("ForcePasswordReset", user.ForcePasswordReset); - customResponse.Add("ResetMasterPassword", string.IsNullOrWhiteSpace(user.MasterPassword)); - customResponse.Add("Kdf", (byte)user.Kdf); - customResponse.Add("KdfIterations", user.KdfIterations); - customResponse.Add("KdfMemory", user.KdfMemory); - customResponse.Add("KdfParallelism", user.KdfParallelism); - customResponse.Add("UserDecryptionOptions", await CreateUserDecryptionOptionsAsync(user, device, GetSubject(context))); - - if (sendRememberToken) - { - var token = await _userManager.GenerateTwoFactorTokenAsync(user, - CoreHelpers.CustomProviderName(TwoFactorProviderType.Remember)); - customResponse.Add("TwoFactorToken", token); - } - - await ResetFailedAuthDetailsAsync(user); - 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) - { - await _eventService.LogUserEventAsync(user.Id, - twoFactorRequest ? EventType.User_FailedLogIn2fa : EventType.User_FailedLogIn); - } - - if (_globalSettings.SelfHosted) - { - _logger.LogWarning(Constants.BypassFiltersEventId, - string.Format("Failed login attempt{0}{1}", twoFactorRequest ? ", 2FA invalid." : ".", - $" {CurrentContext.IpAddress}")); - } - - await Task.Delay(2000); // Delay for brute force. - SetErrorResult(context, - new Dictionary { { "ErrorModel", new ErrorResponseModel(message) } }); - } - - protected abstract void SetTwoFactorResult(T context, Dictionary customResponse); - - protected abstract void SetSsoResult(T context, Dictionary customResponse); - - protected abstract Task SetSuccessResult(T context, User user, List claims, - Dictionary customResponse); - - 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); - } - - private async Task IsValidAuthTypeAsync(User user, string grantType) - { - if (grantType == "authorization_code" || grantType == "client_credentials") - { - // Already using SSO to authorize, finish successfully - // Or login via api key, skip SSO requirement - 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) - { - return false; - } - - // Default - continue validation process - return true; - } - - private bool OrgUsing2fa(IDictionary orgAbilities, Guid orgId) - { - return orgAbilities != null && orgAbilities.ContainsKey(orgId) && - orgAbilities[orgId].Enabled && orgAbilities[orgId].Using2fa; - } - - private Device GetDeviceFromRequest(ValidatedRequest request) - { - var deviceIdentifier = request.Raw["DeviceIdentifier"]?.ToString(); - var deviceType = request.Raw["DeviceType"]?.ToString(); - var deviceName = request.Raw["DeviceName"]?.ToString(); - var devicePushToken = request.Raw["DevicePushToken"]?.ToString(); - - if (string.IsNullOrWhiteSpace(deviceIdentifier) || string.IsNullOrWhiteSpace(deviceType) || - string.IsNullOrWhiteSpace(deviceName) || !Enum.TryParse(deviceType, out DeviceType type)) - { - return null; - } - - return new Device - { - Identifier = deviceIdentifier, - Name = deviceName, - Type = type, - PushToken = string.IsNullOrWhiteSpace(devicePushToken) ? null : devicePushToken - }; - } - - 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; - } - } - - protected async Task KnownDeviceAsync(User user, ValidatedTokenRequest request) => - (await GetKnownDeviceAsync(user, request)) != default; - - protected async Task GetKnownDeviceAsync(User user, ValidatedTokenRequest request) - { - if (user == null) - { - return default; - } - - return await _deviceRepository.GetByIdentifierAsync(GetDeviceFromRequest(request).Identifier, user.Id); - } - - private async Task SaveDeviceAsync(User user, ValidatedTokenRequest request) - { - var device = GetDeviceFromRequest(request); - if (device != null) - { - var existingDevice = await GetKnownDeviceAsync(user, request); - if (existingDevice == null) - { - device.UserId = user.Id; - await _deviceService.SaveAsync(device); - - var now = DateTime.UtcNow; - if (now - user.CreationDate > TimeSpan.FromMinutes(10)) - { - var deviceType = device.Type.GetType().GetMember(device.Type.ToString()) - .FirstOrDefault()?.GetCustomAttribute()?.GetName(); - if (!_globalSettings.DisableEmailNewDevice) - { - await _mailService.SendNewDeviceLoggedInEmail(user.Email, deviceType, now, - CurrentContext.IpAddress); - } - } - - return device; - } - - return existingDevice; - } - - return null; - } - - private async Task ResetFailedAuthDetailsAsync(User user) - { - // Early escape if db hit not necessary - if (user == null || user.FailedLoginCount == 0) - { - return; - } - - user.FailedLoginCount = 0; - user.RevisionDate = DateTime.UtcNow; - await _userRepository.ReplaceAsync(user); - } - - private async Task UpdateFailedAuthDetailsAsync(User user, bool twoFactorInvalid, bool unknownDevice) - { - if (user == null) - { - return; - } - - var utcNow = DateTime.UtcNow; - user.FailedLoginCount = ++user.FailedLoginCount; - user.LastFailedLoginDate = user.RevisionDate = utcNow; - await _userRepository.ReplaceAsync(user); - - if (ValidateFailedAuthEmailConditions(unknownDevice, user)) - { - if (twoFactorInvalid) - { - await _mailService.SendFailedTwoFactorAttemptsEmailAsync(user.Email, utcNow, CurrentContext.IpAddress); - } - else - { - await _mailService.SendFailedLoginAttemptsEmailAsync(user.Email, utcNow, CurrentContext.IpAddress); - } - } - } - - /// - /// 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 - /// current user - /// - private bool ValidateFailedAuthEmailConditions(bool unknownDevice, User user) - { - var failedLoginCeiling = _globalSettings.Captcha.MaximumFailedLoginAttempts; - var failedLoginCount = user?.FailedLoginCount ?? 0; - return unknownDevice && failedLoginCeiling > 0 && failedLoginCount == failedLoginCeiling; - } - - private async Task GetMasterPasswordPolicy(User user) - { - // Check current context/cache to see if user is in any organizations, avoids extra DB call if not - var orgs = (await CurrentContext.OrganizationMembershipAsync(_organizationUserRepository, user.Id)) - .ToList(); - - if (!orgs.Any()) - { - return null; - } - - return new MasterPasswordPolicyResponseModel(await PolicyService.GetMasterPasswordPolicyForUserAsync(user)); - } - -#nullable enable - /// - /// Used to create a list of all possible ways the newly authenticated user can decrypt their vault contents - /// - private async Task CreateUserDecryptionOptionsAsync(User user, Device device, ClaimsPrincipal subject) - { - var ssoConfig = await GetSsoConfigurationDataAsync(subject); - return await UserDecryptionOptionsBuilder - .ForUser(user) - .WithDevice(device) - .WithSso(ssoConfig) - .BuildAsync(); - } - - private async Task GetSsoConfigurationDataAsync(ClaimsPrincipal subject) - { - var organizationClaim = subject?.FindFirstValue("organizationId"); - - if (organizationClaim == null || !Guid.TryParse(organizationClaim, out var organizationId)) - { - return null; - } - - var ssoConfig = await SsoConfigRepository.GetByOrganizationIdAsync(organizationId); - if (ssoConfig == null) - { - return null; - } - - return ssoConfig; - } -} diff --git a/src/Identity/IdentityServer/RequestValidators/BaseRequestValidator.cs b/src/Identity/IdentityServer/RequestValidators/BaseRequestValidator.cs new file mode 100644 index 000000000..185d32a7f --- /dev/null +++ b/src/Identity/IdentityServer/RequestValidators/BaseRequestValidator.cs @@ -0,0 +1,392 @@ +using System.Security.Claims; +using Bit.Core; +using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Services; +using Bit.Core.Auth.Entities; +using Bit.Core.Auth.Enums; +using Bit.Core.Auth.Models.Api.Response; +using Bit.Core.Auth.Repositories; +using Bit.Core.Context; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Identity; +using Bit.Core.Models.Api; +using Bit.Core.Models.Api.Response; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Bit.Core.Settings; +using Bit.Core.Utilities; +using Duende.IdentityServer.Validation; +using Microsoft.AspNetCore.Identity; + +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 ITwoFactorAuthenticationValidator _twoFactorAuthenticationValidator; + private readonly IOrganizationUserRepository _organizationUserRepository; + private readonly IMailService _mailService; + private readonly ILogger _logger; + private readonly GlobalSettings _globalSettings; + private readonly IUserRepository _userRepository; + + protected ICurrentContext CurrentContext { get; } + protected IPolicyService PolicyService { get; } + protected IFeatureService FeatureService { get; } + protected ISsoConfigRepository SsoConfigRepository { get; } + protected IUserService _userService { get; } + protected IUserDecryptionOptionsBuilder UserDecryptionOptionsBuilder { get; } + + public BaseRequestValidator( + UserManager userManager, + IUserService userService, + IEventService eventService, + IDeviceValidator deviceValidator, + ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator, + IOrganizationUserRepository organizationUserRepository, + IMailService mailService, + ILogger logger, + ICurrentContext currentContext, + GlobalSettings globalSettings, + IUserRepository userRepository, + IPolicyService policyService, + IFeatureService featureService, + ISsoConfigRepository ssoConfigRepository, + IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder) + { + _userManager = userManager; + _userService = userService; + _eventService = eventService; + _deviceValidator = deviceValidator; + _twoFactorAuthenticationValidator = twoFactorAuthenticationValidator; + _organizationUserRepository = organizationUserRepository; + _mailService = mailService; + _logger = logger; + CurrentContext = currentContext; + _globalSettings = globalSettings; + PolicyService = policyService; + _userRepository = userRepository; + FeatureService = featureService; + SsoConfigRepository = ssoConfigRepository; + UserDecryptionOptionsBuilder = userDecryptionOptionsBuilder; + } + + protected async Task ValidateAsync(T context, ValidatedTokenRequest request, + CustomValidatorRequestContext validatorContext) + { + var isBot = validatorContext.CaptchaResponse?.IsBot ?? false; + if (isBot) + { + _logger.LogInformation(Constants.BypassFiltersEventId, + "Login attempt for {0} detected as a captcha bot with score {1}.", + request.UserName, validatorContext.CaptchaResponse.Score); + } + + var valid = await ValidateContextAsync(context, validatorContext); + var user = validatorContext.User; + if (!valid) + { + await UpdateFailedAuthDetailsAsync(user, false, !validatorContext.KnownDevice); + } + + if (!valid || isBot) + { + await BuildErrorResultAsync("Username or password is incorrect. Try again.", false, context, user); + return; + } + + 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) + { + // 2FA required and not provided response + if (!validTwoFactorRequest || + !Enum.TryParse(twoFactorProvider, out TwoFactorProviderType twoFactorProviderType)) + { + 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 _twoFactorAuthenticationValidator + .VerifyTwoFactor(user, twoFactorOrganization, twoFactorProviderType, twoFactorToken); + + // 2FA required but request not valid or remember token expired response + if (!verified || isBot) + { + if (twoFactorProviderType != TwoFactorProviderType.Remember) + { + await UpdateFailedAuthDetailsAsync(user, true, !validatorContext.KnownDevice); + await BuildErrorResultAsync("Two-step token is invalid. Try again.", true, context, user); + } + else if (twoFactorProviderType == TwoFactorProviderType.Remember) + { + 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 + { + validTwoFactorRequest = false; + twoFactorRemember = false; + } + + // Force legacy users to the web for migration + if (FeatureService.IsEnabled(FeatureFlagKeys.BlockLegacyUsers)) + { + if (UserService.IsLegacyUser(user) && request.ClientId != "web") + { + await FailAuthForLegacyUserAsync(user, context); + return; + } + } + + if (await IsValidAuthTypeAsync(user, request.GrantType)) + { + var device = await _deviceValidator.SaveDeviceAsync(user, request); + if (device == null) + { + await BuildErrorResultAsync("No device information provided.", false, context, user); + return; + } + await BuildSuccessResultAsync(user, context, device, validTwoFactorRequest && twoFactorRemember); + } + else + { + SetSsoResult(context, + new Dictionary + { + { "ErrorModel", new ErrorResponseModel("SSO authentication is required.") } + }); + } + } + + protected async Task FailAuthForLegacyUserAsync(User user, T context) + { + await BuildErrorResultAsync( + $"Encryption key migration is required. Please log in to the web vault at {_globalSettings.BaseServiceUri.VaultWithHash}", + false, context, user); + } + + protected abstract Task ValidateContextAsync(T context, CustomValidatorRequestContext validatorContext); + + protected async Task BuildSuccessResultAsync(User user, T context, Device device, bool sendRememberToken) + { + await _eventService.LogUserEventAsync(user.Id, EventType.User_LoggedIn); + + var claims = new List(); + + if (device != null) + { + claims.Add(new Claim(Claims.Device, device.Identifier)); + } + + var customResponse = new Dictionary(); + if (!string.IsNullOrWhiteSpace(user.PrivateKey)) + { + customResponse.Add("PrivateKey", user.PrivateKey); + } + + if (!string.IsNullOrWhiteSpace(user.Key)) + { + customResponse.Add("Key", user.Key); + } + + customResponse.Add("MasterPasswordPolicy", await GetMasterPasswordPolicy(user)); + customResponse.Add("ForcePasswordReset", user.ForcePasswordReset); + customResponse.Add("ResetMasterPassword", string.IsNullOrWhiteSpace(user.MasterPassword)); + customResponse.Add("Kdf", (byte)user.Kdf); + customResponse.Add("KdfIterations", user.KdfIterations); + customResponse.Add("KdfMemory", user.KdfMemory); + customResponse.Add("KdfParallelism", user.KdfParallelism); + customResponse.Add("UserDecryptionOptions", await CreateUserDecryptionOptionsAsync(user, device, GetSubject(context))); + + if (sendRememberToken) + { + var token = await _userManager.GenerateTwoFactorTokenAsync(user, + CoreHelpers.CustomProviderName(TwoFactorProviderType.Remember)); + customResponse.Add("TwoFactorToken", token); + } + + await ResetFailedAuthDetailsAsync(user); + await SetSuccessResult(context, user, claims, customResponse); + } + + protected async Task BuildErrorResultAsync(string message, bool twoFactorRequest, T context, User user) + { + if (user != null) + { + await _eventService.LogUserEventAsync(user.Id, + twoFactorRequest ? EventType.User_FailedLogIn2fa : EventType.User_FailedLogIn); + } + + if (_globalSettings.SelfHosted) + { + _logger.LogWarning(Constants.BypassFiltersEventId, + string.Format("Failed login attempt{0}{1}", twoFactorRequest ? ", 2FA invalid." : ".", + $" {CurrentContext.IpAddress}")); + } + + await Task.Delay(2000); // Delay for brute force. + SetErrorResult(context, + new Dictionary { { "ErrorModel", new ErrorResponseModel(message) } }); + } + + protected abstract void SetTwoFactorResult(T context, Dictionary customResponse); + + protected abstract void SetSsoResult(T context, Dictionary customResponse); + + protected abstract Task SetSuccessResult(T context, User user, List claims, + Dictionary customResponse); + + protected abstract void SetErrorResult(T context, Dictionary customResponse); + protected abstract ClaimsPrincipal GetSubject(T context); + + /// + /// 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") + { + // Already using SSO to authorize, finish successfully + // Or login via api key, skip SSO requirement + 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) + { + return false; + } + + // Default - continue validation process + return true; + } + + private async Task ResetFailedAuthDetailsAsync(User user) + { + // Early escape if db hit not necessary + if (user == null || user.FailedLoginCount == 0) + { + return; + } + + user.FailedLoginCount = 0; + user.RevisionDate = DateTime.UtcNow; + await _userRepository.ReplaceAsync(user); + } + + private async Task UpdateFailedAuthDetailsAsync(User user, bool twoFactorInvalid, bool unknownDevice) + { + if (user == null) + { + return; + } + + var utcNow = DateTime.UtcNow; + user.FailedLoginCount = ++user.FailedLoginCount; + user.LastFailedLoginDate = user.RevisionDate = utcNow; + await _userRepository.ReplaceAsync(user); + + if (ValidateFailedAuthEmailConditions(unknownDevice, user)) + { + if (twoFactorInvalid) + { + await _mailService.SendFailedTwoFactorAttemptsEmailAsync(user.Email, utcNow, CurrentContext.IpAddress); + } + else + { + await _mailService.SendFailedLoginAttemptsEmailAsync(user.Email, utcNow, CurrentContext.IpAddress); + } + } + } + + /// + /// 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 + /// current user + /// + private bool ValidateFailedAuthEmailConditions(bool unknownDevice, User user) + { + var failedLoginCeiling = _globalSettings.Captcha.MaximumFailedLoginAttempts; + var failedLoginCount = user?.FailedLoginCount ?? 0; + return unknownDevice && failedLoginCeiling > 0 && failedLoginCount == failedLoginCeiling; + } + + private async Task GetMasterPasswordPolicy(User user) + { + // Check current context/cache to see if user is in any organizations, avoids extra DB call if not + var orgs = (await CurrentContext.OrganizationMembershipAsync(_organizationUserRepository, user.Id)) + .ToList(); + + if (!orgs.Any()) + { + return null; + } + + return new MasterPasswordPolicyResponseModel(await PolicyService.GetMasterPasswordPolicyForUserAsync(user)); + } + +#nullable enable + /// + /// Used to create a list of all possible ways the newly authenticated user can decrypt their vault contents + /// + private async Task CreateUserDecryptionOptionsAsync(User user, Device device, ClaimsPrincipal subject) + { + var ssoConfig = await GetSsoConfigurationDataAsync(subject); + return await UserDecryptionOptionsBuilder + .ForUser(user) + .WithDevice(device) + .WithSso(ssoConfig) + .BuildAsync(); + } + + private async Task GetSsoConfigurationDataAsync(ClaimsPrincipal subject) + { + var organizationClaim = subject?.FindFirstValue("organizationId"); + + if (organizationClaim == null || !Guid.TryParse(organizationClaim, out var organizationId)) + { + return null; + } + + var ssoConfig = await SsoConfigRepository.GetByOrganizationIdAsync(organizationId); + if (ssoConfig == null) + { + return null; + } + + return ssoConfig; + } +} diff --git a/src/Identity/IdentityServer/CustomTokenRequestValidator.cs b/src/Identity/IdentityServer/RequestValidators/CustomTokenRequestValidator.cs similarity index 87% rename from src/Identity/IdentityServer/CustomTokenRequestValidator.cs rename to src/Identity/IdentityServer/RequestValidators/CustomTokenRequestValidator.cs index 3af1337ee..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,29 +26,36 @@ public class CustomTokenRequestValidator : BaseRequestValidator userManager, - IDeviceRepository deviceRepository, - IDeviceService deviceService, 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, deviceRepository, deviceService, userService, eventService, - 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; @@ -71,7 +75,7 @@ public class CustomTokenRequestValidator : BaseRequestValidator { { "encrypted_payload", payload } }; } - - return; } - await ValidateAsync(context, context.Result.ValidatedRequest, new CustomValidatorRequestContext { KnownDevice = true }); } @@ -103,7 +104,6 @@ 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 + Task SaveDeviceAsync(User user, ValidatedTokenRequest request); + /// + /// Check if a device is known to the user. + /// + /// current user trying to authenticate + /// contains raw information that is parsed about the device + /// true if the device is known, false if it is not + Task KnownDeviceAsync(User user, ValidatedTokenRequest request); +} + +public class DeviceValidator( + IDeviceService deviceService, + IDeviceRepository deviceRepository, + GlobalSettings globalSettings, + IMailService mailService, + ICurrentContext currentContext) : IDeviceValidator +{ + private readonly IDeviceService _deviceService = deviceService; + private readonly IDeviceRepository _deviceRepository = deviceRepository; + private readonly GlobalSettings _globalSettings = globalSettings; + private readonly IMailService _mailService = mailService; + private readonly ICurrentContext _currentContext = currentContext; + + /// + /// 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); + if (device != null && user != null) + { + var existingDevice = await GetKnownDeviceAsync(user, device); + if (existingDevice == null) + { + device.UserId = user.Id; + await _deviceService.SaveAsync(device); + + // This makes sure the user isn't sent a "new device" email on their first login + var now = DateTime.UtcNow; + if (now - user.CreationDate > TimeSpan.FromMinutes(10)) + { + var deviceType = device.Type.GetType().GetMember(device.Type.ToString()) + .FirstOrDefault()?.GetCustomAttribute()?.GetName(); + if (!_globalSettings.DisableEmailNewDevice) + { + await _mailService.SendNewDeviceLoggedInEmail(user.Email, deviceType, now, + _currentContext.IpAddress); + } + } + return device; + } + return existingDevice; + } + return null; + } + + public async Task KnownDeviceAsync(User user, ValidatedTokenRequest request) => + (await GetKnownDeviceAsync(user, GetDeviceFromRequest(request))) != default; + + private async Task GetKnownDeviceAsync(User user, Device device) + { + if (user == null || device == null) + { + return default; + } + return await _deviceRepository.GetByIdentifierAsync(device.Identifier, user.Id); + } + + private static Device GetDeviceFromRequest(ValidatedRequest request) + { + var deviceIdentifier = request.Raw["DeviceIdentifier"]?.ToString(); + var requestDeviceType = request.Raw["DeviceType"]?.ToString(); + var deviceName = request.Raw["DeviceName"]?.ToString(); + var devicePushToken = request.Raw["DevicePushToken"]?.ToString(); + + if (string.IsNullOrWhiteSpace(deviceIdentifier) || + string.IsNullOrWhiteSpace(requestDeviceType) || + string.IsNullOrWhiteSpace(deviceName) || + !Enum.TryParse(requestDeviceType, out DeviceType parsedDeviceType)) + { + return null; + } + + return new Device + { + Identifier = deviceIdentifier, + Name = deviceName, + Type = parsedDeviceType, + PushToken = string.IsNullOrWhiteSpace(devicePushToken) ? null : devicePushToken + }; + } +} diff --git a/src/Identity/IdentityServer/ResourceOwnerPasswordValidator.cs b/src/Identity/IdentityServer/RequestValidators/ResourceOwnerPasswordValidator.cs similarity index 87% rename from src/Identity/IdentityServer/ResourceOwnerPasswordValidator.cs rename to src/Identity/IdentityServer/RequestValidators/ResourceOwnerPasswordValidator.cs index cb63bd94e..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 @@ -25,17 +22,14 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator userManager, - IDeviceRepository deviceRepository, - IDeviceService deviceService, IUserService userService, IEventService eventService, - IOrganizationDuoWebTokenProvider organizationDuoWebTokenProvider, - ITemporaryDuoWebV4SDKService duoWebV4SDKService, - IOrganizationRepository organizationRepository, + IDeviceValidator deviceValidator, + ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator, IOrganizationUserRepository organizationUserRepository, - IApplicationCacheService applicationCacheService, IMailService mailService, ILogger logger, ICurrentContext currentContext, @@ -44,19 +38,31 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator tokenDataFactory, IFeatureService featureService, ISsoConfigRepository ssoConfigRepository, IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder) - : base(userManager, deviceRepository, deviceService, userService, eventService, - 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; _captchaValidationService = captchaValidationService; _authRequestRepository = authRequestRepository; + _deviceValidator = deviceValidator; } public async Task ValidateAsync(ResourceOwnerPasswordValidationContext context) @@ -72,7 +78,7 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator + /// 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 79% rename from src/Identity/IdentityServer/WebAuthnGrantValidator.cs rename to src/Identity/IdentityServer/RequestValidators/WebAuthnGrantValidator.cs index 855226565..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 { @@ -27,18 +25,15 @@ public class WebAuthnGrantValidator : BaseRequestValidator _assertionOptionsDataProtector; private readonly IAssertWebAuthnLoginCredentialCommand _assertWebAuthnLoginCredentialCommand; + private readonly IDeviceValidator _deviceValidator; public WebAuthnGrantValidator( UserManager userManager, - IDeviceRepository deviceRepository, - IDeviceService deviceService, IUserService userService, IEventService eventService, - IOrganizationDuoWebTokenProvider organizationDuoWebTokenProvider, - ITemporaryDuoWebV4SDKService duoWebV4SDKService, - IOrganizationRepository organizationRepository, + IDeviceValidator deviceValidator, + ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator, IOrganizationUserRepository organizationUserRepository, - IApplicationCacheService applicationCacheService, IMailService mailService, ILogger logger, ICurrentContext currentContext, @@ -46,19 +41,31 @@ public class WebAuthnGrantValidator : BaseRequestValidator tokenDataFactory, IDataProtectorTokenFactory assertionOptionsDataProtector, IFeatureService featureService, IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder, IAssertWebAuthnLoginCredentialCommand assertWebAuthnLoginCredentialCommand ) - : base(userManager, deviceRepository, deviceService, userService, eventService, - 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; + _deviceValidator = deviceValidator; } string IExtensionGrantValidator.GrantType => "webauthn"; @@ -87,7 +94,7 @@ 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 95002d837..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; @@ -20,6 +21,8 @@ public static class ServiceCollectionExtensions services.AddSingleton(); 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/OrganizationRepository.cs b/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationRepository.cs index bdc2fb4ca..20fdf8315 100644 --- a/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationRepository.cs +++ b/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationRepository.cs @@ -168,7 +168,7 @@ public class OrganizationRepository : Repository, IOrganizat commandType: CommandType.StoredProcedure); } - public async Task GetByClaimedUserDomainAsync(Guid userId) + public async Task> GetByVerifiedUserEmailDomainAsync(Guid userId) { using (var connection = new SqlConnection(ConnectionString)) { @@ -177,7 +177,7 @@ public class OrganizationRepository : Repository, IOrganizat new { UserId = userId }, commandType: CommandType.StoredProcedure); - return result.SingleOrDefault(); + return result.ToList(); } } } 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.Dapper/Billing/Repositories/ClientOrganizationMigrationRecordRepository.cs b/src/Infrastructure.Dapper/Billing/Repositories/ClientOrganizationMigrationRecordRepository.cs new file mode 100644 index 000000000..155abdb4b --- /dev/null +++ b/src/Infrastructure.Dapper/Billing/Repositories/ClientOrganizationMigrationRecordRepository.cs @@ -0,0 +1,39 @@ +using System.Data; +using Bit.Core.Billing.Entities; +using Bit.Core.Billing.Repositories; +using Bit.Core.Settings; +using Bit.Infrastructure.Dapper.Repositories; +using Dapper; +using Microsoft.Data.SqlClient; + +namespace Bit.Infrastructure.Dapper.Billing.Repositories; + +public class ClientOrganizationMigrationRecordRepository( + GlobalSettings globalSettings) : Repository( + globalSettings.SqlServer.ConnectionString, + globalSettings.SqlServer.ReadOnlyConnectionString), IClientOrganizationMigrationRecordRepository +{ + public async Task GetByOrganizationId(Guid organizationId) + { + var sqlConnection = new SqlConnection(ConnectionString); + + var results = await sqlConnection.QueryAsync( + "[dbo].[ClientOrganizationMigrationRecord_ReadByOrganizationId]", + new { OrganizationId = organizationId }, + commandType: CommandType.StoredProcedure); + + return results.FirstOrDefault(); + } + + public async Task> GetByProviderId(Guid providerId) + { + var sqlConnection = new SqlConnection(ConnectionString); + + var results = await sqlConnection.QueryAsync( + "[dbo].[ClientOrganizationMigrationRecord_ReadByProviderId]", + new { ProviderId = providerId }, + commandType: CommandType.StoredProcedure); + + return results.ToArray(); + } +} diff --git a/src/Infrastructure.Dapper/DapperServiceCollectionExtensions.cs b/src/Infrastructure.Dapper/DapperServiceCollectionExtensions.cs index 2d88a90bd..6cfa1ef8b 100644 --- a/src/Infrastructure.Dapper/DapperServiceCollectionExtensions.cs +++ b/src/Infrastructure.Dapper/DapperServiceCollectionExtensions.cs @@ -56,6 +56,8 @@ public static class DapperServiceCollectionExtensions services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services + .AddSingleton(); if (selfHosted) { diff --git a/src/Infrastructure.Dapper/NotificationCenter/Repositories/NotificationRepository.cs b/src/Infrastructure.Dapper/NotificationCenter/Repositories/NotificationRepository.cs index 40bfd4b0e..f70c50f49 100644 --- a/src/Infrastructure.Dapper/NotificationCenter/Repositories/NotificationRepository.cs +++ b/src/Infrastructure.Dapper/NotificationCenter/Repositories/NotificationRepository.cs @@ -2,6 +2,7 @@ using System.Data; using Bit.Core.Enums; using Bit.Core.NotificationCenter.Entities; +using Bit.Core.NotificationCenter.Models.Data; using Bit.Core.NotificationCenter.Models.Filter; using Bit.Core.NotificationCenter.Repositories; using Bit.Core.Settings; @@ -23,12 +24,12 @@ public class NotificationRepository : Repository, INotificat { } - public async Task> GetByUserIdAndStatusAsync(Guid userId, + public async Task> GetByUserIdAndStatusAsync(Guid userId, ClientType clientType, NotificationStatusFilter? statusFilter) { await using var connection = new SqlConnection(ConnectionString); - var results = await connection.QueryAsync( + var results = await connection.QueryAsync( "[dbo].[Notification_ReadByUserIdAndStatus]", new { UserId = userId, ClientType = clientType, statusFilter?.Read, statusFilter?.Deleted }, commandType: CommandType.StoredProcedure); diff --git a/src/Infrastructure.Dapper/Repositories/OrganizationDomainRepository.cs b/src/Infrastructure.Dapper/Repositories/OrganizationDomainRepository.cs index 31d599f0c..1a7085eb1 100644 --- a/src/Infrastructure.Dapper/Repositories/OrganizationDomainRepository.cs +++ b/src/Infrastructure.Dapper/Repositories/OrganizationDomainRepository.cs @@ -71,6 +71,17 @@ public class OrganizationDomainRepository : Repository } } + public async Task> GetVerifiedOrganizationDomainSsoDetailsAsync(string email) + { + await using var connection = new SqlConnection(ConnectionString); + + return await connection + .QueryAsync( + $"[{Schema}].[VerifiedOrganizationDomainSsoDetails_ReadByEmail]", + new { Email = email }, + commandType: CommandType.StoredProcedure); + } + public async Task GetDomainByIdOrganizationIdAsync(Guid id, Guid orgId) { using (var connection = new SqlConnection(ConnectionString)) diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Configurations/OrganizationEntityTypeConfiguration.cs b/src/Infrastructure.EntityFramework/AdminConsole/Configurations/OrganizationEntityTypeConfiguration.cs index 4d6b1b915..47369f5e3 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Configurations/OrganizationEntityTypeConfiguration.cs +++ b/src/Infrastructure.EntityFramework/AdminConsole/Configurations/OrganizationEntityTypeConfiguration.cs @@ -12,10 +12,6 @@ public class OrganizationEntityTypeConfiguration : IEntityTypeConfiguration o.Id) .ValueGeneratedNever(); - builder.Property(c => c.LimitCollectionCreationDeletion) - .ValueGeneratedNever() - .HasDefaultValue(true); - builder.Property(c => c.AllowAdminAccessToAllCollectionItems) .ValueGeneratedNever() .HasDefaultValue(true); diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationRepository.cs b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationRepository.cs index 96c9a912e..b3ee25488 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationRepository.cs +++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationRepository.cs @@ -99,6 +99,9 @@ public class OrganizationRepository : Repository GetByClaimedUserDomainAsync(Guid userId) + public async Task> GetByVerifiedUserEmailDomainAsync(Guid userId) { using (var scope = ServiceScopeFactory.CreateScope()) { @@ -291,7 +294,7 @@ public class OrganizationRepository : 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/AdminConsole/Repositories/Queries/OrganizationUserOrganizationDetailsViewQuery.cs b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/OrganizationUserOrganizationDetailsViewQuery.cs index 1584b26f0..ba278fc91 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/OrganizationUserOrganizationDetailsViewQuery.cs +++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/OrganizationUserOrganizationDetailsViewQuery.cs @@ -66,6 +66,9 @@ public class OrganizationUserOrganizationDetailsViewQuery : IQuery +{ + public void Configure(EntityTypeBuilder builder) + { + builder + .Property(c => c.Id) + .ValueGeneratedNever(); + + builder + .HasIndex(migrationRecord => new { migrationRecord.ProviderId, migrationRecord.OrganizationId }) + .IsUnique(); + + builder.ToTable(nameof(ClientOrganizationMigrationRecord)); + } +} diff --git a/src/Infrastructure.EntityFramework/Billing/Models/ClientOrganizationMigrationRecord.cs b/src/Infrastructure.EntityFramework/Billing/Models/ClientOrganizationMigrationRecord.cs new file mode 100644 index 000000000..4271df292 --- /dev/null +++ b/src/Infrastructure.EntityFramework/Billing/Models/ClientOrganizationMigrationRecord.cs @@ -0,0 +1,16 @@ +using AutoMapper; + +namespace Bit.Infrastructure.EntityFramework.Billing.Models; + +public class ClientOrganizationMigrationRecord : Core.Billing.Entities.ClientOrganizationMigrationRecord +{ + +} + +public class ClientOrganizationMigrationRecordProfile : Profile +{ + public ClientOrganizationMigrationRecordProfile() + { + CreateMap().ReverseMap(); + } +} diff --git a/src/Infrastructure.EntityFramework/Billing/Repositories/ClientOrganizationMigrationRecordRepository.cs b/src/Infrastructure.EntityFramework/Billing/Repositories/ClientOrganizationMigrationRecordRepository.cs new file mode 100644 index 000000000..c7c9a6118 --- /dev/null +++ b/src/Infrastructure.EntityFramework/Billing/Repositories/ClientOrganizationMigrationRecordRepository.cs @@ -0,0 +1,47 @@ +using AutoMapper; +using Bit.Core.Billing.Entities; +using Bit.Core.Billing.Repositories; +using Bit.Infrastructure.EntityFramework.Repositories; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; + +using EFClientOrganizationMigrationRecord = Bit.Infrastructure.EntityFramework.Billing.Models.ClientOrganizationMigrationRecord; + +namespace Bit.Infrastructure.EntityFramework.Billing.Repositories; + +public class ClientOrganizationMigrationRecordRepository( + IMapper mapper, + IServiceScopeFactory serviceScopeFactory) + : Repository( + serviceScopeFactory, + mapper, + context => context.ClientOrganizationMigrationRecords), IClientOrganizationMigrationRecordRepository +{ + public async Task GetByOrganizationId(Guid organizationId) + { + using var serviceScope = ServiceScopeFactory.CreateScope(); + + var databaseContext = GetDatabaseContext(serviceScope); + + var query = + from clientOrganizationMigrationRecord in databaseContext.ClientOrganizationMigrationRecords + where clientOrganizationMigrationRecord.OrganizationId == organizationId + select clientOrganizationMigrationRecord; + + return await query.FirstOrDefaultAsync(); + } + + public async Task> GetByProviderId(Guid providerId) + { + using var serviceScope = ServiceScopeFactory.CreateScope(); + + var databaseContext = GetDatabaseContext(serviceScope); + + var query = + from clientOrganizationMigrationRecord in databaseContext.ClientOrganizationMigrationRecords + where clientOrganizationMigrationRecord.ProviderId == providerId + select clientOrganizationMigrationRecord; + + return await query.ToArrayAsync(); + } +} diff --git a/src/Infrastructure.EntityFramework/EntityFrameworkServiceCollectionExtensions.cs b/src/Infrastructure.EntityFramework/EntityFrameworkServiceCollectionExtensions.cs index 72d0e6006..ad0b46277 100644 --- a/src/Infrastructure.EntityFramework/EntityFrameworkServiceCollectionExtensions.cs +++ b/src/Infrastructure.EntityFramework/EntityFrameworkServiceCollectionExtensions.cs @@ -93,6 +93,8 @@ public static class EntityFrameworkServiceCollectionExtensions services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services + .AddSingleton(); if (selfHosted) { diff --git a/src/Infrastructure.EntityFramework/NotificationCenter/Repositories/NotificationRepository.cs b/src/Infrastructure.EntityFramework/NotificationCenter/Repositories/NotificationRepository.cs index 03ae63c59..a413e7874 100644 --- a/src/Infrastructure.EntityFramework/NotificationCenter/Repositories/NotificationRepository.cs +++ b/src/Infrastructure.EntityFramework/NotificationCenter/Repositories/NotificationRepository.cs @@ -1,9 +1,11 @@ #nullable enable using AutoMapper; using Bit.Core.Enums; +using Bit.Core.NotificationCenter.Models.Data; using Bit.Core.NotificationCenter.Models.Filter; using Bit.Core.NotificationCenter.Repositories; using Bit.Infrastructure.EntityFramework.NotificationCenter.Models; +using Bit.Infrastructure.EntityFramework.NotificationCenter.Repositories.Queries; using Bit.Infrastructure.EntityFramework.Repositories; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; @@ -20,34 +22,13 @@ public class NotificationRepository : Repository> GetByUserIdAsync(Guid userId, ClientType clientType) - { - return await GetByUserIdAndStatusAsync(userId, clientType, new NotificationStatusFilter()); - } - - public async Task> GetByUserIdAndStatusAsync(Guid userId, - ClientType clientType, NotificationStatusFilter? statusFilter) { await using var scope = ServiceScopeFactory.CreateAsyncScope(); var dbContext = GetDatabaseContext(scope); - var notificationQuery = BuildNotificationQuery(dbContext, userId, clientType); + var notificationStatusDetailsViewQuery = new NotificationStatusDetailsViewQuery(userId, clientType); - if (statusFilter != null && (statusFilter.Read != null || statusFilter.Deleted != null)) - { - notificationQuery = from n in notificationQuery - join ns in dbContext.NotificationStatuses on n.Id equals ns.NotificationId - where - ns.UserId == userId && - ( - statusFilter.Read == null || - (statusFilter.Read == true ? ns.ReadDate != null : ns.ReadDate == null) || - statusFilter.Deleted == null || - (statusFilter.Deleted == true ? ns.DeletedDate != null : ns.DeletedDate == null) - ) - select n; - } - - var notifications = await notificationQuery + var notifications = await notificationStatusDetailsViewQuery.Run(dbContext) .OrderByDescending(n => n.Priority) .ThenByDescending(n => n.CreationDate) .ToListAsync(); @@ -55,38 +36,28 @@ public class NotificationRepository : Repository>(notifications); } - private static IQueryable BuildNotificationQuery(DatabaseContext dbContext, Guid userId, - ClientType clientType) + public async Task> GetByUserIdAndStatusAsync(Guid userId, + ClientType clientType, NotificationStatusFilter? statusFilter) { - var clientTypes = new[] { ClientType.All }; - if (clientType != ClientType.All) + await using var scope = ServiceScopeFactory.CreateAsyncScope(); + var dbContext = GetDatabaseContext(scope); + + var notificationStatusDetailsViewQuery = new NotificationStatusDetailsViewQuery(userId, clientType); + + var query = notificationStatusDetailsViewQuery.Run(dbContext); + if (statusFilter != null && (statusFilter.Read != null || statusFilter.Deleted != null)) { - clientTypes = [ClientType.All, clientType]; + query = from n in query + where statusFilter.Read == null || + (statusFilter.Read == true ? n.ReadDate != null : n.ReadDate == null) || + statusFilter.Deleted == null || + (statusFilter.Deleted == true ? n.DeletedDate != null : n.DeletedDate == null) + select n; } - return from n in dbContext.Notifications - join ou in dbContext.OrganizationUsers.Where(ou => ou.UserId == userId) - on n.OrganizationId equals ou.OrganizationId into grouping - from ou in grouping.DefaultIfEmpty() - where - clientTypes.Contains(n.ClientType) && - ( - ( - n.Global && - n.UserId == null && - n.OrganizationId == null - ) || - ( - !n.Global && - n.UserId == userId && - (n.OrganizationId == null || ou != null) - ) || - ( - !n.Global && - n.UserId == null && - ou != null - ) - ) - select n; + return await query + .OrderByDescending(n => n.Priority) + .ThenByDescending(n => n.CreationDate) + .ToListAsync(); } } diff --git a/src/Infrastructure.EntityFramework/NotificationCenter/Repositories/Queries/NotificationStatusDetailsViewQuery.cs b/src/Infrastructure.EntityFramework/NotificationCenter/Repositories/Queries/NotificationStatusDetailsViewQuery.cs new file mode 100644 index 000000000..2f8bade1d --- /dev/null +++ b/src/Infrastructure.EntityFramework/NotificationCenter/Repositories/Queries/NotificationStatusDetailsViewQuery.cs @@ -0,0 +1,63 @@ +#nullable enable +using Bit.Core.Enums; +using Bit.Core.NotificationCenter.Models.Data; +using Bit.Infrastructure.EntityFramework.Repositories; +using Bit.Infrastructure.EntityFramework.Repositories.Queries; + +namespace Bit.Infrastructure.EntityFramework.NotificationCenter.Repositories.Queries; + +public class NotificationStatusDetailsViewQuery(Guid userId, ClientType clientType) : IQuery +{ + public IQueryable Run(DatabaseContext dbContext) + { + var clientTypes = new[] { ClientType.All }; + if (clientType != ClientType.All) + { + clientTypes = [ClientType.All, clientType]; + } + + var query = from n in dbContext.Notifications + join ou in dbContext.OrganizationUsers.Where(ou => ou.UserId == userId) + on n.OrganizationId equals ou.OrganizationId into groupingOrganizationUsers + from ou in groupingOrganizationUsers.DefaultIfEmpty() + join ns in dbContext.NotificationStatuses.Where(ns => ns.UserId == userId) on n.Id equals ns.NotificationId + into groupingNotificationStatus + from ns in groupingNotificationStatus.DefaultIfEmpty() + where + clientTypes.Contains(n.ClientType) && + ( + ( + n.Global && + n.UserId == null && + n.OrganizationId == null + ) || + ( + !n.Global && + n.UserId == userId && + (n.OrganizationId == null || ou != null) + ) || + ( + !n.Global && + n.UserId == null && + ou != null + ) + ) + select new { n, ns }; + + return query.Select(x => new NotificationStatusDetails + { + Id = x.n.Id, + Priority = x.n.Priority, + Global = x.n.Global, + ClientType = x.n.ClientType, + UserId = x.n.UserId, + OrganizationId = x.n.OrganizationId, + Title = x.n.Title, + Body = x.n.Body, + CreationDate = x.n.CreationDate, + RevisionDate = x.n.RevisionDate, + ReadDate = x.ns != null ? x.ns.ReadDate : null, + DeletedDate = x.ns != null ? x.ns.DeletedDate : null, + }); + } +} diff --git a/src/Infrastructure.EntityFramework/Repositories/DatabaseContext.cs b/src/Infrastructure.EntityFramework/Repositories/DatabaseContext.cs index 38682fb8b..d751b929b 100644 --- a/src/Infrastructure.EntityFramework/Repositories/DatabaseContext.cs +++ b/src/Infrastructure.EntityFramework/Repositories/DatabaseContext.cs @@ -74,6 +74,7 @@ public class DatabaseContext : DbContext public DbSet ProviderInvoiceItems { get; set; } public DbSet Notifications { get; set; } public DbSet NotificationStatuses { get; set; } + public DbSet ClientOrganizationMigrationRecords { get; set; } protected override void OnModelCreating(ModelBuilder builder) { diff --git a/src/Infrastructure.EntityFramework/Repositories/OrganizationDomainRepository.cs b/src/Infrastructure.EntityFramework/Repositories/OrganizationDomainRepository.cs index 9135c8bd1..3e2d6e44a 100644 --- a/src/Infrastructure.EntityFramework/Repositories/OrganizationDomainRepository.cs +++ b/src/Infrastructure.EntityFramework/Repositories/OrganizationDomainRepository.cs @@ -95,6 +95,29 @@ public class OrganizationDomainRepository : Repository> GetVerifiedOrganizationDomainSsoDetailsAsync(string email) + { + var domainName = new MailAddress(email).Host; + + using var scope = ServiceScopeFactory.CreateScope(); + var dbContext = GetDatabaseContext(scope); + return await (from o in dbContext.Organizations + from od in o.Domains + join s in dbContext.SsoConfigs on o.Id equals s.OrganizationId into sJoin + from s in sJoin.DefaultIfEmpty() + where od.DomainName == domainName + && o.Enabled + && s.Enabled + && od.VerifiedDate != null + select new VerifiedOrganizationDomainSsoDetail( + o.Id, + o.Name, + od.DomainName, + o.Identifier)) + .AsNoTracking() + .ToListAsync(); + } + public async Task GetDomainByIdOrganizationIdAsync(Guid id, Guid orgId) { using var scope = ServiceScopeFactory.CreateScope(); diff --git a/src/SharedWeb/SharedWeb.csproj b/src/SharedWeb/SharedWeb.csproj index a4c500d24..8d1097eee 100644 --- a/src/SharedWeb/SharedWeb.csproj +++ b/src/SharedWeb/SharedWeb.csproj @@ -7,7 +7,7 @@ - + diff --git a/src/SharedWeb/Utilities/RequestLoggingMiddleware.cs b/src/SharedWeb/Utilities/RequestLoggingMiddleware.cs new file mode 100644 index 000000000..77efdbfcf --- /dev/null +++ b/src/SharedWeb/Utilities/RequestLoggingMiddleware.cs @@ -0,0 +1,124 @@ +using System.Collections; +using Bit.Core; +using Bit.Core.Services; +using Bit.Core.Settings; +using Bit.Core.Utilities; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; + +#nullable enable + +namespace Bit.SharedWeb.Utilities; + +public sealed class RequestLoggingMiddleware +{ + private readonly RequestDelegate _next; + private readonly ILogger _logger; + private readonly GlobalSettings _globalSettings; + + public RequestLoggingMiddleware(RequestDelegate next, ILogger logger, GlobalSettings globalSettings) + { + _next = next; + _logger = logger; + _globalSettings = globalSettings; + } + + public Task Invoke(HttpContext context, IFeatureService featureService) + { + if (!featureService.IsEnabled(FeatureFlagKeys.RemoveServerVersionHeader)) + { + context.Response.OnStarting(() => + { + context.Response.Headers.Append("Server-Version", AssemblyHelpers.GetVersion()); + return Task.CompletedTask; + }); + } + + using (_logger.BeginScope( + new RequestLogScope(context.GetIpAddress(_globalSettings), + GetHeaderValue(context, "user-agent"), + GetHeaderValue(context, "device-type"), + GetHeaderValue(context, "device-type"), + GetHeaderValue(context, "bitwarden-client-version")))) + { + return _next(context); + } + + static string? GetHeaderValue(HttpContext httpContext, string header) + { + if (httpContext.Request.Headers.TryGetValue(header, out var value)) + { + return value; + } + + return null; + } + } + + + private sealed class RequestLogScope : IReadOnlyList> + { + private string? _cachedToString; + + public RequestLogScope(string? ipAddress, string? userAgent, string? deviceType, string? origin, string? clientVersion) + { + IpAddress = ipAddress; + UserAgent = userAgent; + DeviceType = deviceType; + Origin = origin; + ClientVersion = clientVersion; + } + + public KeyValuePair this[int index] + { + get + { + if (index == 0) + { + return new KeyValuePair(nameof(IpAddress), IpAddress); + } + else if (index == 1) + { + return new KeyValuePair(nameof(UserAgent), UserAgent); + } + else if (index == 2) + { + return new KeyValuePair(nameof(DeviceType), DeviceType); + } + else if (index == 3) + { + return new KeyValuePair(nameof(Origin), Origin); + } + else if (index == 4) + { + return new KeyValuePair(nameof(ClientVersion), ClientVersion); + } + + throw new ArgumentOutOfRangeException(nameof(index)); + } + } + + public int Count => 5; + + public string? IpAddress { get; } + public string? UserAgent { get; } + public string? DeviceType { get; } + public string? Origin { get; } + public string? ClientVersion { get; } + + public IEnumerator> GetEnumerator() + { + for (var i = 0; i < Count; i++) + { + yield return this[i]; + } + } + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public override string ToString() + { + _cachedToString ??= $"IpAddress:{IpAddress} UserAgent:{UserAgent} DeviceType:{DeviceType} Origin:{Origin} ClientVersion:{ClientVersion}"; + return _cachedToString; + } + } +} diff --git a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs index be451ea31..5a5585952 100644 --- a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs +++ b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs @@ -4,6 +4,7 @@ using System.Security.Claims; using System.Security.Cryptography.X509Certificates; using AspNetCoreRateLimit; using Bit.Core.AdminConsole.Models.Business.Tokenables; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies; using Bit.Core.AdminConsole.Services; using Bit.Core.AdminConsole.Services.Implementations; using Bit.Core.AdminConsole.Services.NoopImplementations; @@ -24,6 +25,7 @@ using Bit.Core.Enums; using Bit.Core.HostedServices; using Bit.Core.Identity; using Bit.Core.IdentityServer; +using Bit.Core.NotificationHub; using Bit.Core.OrganizationFeatures; using Bit.Core.Repositories; using Bit.Core.Resources; @@ -48,7 +50,6 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.HttpOverrides; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc.Localization; @@ -60,7 +61,6 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Serilog.Context; using StackExchange.Redis; using NoopRepos = Bit.Core.Repositories.Noop; using Role = Bit.Core.Entities.Role; @@ -104,9 +104,9 @@ public static class ServiceCollectionExtensions services.AddUserServices(globalSettings); services.AddTrialInitiationServices(); services.AddOrganizationServices(globalSettings); + services.AddPolicyServices(); services.AddScoped(); services.AddScoped(); - services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddSingleton(); @@ -265,20 +265,35 @@ public static class ServiceCollectionExtensions } services.AddSingleton(); - if (globalSettings.SelfHosted && - CoreHelpers.SettingHasValue(globalSettings.PushRelayBaseUri) && - globalSettings.Installation?.Id != null && - CoreHelpers.SettingHasValue(globalSettings.Installation?.Key)) + if (globalSettings.SelfHosted) { - services.AddSingleton(); + if (CoreHelpers.SettingHasValue(globalSettings.PushRelayBaseUri) && + globalSettings.Installation?.Id != null && + CoreHelpers.SettingHasValue(globalSettings.Installation?.Key)) + { + services.AddKeyedSingleton("implementation"); + services.AddSingleton(); + } + else + { + services.AddSingleton(); + } + + if (CoreHelpers.SettingHasValue(globalSettings.InternalIdentityKey) && + CoreHelpers.SettingHasValue(globalSettings.BaseServiceUri.InternalNotifications)) + { + services.AddKeyedSingleton("implementation"); + } } else if (!globalSettings.SelfHosted) { + services.AddSingleton(); services.AddSingleton(); - } - else - { - services.AddSingleton(); + services.AddKeyedSingleton("implementation"); + if (CoreHelpers.SettingHasValue(globalSettings.Notifications?.ConnectionString)) + { + services.AddKeyedSingleton("implementation"); + } } if (!globalSettings.SelfHosted && CoreHelpers.SettingHasValue(globalSettings.Mail.ConnectionString)) @@ -540,31 +555,7 @@ public static class ServiceCollectionExtensions public static void UseDefaultMiddleware(this IApplicationBuilder app, IWebHostEnvironment env, GlobalSettings globalSettings) { - string GetHeaderValue(HttpContext httpContext, string header) - { - if (httpContext.Request.Headers.ContainsKey(header)) - { - return httpContext.Request.Headers[header]; - } - return null; - } - - // Add version information to response headers - app.Use(async (httpContext, next) => - { - using (LogContext.PushProperty("IPAddress", httpContext.GetIpAddress(globalSettings))) - using (LogContext.PushProperty("UserAgent", GetHeaderValue(httpContext, "user-agent"))) - using (LogContext.PushProperty("DeviceType", GetHeaderValue(httpContext, "device-type"))) - using (LogContext.PushProperty("Origin", GetHeaderValue(httpContext, "origin"))) - { - httpContext.Response.OnStarting((state) => - { - httpContext.Response.Headers.Append("Server-Version", AssemblyHelpers.GetVersion()); - return Task.FromResult(0); - }, null); - await next.Invoke(); - } - }); + app.UseMiddleware(); } public static void UseForwardedHeaders(this IApplicationBuilder app, IGlobalSettings globalSettings) diff --git a/src/Sql/Billing/dbo/Stored Procedures/ClientOrganizationMigrationRecord_Create.sql b/src/Sql/Billing/dbo/Stored Procedures/ClientOrganizationMigrationRecord_Create.sql new file mode 100644 index 000000000..5ee193fe0 --- /dev/null +++ b/src/Sql/Billing/dbo/Stored Procedures/ClientOrganizationMigrationRecord_Create.sql @@ -0,0 +1,45 @@ +CREATE PROCEDURE [dbo].[ClientOrganizationMigrationRecord_Create] + @Id UNIQUEIDENTIFIER OUTPUT, + @OrganizationId UNIQUEIDENTIFIER, + @ProviderId UNIQUEIDENTIFIER, + @PlanType TINYINT, + @Seats SMALLINT, + @MaxStorageGb SMALLINT, + @GatewayCustomerId VARCHAR(50), + @GatewaySubscriptionId VARCHAR(50), + @ExpirationDate DATETIME2(7), + @MaxAutoscaleSeats INT, + @Status TINYINT +AS +BEGIN + SET NOCOUNT ON + + INSERT INTO [dbo].[ClientOrganizationMigrationRecord] + ( + [Id], + [OrganizationId], + [ProviderId], + [PlanType], + [Seats], + [MaxStorageGb], + [GatewayCustomerId], + [GatewaySubscriptionId], + [ExpirationDate], + [MaxAutoscaleSeats], + [Status] + ) + VALUES + ( + @Id, + @OrganizationId, + @ProviderId, + @PlanType, + @Seats, + @MaxStorageGb, + @GatewayCustomerId, + @GatewaySubscriptionId, + @ExpirationDate, + @MaxAutoscaleSeats, + @Status + ) +END diff --git a/src/Sql/Billing/dbo/Stored Procedures/ClientOrganizationMigrationRecord_DeleteById.sql b/src/Sql/Billing/dbo/Stored Procedures/ClientOrganizationMigrationRecord_DeleteById.sql new file mode 100644 index 000000000..fd54e6fbc --- /dev/null +++ b/src/Sql/Billing/dbo/Stored Procedures/ClientOrganizationMigrationRecord_DeleteById.sql @@ -0,0 +1,12 @@ +CREATE PROCEDURE [dbo].[ClientOrganizationMigrationRecord_DeleteById] + @Id UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + DELETE + FROM + [dbo].[ClientOrganizationMigrationRecord] + WHERE + [Id] = @Id +END diff --git a/src/Sql/Billing/dbo/Stored Procedures/ClientOrganizationMigrationRecord_ReadById.sql b/src/Sql/Billing/dbo/Stored Procedures/ClientOrganizationMigrationRecord_ReadById.sql new file mode 100644 index 000000000..a796c6f66 --- /dev/null +++ b/src/Sql/Billing/dbo/Stored Procedures/ClientOrganizationMigrationRecord_ReadById.sql @@ -0,0 +1,13 @@ +CREATE PROCEDURE [dbo].[ClientOrganizationMigrationRecord_ReadById] + @Id UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT + * + FROM + [dbo].[ClientOrganizationMigrationRecordView] + WHERE + [Id] = @Id +END diff --git a/src/Sql/Billing/dbo/Stored Procedures/ClientOrganizationMigrationRecord_ReadByOrganizationId.sql b/src/Sql/Billing/dbo/Stored Procedures/ClientOrganizationMigrationRecord_ReadByOrganizationId.sql new file mode 100644 index 000000000..f7dcb3674 --- /dev/null +++ b/src/Sql/Billing/dbo/Stored Procedures/ClientOrganizationMigrationRecord_ReadByOrganizationId.sql @@ -0,0 +1,13 @@ +CREATE PROCEDURE [dbo].[ClientOrganizationMigrationRecord_ReadByOrganizationId] + @OrganizationId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT + * + FROM + [dbo].[ClientOrganizationMigrationRecordView] + WHERE + [OrganizationId] = @OrganizationId +END diff --git a/src/Sql/Billing/dbo/Stored Procedures/ClientOrganizationMigrationRecord_ReadByProviderId.sql b/src/Sql/Billing/dbo/Stored Procedures/ClientOrganizationMigrationRecord_ReadByProviderId.sql new file mode 100644 index 000000000..c2577da05 --- /dev/null +++ b/src/Sql/Billing/dbo/Stored Procedures/ClientOrganizationMigrationRecord_ReadByProviderId.sql @@ -0,0 +1,13 @@ +CREATE PROCEDURE [dbo].[ClientOrganizationMigrationRecord_ReadByProviderId] + @ProviderId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT + * + FROM + [dbo].[ClientOrganizationMigrationRecordView] + WHERE + [ProviderId] = @ProviderId +END diff --git a/src/Sql/Billing/dbo/Stored Procedures/ClientOrganizationMigrationRecord_Update.sql b/src/Sql/Billing/dbo/Stored Procedures/ClientOrganizationMigrationRecord_Update.sql new file mode 100644 index 000000000..bf7d2d6ae --- /dev/null +++ b/src/Sql/Billing/dbo/Stored Procedures/ClientOrganizationMigrationRecord_Update.sql @@ -0,0 +1,32 @@ +CREATE PROCEDURE [dbo].[ClientOrganizationMigrationRecord_Update] + @Id UNIQUEIDENTIFIER OUTPUT, + @OrganizationId UNIQUEIDENTIFIER, + @ProviderId UNIQUEIDENTIFIER, + @PlanType TINYINT, + @Seats SMALLINT, + @MaxStorageGb SMALLINT, + @GatewayCustomerId VARCHAR(50), + @GatewaySubscriptionId VARCHAR(50), + @ExpirationDate DATETIME2(7), + @MaxAutoscaleSeats INT, + @Status TINYINT +AS +BEGIN + SET NOCOUNT ON + + UPDATE + [dbo].[ClientOrganizationMigrationRecord] + SET + [OrganizationId] = @OrganizationId, + [ProviderId] = @ProviderId, + [PlanType] = @PlanType, + [Seats] = @Seats, + [MaxStorageGb] = @MaxStorageGb, + [GatewayCustomerId] = @GatewayCustomerId, + [GatewaySubscriptionId] = @GatewaySubscriptionId, + [ExpirationDate] = @ExpirationDate, + [MaxAutoscaleSeats] = @MaxAutoscaleSeats, + [Status] = @Status + WHERE + [Id] = @Id +END diff --git a/src/Sql/Billing/Stored Procedures/ProviderInvoiceItem_Create.sql b/src/Sql/Billing/dbo/Stored Procedures/ProviderInvoiceItem_Create.sql similarity index 100% rename from src/Sql/Billing/Stored Procedures/ProviderInvoiceItem_Create.sql rename to src/Sql/Billing/dbo/Stored Procedures/ProviderInvoiceItem_Create.sql diff --git a/src/Sql/Billing/Stored Procedures/ProviderInvoiceItem_DeleteById.sql b/src/Sql/Billing/dbo/Stored Procedures/ProviderInvoiceItem_DeleteById.sql similarity index 100% rename from src/Sql/Billing/Stored Procedures/ProviderInvoiceItem_DeleteById.sql rename to src/Sql/Billing/dbo/Stored Procedures/ProviderInvoiceItem_DeleteById.sql diff --git a/src/Sql/Billing/Stored Procedures/ProviderInvoiceItem_ReadById.sql b/src/Sql/Billing/dbo/Stored Procedures/ProviderInvoiceItem_ReadById.sql similarity index 100% rename from src/Sql/Billing/Stored Procedures/ProviderInvoiceItem_ReadById.sql rename to src/Sql/Billing/dbo/Stored Procedures/ProviderInvoiceItem_ReadById.sql diff --git a/src/Sql/Billing/Stored Procedures/ProviderInvoiceItem_ReadByInvoiceId.sql b/src/Sql/Billing/dbo/Stored Procedures/ProviderInvoiceItem_ReadByInvoiceId.sql similarity index 100% rename from src/Sql/Billing/Stored Procedures/ProviderInvoiceItem_ReadByInvoiceId.sql rename to src/Sql/Billing/dbo/Stored Procedures/ProviderInvoiceItem_ReadByInvoiceId.sql diff --git a/src/Sql/Billing/Stored Procedures/ProviderInvoiceItem_ReadByProviderId.sql b/src/Sql/Billing/dbo/Stored Procedures/ProviderInvoiceItem_ReadByProviderId.sql similarity index 100% rename from src/Sql/Billing/Stored Procedures/ProviderInvoiceItem_ReadByProviderId.sql rename to src/Sql/Billing/dbo/Stored Procedures/ProviderInvoiceItem_ReadByProviderId.sql diff --git a/src/Sql/Billing/Stored Procedures/ProviderInvoiceItem_Update.sql b/src/Sql/Billing/dbo/Stored Procedures/ProviderInvoiceItem_Update.sql similarity index 100% rename from src/Sql/Billing/Stored Procedures/ProviderInvoiceItem_Update.sql rename to src/Sql/Billing/dbo/Stored Procedures/ProviderInvoiceItem_Update.sql diff --git a/src/Sql/Billing/Stored Procedures/ProviderPlan_Create.sql b/src/Sql/Billing/dbo/Stored Procedures/ProviderPlan_Create.sql similarity index 100% rename from src/Sql/Billing/Stored Procedures/ProviderPlan_Create.sql rename to src/Sql/Billing/dbo/Stored Procedures/ProviderPlan_Create.sql diff --git a/src/Sql/Billing/Stored Procedures/ProviderPlan_DeleteById.sql b/src/Sql/Billing/dbo/Stored Procedures/ProviderPlan_DeleteById.sql similarity index 100% rename from src/Sql/Billing/Stored Procedures/ProviderPlan_DeleteById.sql rename to src/Sql/Billing/dbo/Stored Procedures/ProviderPlan_DeleteById.sql diff --git a/src/Sql/Billing/Stored Procedures/ProviderPlan_ReadById.sql b/src/Sql/Billing/dbo/Stored Procedures/ProviderPlan_ReadById.sql similarity index 100% rename from src/Sql/Billing/Stored Procedures/ProviderPlan_ReadById.sql rename to src/Sql/Billing/dbo/Stored Procedures/ProviderPlan_ReadById.sql diff --git a/src/Sql/Billing/Stored Procedures/ProviderPlan_ReadByProviderId.sql b/src/Sql/Billing/dbo/Stored Procedures/ProviderPlan_ReadByProviderId.sql similarity index 100% rename from src/Sql/Billing/Stored Procedures/ProviderPlan_ReadByProviderId.sql rename to src/Sql/Billing/dbo/Stored Procedures/ProviderPlan_ReadByProviderId.sql diff --git a/src/Sql/Billing/Stored Procedures/ProviderPlan_Update.sql b/src/Sql/Billing/dbo/Stored Procedures/ProviderPlan_Update.sql similarity index 100% rename from src/Sql/Billing/Stored Procedures/ProviderPlan_Update.sql rename to src/Sql/Billing/dbo/Stored Procedures/ProviderPlan_Update.sql diff --git a/src/Sql/Billing/dbo/Tables/ClientOrganizationMigrationRecord.sql b/src/Sql/Billing/dbo/Tables/ClientOrganizationMigrationRecord.sql new file mode 100644 index 000000000..c53f71c24 --- /dev/null +++ b/src/Sql/Billing/dbo/Tables/ClientOrganizationMigrationRecord.sql @@ -0,0 +1,15 @@ +CREATE TABLE [dbo].[ClientOrganizationMigrationRecord] ( + [Id] UNIQUEIDENTIFIER NOT NULL, + [OrganizationId] UNIQUEIDENTIFIER NOT NULL, + [ProviderId] UNIQUEIDENTIFIER NOT NULL, + [PlanType] TINYINT NOT NULL, + [Seats] SMALLINT NOT NULL, + [MaxStorageGb] SMALLINT NULL, + [GatewayCustomerId] VARCHAR(50) NOT NULL, + [GatewaySubscriptionId] VARCHAR(50) NOT NULL, + [ExpirationDate] DATETIME2(7) NULL, + [MaxAutoscaleSeats] INT NULL, + [Status] TINYINT NOT NULL, + CONSTRAINT [PK_ClientOrganizationMigrationRecord] PRIMARY KEY CLUSTERED ([Id] ASC), + CONSTRAINT [PK_OrganizationIdProviderId] UNIQUE ([ProviderId], [OrganizationId]) +); diff --git a/src/Sql/Billing/Tables/ProviderInvoiceItem.sql b/src/Sql/Billing/dbo/Tables/ProviderInvoiceItem.sql similarity index 100% rename from src/Sql/Billing/Tables/ProviderInvoiceItem.sql rename to src/Sql/Billing/dbo/Tables/ProviderInvoiceItem.sql diff --git a/src/Sql/Billing/Tables/ProviderPlan.sql b/src/Sql/Billing/dbo/Tables/ProviderPlan.sql similarity index 100% rename from src/Sql/Billing/Tables/ProviderPlan.sql rename to src/Sql/Billing/dbo/Tables/ProviderPlan.sql diff --git a/src/Sql/Billing/dbo/Views/ClientOrganizationMigrationRecordView.sql b/src/Sql/Billing/dbo/Views/ClientOrganizationMigrationRecordView.sql new file mode 100644 index 000000000..ffa47ccc0 --- /dev/null +++ b/src/Sql/Billing/dbo/Views/ClientOrganizationMigrationRecordView.sql @@ -0,0 +1,6 @@ +CREATE VIEW [dbo].[ClientOrganizationMigrationRecordView] +AS +SELECT + * +FROM + [dbo].[ClientOrganizationMigrationRecord] diff --git a/src/Sql/Billing/Views/ProviderInvoiceItemView.sql b/src/Sql/Billing/dbo/Views/ProviderInvoiceItemView.sql similarity index 100% rename from src/Sql/Billing/Views/ProviderInvoiceItemView.sql rename to src/Sql/Billing/dbo/Views/ProviderInvoiceItemView.sql diff --git a/src/Sql/Billing/Views/ProviderPlanView.sql b/src/Sql/Billing/dbo/Views/ProviderPlanView.sql similarity index 100% rename from src/Sql/Billing/Views/ProviderPlanView.sql rename to src/Sql/Billing/dbo/Views/ProviderPlanView.sql diff --git a/src/Sql/NotificationCenter/dbo/Stored Procedures/Notification_ReadByUserIdAndStatus.sql b/src/Sql/NotificationCenter/dbo/Stored Procedures/Notification_ReadByUserIdAndStatus.sql index baf144501..b98f85f73 100644 --- a/src/Sql/NotificationCenter/dbo/Stored Procedures/Notification_ReadByUserIdAndStatus.sql +++ b/src/Sql/NotificationCenter/dbo/Stored Procedures/Notification_ReadByUserIdAndStatus.sql @@ -8,12 +8,11 @@ BEGIN SET NOCOUNT ON SELECT n.* - FROM [dbo].[NotificationView] n + FROM [dbo].[NotificationStatusDetailsView] n LEFT JOIN [dbo].[OrganizationUserView] ou ON n.[OrganizationId] = ou.[OrganizationId] AND ou.[UserId] = @UserId - LEFT JOIN [dbo].[NotificationStatusView] ns ON n.[Id] = ns.[NotificationId] - AND ns.[UserId] = @UserId - WHERE [ClientType] IN (0, CASE WHEN @ClientType != 0 THEN @ClientType END) + WHERE (n.[NotificationStatusUserId] IS NULL OR n.[NotificationStatusUserId] = @UserId) + AND [ClientType] IN (0, CASE WHEN @ClientType != 0 THEN @ClientType END) AND ([Global] = 1 OR (n.[UserId] = @UserId AND (n.[OrganizationId] IS NULL @@ -21,14 +20,14 @@ BEGIN OR (n.[UserId] IS NULL AND ou.[OrganizationId] IS NOT NULL)) AND ((@Read IS NULL AND @Deleted IS NULL) - OR (ns.[NotificationId] IS NOT NULL + OR (n.[NotificationStatusUserId] IS NOT NULL AND ((@Read IS NULL - OR IIF((@Read = 1 AND ns.[ReadDate] IS NOT NULL) OR - (@Read = 0 AND ns.[ReadDate] IS NULL), + OR IIF((@Read = 1 AND n.[ReadDate] IS NOT NULL) OR + (@Read = 0 AND n.[ReadDate] IS NULL), 1, 0) = 1) OR (@Deleted IS NULL - OR IIF((@Deleted = 1 AND ns.[DeletedDate] IS NOT NULL) OR - (@Deleted = 0 AND ns.[DeletedDate] IS NULL), + OR IIF((@Deleted = 1 AND n.[DeletedDate] IS NOT NULL) OR + (@Deleted = 0 AND n.[DeletedDate] IS NULL), 1, 0) = 1)))) ORDER BY [Priority] DESC, n.[CreationDate] DESC END diff --git a/src/Sql/NotificationCenter/dbo/Views/NotificationStatusDetailsView.sql b/src/Sql/NotificationCenter/dbo/Views/NotificationStatusDetailsView.sql new file mode 100644 index 000000000..5264be200 --- /dev/null +++ b/src/Sql/NotificationCenter/dbo/Views/NotificationStatusDetailsView.sql @@ -0,0 +1,13 @@ +CREATE VIEW [dbo].[NotificationStatusDetailsView] +AS +SELECT + N.*, + NS.UserId AS NotificationStatusUserId, + NS.ReadDate, + NS.DeletedDate +FROM + [dbo].[Notification] AS N +LEFT JOIN + [dbo].[NotificationStatus] as NS +ON + N.[Id] = NS.[NotificationId] diff --git a/src/Sql/Sql.sqlproj b/src/Sql/Sql.sqlproj index 7c522cf8f..65524fca4 100644 --- a/src/Sql/Sql.sqlproj +++ b/src/Sql/Sql.sqlproj @@ -19,4 +19,7 @@ 71502 + + + diff --git a/src/Sql/dbo/Stored Procedures/CollectionCipher_ReadByUserId.sql b/src/Sql/dbo/Stored Procedures/CollectionCipher_ReadByUserId.sql index 37971870c..907a31c7f 100644 --- a/src/Sql/dbo/Stored Procedures/CollectionCipher_ReadByUserId.sql +++ b/src/Sql/dbo/Stored Procedures/CollectionCipher_ReadByUserId.sql @@ -4,6 +4,21 @@ AS BEGIN SET NOCOUNT ON + SELECT + CC.* + FROM + [dbo].[CollectionCipher] CC + INNER JOIN + [dbo].[Collection] S ON S.[Id] = CC.[CollectionId] + INNER JOIN + [dbo].[OrganizationUser] OU ON OU.[OrganizationId] = S.[OrganizationId] AND OU.[UserId] = @UserId + INNER JOIN + [dbo].[CollectionUser] CU ON CU.[CollectionId] = S.[Id] AND CU.[OrganizationUserId] = OU.[Id] + WHERE + OU.[Status] = 2 + + UNION ALL + SELECT CC.* FROM @@ -12,18 +27,13 @@ BEGIN [dbo].[Collection] S ON S.[Id] = CC.[CollectionId] INNER JOIN [dbo].[OrganizationUser] OU ON OU.[OrganizationId] = S.[OrganizationId] AND OU.[UserId] = @UserId + INNER JOIN + [dbo].[GroupUser] GU ON GU.[OrganizationUserId] = OU.[Id] + INNER JOIN + [dbo].[CollectionGroup] CG ON CG.[CollectionId] = CC.[CollectionId] AND CG.[GroupId] = GU.[GroupId] LEFT JOIN [dbo].[CollectionUser] CU ON CU.[CollectionId] = S.[Id] AND CU.[OrganizationUserId] = OU.[Id] - LEFT JOIN - [dbo].[GroupUser] GU ON CU.[CollectionId] IS NULL AND GU.[OrganizationUserId] = OU.[Id] - LEFT JOIN - [dbo].[Group] G ON G.[Id] = GU.[GroupId] - LEFT JOIN - [dbo].[CollectionGroup] CG ON CG.[CollectionId] = CC.[CollectionId] AND CG.[GroupId] = GU.[GroupId] WHERE - OU.[Status] = 2 -- Confirmed - AND ( - CU.[CollectionId] IS NOT NULL - OR CG.[CollectionId] IS NOT NULL - ) + OU.[Status] = 2 + AND CU.[CollectionId] IS NULL END diff --git a/src/Sql/dbo/Stored Procedures/Organization_Create.sql b/src/Sql/dbo/Stored Procedures/Organization_Create.sql index 5ddfa1650..9084f0dff 100644 --- a/src/Sql/dbo/Stored Procedures/Organization_Create.sql +++ b/src/Sql/dbo/Stored Procedures/Organization_Create.sql @@ -1,4 +1,4 @@ -CREATE PROCEDURE [dbo].[Organization_Create] +CREATE PROCEDURE [dbo].[Organization_Create] @Id UNIQUEIDENTIFIER OUTPUT, @Identifier NVARCHAR(50), @Name NVARCHAR(50), @@ -51,12 +51,17 @@ @MaxAutoscaleSmSeats INT= null, @MaxAutoscaleSmServiceAccounts INT = null, @SecretsManagerBeta BIT = 0, - @LimitCollectionCreationDeletion BIT = 0, + @LimitCollectionCreationDeletion BIT = NULL, -- Deprecated https://bitwarden.atlassian.net/browse/PM-10863 + @LimitCollectionCreation BIT = NULL, + @LimitCollectionDeletion BIT = NULL, @AllowAdminAccessToAllCollectionItems BIT = 0 AS BEGIN SET NOCOUNT ON + SET @LimitCollectionCreation = COALESCE(@LimitCollectionCreation, @LimitCollectionCreationDeletion, 0); + SET @LimitCollectionDeletion = COALESCE(@LimitCollectionDeletion, @LimitCollectionCreationDeletion, 0); + INSERT INTO [dbo].[Organization] ( [Id], @@ -111,7 +116,9 @@ BEGIN [MaxAutoscaleSmSeats], [MaxAutoscaleSmServiceAccounts], [SecretsManagerBeta], - [LimitCollectionCreationDeletion], + [LimitCollectionCreationDeletion], -- Deprecated https://bitwarden.atlassian.net/browse/PM-10863 + [LimitCollectionCreation], + [LimitCollectionDeletion], [AllowAdminAccessToAllCollectionItems] ) VALUES @@ -168,7 +175,9 @@ BEGIN @MaxAutoscaleSmSeats, @MaxAutoscaleSmServiceAccounts, @SecretsManagerBeta, - @LimitCollectionCreationDeletion, + COALESCE(@LimitCollectionCreation, @LimitCollectionDeletion, 0), -- Deprecated https://bitwarden.atlassian.net/browse/PM-10863) + @LimitCollectionCreation, + @LimitCollectionDeletion, @AllowAdminAccessToAllCollectionItems ) END diff --git a/src/Sql/dbo/Stored Procedures/Organization_ReadAbilities.sql b/src/Sql/dbo/Stored Procedures/Organization_ReadAbilities.sql index 7a10f309d..fc85dad24 100644 --- a/src/Sql/dbo/Stored Procedures/Organization_ReadAbilities.sql +++ b/src/Sql/dbo/Stored Procedures/Organization_ReadAbilities.sql @@ -1,4 +1,4 @@ -CREATE PROCEDURE [dbo].[Organization_ReadAbilities] +CREATE PROCEDURE [dbo].[Organization_ReadAbilities] AS BEGIN SET NOCOUNT ON @@ -21,7 +21,9 @@ BEGIN [UseResetPassword], [UsePolicies], [Enabled], - [LimitCollectionCreationDeletion], + [LimitCollectionCreationDeletion], -- Deprecated https://bitwarden.atlassian.net/browse/PM-10863 + [LimitCollectionCreation], + [LimitCollectionDeletion], [AllowAdminAccessToAllCollectionItems] FROM [dbo].[Organization] diff --git a/src/Sql/dbo/Stored Procedures/Organization_Update.sql b/src/Sql/dbo/Stored Procedures/Organization_Update.sql index c76045a49..630f48d2a 100644 --- a/src/Sql/dbo/Stored Procedures/Organization_Update.sql +++ b/src/Sql/dbo/Stored Procedures/Organization_Update.sql @@ -1,4 +1,4 @@ -CREATE PROCEDURE [dbo].[Organization_Update] +CREATE PROCEDURE [dbo].[Organization_Update] @Id UNIQUEIDENTIFIER, @Identifier NVARCHAR(50), @Name NVARCHAR(50), @@ -51,12 +51,17 @@ @MaxAutoscaleSmSeats INT = null, @MaxAutoscaleSmServiceAccounts INT = null, @SecretsManagerBeta BIT = 0, - @LimitCollectionCreationDeletion BIT = 0, + @LimitCollectionCreationDeletion BIT = null, -- Deprecated https://bitwarden.atlassian.net/browse/PM-10863 + @LimitCollectionCreation BIT = null, + @LimitCollectionDeletion BIT = null, @AllowAdminAccessToAllCollectionItems BIT = 0 AS BEGIN SET NOCOUNT ON + SET @LimitCollectionCreation = COALESCE(@LimitCollectionCreation, @LimitCollectionCreationDeletion, 0); + SET @LimitCollectionDeletion = COALESCE(@LimitCollectionDeletion, @LimitCollectionCreationDeletion, 0); + UPDATE [dbo].[Organization] SET @@ -111,7 +116,9 @@ BEGIN [MaxAutoscaleSmSeats] = @MaxAutoscaleSmSeats, [MaxAutoscaleSmServiceAccounts] = @MaxAutoscaleSmServiceAccounts, [SecretsManagerBeta] = @SecretsManagerBeta, - [LimitCollectionCreationDeletion] = @LimitCollectionCreationDeletion, + [LimitCollectionCreationDeletion] = COALESCE(@LimitCollectionCreation, @LimitCollectionDeletion, 0), + [LimitCollectionCreation] = @LimitCollectionCreation, + [LimitCollectionDeletion] = @LimitCollectionDeletion, [AllowAdminAccessToAllCollectionItems] = @AllowAdminAccessToAllCollectionItems WHERE [Id] = @Id diff --git a/src/Sql/dbo/Stored Procedures/VerfiedOrganaizationDomainSsoDetails_ReadByEmail.sql b/src/Sql/dbo/Stored Procedures/VerfiedOrganaizationDomainSsoDetails_ReadByEmail.sql new file mode 100644 index 000000000..a32b42f6c --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/VerfiedOrganaizationDomainSsoDetails_ReadByEmail.sql @@ -0,0 +1,23 @@ +CREATE PROCEDURE [dbo].[VerifiedOrganizationDomainSsoDetails_ReadByEmail] +@Email NVARCHAR(256) +AS +BEGIN + SET NOCOUNT ON + + DECLARE @Domain NVARCHAR(256) + + SELECT @Domain = SUBSTRING(@Email, CHARINDEX( '@', @Email) + 1, LEN(@Email)) + + SELECT + O.Id AS OrganizationId, + O.Name AS OrganizationName, + O.Identifier AS OrganizationIdentifier, + OD.DomainName + FROM [dbo].[OrganizationView] O + INNER JOIN [dbo].[OrganizationDomainView] OD ON O.Id = OD.OrganizationId + LEFT JOIN [dbo].[Ssoconfig] S ON O.Id = S.OrganizationId + WHERE OD.DomainName = @Domain + AND O.Enabled = 1 + AND OD.VerifiedDate IS NOT NULL + AND S.Enabled = 1 +END diff --git a/src/Sql/dbo/Tables/Organization.sql b/src/Sql/dbo/Tables/Organization.sql index de6fa6283..1f181e5ee 100644 --- a/src/Sql/dbo/Tables/Organization.sql +++ b/src/Sql/dbo/Tables/Organization.sql @@ -1,4 +1,4 @@ -CREATE TABLE [dbo].[Organization] ( +CREATE TABLE [dbo].[Organization] ( [Id] UNIQUEIDENTIFIER NOT NULL, [Identifier] NVARCHAR (50) NULL, [Name] NVARCHAR (50) NOT NULL, @@ -52,6 +52,8 @@ [MaxAutoscaleSmServiceAccounts] INT NULL, [SecretsManagerBeta] BIT NOT NULL CONSTRAINT [DF_Organization_SecretsManagerBeta] DEFAULT (0), [LimitCollectionCreationDeletion] BIT NOT NULL CONSTRAINT [DF_Organization_LimitCollectionCreationDeletion] DEFAULT (0), + [LimitCollectionCreation] BIT NOT NULL CONSTRAINT [DF_Organization_LimitCollectionCreation] DEFAULT (0), + [LimitCollectionDeletion] BIT NOT NULL CONSTRAINT [DF_Organization_LimitCollectionDeletion] DEFAULT (0), [AllowAdminAccessToAllCollectionItems] BIT NOT NULL CONSTRAINT [DF_Organization_AllowAdminAccessToAllCollectionItems] DEFAULT (0), CONSTRAINT [PK_Organization] PRIMARY KEY CLUSTERED ([Id] ASC) ); diff --git a/src/Sql/dbo/Views/OrganizationUserOrganizationDetailsView.sql b/src/Sql/dbo/Views/OrganizationUserOrganizationDetailsView.sql index 14343ce5c..cbc54aeeb 100644 --- a/src/Sql/dbo/Views/OrganizationUserOrganizationDetailsView.sql +++ b/src/Sql/dbo/Views/OrganizationUserOrganizationDetailsView.sql @@ -1,4 +1,4 @@ -CREATE VIEW [dbo].[OrganizationUserOrganizationDetailsView] +CREATE VIEW [dbo].[OrganizationUserOrganizationDetailsView] AS SELECT OU.[UserId], @@ -46,7 +46,9 @@ SELECT O.[UsePasswordManager], O.[SmSeats], O.[SmServiceAccounts], - O.[LimitCollectionCreationDeletion], + O.[LimitCollectionCreationDeletion], -- Deprecated https://bitwarden.atlassian.net/browse/PM-10863 + O.[LimitCollectionCreation], + O.[LimitCollectionDeletion], O.[AllowAdminAccessToAllCollectionItems] FROM [dbo].[OrganizationUser] OU diff --git a/src/Sql/dbo/Views/ProviderUserProviderOrganizationDetailsView.sql b/src/Sql/dbo/Views/ProviderUserProviderOrganizationDetailsView.sql index f2be08ebf..e90d4ad6f 100644 --- a/src/Sql/dbo/Views/ProviderUserProviderOrganizationDetailsView.sql +++ b/src/Sql/dbo/Views/ProviderUserProviderOrganizationDetailsView.sql @@ -1,4 +1,4 @@ -CREATE VIEW [dbo].[ProviderUserProviderOrganizationDetailsView] +CREATE VIEW [dbo].[ProviderUserProviderOrganizationDetailsView] AS SELECT PU.[UserId], @@ -32,7 +32,9 @@ SELECT PU.[Id] ProviderUserId, P.[Name] ProviderName, O.[PlanType], - O.[LimitCollectionCreationDeletion], + O.[LimitCollectionCreationDeletion], -- Deprecated https://bitwarden.atlassian.net/browse/PM-10863 + O.[LimitCollectionCreation], + O.[LimitCollectionDeletion], O.[AllowAdminAccessToAllCollectionItems] FROM [dbo].[ProviderUser] PU 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/OrganizationDomainControllerTests.cs b/test/Api.Test/AdminConsole/Controllers/OrganizationDomainControllerTests.cs index 2f7430341..352f089db 100644 --- a/test/Api.Test/AdminConsole/Controllers/OrganizationDomainControllerTests.cs +++ b/test/Api.Test/AdminConsole/Controllers/OrganizationDomainControllerTests.cs @@ -229,13 +229,13 @@ public class OrganizationDomainControllerTests sutProvider.GetDependency() .GetDomainByIdOrganizationIdAsync(organizationDomain.Id, organizationDomain.OrganizationId) .Returns(organizationDomain); - sutProvider.GetDependency().VerifyOrganizationDomainAsync(organizationDomain) + sutProvider.GetDependency().UserVerifyOrganizationDomainAsync(organizationDomain) .Returns(new OrganizationDomain()); var result = await sutProvider.Sut.Verify(organizationDomain.OrganizationId, organizationDomain.Id); await sutProvider.GetDependency().Received(1) - .VerifyOrganizationDomainAsync(organizationDomain); + .UserVerifyOrganizationDomainAsync(organizationDomain); Assert.IsType(result); } @@ -316,4 +316,26 @@ public class OrganizationDomainControllerTests Assert.IsType(result); } + + [Theory, BitAutoData] + public async Task GetVerifiedOrgDomainSsoDetails_ShouldThrowNotFound_WhenEmailHasNotClaimedDomain( + OrganizationDomainSsoDetailsRequestModel model, SutProvider sutProvider) + { + sutProvider.GetDependency() + .GetVerifiedOrganizationDomainSsoDetailsAsync(model.Email).Returns(Array.Empty()); + + await Assert.ThrowsAsync(() => sutProvider.Sut.GetOrgDomainSsoDetails(model)); + } + + [Theory, BitAutoData] + public async Task GetVerifiedOrgDomainSsoDetails_ShouldReturnOrganizationDomainSsoDetails_WhenEmailHasClaimedDomain( + OrganizationDomainSsoDetailsRequestModel model, IEnumerable ssoDetailsData, SutProvider sutProvider) + { + sutProvider.GetDependency() + .GetVerifiedOrganizationDomainSsoDetailsAsync(model.Email).Returns(ssoDetailsData); + + var result = await sutProvider.Sut.GetVerifiedOrgDomainSsoDetailsAsync(model); + + Assert.IsType(result); + } } diff --git a/test/Api.Test/AdminConsole/Controllers/OrganizationUsersControllerTests.cs b/test/Api.Test/AdminConsole/Controllers/OrganizationUsersControllerTests.cs index 492112e5a..2ff5c0cb4 100644 --- a/test/Api.Test/AdminConsole/Controllers/OrganizationUsersControllerTests.cs +++ b/test/Api.Test/AdminConsole/Controllers/OrganizationUsersControllerTests.cs @@ -3,6 +3,7 @@ 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 +16,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 +187,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))); } @@ -368,7 +402,7 @@ public class OrganizationUsersControllerTests 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/AdminConsole/Controllers/OrganizationsControllerTests.cs b/test/Api.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs index 156df1476..25227fec7 100644 --- a/test/Api.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs +++ b/test/Api.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs @@ -7,6 +7,7 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Models.Business.Tokenables; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationApiKeys.Interfaces; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Auth.Entities; using Bit.Core.Auth.Enums; @@ -49,6 +50,7 @@ public class OrganizationsControllerTests : IDisposable private readonly IProviderRepository _providerRepository; private readonly IProviderBillingService _providerBillingService; private readonly IDataProtectorTokenFactory _orgDeleteTokenDataFactory; + private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand; private readonly OrganizationsController _sut; @@ -72,6 +74,7 @@ public class OrganizationsControllerTests : IDisposable _providerRepository = Substitute.For(); _providerBillingService = Substitute.For(); _orgDeleteTokenDataFactory = Substitute.For>(); + _removeOrganizationUserCommand = Substitute.For(); _sut = new OrganizationsController( _organizationRepository, @@ -91,7 +94,8 @@ public class OrganizationsControllerTests : IDisposable _pushNotificationService, _providerRepository, _providerBillingService, - _orgDeleteTokenDataFactory); + _orgDeleteTokenDataFactory, + _removeOrganizationUserCommand); } public void Dispose() @@ -120,13 +124,12 @@ public class OrganizationsControllerTests : IDisposable _ssoConfigRepository.GetByOrganizationIdAsync(orgId).Returns(ssoConfig); _userService.GetUserByPrincipalAsync(Arg.Any()).Returns(user); - var exception = await Assert.ThrowsAsync( - () => _sut.Leave(orgId.ToString())); + var exception = await Assert.ThrowsAsync(() => _sut.Leave(orgId)); Assert.Contains("Your organization's Single Sign-On settings prevent you from leaving.", exception.Message); - await _organizationService.DidNotReceiveWithAnyArgs().RemoveUserAsync(default, default); + await _removeOrganizationUserCommand.DidNotReceiveWithAnyArgs().RemoveUserAsync(default, default); } [Theory] @@ -155,8 +158,9 @@ public class OrganizationsControllerTests : IDisposable _ssoConfigRepository.GetByOrganizationIdAsync(orgId).Returns(ssoConfig); _userService.GetUserByPrincipalAsync(Arg.Any()).Returns(user); - await _organizationService.RemoveUserAsync(orgId, user.Id); - await _organizationService.Received(1).RemoveUserAsync(orgId, user.Id); + await _sut.Leave(orgId); + + await _removeOrganizationUserCommand.Received(1).RemoveUserAsync(orgId, user.Id); } [Theory, AutoData] diff --git a/test/Api.Test/Auth/Controllers/AccountsControllerTests.cs b/test/Api.Test/Auth/Controllers/AccountsControllerTests.cs index a16a9cb55..4127c92ee 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() { diff --git a/test/Api.Test/Billing/Controllers/OrganizationBillingControllerTests.cs b/test/Api.Test/Billing/Controllers/OrganizationBillingControllerTests.cs index 70ca59940..b46fd307e 100644 --- a/test/Api.Test/Billing/Controllers/OrganizationBillingControllerTests.cs +++ b/test/Api.Test/Billing/Controllers/OrganizationBillingControllerTests.cs @@ -52,7 +52,7 @@ public class OrganizationBillingControllerTests { sutProvider.GetDependency().AccessMembersTab(organizationId).Returns(true); sutProvider.GetDependency().GetMetadata(organizationId) - .Returns(new OrganizationMetadata(true)); + .Returns(new OrganizationMetadata(true, true)); var result = await sutProvider.Sut.GetMetadataAsync(organizationId); @@ -60,6 +60,7 @@ public class OrganizationBillingControllerTests var organizationMetadataResponse = ((Ok)result).Value; + Assert.True(organizationMetadataResponse.IsEligibleForSelfHost); Assert.True(organizationMetadataResponse.IsOnSecretsManagerStandalone); } diff --git a/test/Api.Test/Billing/Controllers/OrganizationsControllerTests.cs b/test/Api.Test/Billing/Controllers/OrganizationsControllerTests.cs index 8fb648e89..ec6047fbf 100644 --- a/test/Api.Test/Billing/Controllers/OrganizationsControllerTests.cs +++ b/test/Api.Test/Billing/Controllers/OrganizationsControllerTests.cs @@ -4,8 +4,8 @@ using Bit.Api.AdminConsole.Models.Request.Organizations; using Bit.Api.Billing.Controllers; using Bit.Api.Models.Request.Organizations; using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.Repositories; -using Bit.Core.Auth.Entities; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Models.Data; using Bit.Core.Auth.Repositories; @@ -46,6 +46,7 @@ public class OrganizationsControllerTests : IDisposable private readonly IAddSecretsManagerSubscriptionCommand _addSecretsManagerSubscriptionCommand; private readonly IReferenceEventService _referenceEventService; private readonly ISubscriberService _subscriberService; + private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand; private readonly OrganizationsController _sut; @@ -68,6 +69,7 @@ public class OrganizationsControllerTests : IDisposable _addSecretsManagerSubscriptionCommand = Substitute.For(); _referenceEventService = Substitute.For(); _subscriberService = Substitute.For(); + _removeOrganizationUserCommand = Substitute.For(); _sut = new OrganizationsController( _organizationRepository, @@ -91,36 +93,6 @@ public class OrganizationsControllerTests : IDisposable _sut?.Dispose(); } - [Theory] - [InlineAutoData(true, false)] - [InlineAutoData(false, true)] - [InlineAutoData(false, false)] - public async Task OrganizationsController_UserCanLeaveOrganizationThatDoesntProvideKeyConnector( - bool keyConnectorEnabled, bool userUsesKeyConnector, Guid orgId, User user) - { - var ssoConfig = new SsoConfig - { - Id = default, - Data = new SsoConfigurationData - { - MemberDecryptionType = keyConnectorEnabled - ? MemberDecryptionType.KeyConnector - : MemberDecryptionType.MasterPassword - }.Serialize(), - Enabled = true, - OrganizationId = orgId, - }; - - user.UsesKeyConnector = userUsesKeyConnector; - - _currentContext.OrganizationUser(orgId).Returns(true); - _ssoConfigRepository.GetByOrganizationIdAsync(orgId).Returns(ssoConfig); - _userService.GetUserByPrincipalAsync(Arg.Any()).Returns(user); - - await _organizationService.RemoveUserAsync(orgId, user.Id); - await _organizationService.Received(1).RemoveUserAsync(orgId, user.Id); - } - [Theory, AutoData] public async Task OrganizationsController_PostUpgrade_UserCannotEditSubscription_ThrowsNotFoundException( Guid organizationId, 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/Api.Test/Vault/AuthorizationHandlers/BulkCollectionAuthorizationHandlerTests.cs b/test/Api.Test/Vault/AuthorizationHandlers/BulkCollectionAuthorizationHandlerTests.cs index b94162be8..3336c0f4d 100644 --- a/test/Api.Test/Vault/AuthorizationHandlers/BulkCollectionAuthorizationHandlerTests.cs +++ b/test/Api.Test/Vault/AuthorizationHandlers/BulkCollectionAuthorizationHandlerTests.cs @@ -1,5 +1,6 @@ using System.Security.Claims; using Bit.Api.Vault.AuthorizationHandlers.Collections; +using Bit.Core; using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Enums; @@ -32,7 +33,10 @@ public class BulkCollectionAuthorizationHandlerTests organization.Type = userType; organization.Permissions = new Permissions(); - ArrangeOrganizationAbility(sutProvider, organization, true); + // `LimitCollectonCreationDeletionSplit` feature flag state isn't + // relevant for this test. The flag is never checked for in this + // test. This is asserted below. + ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureEnabled(sutProvider, organization, true, true); var context = new AuthorizationHandlerContext( new[] { BulkCollectionOperations.Create }, @@ -44,11 +48,12 @@ public class BulkCollectionAuthorizationHandlerTests await sutProvider.Sut.HandleAsync(context); + sutProvider.GetDependency().DidNotReceiveWithAnyArgs().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit); Assert.True(context.HasSucceeded); } [Theory, BitAutoData, CollectionCustomization] - public async Task CanCreateAsync_WhenUser_WithLimitCollectionCreationDeletionFalse_Success( + public async Task CanCreateAsync_WhenUser_WithLimitCollectionCreationFalse_WithLimitCollectionCreationDeletionSplitFeatureDisabled_Success( SutProvider sutProvider, ICollection collections, CurrentContextOrganization organization) @@ -57,7 +62,7 @@ public class BulkCollectionAuthorizationHandlerTests organization.Type = OrganizationUserType.User; - ArrangeOrganizationAbility(sutProvider, organization, false); + ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureDisabled(sutProvider, organization, false, false); var context = new AuthorizationHandlerContext( new[] { BulkCollectionOperations.Create }, @@ -66,16 +71,49 @@ public class BulkCollectionAuthorizationHandlerTests sutProvider.GetDependency().UserId.Returns(actingUserId); sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit) + .Returns(false); await sutProvider.Sut.HandleAsync(context); + sutProvider.GetDependency().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit); + Assert.True(context.HasSucceeded); + } + + [Theory, BitAutoData, CollectionCustomization] + public async Task CanCreateAsync_WhenUser_WithLimitCollectionCreationFalse_WithLimitCollectionCreationDeletionSplitFeatureEnabled_Success( + SutProvider sutProvider, + ICollection collections, + CurrentContextOrganization organization) + { + var actingUserId = Guid.NewGuid(); + + organization.Type = OrganizationUserType.User; + + ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureEnabled(sutProvider, organization, false, false); + + var context = new AuthorizationHandlerContext( + new[] { BulkCollectionOperations.Create }, + new ClaimsPrincipal(), + collections); + + sutProvider.GetDependency().UserId.Returns(actingUserId); + sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit) + .Returns(true); + + await sutProvider.Sut.HandleAsync(context); + + sutProvider.GetDependency().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit); Assert.True(context.HasSucceeded); } [Theory, CollectionCustomization] [BitAutoData(OrganizationUserType.User)] [BitAutoData(OrganizationUserType.Custom)] - public async Task CanCreateAsync_WhenMissingPermissions_NoSuccess( + public async Task CanCreateAsync_WhenMissingPermissions_WithLimitCollectionCreationDeletionSplitFeatureDisabled_NoSuccess( OrganizationUserType userType, SutProvider sutProvider, ICollection collections, @@ -92,7 +130,7 @@ public class BulkCollectionAuthorizationHandlerTests ManageUsers = false }; - ArrangeOrganizationAbility(sutProvider, organization, true); + ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureDisabled(sutProvider, organization, true, true); var context = new AuthorizationHandlerContext( new[] { BulkCollectionOperations.Create }, @@ -102,21 +140,61 @@ public class BulkCollectionAuthorizationHandlerTests sutProvider.GetDependency().UserId.Returns(actingUserId); sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); sutProvider.GetDependency().ProviderUserForOrgAsync(Arg.Any()).Returns(false); + sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(false); await sutProvider.Sut.HandleAsync(context); + sutProvider.GetDependency().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit); + Assert.False(context.HasSucceeded); + } + + [Theory, CollectionCustomization] + [BitAutoData(OrganizationUserType.User)] + [BitAutoData(OrganizationUserType.Custom)] + public async Task CanCreateAsync_WhenMissingPermissions_WithLimitCollectionCreationDeletionSplitFeatureEnabled_NoSuccess( + OrganizationUserType userType, + SutProvider sutProvider, + ICollection collections, + CurrentContextOrganization organization) + { + var actingUserId = Guid.NewGuid(); + + organization.Type = userType; + organization.Permissions = new Permissions + { + EditAnyCollection = false, + DeleteAnyCollection = false, + ManageGroups = false, + ManageUsers = false + }; + + ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureEnabled(sutProvider, organization, true, true); + + var context = new AuthorizationHandlerContext( + new[] { BulkCollectionOperations.Create }, + new ClaimsPrincipal(), + collections); + + sutProvider.GetDependency().UserId.Returns(actingUserId); + sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); + sutProvider.GetDependency().ProviderUserForOrgAsync(Arg.Any()).Returns(false); + sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(true); + + await sutProvider.Sut.HandleAsync(context); + + sutProvider.GetDependency().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit); Assert.False(context.HasSucceeded); } [Theory, BitAutoData, CollectionCustomization] - public async Task CanCreateAsync_WhenMissingOrgAccess_NoSuccess( + public async Task CanCreateAsync_WhenMissingOrgAccess_WithLimitCollectionCreationDeletionSplitDisabled_NoSuccess( Guid userId, CurrentContextOrganization organization, List collections, SutProvider sutProvider) { collections.ForEach(c => c.OrganizationId = organization.Id); - ArrangeOrganizationAbility(sutProvider, organization, true); + ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureDisabled(sutProvider, organization, true, true); var context = new AuthorizationHandlerContext( new[] { BulkCollectionOperations.Create }, @@ -127,8 +205,38 @@ public class BulkCollectionAuthorizationHandlerTests sutProvider.GetDependency().UserId.Returns(userId); sutProvider.GetDependency().GetOrganization(Arg.Any()).Returns((CurrentContextOrganization)null); sutProvider.GetDependency().ProviderUserForOrgAsync(Arg.Any()).Returns(false); + sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(false); await sutProvider.Sut.HandleAsync(context); + + sutProvider.GetDependency().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit); + Assert.False(context.HasSucceeded); + } + + [Theory, BitAutoData, CollectionCustomization] + public async Task CanCreateAsync_WhenMissingOrgAccess_WithLimitCollectionCreationDeletionSplitEnabled_NoSuccess( + Guid userId, + CurrentContextOrganization organization, + List collections, + SutProvider sutProvider) + { + collections.ForEach(c => c.OrganizationId = organization.Id); + ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureEnabled(sutProvider, organization, true, true); + + var context = new AuthorizationHandlerContext( + new[] { BulkCollectionOperations.Create }, + new ClaimsPrincipal(), + collections + ); + + sutProvider.GetDependency().UserId.Returns(userId); + sutProvider.GetDependency().GetOrganization(Arg.Any()).Returns((CurrentContextOrganization)null); + sutProvider.GetDependency().ProviderUserForOrgAsync(Arg.Any()).Returns(false); + sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(true); + + await sutProvider.Sut.HandleAsync(context); + + sutProvider.GetDependency().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit); Assert.False(context.HasSucceeded); } @@ -904,7 +1012,10 @@ public class BulkCollectionAuthorizationHandlerTests DeleteAnyCollection = true }; - ArrangeOrganizationAbility(sutProvider, organization, true); + // `LimitCollectonCreationDeletionSplit` feature flag state isn't + // relevant for this test. The flag is never checked for in this + // test. This is asserted below. + ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureEnabled(sutProvider, organization, true, true); var context = new AuthorizationHandlerContext( new[] { BulkCollectionOperations.Delete }, @@ -916,6 +1027,7 @@ public class BulkCollectionAuthorizationHandlerTests await sutProvider.Sut.HandleAsync(context); + sutProvider.GetDependency().DidNotReceiveWithAnyArgs().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit); Assert.True(context.HasSucceeded); } @@ -931,7 +1043,10 @@ public class BulkCollectionAuthorizationHandlerTests organization.Type = userType; organization.Permissions = new Permissions(); - ArrangeOrganizationAbility(sutProvider, organization, true); + // `LimitCollectonCreationDeletionSplit` feature flag state isn't + // relevant for this test. The flag is never checked for in this + // test. This is asserted below. + ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureEnabled(sutProvider, organization, true, true); var context = new AuthorizationHandlerContext( new[] { BulkCollectionOperations.Delete }, @@ -943,11 +1058,12 @@ public class BulkCollectionAuthorizationHandlerTests await sutProvider.Sut.HandleAsync(context); + sutProvider.GetDependency().DidNotReceiveWithAnyArgs().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit); Assert.True(context.HasSucceeded); } [Theory, BitAutoData, CollectionCustomization] - public async Task CanDeleteAsync_WhenUser_LimitCollectionCreationDeletionFalse_WithCanManagePermission_Success( + public async Task CanDeleteAsync_WhenUser_LimitCollectionDeletionFalse_WithCanManagePermission_WithLimitCollectionCreationDeletionSplitFeatureDisabled_Success( SutProvider sutProvider, ICollection collections, CurrentContextOrganization organization) @@ -957,11 +1073,12 @@ public class BulkCollectionAuthorizationHandlerTests organization.Type = OrganizationUserType.User; organization.Permissions = new Permissions(); - ArrangeOrganizationAbility(sutProvider, organization, false); + ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureDisabled(sutProvider, organization, false, false); sutProvider.GetDependency().UserId.Returns(actingUserId); sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); sutProvider.GetDependency().GetManyByUserIdAsync(actingUserId).Returns(collections); + sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(false); foreach (var c in collections) { @@ -975,6 +1092,41 @@ public class BulkCollectionAuthorizationHandlerTests await sutProvider.Sut.HandleAsync(context); + sutProvider.GetDependency().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit); + Assert.True(context.HasSucceeded); + } + + [Theory, BitAutoData, CollectionCustomization] + public async Task CanDeleteAsync_WhenUser_LimitCollectionDeletionFalse_WithCanManagePermission_WithLimitCollectionCreationDeletionSplitFeatureEnabled_Success( + SutProvider sutProvider, + ICollection collections, + CurrentContextOrganization organization) + { + var actingUserId = Guid.NewGuid(); + + organization.Type = OrganizationUserType.User; + organization.Permissions = new Permissions(); + + ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureEnabled(sutProvider, organization, false, false); + + sutProvider.GetDependency().UserId.Returns(actingUserId); + sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); + sutProvider.GetDependency().GetManyByUserIdAsync(actingUserId).Returns(collections); + sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(true); + + foreach (var c in collections) + { + c.Manage = true; + } + + var context = new AuthorizationHandlerContext( + new[] { BulkCollectionOperations.Delete }, + new ClaimsPrincipal(), + collections); + + await sutProvider.Sut.HandleAsync(context); + + sutProvider.GetDependency().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit); Assert.True(context.HasSucceeded); } @@ -982,7 +1134,7 @@ public class BulkCollectionAuthorizationHandlerTests [BitAutoData(OrganizationUserType.Admin)] [BitAutoData(OrganizationUserType.Owner)] [BitAutoData(OrganizationUserType.User)] - public async Task CanDeleteAsync_LimitCollectionCreationDeletionFalse_AllowAdminAccessToAllCollectionItemsFalse_WithCanManagePermission_Success( + public async Task CanDeleteAsync_LimitCollectionDeletionFalse_AllowAdminAccessToAllCollectionItemsFalse_WithCanManagePermission_WithLimitCollectionCreationDeletionSplitFeatureDisabled_Success( OrganizationUserType userType, SutProvider sutProvider, ICollection collections, @@ -993,11 +1145,12 @@ public class BulkCollectionAuthorizationHandlerTests organization.Type = userType; organization.Permissions = new Permissions(); - ArrangeOrganizationAbility(sutProvider, organization, false, false); + ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureDisabled(sutProvider, organization, false, false, false); sutProvider.GetDependency().UserId.Returns(actingUserId); sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); sutProvider.GetDependency().GetManyByUserIdAsync(actingUserId).Returns(collections); + sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(false); foreach (var c in collections) { @@ -1011,13 +1164,15 @@ public class BulkCollectionAuthorizationHandlerTests await sutProvider.Sut.HandleAsync(context); + sutProvider.GetDependency().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit); Assert.True(context.HasSucceeded); } [Theory, CollectionCustomization] [BitAutoData(OrganizationUserType.Admin)] [BitAutoData(OrganizationUserType.Owner)] - public async Task CanDeleteAsync_WhenAdminOrOwner_LimitCollectionCreationDeletionTrue_AllowAdminAccessToAllCollectionItemsFalse_WithCanManagePermission_Success( + [BitAutoData(OrganizationUserType.User)] + public async Task CanDeleteAsync_LimitCollectionDeletionFalse_AllowAdminAccessToAllCollectionItemsFalse_WithCanManagePermission_WithLimitCollectionCreationDeletionSplitFeatureEnabled_Success( OrganizationUserType userType, SutProvider sutProvider, ICollection collections, @@ -1028,11 +1183,12 @@ public class BulkCollectionAuthorizationHandlerTests organization.Type = userType; organization.Permissions = new Permissions(); - ArrangeOrganizationAbility(sutProvider, organization, true, false); + ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureEnabled(sutProvider, organization, false, false, false); sutProvider.GetDependency().UserId.Returns(actingUserId); sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); sutProvider.GetDependency().GetManyByUserIdAsync(actingUserId).Returns(collections); + sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(true); foreach (var c in collections) { @@ -1046,13 +1202,14 @@ public class BulkCollectionAuthorizationHandlerTests await sutProvider.Sut.HandleAsync(context); + sutProvider.GetDependency().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit); Assert.True(context.HasSucceeded); } [Theory, CollectionCustomization] [BitAutoData(OrganizationUserType.Admin)] [BitAutoData(OrganizationUserType.Owner)] - public async Task CanDeleteAsync_WhenAdminOrOwner_LimitCollectionCreationDeletionTrue_AllowAdminAccessToAllCollectionItemsFalse_WithoutCanManagePermission_Failure( + public async Task CanDeleteAsync_WhenAdminOrOwner_LimitCollectionDeletionTrue_AllowAdminAccessToAllCollectionItemsFalse_WithCanManagePermission_WithLimitCollectionCreationDeletionSplitFeatureDisabled_Success( OrganizationUserType userType, SutProvider sutProvider, ICollection collections, @@ -1063,12 +1220,87 @@ public class BulkCollectionAuthorizationHandlerTests organization.Type = userType; organization.Permissions = new Permissions(); - ArrangeOrganizationAbility(sutProvider, organization, true, false); + ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureDisabled(sutProvider, organization, true, true, false); + + sutProvider.GetDependency().UserId.Returns(actingUserId); + sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); + sutProvider.GetDependency().GetManyByUserIdAsync(actingUserId).Returns(collections); + sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(false); + + foreach (var c in collections) + { + c.Manage = true; + } + + var context = new AuthorizationHandlerContext( + new[] { BulkCollectionOperations.Delete }, + new ClaimsPrincipal(), + collections); + + await sutProvider.Sut.HandleAsync(context); + + sutProvider.GetDependency().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit); + Assert.True(context.HasSucceeded); + } + + [Theory, CollectionCustomization] + [BitAutoData(OrganizationUserType.Admin)] + [BitAutoData(OrganizationUserType.Owner)] + public async Task CanDeleteAsync_WhenAdminOrOwner_LimitCollectionDeletionTrue_AllowAdminAccessToAllCollectionItemsFalse_WithCanManagePermission_WithLimitCollectionCreationDeletionSplitFeatureEnabled_Success( + OrganizationUserType userType, + SutProvider sutProvider, + ICollection collections, + CurrentContextOrganization organization) + { + var actingUserId = Guid.NewGuid(); + + organization.Type = userType; + organization.Permissions = new Permissions(); + + ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureEnabled(sutProvider, organization, true, true, false); + + sutProvider.GetDependency().UserId.Returns(actingUserId); + sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); + sutProvider.GetDependency().GetManyByUserIdAsync(actingUserId).Returns(collections); + sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(true); + + foreach (var c in collections) + { + c.Manage = true; + } + + var context = new AuthorizationHandlerContext( + new[] { BulkCollectionOperations.Delete }, + new ClaimsPrincipal(), + collections); + + await sutProvider.Sut.HandleAsync(context); + + sutProvider.GetDependency().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit); + Assert.True(context.HasSucceeded); + } + + [Theory, CollectionCustomization] + [BitAutoData(OrganizationUserType.Admin)] + [BitAutoData(OrganizationUserType.Owner)] + public async Task CanDeleteAsync_WhenAdminOrOwner_LimitCollectionDeletionTrue_AllowAdminAccessToAllCollectionItemsFalse_WithoutCanManagePermission_WithLimitCollectionCreationDeletionSplitFeatureDisabled_Failure( + OrganizationUserType userType, + SutProvider sutProvider, + ICollection collections, + CurrentContextOrganization organization) + { + var actingUserId = Guid.NewGuid(); + + organization.Type = userType; + organization.Permissions = new Permissions(); + + ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureDisabled(sutProvider, organization, true, true, false); sutProvider.GetDependency().UserId.Returns(actingUserId); sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); sutProvider.GetDependency().GetManyByUserIdAsync(actingUserId).Returns(collections); sutProvider.GetDependency().ProviderUserForOrgAsync(Arg.Any()).Returns(false); + sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(false); foreach (var c in collections) { @@ -1082,11 +1314,50 @@ public class BulkCollectionAuthorizationHandlerTests await sutProvider.Sut.HandleAsync(context); + sutProvider.GetDependency().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit); + Assert.False(context.HasSucceeded); + } + + [Theory, CollectionCustomization] + [BitAutoData(OrganizationUserType.Admin)] + [BitAutoData(OrganizationUserType.Owner)] + public async Task CanDeleteAsync_WhenAdminOrOwner_LimitCollectionDeletionTrue_AllowAdminAccessToAllCollectionItemsFalse_WithoutCanManagePermission_WithLimitCollectionCreationDeletionSplitFeatureEnabled_Failure( + OrganizationUserType userType, + SutProvider sutProvider, + ICollection collections, + CurrentContextOrganization organization) + { + var actingUserId = Guid.NewGuid(); + + organization.Type = userType; + organization.Permissions = new Permissions(); + + ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureEnabled(sutProvider, organization, true, true, false); + + sutProvider.GetDependency().UserId.Returns(actingUserId); + sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); + sutProvider.GetDependency().GetManyByUserIdAsync(actingUserId).Returns(collections); + sutProvider.GetDependency().ProviderUserForOrgAsync(Arg.Any()).Returns(false); + sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(true); + + foreach (var c in collections) + { + c.Manage = false; + } + + var context = new AuthorizationHandlerContext( + new[] { BulkCollectionOperations.Delete }, + new ClaimsPrincipal(), + collections); + + await sutProvider.Sut.HandleAsync(context); + + sutProvider.GetDependency().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit); Assert.False(context.HasSucceeded); } [Theory, BitAutoData, CollectionCustomization] - public async Task CanDeleteAsync_WhenUser_LimitCollectionCreationDeletionTrue_AllowAdminAccessToAllCollectionItemsTrue_Failure( + public async Task CanDeleteAsync_WhenUser_LimitCollectionDeletionTrue_AllowAdminAccessToAllCollectionItemsTrue_WithLimitCollectionCreationDeletionSplitFeatureDisabled_Failure( SutProvider sutProvider, ICollection collections, CurrentContextOrganization organization) @@ -1096,12 +1367,13 @@ public class BulkCollectionAuthorizationHandlerTests organization.Type = OrganizationUserType.User; organization.Permissions = new Permissions(); - ArrangeOrganizationAbility(sutProvider, organization, true); + ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureDisabled(sutProvider, organization, true, true); sutProvider.GetDependency().UserId.Returns(actingUserId); sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); sutProvider.GetDependency().GetManyByUserIdAsync(actingUserId).Returns(collections); sutProvider.GetDependency().ProviderUserForOrgAsync(Arg.Any()).Returns(false); + sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(false); foreach (var c in collections) { @@ -1115,11 +1387,12 @@ public class BulkCollectionAuthorizationHandlerTests await sutProvider.Sut.HandleAsync(context); + sutProvider.GetDependency().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit); Assert.False(context.HasSucceeded); } [Theory, BitAutoData, CollectionCustomization] - public async Task CanDeleteAsync_WhenUser_LimitCollectionCreationDeletionTrue_AllowAdminAccessToAllCollectionItemsFalse_Failure( + public async Task CanDeleteAsync_WhenUser_LimitCollectionDeletionTrue_AllowAdminAccessToAllCollectionItemsTrue_WithLimitCollectionCreationDeletionSplitFeatureEnabled_Failure( SutProvider sutProvider, ICollection collections, CurrentContextOrganization organization) @@ -1129,12 +1402,13 @@ public class BulkCollectionAuthorizationHandlerTests organization.Type = OrganizationUserType.User; organization.Permissions = new Permissions(); - ArrangeOrganizationAbility(sutProvider, organization, true, false); + ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureEnabled(sutProvider, organization, true, true); sutProvider.GetDependency().UserId.Returns(actingUserId); sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); sutProvider.GetDependency().GetManyByUserIdAsync(actingUserId).Returns(collections); sutProvider.GetDependency().ProviderUserForOrgAsync(Arg.Any()).Returns(false); + sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(true); foreach (var c in collections) { @@ -1148,13 +1422,88 @@ public class BulkCollectionAuthorizationHandlerTests await sutProvider.Sut.HandleAsync(context); + sutProvider.GetDependency().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit); + Assert.False(context.HasSucceeded); + } + + [Theory, BitAutoData, CollectionCustomization] + public async Task CanDeleteAsync_WhenUser_LimitCollectionDeletionTrue_AllowAdminAccessToAllCollectionItemsFalse_WithLimitCollectionCreationDeletionSplitFeatureDisabled_Failure( + SutProvider sutProvider, + ICollection collections, + CurrentContextOrganization organization) + { + var actingUserId = Guid.NewGuid(); + + organization.Type = OrganizationUserType.User; + organization.Permissions = new Permissions(); + + ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureDisabled(sutProvider, organization, true, true, false); + + sutProvider.GetDependency().UserId.Returns(actingUserId); + sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); + sutProvider.GetDependency().GetManyByUserIdAsync(actingUserId).Returns(collections); + sutProvider.GetDependency().ProviderUserForOrgAsync(Arg.Any()).Returns(false); + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit) + .Returns(false); + + foreach (var c in collections) + { + c.Manage = true; + } + + var context = new AuthorizationHandlerContext( + new[] { BulkCollectionOperations.Delete }, + new ClaimsPrincipal(), + collections); + + await sutProvider.Sut.HandleAsync(context); + + sutProvider.GetDependency().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit); + Assert.False(context.HasSucceeded); + } + + [Theory, BitAutoData, CollectionCustomization] + public async Task CanDeleteAsync_WhenUser_LimitCollectionDeletionTrue_AllowAdminAccessToAllCollectionItemsFalse_WithLimitCollectionCreationDeletionSplitFeatureEnabled_Failure( + SutProvider sutProvider, + ICollection collections, + CurrentContextOrganization organization) + { + var actingUserId = Guid.NewGuid(); + + organization.Type = OrganizationUserType.User; + organization.Permissions = new Permissions(); + + ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureEnabled(sutProvider, organization, true, true, false); + + sutProvider.GetDependency().UserId.Returns(actingUserId); + sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); + sutProvider.GetDependency().GetManyByUserIdAsync(actingUserId).Returns(collections); + sutProvider.GetDependency().ProviderUserForOrgAsync(Arg.Any()).Returns(false); + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit) + .Returns(true); + + foreach (var c in collections) + { + c.Manage = true; + } + + var context = new AuthorizationHandlerContext( + new[] { BulkCollectionOperations.Delete }, + new ClaimsPrincipal(), + collections); + + await sutProvider.Sut.HandleAsync(context); + + sutProvider.GetDependency().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit); Assert.False(context.HasSucceeded); } [Theory, CollectionCustomization] [BitAutoData(OrganizationUserType.User)] [BitAutoData(OrganizationUserType.Custom)] - public async Task CanDeleteAsync_WhenMissingPermissions_NoSuccess( + public async Task CanDeleteAsync_WhenMissingPermissions_WithLimitCollectionCreationDeletionSplitFeatureDisabled_NoSuccess( OrganizationUserType userType, SutProvider sutProvider, ICollection collections, @@ -1171,7 +1520,7 @@ public class BulkCollectionAuthorizationHandlerTests ManageUsers = false }; - ArrangeOrganizationAbility(sutProvider, organization, true); + ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureDisabled(sutProvider, organization, true, true); var context = new AuthorizationHandlerContext( new[] { BulkCollectionOperations.Delete }, @@ -1181,14 +1530,54 @@ public class BulkCollectionAuthorizationHandlerTests sutProvider.GetDependency().UserId.Returns(actingUserId); sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); sutProvider.GetDependency().ProviderUserForOrgAsync(Arg.Any()).Returns(false); + sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(false); await sutProvider.Sut.HandleAsync(context); + sutProvider.GetDependency().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit); + Assert.False(context.HasSucceeded); + } + + [Theory, CollectionCustomization] + [BitAutoData(OrganizationUserType.User)] + [BitAutoData(OrganizationUserType.Custom)] + public async Task CanDeleteAsync_WhenMissingPermissions_WithLimitCollectionCreationDeletionSplitFeatureEnabled_NoSuccess( + OrganizationUserType userType, + SutProvider sutProvider, + ICollection collections, + CurrentContextOrganization organization) + { + var actingUserId = Guid.NewGuid(); + + organization.Type = userType; + organization.Permissions = new Permissions + { + EditAnyCollection = false, + DeleteAnyCollection = false, + ManageGroups = false, + ManageUsers = false + }; + + ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureEnabled(sutProvider, organization, true, true); + + var context = new AuthorizationHandlerContext( + new[] { BulkCollectionOperations.Delete }, + new ClaimsPrincipal(), + collections); + + sutProvider.GetDependency().UserId.Returns(actingUserId); + sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); + sutProvider.GetDependency().ProviderUserForOrgAsync(Arg.Any()).Returns(false); + sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(true); + + await sutProvider.Sut.HandleAsync(context); + + sutProvider.GetDependency().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit); Assert.False(context.HasSucceeded); } [Theory, BitAutoData, CollectionCustomization] - public async Task CanDeleteAsync_WhenMissingOrgAccess_NoSuccess( + public async Task CanDeleteAsync_WhenMissingOrgAccess_WithLimitCollectionCreationDeletionSplitFeatureDisabled_NoSuccess( Guid userId, ICollection collections, SutProvider sutProvider) @@ -1202,8 +1591,34 @@ public class BulkCollectionAuthorizationHandlerTests sutProvider.GetDependency().UserId.Returns(userId); sutProvider.GetDependency().GetOrganization(Arg.Any()).Returns((CurrentContextOrganization)null); sutProvider.GetDependency().ProviderUserForOrgAsync(Arg.Any()).Returns(false); + sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(false); await sutProvider.Sut.HandleAsync(context); + + sutProvider.GetDependency().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit); + Assert.False(context.HasSucceeded); + } + + [Theory, BitAutoData, CollectionCustomization] + public async Task CanDeleteAsync_WhenMissingOrgAccess_WithLimitCollectionCreationDeletionSplitFeatureEnabled_NoSuccess( + Guid userId, + ICollection collections, + SutProvider sutProvider) + { + var context = new AuthorizationHandlerContext( + new[] { BulkCollectionOperations.Delete }, + new ClaimsPrincipal(), + collections + ); + + sutProvider.GetDependency().UserId.Returns(userId); + sutProvider.GetDependency().GetOrganization(Arg.Any()).Returns((CurrentContextOrganization)null); + sutProvider.GetDependency().ProviderUserForOrgAsync(Arg.Any()).Returns(false); + sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(true); + + await sutProvider.Sut.HandleAsync(context); + + sutProvider.GetDependency().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit); Assert.False(context.HasSucceeded); } @@ -1224,6 +1639,7 @@ public class BulkCollectionAuthorizationHandlerTests await sutProvider.Sut.HandleAsync(context); Assert.True(context.HasFailed); sutProvider.GetDependency().DidNotReceiveWithAnyArgs(); + sutProvider.GetDependency().DidNotReceiveWithAnyArgs().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit); } [Theory, BitAutoData, CollectionCustomization] @@ -1247,10 +1663,11 @@ public class BulkCollectionAuthorizationHandlerTests var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.HandleAsync(context)); Assert.Equal("Requested collections must belong to the same organization.", exception.Message); sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetOrganization(default); + sutProvider.GetDependency().DidNotReceiveWithAnyArgs().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit); } [Theory, BitAutoData, CollectionCustomization] - public async Task HandleRequirementAsync_Provider_Success( + public async Task HandleRequirementAsync_Provider_WithLimitCollectionCreationDeletionSplitFeatureDisabled_Success( SutProvider sutProvider, ICollection collections) { @@ -1286,6 +1703,63 @@ public class BulkCollectionAuthorizationHandlerTests sutProvider.GetDependency().GetOrganizationAbilitiesAsync() .Returns(organizationAbilities); sutProvider.GetDependency().ProviderUserForOrgAsync(Arg.Any()).Returns(true); + sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(false); + + var context = new AuthorizationHandlerContext( + new[] { op }, + new ClaimsPrincipal(), + collections + ); + + await sutProvider.Sut.HandleAsync(context); + + Assert.True(context.HasSucceeded); + await sutProvider.GetDependency().Received().ProviderUserForOrgAsync(orgId); + + // Recreate the SUT to reset the mocks/dependencies between tests + sutProvider.Recreate(); + } + } + + [Theory, BitAutoData, CollectionCustomization] + public async Task HandleRequirementAsync_Provider_WithLimitCollectionCreationDeletionSplitFeatureEnabled_Success( + SutProvider sutProvider, + ICollection collections) + { + var actingUserId = Guid.NewGuid(); + var orgId = collections.First().OrganizationId; + + var organizationAbilities = new Dictionary + { + { collections.First().OrganizationId, + new OrganizationAbility + { + LimitCollectionCreation = true, + LimitCollectionDeletion = true, + AllowAdminAccessToAllCollectionItems = true + } + } + }; + + var operationsToTest = new[] + { + BulkCollectionOperations.Create, + BulkCollectionOperations.Read, + BulkCollectionOperations.ReadAccess, + BulkCollectionOperations.Update, + BulkCollectionOperations.ModifyUserAccess, + BulkCollectionOperations.ModifyGroupAccess, + BulkCollectionOperations.Delete, + }; + + foreach (var op in operationsToTest) + { + sutProvider.GetDependency().UserId.Returns(actingUserId); + sutProvider.GetDependency().GetOrganization(orgId).Returns((CurrentContextOrganization)null); + sutProvider.GetDependency().GetOrganizationAbilitiesAsync() + .Returns(organizationAbilities); + sutProvider.GetDependency().ProviderUserForOrgAsync(Arg.Any()).Returns(true); + sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(true); var context = new AuthorizationHandlerContext( new[] { op }, @@ -1336,14 +1810,37 @@ public class BulkCollectionAuthorizationHandlerTests await sutProvider.GetDependency().Received(1).GetManyByUserIdAsync(Arg.Any()); } - private static void ArrangeOrganizationAbility( + private static void ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureDisabled( SutProvider sutProvider, - CurrentContextOrganization organization, bool limitCollectionCreationDeletion, + CurrentContextOrganization organization, + bool limitCollectionCreation, + bool limitCollectionDeletion, bool allowAdminAccessToAllCollectionItems = true) { var organizationAbility = new OrganizationAbility(); organizationAbility.Id = organization.Id; - organizationAbility.LimitCollectionCreationDeletion = limitCollectionCreationDeletion; + + organizationAbility.LimitCollectionCreationDeletion = limitCollectionCreation || limitCollectionDeletion; + + organizationAbility.AllowAdminAccessToAllCollectionItems = allowAdminAccessToAllCollectionItems; + + sutProvider.GetDependency().GetOrganizationAbilityAsync(organizationAbility.Id) + .Returns(organizationAbility); + } + + private static void ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureEnabled( + SutProvider sutProvider, + CurrentContextOrganization organization, + bool limitCollectionCreation, + bool limitCollectionDeletion, + bool allowAdminAccessToAllCollectionItems = true) + { + var organizationAbility = new OrganizationAbility(); + organizationAbility.Id = organization.Id; + + organizationAbility.LimitCollectionCreation = limitCollectionCreation; + organizationAbility.LimitCollectionDeletion = limitCollectionDeletion; + organizationAbility.AllowAdminAccessToAllCollectionItems = allowAdminAccessToAllCollectionItems; sutProvider.GetDependency().GetOrganizationAbilityAsync(organizationAbility.Id) diff --git a/test/Common/AutoFixture/ControllerCustomization.cs b/test/Common/AutoFixture/ControllerCustomization.cs index f695f86b5..91fffbf09 100644 --- a/test/Common/AutoFixture/ControllerCustomization.cs +++ b/test/Common/AutoFixture/ControllerCustomization.cs @@ -1,6 +1,5 @@ using AutoFixture; using Microsoft.AspNetCore.Mvc; -using Org.BouncyCastle.Security; namespace Bit.Test.Common.AutoFixture; @@ -15,7 +14,7 @@ public class ControllerCustomization : ICustomization { if (!controllerType.IsAssignableTo(typeof(Controller))) { - throw new InvalidParameterException($"{nameof(controllerType)} must derive from {typeof(Controller).Name}"); + throw new Exception($"{nameof(controllerType)} must derive from {typeof(Controller).Name}"); } _controllerType = controllerType; diff --git a/test/Common/AutoFixture/SutProvider.cs b/test/Common/AutoFixture/SutProvider.cs index ac953965b..fefe6c3eb 100644 --- a/test/Common/AutoFixture/SutProvider.cs +++ b/test/Common/AutoFixture/SutProvider.cs @@ -127,7 +127,6 @@ public class SutProvider : ISutProvider return _sutProvider.GetDependency(parameterInfo.ParameterType, ""); } - // This is the equivalent of _fixture.Create, but no overload for // Create(Type type) exists. var dependency = new SpecimenContext(_fixture).Resolve(new SeededRequest(parameterInfo.ParameterType, diff --git a/test/Common/AutoFixture/SutProviderExtensions.cs b/test/Common/AutoFixture/SutProviderExtensions.cs index 1fdf22653..bdc860416 100644 --- a/test/Common/AutoFixture/SutProviderExtensions.cs +++ b/test/Common/AutoFixture/SutProviderExtensions.cs @@ -1,6 +1,7 @@ using AutoFixture; using Bit.Core.Services; using Bit.Core.Settings; +using Microsoft.Extensions.Time.Testing; using NSubstitute; using RichardSzalay.MockHttp; @@ -47,4 +48,19 @@ public static class SutProviderExtensions .SetDependency(mockHttpClientFactory) .Create(); } + + /// + /// Configures SutProvider to use FakeTimeProvider. + /// It is registered under both the TimeProvider type and the FakeTimeProvider type + /// so that it can be retrieved in a type-safe manner with GetDependency. + /// This can be chained with other builder methods; make sure to call + /// before use. + /// + public static SutProvider WithFakeTimeProvider(this SutProvider sutProvider) + { + var fakeTimeProvider = new FakeTimeProvider(); + return sutProvider + .SetDependency((TimeProvider)fakeTimeProvider) + .SetDependency(fakeTimeProvider); + } } diff --git a/test/Common/Common.csproj b/test/Common/Common.csproj index 1893487d2..2f11798ce 100644 --- a/test/Common/Common.csproj +++ b/test/Common/Common.csproj @@ -5,6 +5,7 @@ + @@ -14,7 +15,7 @@ - + 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/AutoFixture/PolicyFixtures.cs b/test/Core.Test/AdminConsole/AutoFixture/PolicyFixtures.cs index f70fd579e..09b112c43 100644 --- a/test/Core.Test/AdminConsole/AutoFixture/PolicyFixtures.cs +++ b/test/Core.Test/AdminConsole/AutoFixture/PolicyFixtures.cs @@ -9,10 +9,12 @@ namespace Bit.Core.Test.AdminConsole.AutoFixture; internal class PolicyCustomization : ICustomization { public PolicyType Type { get; set; } + public bool Enabled { get; set; } - public PolicyCustomization(PolicyType type) + public PolicyCustomization(PolicyType type, bool enabled) { Type = type; + Enabled = enabled; } public void Customize(IFixture fixture) @@ -20,21 +22,23 @@ internal class PolicyCustomization : ICustomization fixture.Customize(composer => composer .With(o => o.OrganizationId, Guid.NewGuid()) .With(o => o.Type, Type) - .With(o => o.Enabled, true)); + .With(o => o.Enabled, Enabled)); } } public class PolicyAttribute : CustomizeAttribute { private readonly PolicyType _type; + private readonly bool _enabled; - public PolicyAttribute(PolicyType type) + public PolicyAttribute(PolicyType type, bool enabled = true) { _type = type; + _enabled = enabled; } public override ICustomization GetCustomization(ParameterInfo parameter) { - return new PolicyCustomization(_type); + return new PolicyCustomization(_type, _enabled); } } diff --git a/test/Core.Test/AdminConsole/AutoFixture/PolicyUpdateFixtures.cs b/test/Core.Test/AdminConsole/AutoFixture/PolicyUpdateFixtures.cs new file mode 100644 index 000000000..dff9b5717 --- /dev/null +++ b/test/Core.Test/AdminConsole/AutoFixture/PolicyUpdateFixtures.cs @@ -0,0 +1,25 @@ +using System.Reflection; +using AutoFixture; +using AutoFixture.Xunit2; +using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models; + +namespace Bit.Core.Test.AdminConsole.AutoFixture; + +internal class PolicyUpdateCustomization(PolicyType type, bool enabled) : ICustomization +{ + public void Customize(IFixture fixture) + { + fixture.Customize(composer => composer + .With(o => o.Type, type) + .With(o => o.Enabled, enabled)); + } +} + +public class PolicyUpdateAttribute(PolicyType type, bool enabled = true) : CustomizeAttribute +{ + public override ICustomization GetCustomization(ParameterInfo parameter) + { + return new PolicyUpdateCustomization(type, enabled); + } +} 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 416d86c5d..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 VerifyOrganizationDomain_ShouldThrowConflict_WhenDomainHasBeenClaimed(Guid id, + public async Task UserVerifyOrganizationDomainAsync_ShouldThrowConflict_WhenDomainHasBeenClaimed(Guid id, SutProvider sutProvider) { var expected = new OrganizationDomain @@ -30,14 +33,14 @@ public class VerifyOrganizationDomainCommandTests .GetByIdAsync(id) .Returns(expected); - var requestAction = async () => await sutProvider.Sut.VerifyOrganizationDomainAsync(expected); + var requestAction = async () => await sutProvider.Sut.UserVerifyOrganizationDomainAsync(expected); var exception = await Assert.ThrowsAsync(requestAction); Assert.Contains("Domain has already been verified.", exception.Message); } [Theory, BitAutoData] - public async Task VerifyOrganizationDomain_ShouldThrowConflict_WhenDomainHasBeenClaimedByAnotherOrganization(Guid id, + public async Task UserVerifyOrganizationDomainAsync_ShouldThrowConflict_WhenDomainHasBeenClaimedByAnotherOrganization(Guid id, SutProvider sutProvider) { var expected = new OrganizationDomain @@ -54,14 +57,14 @@ public class VerifyOrganizationDomainCommandTests .GetClaimedDomainsByDomainNameAsync(expected.DomainName) .Returns(new List { expected }); - var requestAction = async () => await sutProvider.Sut.VerifyOrganizationDomainAsync(expected); + var requestAction = async () => await sutProvider.Sut.UserVerifyOrganizationDomainAsync(expected); var exception = await Assert.ThrowsAsync(requestAction); Assert.Contains("The domain is not available to be claimed.", exception.Message); } [Theory, BitAutoData] - public async Task VerifyOrganizationDomain_ShouldVerifyDomainUpdateAndLogEvent_WhenTxtRecordExists(Guid id, + public async Task UserVerifyOrganizationDomainAsync_ShouldVerifyDomainUpdateAndLogEvent_WhenTxtRecordExists(Guid id, SutProvider sutProvider) { var expected = new OrganizationDomain @@ -81,7 +84,7 @@ public class VerifyOrganizationDomainCommandTests .ResolveAsync(expected.DomainName, Arg.Any()) .Returns(true); - var result = await sutProvider.Sut.VerifyOrganizationDomainAsync(expected); + var result = await sutProvider.Sut.UserVerifyOrganizationDomainAsync(expected); Assert.NotNull(result.VerifiedDate); await sutProvider.GetDependency().Received(1) @@ -91,7 +94,7 @@ public class VerifyOrganizationDomainCommandTests } [Theory, BitAutoData] - public async Task VerifyOrganizationDomain_ShouldNotSetVerifiedDate_WhenTxtRecordDoesNotExist(Guid id, + public async Task UserVerifyOrganizationDomainAsync_ShouldNotSetVerifiedDate_WhenTxtRecordDoesNotExist(Guid id, SutProvider sutProvider) { var expected = new OrganizationDomain @@ -111,10 +114,123 @@ public class VerifyOrganizationDomainCommandTests .ResolveAsync(expected.DomainName, Arg.Any()) .Returns(false); - var result = await sutProvider.Sut.VerifyOrganizationDomainAsync(expected); + var result = await sutProvider.Sut.UserVerifyOrganizationDomainAsync(expected); Assert.Null(result.VerifiedDate); await sutProvider.GetDependency().Received(1) .LogOrganizationDomainEventAsync(Arg.Any(), EventType.OrganizationDomain_NotVerified); } + + + [Theory, BitAutoData] + public async Task SystemVerifyOrganizationDomainAsync_CallsEventServiceWithUpdatedJobRunCount(SutProvider sutProvider) + { + var domain = new OrganizationDomain() + { + Id = Guid.NewGuid(), + OrganizationId = Guid.NewGuid(), + CreationDate = DateTime.UtcNow, + DomainName = "test.com", + Txt = "btw+12345", + }; + + _ = await sutProvider.Sut.SystemVerifyOrganizationDomainAsync(domain); + + await sutProvider.GetDependency().ReceivedWithAnyArgs(1) + .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/OrganizationFeatures/OrganizationUsers/DeleteManagedOrganizationUserAccountCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteManagedOrganizationUserAccountCommandTests.cs index 585c5fc8d..81e83d745 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteManagedOrganizationUserAccountCommandTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteManagedOrganizationUserAccountCommandTests.cs @@ -40,7 +40,7 @@ public class DeleteManagedOrganizationUserAccountCommandTests Arg.Is>(ids => ids.Contains(organizationUser.Id))) .Returns(new Dictionary { { organizationUser.Id, true } }); - sutProvider.GetDependency() + sutProvider.GetDependency() .HasConfirmedOwnersExceptAsync( organizationUser.OrganizationId, Arg.Is>(ids => ids.Contains(organizationUser.Id)), @@ -184,7 +184,7 @@ public class DeleteManagedOrganizationUserAccountCommandTests .OrganizationOwner(organizationUser.OrganizationId) .Returns(true); - sutProvider.GetDependency() + sutProvider.GetDependency() .HasConfirmedOwnersExceptAsync( organizationUser.OrganizationId, Arg.Is>(ids => ids.Contains(organizationUser.Id)), @@ -399,8 +399,8 @@ public class DeleteManagedOrganizationUserAccountCommandTests .OrganizationOwner(orgUser.OrganizationId) .Returns(true); - sutProvider.GetDependency() - .HasConfirmedOwnersExceptAsync(orgUser.OrganizationId, Arg.Any>(), true) + sutProvider.GetDependency() + .HasConfirmedOwnersExceptAsync(orgUser.OrganizationId, Arg.Any>(), Arg.Any()) .Returns(false); // Act diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/HasConfirmedOwnersExceptQueryTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/HasConfirmedOwnersExceptQueryTests.cs new file mode 100644 index 000000000..77e99a8e2 --- /dev/null +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/HasConfirmedOwnersExceptQueryTests.cs @@ -0,0 +1,139 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Entities.Provider; +using Bit.Core.AdminConsole.Enums.Provider; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers; +using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Repositories; +using Bit.Core.Test.AutoFixture.OrganizationUserFixtures; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers; + +[SutProviderCustomize] +public class HasConfirmedOwnersExceptQueryTests +{ + [Theory, BitAutoData] + public async Task HasConfirmedOwnersExcept_WithConfirmedOwner_WithNoException_ReturnsTrue( + Organization organization, + [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner, + SutProvider sutProvider) + { + sutProvider.GetDependency() + .GetManyByOrganizationAsync(organization.Id, OrganizationUserType.Owner) + .Returns(new List { owner }); + + var result = await sutProvider.Sut.HasConfirmedOwnersExceptAsync(organization.Id, new List(), true); + + Assert.True(result); + } + + [Theory, BitAutoData] + public async Task HasConfirmedOwnersExcept_ExcludingConfirmedOwner_ReturnsFalse( + Organization organization, + [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner, + SutProvider sutProvider) + { + sutProvider.GetDependency() + .GetManyByOrganizationAsync(organization.Id, OrganizationUserType.Owner) + .Returns(new List { owner }); + + var result = await sutProvider.Sut.HasConfirmedOwnersExceptAsync(organization.Id, new List { owner.Id }, true); + + Assert.False(result); + } + + [Theory, BitAutoData] + public async Task HasConfirmedOwnersExcept_WithInvitedOwner_ReturnsFalse( + Organization organization, + [OrganizationUser(OrganizationUserStatusType.Invited, OrganizationUserType.Owner)] OrganizationUser owner, + SutProvider sutProvider) + { + sutProvider.GetDependency() + .GetManyByOrganizationAsync(organization.Id, OrganizationUserType.Owner) + .Returns(new List { owner }); + + var result = await sutProvider.Sut.HasConfirmedOwnersExceptAsync(organization.Id, new List(), true); + + Assert.False(result); + } + + [Theory] + [BitAutoData(true)] + [BitAutoData(false)] + public async Task HasConfirmedOwnersExcept_WithConfirmedProviderUser_IncludeProviderTrue_ReturnsTrue( + bool includeProvider, + Organization organization, + ProviderUser providerUser, + SutProvider sutProvider) + { + providerUser.Status = ProviderUserStatusType.Confirmed; + + sutProvider.GetDependency() + .GetManyByOrganizationAsync(organization.Id, ProviderUserStatusType.Confirmed) + .Returns(new List { providerUser }); + + var result = await sutProvider.Sut.HasConfirmedOwnersExceptAsync(organization.Id, new List(), includeProvider); + + Assert.Equal(includeProvider, result); + } + + [Theory, BitAutoData] + public async Task HasConfirmedOwnersExceptAsync_WithConfirmedOwners_ReturnsTrue( + Guid organizationId, + IEnumerable organizationUsersId, + ICollection owners, + SutProvider sutProvider) + { + sutProvider.GetDependency() + .GetManyByOrganizationAsync(organizationId, OrganizationUserType.Owner) + .Returns(owners); + + var result = await sutProvider.Sut.HasConfirmedOwnersExceptAsync(organizationId, organizationUsersId); + + Assert.True(result); + } + + [Theory, BitAutoData] + public async Task HasConfirmedOwnersExceptAsync_WithConfirmedProviders_ReturnsTrue( + Guid organizationId, + IEnumerable organizationUsersId, + ICollection providerUsers, + SutProvider sutProvider) + { + sutProvider.GetDependency() + .GetManyByOrganizationAsync(organizationId, OrganizationUserType.Owner) + .Returns(new List()); + + sutProvider.GetDependency() + .GetManyByOrganizationAsync(organizationId, ProviderUserStatusType.Confirmed) + .Returns(providerUsers); + + var result = await sutProvider.Sut.HasConfirmedOwnersExceptAsync(organizationId, organizationUsersId); + + Assert.True(result); + } + + [Theory, BitAutoData] + public async Task HasConfirmedOwnersExceptAsync_WithNoConfirmedOwnersOrProviders_ReturnsFalse( + Guid organizationId, + IEnumerable organizationUsersId, + SutProvider sutProvider) + { + sutProvider.GetDependency() + .GetManyByOrganizationAsync(organizationId, OrganizationUserType.Owner) + .Returns(new List()); + + sutProvider.GetDependency() + .GetManyByOrganizationAsync(organizationId, ProviderUserStatusType.Confirmed) + .Returns(new List()); + + var result = await sutProvider.Sut.HasConfirmedOwnersExceptAsync(organizationId, organizationUsersId); + + Assert.False(result); + } +} diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/RemoveOrganizationUserCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/RemoveOrganizationUserCommandTests.cs index 8d25f13cb..2d10ce626 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/RemoveOrganizationUserCommandTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/RemoveOrganizationUserCommandTests.cs @@ -1,9 +1,12 @@ using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; +using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Repositories; using Bit.Core.Services; +using Bit.Core.Test.AutoFixture.OrganizationUserFixtures; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; @@ -14,33 +17,38 @@ namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers; [SutProviderCustomize] public class RemoveOrganizationUserCommandTests { - [Theory] - [BitAutoData] - public async Task RemoveUser_Success(SutProvider sutProvider, Guid organizationId, Guid organizationUserId) + [Theory, BitAutoData] + public async Task RemoveUser_Success( + [OrganizationUser(type: OrganizationUserType.User)] OrganizationUser organizationUser, + [OrganizationUser(type: OrganizationUserType.Owner)] OrganizationUser deletingUser, + SutProvider sutProvider) { - sutProvider.GetDependency() - .GetByIdAsync(organizationUserId) - .Returns(new OrganizationUser - { - Id = organizationUserId, - OrganizationId = organizationId - }); + var organizationUserRepository = sutProvider.GetDependency(); + var currentContext = sutProvider.GetDependency(); - await sutProvider.Sut.RemoveUserAsync(organizationId, organizationUserId, null); + organizationUser.OrganizationId = deletingUser.OrganizationId; + organizationUserRepository.GetByIdAsync(organizationUser.Id).Returns(organizationUser); + organizationUserRepository.GetByIdAsync(deletingUser.Id).Returns(deletingUser); + currentContext.OrganizationOwner(deletingUser.OrganizationId).Returns(true); - await sutProvider.GetDependency().Received(1).RemoveUserAsync(organizationId, organizationUserId, null); + await sutProvider.Sut.RemoveUserAsync(deletingUser.OrganizationId, organizationUser.Id, deletingUser.UserId); + + await organizationUserRepository.Received(1).DeleteAsync(organizationUser); + await sutProvider.GetDependency().Received(1).LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Removed); } [Theory] [BitAutoData] - public async Task RemoveUser_NotFound_Throws(SutProvider sutProvider, Guid organizationId, Guid organizationUserId) + public async Task RemoveUser_NotFound_ThrowsException(SutProvider sutProvider, + Guid organizationId, Guid organizationUserId) { await Assert.ThrowsAsync(async () => await sutProvider.Sut.RemoveUserAsync(organizationId, organizationUserId, null)); } [Theory] [BitAutoData] - public async Task RemoveUser_MismatchingOrganizationId_Throws(SutProvider sutProvider, Guid organizationId, Guid organizationUserId) + public async Task RemoveUser_MismatchingOrganizationId_ThrowsException( + SutProvider sutProvider, Guid organizationId, Guid organizationUserId) { sutProvider.GetDependency() .GetByIdAsync(organizationUserId) @@ -53,20 +61,249 @@ public class RemoveOrganizationUserCommandTests await Assert.ThrowsAsync(async () => await sutProvider.Sut.RemoveUserAsync(organizationId, organizationUserId, null)); } - [Theory] - [BitAutoData] - public async Task RemoveUser_WithEventSystemUser_Success(SutProvider sutProvider, Guid organizationId, Guid organizationUserId, EventSystemUser eventSystemUser) + [Theory, BitAutoData] + public async Task RemoveUser_InvalidUser_ThrowsException( + OrganizationUser organizationUser, OrganizationUser deletingUser, + SutProvider sutProvider) { - sutProvider.GetDependency() - .GetByIdAsync(organizationUserId) - .Returns(new OrganizationUser - { - Id = organizationUserId, - OrganizationId = organizationId - }); + var organizationUserRepository = sutProvider.GetDependency(); - await sutProvider.Sut.RemoveUserAsync(organizationId, organizationUserId, eventSystemUser); + organizationUserRepository.GetByIdAsync(organizationUser.Id).Returns(organizationUser); - await sutProvider.GetDependency().Received(1).RemoveUserAsync(organizationId, organizationUserId, eventSystemUser); + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.RemoveUserAsync(Guid.NewGuid(), organizationUser.Id, deletingUser.UserId)); + Assert.Contains("User not found.", exception.Message); + } + + [Theory, BitAutoData] + public async Task RemoveUser_RemoveYourself_ThrowsException(OrganizationUser deletingUser, SutProvider sutProvider) + { + var organizationUserRepository = sutProvider.GetDependency(); + + organizationUserRepository.GetByIdAsync(deletingUser.Id).Returns(deletingUser); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.RemoveUserAsync(deletingUser.OrganizationId, deletingUser.Id, deletingUser.UserId)); + Assert.Contains("You cannot remove yourself.", exception.Message); + } + + [Theory, BitAutoData] + public async Task RemoveUser_NonOwnerRemoveOwner_ThrowsException( + [OrganizationUser(type: OrganizationUserType.Owner)] OrganizationUser organizationUser, + [OrganizationUser(type: OrganizationUserType.Admin)] OrganizationUser deletingUser, + SutProvider sutProvider) + { + var organizationUserRepository = sutProvider.GetDependency(); + var currentContext = sutProvider.GetDependency(); + + organizationUser.OrganizationId = deletingUser.OrganizationId; + organizationUserRepository.GetByIdAsync(organizationUser.Id).Returns(organizationUser); + currentContext.OrganizationAdmin(deletingUser.OrganizationId).Returns(true); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.RemoveUserAsync(deletingUser.OrganizationId, organizationUser.Id, deletingUser.UserId)); + Assert.Contains("Only owners can delete other owners.", exception.Message); + } + + [Theory, BitAutoData] + public async Task RemoveUser_RemovingLastOwner_ThrowsException( + [OrganizationUser(type: OrganizationUserType.Owner)] OrganizationUser organizationUser, + OrganizationUser deletingUser, + SutProvider sutProvider) + { + var organizationUserRepository = sutProvider.GetDependency(); + var hasConfirmedOwnersExceptQuery = sutProvider.GetDependency(); + + organizationUser.OrganizationId = deletingUser.OrganizationId; + organizationUserRepository.GetByIdAsync(organizationUser.Id).Returns(organizationUser); + hasConfirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync( + deletingUser.OrganizationId, + Arg.Is>(i => i.Contains(organizationUser.Id)), Arg.Any()) + .Returns(false); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.RemoveUserAsync(deletingUser.OrganizationId, organizationUser.Id, null)); + Assert.Contains("Organization must have at least one confirmed owner.", exception.Message); + hasConfirmedOwnersExceptQuery + .Received(1) + .HasConfirmedOwnersExceptAsync( + organizationUser.OrganizationId, + Arg.Is>(i => i.Contains(organizationUser.Id)), true); + } + + [Theory, BitAutoData] + public async Task RemoveUser_WithEventSystemUser_Success( + [OrganizationUser(type: OrganizationUserType.User)] OrganizationUser organizationUser, + EventSystemUser eventSystemUser, + SutProvider sutProvider) + { + var organizationUserRepository = sutProvider.GetDependency(); + + organizationUserRepository.GetByIdAsync(organizationUser.Id).Returns(organizationUser); + + await sutProvider.Sut.RemoveUserAsync(organizationUser.OrganizationId, organizationUser.Id, eventSystemUser); + + await sutProvider.GetDependency().Received(1).LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Removed, eventSystemUser); + } + + [Theory, BitAutoData] + public async Task RemoveUser_ByUserId_Success( + [OrganizationUser(type: OrganizationUserType.User)] OrganizationUser organizationUser, + SutProvider sutProvider) + { + var organizationUserRepository = sutProvider.GetDependency(); + + organizationUserRepository + .GetByOrganizationAsync(organizationUser.OrganizationId, organizationUser.UserId!.Value) + .Returns(organizationUser); + + sutProvider.GetDependency() + .HasConfirmedOwnersExceptAsync( + organizationUser.OrganizationId, + Arg.Is>(i => i.Contains(organizationUser.Id)), + Arg.Any()) + .Returns(true); + + await sutProvider.Sut.RemoveUserAsync(organizationUser.OrganizationId, organizationUser.UserId.Value); + + await sutProvider.GetDependency().Received(1).LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Removed); + } + + [Theory, BitAutoData] + public async Task RemoveUser_ByUserId_NotFound_ThrowsException(SutProvider sutProvider, + Guid organizationId, Guid userId) + { + await Assert.ThrowsAsync(async () => await sutProvider.Sut.RemoveUserAsync(organizationId, userId)); + } + + [Theory, BitAutoData] + public async Task RemoveUser_ByUserId_InvalidUser_ThrowsException(OrganizationUser organizationUser, + SutProvider sutProvider) + { + var organizationUserRepository = sutProvider.GetDependency(); + + organizationUserRepository.GetByOrganizationAsync(organizationUser.OrganizationId, organizationUser.UserId!.Value).Returns(organizationUser); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.RemoveUserAsync(Guid.NewGuid(), organizationUser.UserId.Value)); + Assert.Contains("User not found.", exception.Message); + } + + [Theory, BitAutoData] + public async Task RemoveUser_ByUserId_RemovingLastOwner_ThrowsException( + [OrganizationUser(type: OrganizationUserType.Owner)] OrganizationUser organizationUser, + SutProvider sutProvider) + { + var organizationUserRepository = sutProvider.GetDependency(); + var hasConfirmedOwnersExceptQuery = sutProvider.GetDependency(); + + organizationUserRepository.GetByOrganizationAsync(organizationUser.OrganizationId, organizationUser.UserId!.Value).Returns(organizationUser); + hasConfirmedOwnersExceptQuery + .HasConfirmedOwnersExceptAsync( + organizationUser.OrganizationId, + Arg.Is>(i => i.Contains(organizationUser.Id)), + Arg.Any()) + .Returns(false); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.RemoveUserAsync(organizationUser.OrganizationId, organizationUser.UserId.Value)); + Assert.Contains("Organization must have at least one confirmed owner.", exception.Message); + hasConfirmedOwnersExceptQuery + .Received(1) + .HasConfirmedOwnersExceptAsync( + organizationUser.OrganizationId, + Arg.Is>(i => i.Contains(organizationUser.Id)), + Arg.Any()); + } + + [Theory, BitAutoData] + public async Task RemoveUsers_FilterInvalid_ThrowsException(OrganizationUser organizationUser, OrganizationUser deletingUser, + SutProvider sutProvider) + { + var organizationUserRepository = sutProvider.GetDependency(); + var organizationUsers = new[] { organizationUser }; + var organizationUserIds = organizationUsers.Select(u => u.Id); + organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(organizationUsers); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.RemoveUsersAsync(deletingUser.OrganizationId, organizationUserIds, deletingUser.UserId)); + Assert.Contains("Users invalid.", exception.Message); + } + + [Theory, BitAutoData] + public async Task RemoveUsers_RemoveYourself_ThrowsException( + OrganizationUser deletingUser, + SutProvider sutProvider) + { + var organizationUserRepository = sutProvider.GetDependency(); + var organizationUsers = new[] { deletingUser }; + var organizationUserIds = organizationUsers.Select(u => u.Id); + organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(organizationUsers); + sutProvider.GetDependency() + .HasConfirmedOwnersExceptAsync(deletingUser.OrganizationId, Arg.Any>()) + .Returns(true); + + var result = await sutProvider.Sut.RemoveUsersAsync(deletingUser.OrganizationId, organizationUserIds, deletingUser.UserId); + Assert.Contains("You cannot remove yourself.", result[0].Item2); + } + + [Theory, BitAutoData] + public async Task RemoveUsers_NonOwnerRemoveOwner_ThrowsException( + [OrganizationUser(type: OrganizationUserType.Admin)] OrganizationUser deletingUser, + [OrganizationUser(type: OrganizationUserType.Owner)] OrganizationUser orgUser1, + [OrganizationUser(OrganizationUserStatusType.Confirmed)] OrganizationUser orgUser2, + SutProvider sutProvider) + { + var organizationUserRepository = sutProvider.GetDependency(); + + orgUser1.OrganizationId = orgUser2.OrganizationId = deletingUser.OrganizationId; + var organizationUsers = new[] { orgUser1 }; + var organizationUserIds = organizationUsers.Select(u => u.Id); + organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(organizationUsers); + sutProvider.GetDependency() + .HasConfirmedOwnersExceptAsync(deletingUser.OrganizationId, Arg.Any>()) + .Returns(true); + + var result = await sutProvider.Sut.RemoveUsersAsync(deletingUser.OrganizationId, organizationUserIds, deletingUser.UserId); + Assert.Contains("Only owners can delete other owners.", result[0].Item2); + } + + [Theory, BitAutoData] + public async Task RemoveUsers_LastOwner_ThrowsException( + [OrganizationUser(status: OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser orgUser, + SutProvider sutProvider) + { + var organizationUserRepository = sutProvider.GetDependency(); + + var organizationUsers = new[] { orgUser }; + var organizationUserIds = organizationUsers.Select(u => u.Id); + organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(organizationUsers); + organizationUserRepository.GetManyByOrganizationAsync(orgUser.OrganizationId, OrganizationUserType.Owner).Returns(organizationUsers); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.RemoveUsersAsync(orgUser.OrganizationId, organizationUserIds, null)); + Assert.Contains("Organization must have at least one confirmed owner.", exception.Message); + } + + [Theory, BitAutoData] + public async Task RemoveUsers_Success( + [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser deletingUser, + [OrganizationUser(type: OrganizationUserType.Owner)] OrganizationUser orgUser1, OrganizationUser orgUser2, + SutProvider sutProvider) + { + var organizationUserRepository = sutProvider.GetDependency(); + var currentContext = sutProvider.GetDependency(); + + orgUser1.OrganizationId = orgUser2.OrganizationId = deletingUser.OrganizationId; + var organizationUsers = new[] { orgUser1, orgUser2 }; + var organizationUserIds = organizationUsers.Select(u => u.Id); + organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(organizationUsers); + organizationUserRepository.GetByIdAsync(deletingUser.Id).Returns(deletingUser); + sutProvider.GetDependency() + .HasConfirmedOwnersExceptAsync(deletingUser.OrganizationId, Arg.Any>()) + .Returns(true); + currentContext.OrganizationOwner(deletingUser.OrganizationId).Returns(true); + + await sutProvider.Sut.RemoveUsersAsync(deletingUser.OrganizationId, organizationUserIds, deletingUser.UserId); } } diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/UpdateOrganizationUserCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/UpdateOrganizationUserCommandTests.cs index a7db63fb5..73bf00474 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/UpdateOrganizationUserCommandTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/UpdateOrganizationUserCommandTests.cs @@ -1,6 +1,7 @@ using System.Text.Json; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Entities; using Bit.Core.Enums; @@ -169,7 +170,7 @@ public class UpdateOrganizationUserCommandTests await organizationService.Received(1).ValidateOrganizationCustomPermissionsEnabledAsync( newUserData.OrganizationId, newUserData.Type); - await organizationService.Received(1).HasConfirmedOwnersExceptAsync( + await sutProvider.GetDependency().Received(1).HasConfirmedOwnersExceptAsync( newUserData.OrganizationId, Arg.Is>(i => i.Contains(newUserData.Id))); } @@ -187,7 +188,7 @@ public class UpdateOrganizationUserCommandTests newUser.UserId = oldUser.UserId; newUser.OrganizationId = oldUser.OrganizationId = organization.Id; organizationUserRepository.GetByIdAsync(oldUser.Id).Returns(oldUser); - organizationService + sutProvider.GetDependency() .HasConfirmedOwnersExceptAsync( oldUser.OrganizationId, Arg.Is>(i => i.Contains(oldUser.Id))) diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidatorFixtures.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidatorFixtures.cs new file mode 100644 index 000000000..ba4741d8b --- /dev/null +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidatorFixtures.cs @@ -0,0 +1,43 @@ +#nullable enable + +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models; +using NSubstitute; + +namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies; + +public class FakeSingleOrgPolicyValidator : IPolicyValidator +{ + public PolicyType Type => PolicyType.SingleOrg; + public IEnumerable RequiredPolicies => Array.Empty(); + + public readonly Func> ValidateAsyncMock = Substitute.For>>(); + public readonly Action OnSaveSideEffectsAsyncMock = Substitute.For>(); + + public Task ValidateAsync(PolicyUpdate policyUpdate, Policy? currentPolicy) + { + return ValidateAsyncMock(policyUpdate, currentPolicy); + } + + public Task OnSaveSideEffectsAsync(PolicyUpdate policyUpdate, Policy? currentPolicy) + { + OnSaveSideEffectsAsyncMock(policyUpdate, currentPolicy); + return Task.FromResult(0); + } +} +public class FakeRequireSsoPolicyValidator : IPolicyValidator +{ + public PolicyType Type => PolicyType.RequireSso; + public IEnumerable RequiredPolicies => [PolicyType.SingleOrg]; + public Task ValidateAsync(PolicyUpdate policyUpdate, Policy? currentPolicy) => Task.FromResult(""); + public Task OnSaveSideEffectsAsync(PolicyUpdate policyUpdate, Policy? currentPolicy) => Task.FromResult(0); +} +public class FakeVaultTimeoutPolicyValidator : IPolicyValidator +{ + public PolicyType Type => PolicyType.MaximumVaultTimeout; + public IEnumerable RequiredPolicies => [PolicyType.SingleOrg]; + public Task ValidateAsync(PolicyUpdate policyUpdate, Policy? currentPolicy) => Task.FromResult(""); + public Task OnSaveSideEffectsAsync(PolicyUpdate policyUpdate, Policy? currentPolicy) => Task.FromResult(0); +} diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidatorHelpersTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidatorHelpersTests.cs new file mode 100644 index 000000000..99f99706f --- /dev/null +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidatorHelpersTests.cs @@ -0,0 +1,64 @@ +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators; +using Bit.Core.Auth.Entities; +using Bit.Core.Auth.Enums; +using Bit.Core.Auth.Models.Data; +using Xunit; + +namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies; + +public class PolicyValidatorHelpersTests +{ + [Fact] + public void ValidateDecryptionOptionsNotEnabled_RequiredByKeyConnector_ValidationError() + { + var ssoConfig = new SsoConfig(); + ssoConfig.SetData(new SsoConfigurationData { MemberDecryptionType = MemberDecryptionType.KeyConnector }); + + var result = ssoConfig.ValidateDecryptionOptionsNotEnabled([MemberDecryptionType.KeyConnector]); + + Assert.Contains("Key Connector is enabled", result); + } + + [Fact] + public void ValidateDecryptionOptionsNotEnabled_RequiredByTDE_ValidationError() + { + var ssoConfig = new SsoConfig(); + ssoConfig.SetData(new SsoConfigurationData { MemberDecryptionType = MemberDecryptionType.TrustedDeviceEncryption }); + + var result = ssoConfig.ValidateDecryptionOptionsNotEnabled([MemberDecryptionType.TrustedDeviceEncryption]); + + Assert.Contains("Trusted device encryption is on", result); + } + + [Fact] + public void ValidateDecryptionOptionsNotEnabled_NullSsoConfig_NoValidationError() + { + var ssoConfig = new SsoConfig(); + var result = ssoConfig.ValidateDecryptionOptionsNotEnabled([MemberDecryptionType.KeyConnector]); + + Assert.True(string.IsNullOrEmpty(result)); + } + + [Fact] + public void ValidateDecryptionOptionsNotEnabled_RequiredOptionNotEnabled_NoValidationError() + { + var ssoConfig = new SsoConfig(); + ssoConfig.SetData(new SsoConfigurationData { MemberDecryptionType = MemberDecryptionType.KeyConnector }); + + var result = ssoConfig.ValidateDecryptionOptionsNotEnabled([MemberDecryptionType.TrustedDeviceEncryption]); + + Assert.True(string.IsNullOrEmpty(result)); + } + + [Fact] + public void ValidateDecryptionOptionsNotEnabled_SsoConfigDisabled_NoValidationError() + { + var ssoConfig = new SsoConfig(); + ssoConfig.Enabled = false; + ssoConfig.SetData(new SsoConfigurationData { MemberDecryptionType = MemberDecryptionType.KeyConnector }); + + var result = ssoConfig.ValidateDecryptionOptionsNotEnabled([MemberDecryptionType.KeyConnector]); + + Assert.True(string.IsNullOrEmpty(result)); + } +} diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/RequireSsoPolicyValidatorTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/RequireSsoPolicyValidatorTests.cs new file mode 100644 index 000000000..d3af765f7 --- /dev/null +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/RequireSsoPolicyValidatorTests.cs @@ -0,0 +1,75 @@ +namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.PolicyValidators; + +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators; +using Bit.Core.Auth.Entities; +using Bit.Core.Auth.Enums; +using Bit.Core.Auth.Models.Data; +using Bit.Core.Auth.Repositories; +using Bit.Core.Test.AdminConsole.AutoFixture; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; + +[SutProviderCustomize] +public class RequireSsoPolicyValidatorTests +{ + [Theory, BitAutoData] + public async Task ValidateAsync_DisablingPolicy_KeyConnectorEnabled_ValidationError( + [PolicyUpdate(PolicyType.SingleOrg, false)] PolicyUpdate policyUpdate, + [Policy(PolicyType.SingleOrg)] Policy policy, + SutProvider sutProvider) + { + policy.OrganizationId = policyUpdate.OrganizationId; + + var ssoConfig = new SsoConfig { Enabled = true }; + ssoConfig.SetData(new SsoConfigurationData { MemberDecryptionType = MemberDecryptionType.KeyConnector }); + + sutProvider.GetDependency() + .GetByOrganizationIdAsync(policyUpdate.OrganizationId) + .Returns(ssoConfig); + + var result = await sutProvider.Sut.ValidateAsync(policyUpdate, policy); + Assert.Contains("Key Connector is enabled", result, StringComparison.OrdinalIgnoreCase); + } + + [Theory, BitAutoData] + public async Task ValidateAsync_DisablingPolicy_TdeEnabled_ValidationError( + [PolicyUpdate(PolicyType.SingleOrg, false)] PolicyUpdate policyUpdate, + [Policy(PolicyType.SingleOrg)] Policy policy, + SutProvider sutProvider) + { + policy.OrganizationId = policyUpdate.OrganizationId; + + var ssoConfig = new SsoConfig { Enabled = true }; + ssoConfig.SetData(new SsoConfigurationData { MemberDecryptionType = MemberDecryptionType.TrustedDeviceEncryption }); + + sutProvider.GetDependency() + .GetByOrganizationIdAsync(policyUpdate.OrganizationId) + .Returns(ssoConfig); + + var result = await sutProvider.Sut.ValidateAsync(policyUpdate, policy); + Assert.Contains("Trusted device encryption is on", result, StringComparison.OrdinalIgnoreCase); + } + + [Theory, BitAutoData] + public async Task ValidateAsync_DisablingPolicy_DecryptionOptionsNotEnabled_Success( + [PolicyUpdate(PolicyType.ResetPassword, false)] PolicyUpdate policyUpdate, + [Policy(PolicyType.ResetPassword)] Policy policy, + SutProvider sutProvider) + { + policy.OrganizationId = policyUpdate.OrganizationId; + + var ssoConfig = new SsoConfig { Enabled = false }; + + sutProvider.GetDependency() + .GetByOrganizationIdAsync(policyUpdate.OrganizationId) + .Returns(ssoConfig); + + var result = await sutProvider.Sut.ValidateAsync(policyUpdate, policy); + Assert.True(string.IsNullOrEmpty(result)); + } +} diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/ResetPasswordPolicyValidatorTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/ResetPasswordPolicyValidatorTests.cs new file mode 100644 index 000000000..83939406b --- /dev/null +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/ResetPasswordPolicyValidatorTests.cs @@ -0,0 +1,71 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators; +using Bit.Core.Auth.Entities; +using Bit.Core.Auth.Enums; +using Bit.Core.Auth.Models.Data; +using Bit.Core.Auth.Repositories; +using Bit.Core.Test.AdminConsole.AutoFixture; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.PolicyValidators; + +[SutProviderCustomize] +public class ResetPasswordPolicyValidatorTests +{ + [Theory] + [BitAutoData(true, false)] + [BitAutoData(false, true)] + [BitAutoData(false, false)] + public async Task ValidateAsync_DisablingPolicy_TdeEnabled_ValidationError( + bool policyEnabled, + bool autoEnrollEnabled, + [PolicyUpdate(PolicyType.ResetPassword)] PolicyUpdate policyUpdate, + [Policy(PolicyType.ResetPassword)] Policy policy, + SutProvider sutProvider) + { + policyUpdate.Enabled = policyEnabled; + policyUpdate.SetDataModel(new ResetPasswordDataModel + { + AutoEnrollEnabled = autoEnrollEnabled + }); + policy.OrganizationId = policyUpdate.OrganizationId; + + var ssoConfig = new SsoConfig { Enabled = true }; + ssoConfig.SetData(new SsoConfigurationData { MemberDecryptionType = MemberDecryptionType.TrustedDeviceEncryption }); + + sutProvider.GetDependency() + .GetByOrganizationIdAsync(policyUpdate.OrganizationId) + .Returns(ssoConfig); + + var result = await sutProvider.Sut.ValidateAsync(policyUpdate, policy); + Assert.Contains("Trusted device encryption is on and requires this policy.", result, StringComparison.OrdinalIgnoreCase); + } + + [Theory, BitAutoData] + public async Task ValidateAsync_DisablingPolicy_TdeNotEnabled_Success( + [PolicyUpdate(PolicyType.ResetPassword, false)] PolicyUpdate policyUpdate, + [Policy(PolicyType.ResetPassword)] Policy policy, + SutProvider sutProvider) + { + policyUpdate.SetDataModel(new ResetPasswordDataModel + { + AutoEnrollEnabled = false + }); + policy.OrganizationId = policyUpdate.OrganizationId; + + var ssoConfig = new SsoConfig { Enabled = false }; + + sutProvider.GetDependency() + .GetByOrganizationIdAsync(policyUpdate.OrganizationId) + .Returns(ssoConfig); + + var result = await sutProvider.Sut.ValidateAsync(policyUpdate, policy); + Assert.True(string.IsNullOrEmpty(result)); + } +} diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SingleOrgPolicyValidatorTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SingleOrgPolicyValidatorTests.cs new file mode 100644 index 000000000..76ee57484 --- /dev/null +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SingleOrgPolicyValidatorTests.cs @@ -0,0 +1,129 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators; +using Bit.Core.Auth.Entities; +using Bit.Core.Auth.Enums; +using Bit.Core.Auth.Models.Data; +using Bit.Core.Auth.Repositories; +using Bit.Core.Context; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Models.Data.Organizations.OrganizationUsers; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Bit.Core.Test.AdminConsole.AutoFixture; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.PolicyValidators; + +[SutProviderCustomize] +public class SingleOrgPolicyValidatorTests +{ + [Theory, BitAutoData] + public async Task ValidateAsync_DisablingPolicy_KeyConnectorEnabled_ValidationError( + [PolicyUpdate(PolicyType.SingleOrg, false)] PolicyUpdate policyUpdate, + [Policy(PolicyType.SingleOrg)] Policy policy, + SutProvider sutProvider) + { + policy.OrganizationId = policyUpdate.OrganizationId; + + var ssoConfig = new SsoConfig { Enabled = true }; + ssoConfig.SetData(new SsoConfigurationData { MemberDecryptionType = MemberDecryptionType.KeyConnector }); + + sutProvider.GetDependency() + .GetByOrganizationIdAsync(policyUpdate.OrganizationId) + .Returns(ssoConfig); + + var result = await sutProvider.Sut.ValidateAsync(policyUpdate, policy); + Assert.Contains("Key Connector is enabled", result, StringComparison.OrdinalIgnoreCase); + } + + [Theory, BitAutoData] + public async Task ValidateAsync_DisablingPolicy_KeyConnectorNotEnabled_Success( + [PolicyUpdate(PolicyType.ResetPassword, false)] PolicyUpdate policyUpdate, + [Policy(PolicyType.ResetPassword)] Policy policy, + SutProvider sutProvider) + { + policy.OrganizationId = policyUpdate.OrganizationId; + + var ssoConfig = new SsoConfig { Enabled = false }; + + sutProvider.GetDependency() + .GetByOrganizationIdAsync(policyUpdate.OrganizationId) + .Returns(ssoConfig); + + var result = await sutProvider.Sut.ValidateAsync(policyUpdate, policy); + Assert.True(string.IsNullOrEmpty(result)); + } + + [Theory, BitAutoData] + public async Task OnSaveSideEffectsAsync_RemovesNonCompliantUsers( + [PolicyUpdate(PolicyType.SingleOrg)] PolicyUpdate policyUpdate, + [Policy(PolicyType.SingleOrg, false)] Policy policy, + Guid savingUserId, + Guid nonCompliantUserId, + Organization organization, SutProvider sutProvider) + { + policy.OrganizationId = organization.Id = policyUpdate.OrganizationId; + + var compliantUser1 = new OrganizationUserUserDetails + { + OrganizationId = organization.Id, + Type = OrganizationUserType.User, + Status = OrganizationUserStatusType.Confirmed, + UserId = new Guid(), + Email = "user1@example.com" + }; + + var compliantUser2 = new OrganizationUserUserDetails + { + OrganizationId = organization.Id, + Type = OrganizationUserType.User, + Status = OrganizationUserStatusType.Confirmed, + UserId = new Guid(), + Email = "user2@example.com" + }; + + var nonCompliantUser = new OrganizationUserUserDetails + { + OrganizationId = organization.Id, + Type = OrganizationUserType.User, + Status = OrganizationUserStatusType.Confirmed, + UserId = nonCompliantUserId, + Email = "user3@example.com" + }; + + sutProvider.GetDependency() + .GetManyDetailsByOrganizationAsync(policyUpdate.OrganizationId) + .Returns([compliantUser1, compliantUser2, nonCompliantUser]); + + var otherOrganizationUser = new OrganizationUser + { + OrganizationId = new Guid(), + UserId = nonCompliantUserId, + Status = OrganizationUserStatusType.Confirmed + }; + + sutProvider.GetDependency() + .GetManyByManyUsersAsync(Arg.Is>(ids => ids.Contains(nonCompliantUserId))) + .Returns([otherOrganizationUser]); + + sutProvider.GetDependency().UserId.Returns(savingUserId); + sutProvider.GetDependency().GetByIdAsync(policyUpdate.OrganizationId).Returns(organization); + + await sutProvider.Sut.OnSaveSideEffectsAsync(policyUpdate, policy); + + await sutProvider.GetDependency() + .Received(1) + .RemoveUserAsync(policyUpdate.OrganizationId, nonCompliantUser.Id, savingUserId); + await sutProvider.GetDependency() + .Received(1) + .SendOrganizationUserRemovedForPolicySingleOrgEmailAsync(organization.DisplayName(), + "user3@example.com"); + } +} diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/TwoFactorAuthenticationPolicyValidatorTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/TwoFactorAuthenticationPolicyValidatorTests.cs new file mode 100644 index 000000000..4dce13174 --- /dev/null +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/TwoFactorAuthenticationPolicyValidatorTests.cs @@ -0,0 +1,209 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators; +using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; +using Bit.Core.Context; +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.Models.Data.Organizations.OrganizationUsers; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Bit.Core.Test.AdminConsole.AutoFixture; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.PolicyValidators; + +[SutProviderCustomize] +public class TwoFactorAuthenticationPolicyValidatorTests +{ + [Theory, BitAutoData] + public async Task OnSaveSideEffectsAsync_RemovesNonCompliantUsers( + Organization organization, + [PolicyUpdate(PolicyType.TwoFactorAuthentication)] PolicyUpdate policyUpdate, + [Policy(PolicyType.TwoFactorAuthentication, false)] Policy policy, + SutProvider sutProvider) + { + policy.OrganizationId = organization.Id = policyUpdate.OrganizationId; + sutProvider.GetDependency().GetByIdAsync(organization.Id).Returns(organization); + + var orgUserDetailUserInvited = new OrganizationUserUserDetails + { + Id = Guid.NewGuid(), + Status = OrganizationUserStatusType.Invited, + Type = OrganizationUserType.User, + // Needs to be different from what is passed in as the savingUserId to Sut.SaveAsync + Email = "user1@test.com", + Name = "TEST", + UserId = Guid.NewGuid(), + HasMasterPassword = false + }; + var orgUserDetailUserAcceptedWith2FA = new OrganizationUserUserDetails + { + Id = Guid.NewGuid(), + Status = OrganizationUserStatusType.Accepted, + Type = OrganizationUserType.User, + // Needs to be different from what is passed in as the savingUserId to Sut.SaveAsync + Email = "user2@test.com", + Name = "TEST", + UserId = Guid.NewGuid(), + HasMasterPassword = true + }; + var orgUserDetailUserAcceptedWithout2FA = new OrganizationUserUserDetails + { + Id = Guid.NewGuid(), + Status = OrganizationUserStatusType.Accepted, + Type = OrganizationUserType.User, + // Needs to be different from what is passed in as the savingUserId to Sut.SaveAsync + Email = "user3@test.com", + Name = "TEST", + UserId = Guid.NewGuid(), + HasMasterPassword = true + }; + var orgUserDetailAdmin = new OrganizationUserUserDetails + { + Id = Guid.NewGuid(), + Status = OrganizationUserStatusType.Confirmed, + Type = OrganizationUserType.Admin, + // Needs to be different from what is passed in as the savingUserId to Sut.SaveAsync + Email = "admin@test.com", + Name = "ADMIN", + UserId = Guid.NewGuid(), + HasMasterPassword = false + }; + + sutProvider.GetDependency() + .GetManyDetailsByOrganizationAsync(policyUpdate.OrganizationId) + .Returns(new List + { + orgUserDetailUserInvited, + orgUserDetailUserAcceptedWith2FA, + orgUserDetailUserAcceptedWithout2FA, + orgUserDetailAdmin + }); + + sutProvider.GetDependency() + .TwoFactorIsEnabledAsync(Arg.Any>()) + .Returns(new List<(OrganizationUserUserDetails user, bool hasTwoFactor)>() + { + (orgUserDetailUserInvited, false), + (orgUserDetailUserAcceptedWith2FA, true), + (orgUserDetailUserAcceptedWithout2FA, false), + (orgUserDetailAdmin, false), + }); + + var savingUserId = Guid.NewGuid(); + sutProvider.GetDependency().UserId.Returns(savingUserId); + + await sutProvider.Sut.OnSaveSideEffectsAsync(policyUpdate, policy); + + var removeOrganizationUserCommand = sutProvider.GetDependency(); + + await removeOrganizationUserCommand.Received() + .RemoveUserAsync(policy.OrganizationId, orgUserDetailUserAcceptedWithout2FA.Id, savingUserId); + await sutProvider.GetDependency().Received() + .SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(organization.DisplayName(), orgUserDetailUserAcceptedWithout2FA.Email); + + await removeOrganizationUserCommand.DidNotReceive() + .RemoveUserAsync(policy.OrganizationId, orgUserDetailUserInvited.Id, savingUserId); + await sutProvider.GetDependency().DidNotReceive() + .SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(organization.DisplayName(), orgUserDetailUserInvited.Email); + await removeOrganizationUserCommand.DidNotReceive() + .RemoveUserAsync(policy.OrganizationId, orgUserDetailUserAcceptedWith2FA.Id, savingUserId); + await sutProvider.GetDependency().DidNotReceive() + .SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(organization.DisplayName(), orgUserDetailUserAcceptedWith2FA.Email); + await removeOrganizationUserCommand.DidNotReceive() + .RemoveUserAsync(policy.OrganizationId, orgUserDetailAdmin.Id, savingUserId); + await sutProvider.GetDependency().DidNotReceive() + .SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(organization.DisplayName(), orgUserDetailAdmin.Email); + } + + [Theory, BitAutoData] + public async Task OnSaveSideEffectsAsync_UsersToBeRemovedDontHaveMasterPasswords_Throws( + Organization organization, + [PolicyUpdate(PolicyType.TwoFactorAuthentication)] PolicyUpdate policyUpdate, + [Policy(PolicyType.TwoFactorAuthentication, false)] Policy policy, + SutProvider sutProvider) + { + policy.OrganizationId = organization.Id = policyUpdate.OrganizationId; + + var orgUserDetailUserWith2FAAndMP = new OrganizationUserUserDetails + { + Id = Guid.NewGuid(), + Status = OrganizationUserStatusType.Confirmed, + Type = OrganizationUserType.User, + // Needs to be different from what is passed in as the savingUserId to Sut.SaveAsync + Email = "user1@test.com", + Name = "TEST", + UserId = Guid.NewGuid(), + HasMasterPassword = true + }; + var orgUserDetailUserWith2FANoMP = new OrganizationUserUserDetails + { + Id = Guid.NewGuid(), + Status = OrganizationUserStatusType.Confirmed, + Type = OrganizationUserType.User, + // Needs to be different from what is passed in as the savingUserId to Sut.SaveAsync + Email = "user2@test.com", + Name = "TEST", + UserId = Guid.NewGuid(), + HasMasterPassword = false + }; + var orgUserDetailUserWithout2FA = new OrganizationUserUserDetails + { + Id = Guid.NewGuid(), + Status = OrganizationUserStatusType.Confirmed, + Type = OrganizationUserType.User, + // Needs to be different from what is passed in as the savingUserId to Sut.SaveAsync + Email = "user3@test.com", + Name = "TEST", + UserId = Guid.NewGuid(), + HasMasterPassword = false + }; + var orgUserDetailAdmin = new OrganizationUserUserDetails + { + Id = Guid.NewGuid(), + Status = OrganizationUserStatusType.Confirmed, + Type = OrganizationUserType.Admin, + // Needs to be different from what is passed in as the savingUserId to Sut.SaveAsync + Email = "admin@test.com", + Name = "ADMIN", + UserId = Guid.NewGuid(), + HasMasterPassword = false + }; + + sutProvider.GetDependency() + .GetManyDetailsByOrganizationAsync(policy.OrganizationId) + .Returns(new List + { + orgUserDetailUserWith2FAAndMP, + orgUserDetailUserWith2FANoMP, + orgUserDetailUserWithout2FA, + orgUserDetailAdmin + }); + + sutProvider.GetDependency() + .TwoFactorIsEnabledAsync(Arg.Is>(ids => + ids.Contains(orgUserDetailUserWith2FANoMP.UserId.Value) + && ids.Contains(orgUserDetailUserWithout2FA.UserId.Value) + && ids.Contains(orgUserDetailAdmin.UserId.Value))) + .Returns(new List<(Guid userId, bool hasTwoFactor)>() + { + (orgUserDetailUserWith2FANoMP.UserId.Value, true), + (orgUserDetailUserWithout2FA.UserId.Value, false), + (orgUserDetailAdmin.UserId.Value, false), + }); + + var badRequestException = await Assert.ThrowsAsync( + () => sutProvider.Sut.OnSaveSideEffectsAsync(policyUpdate, policy)); + + Assert.Contains("Policy could not be enabled. Non-compliant members will lose access to their accounts. Identify members without two-step login from the policies column in the members page.", badRequestException.Message, StringComparison.OrdinalIgnoreCase); + + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .RemoveUserAsync(organizationId: default, organizationUserId: default, deletingUserId: default); + } +} diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/SavePolicyCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/SavePolicyCommandTests.cs new file mode 100644 index 000000000..342ede9c8 --- /dev/null +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/SavePolicyCommandTests.cs @@ -0,0 +1,330 @@ +#nullable enable + +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Implementations; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models; +using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Exceptions; +using Bit.Core.Models.Data.Organizations; +using Bit.Core.Services; +using Bit.Core.Test.AdminConsole.AutoFixture; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Microsoft.Extensions.Time.Testing; +using NSubstitute; +using Xunit; +using EventType = Bit.Core.Enums.EventType; + +namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies; + +public class SavePolicyCommandTests +{ + [Theory, BitAutoData] + public async Task SaveAsync_NewPolicy_Success([PolicyUpdate(PolicyType.SingleOrg)] PolicyUpdate policyUpdate) + { + var fakePolicyValidator = new FakeSingleOrgPolicyValidator(); + fakePolicyValidator.ValidateAsyncMock(policyUpdate, null).Returns(""); + var sutProvider = SutProviderFactory([fakePolicyValidator]); + + ArrangeOrganization(sutProvider, policyUpdate); + sutProvider.GetDependency().GetManyByOrganizationIdAsync(policyUpdate.OrganizationId).Returns([]); + + var creationDate = sutProvider.GetDependency().Start; + + await sutProvider.Sut.SaveAsync(policyUpdate); + + await fakePolicyValidator.ValidateAsyncMock.Received(1).Invoke(policyUpdate, null); + fakePolicyValidator.OnSaveSideEffectsAsyncMock.Received(1).Invoke(policyUpdate, null); + + await AssertPolicySavedAsync(sutProvider, policyUpdate); + await sutProvider.GetDependency().Received(1).UpsertAsync(Arg.Is(p => + p.CreationDate == creationDate && + p.RevisionDate == creationDate)); + } + + [Theory, BitAutoData] + public async Task SaveAsync_ExistingPolicy_Success( + [PolicyUpdate(PolicyType.SingleOrg)] PolicyUpdate policyUpdate, + [Policy(PolicyType.SingleOrg, false)] Policy currentPolicy) + { + var fakePolicyValidator = new FakeSingleOrgPolicyValidator(); + fakePolicyValidator.ValidateAsyncMock(policyUpdate, null).Returns(""); + var sutProvider = SutProviderFactory([fakePolicyValidator]); + + currentPolicy.OrganizationId = policyUpdate.OrganizationId; + sutProvider.GetDependency() + .GetByOrganizationIdTypeAsync(policyUpdate.OrganizationId, policyUpdate.Type) + .Returns(currentPolicy); + + ArrangeOrganization(sutProvider, policyUpdate); + sutProvider.GetDependency() + .GetManyByOrganizationIdAsync(policyUpdate.OrganizationId) + .Returns([currentPolicy]); + + // Store mutable properties separately to assert later + var id = currentPolicy.Id; + var organizationId = currentPolicy.OrganizationId; + var type = currentPolicy.Type; + var creationDate = currentPolicy.CreationDate; + var revisionDate = sutProvider.GetDependency().Start; + + await sutProvider.Sut.SaveAsync(policyUpdate); + + await fakePolicyValidator.ValidateAsyncMock.Received(1).Invoke(policyUpdate, currentPolicy); + fakePolicyValidator.OnSaveSideEffectsAsyncMock.Received(1).Invoke(policyUpdate, currentPolicy); + + await AssertPolicySavedAsync(sutProvider, policyUpdate); + // Additional assertions to ensure certain properties have or have not been updated + await sutProvider.GetDependency().Received(1).UpsertAsync(Arg.Is(p => + p.Id == id && + p.OrganizationId == organizationId && + p.Type == type && + p.CreationDate == creationDate && + p.RevisionDate == revisionDate)); + } + + [Fact] + public void Constructor_DuplicatePolicyValidators_Throws() + { + var exception = Assert.Throws(() => + new SavePolicyCommand( + Substitute.For(), + Substitute.For(), + Substitute.For(), + [new FakeSingleOrgPolicyValidator(), new FakeSingleOrgPolicyValidator()], + Substitute.For() + )); + Assert.Contains("Duplicate PolicyValidator for SingleOrg policy", exception.Message); + } + + [Theory, BitAutoData] + public async Task SaveAsync_OrganizationDoesNotExist_ThrowsBadRequest(PolicyUpdate policyUpdate) + { + var sutProvider = SutProviderFactory(); + sutProvider.GetDependency() + .GetOrganizationAbilityAsync(policyUpdate.OrganizationId) + .Returns(Task.FromResult(null)); + + var badRequestException = await Assert.ThrowsAsync( + () => sutProvider.Sut.SaveAsync(policyUpdate)); + + Assert.Contains("Organization not found", badRequestException.Message, StringComparison.OrdinalIgnoreCase); + await AssertPolicyNotSavedAsync(sutProvider); + } + + [Theory, BitAutoData] + public async Task SaveAsync_OrganizationCannotUsePolicies_ThrowsBadRequest(PolicyUpdate policyUpdate) + { + var sutProvider = SutProviderFactory(); + sutProvider.GetDependency() + .GetOrganizationAbilityAsync(policyUpdate.OrganizationId) + .Returns(new OrganizationAbility + { + Id = policyUpdate.OrganizationId, + UsePolicies = false + }); + + var badRequestException = await Assert.ThrowsAsync( + () => sutProvider.Sut.SaveAsync(policyUpdate)); + + Assert.Contains("cannot use policies", badRequestException.Message, StringComparison.OrdinalIgnoreCase); + await AssertPolicyNotSavedAsync(sutProvider); + } + + [Theory, BitAutoData] + public async Task SaveAsync_RequiredPolicyIsNull_Throws( + [PolicyUpdate(PolicyType.RequireSso)] PolicyUpdate policyUpdate) + { + var sutProvider = SutProviderFactory([ + new FakeRequireSsoPolicyValidator(), + new FakeSingleOrgPolicyValidator() + ]); + + ArrangeOrganization(sutProvider, policyUpdate); + sutProvider.GetDependency() + .GetManyByOrganizationIdAsync(policyUpdate.OrganizationId) + .Returns([]); + + var badRequestException = await Assert.ThrowsAsync( + () => sutProvider.Sut.SaveAsync(policyUpdate)); + + Assert.Contains("Turn on the Single organization policy because it is required for the Require single sign-on authentication policy", badRequestException.Message, StringComparison.OrdinalIgnoreCase); + await AssertPolicyNotSavedAsync(sutProvider); + } + + [Theory, BitAutoData] + public async Task SaveAsync_RequiredPolicyNotEnabled_Throws( + [PolicyUpdate(PolicyType.RequireSso)] PolicyUpdate policyUpdate, + [Policy(PolicyType.SingleOrg, false)] Policy singleOrgPolicy) + { + var sutProvider = SutProviderFactory([ + new FakeRequireSsoPolicyValidator(), + new FakeSingleOrgPolicyValidator() + ]); + + ArrangeOrganization(sutProvider, policyUpdate); + sutProvider.GetDependency() + .GetManyByOrganizationIdAsync(policyUpdate.OrganizationId) + .Returns([singleOrgPolicy]); + + var badRequestException = await Assert.ThrowsAsync( + () => sutProvider.Sut.SaveAsync(policyUpdate)); + + Assert.Contains("Turn on the Single organization policy because it is required for the Require single sign-on authentication policy", badRequestException.Message, StringComparison.OrdinalIgnoreCase); + await AssertPolicyNotSavedAsync(sutProvider); + } + + [Theory, BitAutoData] + public async Task SaveAsync_RequiredPolicyEnabled_Success( + [PolicyUpdate(PolicyType.RequireSso)] PolicyUpdate policyUpdate, + [Policy(PolicyType.SingleOrg)] Policy singleOrgPolicy) + { + var sutProvider = SutProviderFactory([ + new FakeRequireSsoPolicyValidator(), + new FakeSingleOrgPolicyValidator() + ]); + + ArrangeOrganization(sutProvider, policyUpdate); + sutProvider.GetDependency() + .GetManyByOrganizationIdAsync(policyUpdate.OrganizationId) + .Returns([singleOrgPolicy]); + + await sutProvider.Sut.SaveAsync(policyUpdate); + await AssertPolicySavedAsync(sutProvider, policyUpdate); + } + + [Theory, BitAutoData] + public async Task SaveAsync_DependentPolicyIsEnabled_Throws( + [PolicyUpdate(PolicyType.SingleOrg, false)] PolicyUpdate policyUpdate, + [Policy(PolicyType.SingleOrg)] Policy currentPolicy, + [Policy(PolicyType.RequireSso)] Policy requireSsoPolicy) // depends on Single Org + { + var sutProvider = SutProviderFactory([ + new FakeRequireSsoPolicyValidator(), + new FakeSingleOrgPolicyValidator() + ]); + + ArrangeOrganization(sutProvider, policyUpdate); + sutProvider.GetDependency() + .GetManyByOrganizationIdAsync(policyUpdate.OrganizationId) + .Returns([currentPolicy, requireSsoPolicy]); + + var badRequestException = await Assert.ThrowsAsync( + () => sutProvider.Sut.SaveAsync(policyUpdate)); + + Assert.Contains("Turn off the Require single sign-on authentication policy because it requires the Single organization policy", badRequestException.Message, StringComparison.OrdinalIgnoreCase); + await AssertPolicyNotSavedAsync(sutProvider); + } + + [Theory, BitAutoData] + public async Task SaveAsync_MultipleDependentPoliciesAreEnabled_Throws( + [PolicyUpdate(PolicyType.SingleOrg, false)] PolicyUpdate policyUpdate, + [Policy(PolicyType.SingleOrg)] Policy currentPolicy, + [Policy(PolicyType.RequireSso)] Policy requireSsoPolicy, // depends on Single Org + [Policy(PolicyType.MaximumVaultTimeout)] Policy vaultTimeoutPolicy) // depends on Single Org + { + var sutProvider = SutProviderFactory([ + new FakeRequireSsoPolicyValidator(), + new FakeSingleOrgPolicyValidator(), + new FakeVaultTimeoutPolicyValidator() + ]); + + ArrangeOrganization(sutProvider, policyUpdate); + sutProvider.GetDependency() + .GetManyByOrganizationIdAsync(policyUpdate.OrganizationId) + .Returns([currentPolicy, requireSsoPolicy, vaultTimeoutPolicy]); + + var badRequestException = await Assert.ThrowsAsync( + () => sutProvider.Sut.SaveAsync(policyUpdate)); + + Assert.Contains("Turn off all of the policies that require the Single organization policy", badRequestException.Message, StringComparison.OrdinalIgnoreCase); + await AssertPolicyNotSavedAsync(sutProvider); + } + + [Theory, BitAutoData] + public async Task SaveAsync_DependentPolicyNotEnabled_Success( + [PolicyUpdate(PolicyType.SingleOrg, false)] PolicyUpdate policyUpdate, + [Policy(PolicyType.SingleOrg)] Policy currentPolicy, + [Policy(PolicyType.RequireSso, false)] Policy requireSsoPolicy) // depends on Single Org but is not enabled + { + var sutProvider = SutProviderFactory([ + new FakeRequireSsoPolicyValidator(), + new FakeSingleOrgPolicyValidator() + ]); + + ArrangeOrganization(sutProvider, policyUpdate); + sutProvider.GetDependency() + .GetManyByOrganizationIdAsync(policyUpdate.OrganizationId) + .Returns([currentPolicy, requireSsoPolicy]); + + await sutProvider.Sut.SaveAsync(policyUpdate); + + await AssertPolicySavedAsync(sutProvider, policyUpdate); + } + + [Theory, BitAutoData] + public async Task SaveAsync_ThrowsOnValidationError([PolicyUpdate(PolicyType.SingleOrg)] PolicyUpdate policyUpdate) + { + var fakePolicyValidator = new FakeSingleOrgPolicyValidator(); + fakePolicyValidator.ValidateAsyncMock(policyUpdate, null).Returns("Validation error!"); + var sutProvider = SutProviderFactory([fakePolicyValidator]); + + ArrangeOrganization(sutProvider, policyUpdate); + sutProvider.GetDependency().GetManyByOrganizationIdAsync(policyUpdate.OrganizationId).Returns([]); + + var badRequestException = await Assert.ThrowsAsync( + () => sutProvider.Sut.SaveAsync(policyUpdate)); + + Assert.Contains("Validation error!", badRequestException.Message, StringComparison.OrdinalIgnoreCase); + await AssertPolicyNotSavedAsync(sutProvider); + } + + /// + /// Returns a new SutProvider with the PolicyValidators registered in the Sut. + /// + private static SutProvider SutProviderFactory(IEnumerable? policyValidators = null) + { + return new SutProvider() + .WithFakeTimeProvider() + .SetDependency(typeof(IEnumerable), policyValidators ?? []) + .Create(); + } + + private static void ArrangeOrganization(SutProvider sutProvider, PolicyUpdate policyUpdate) + { + sutProvider.GetDependency() + .GetOrganizationAbilityAsync(policyUpdate.OrganizationId) + .Returns(new OrganizationAbility + { + Id = policyUpdate.OrganizationId, + UsePolicies = true + }); + } + + private static async Task AssertPolicyNotSavedAsync(SutProvider sutProvider) + { + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .UpsertAsync(default!); + + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .LogPolicyEventAsync(default, default); + } + + private static async Task AssertPolicySavedAsync(SutProvider sutProvider, PolicyUpdate policyUpdate) + { + var expectedPolicy = () => Arg.Is(p => + p.Type == policyUpdate.Type && + p.OrganizationId == policyUpdate.OrganizationId && + p.Enabled == policyUpdate.Enabled && + p.Data == policyUpdate.Data); + + await sutProvider.GetDependency().Received(1).UpsertAsync(expectedPolicy()); + + await sutProvider.GetDependency().Received(1) + .LogPolicyEventAsync(expectedPolicy(), EventType.Policy_Updated); + } +} diff --git a/test/Core.Test/AdminConsole/Services/OrganizationDomainServiceTests.cs b/test/Core.Test/AdminConsole/Services/OrganizationDomainServiceTests.cs index ddd9accd0..210726061 100644 --- a/test/Core.Test/AdminConsole/Services/OrganizationDomainServiceTests.cs +++ b/test/Core.Test/AdminConsole/Services/OrganizationDomainServiceTests.cs @@ -1,8 +1,7 @@ -using Bit.Core.AdminConsole.Services.Implementations; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces; +using Bit.Core.AdminConsole.Services.Implementations; using Bit.Core.Entities; -using Bit.Core.Enums; using Bit.Core.Repositories; -using Bit.Core.Services; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; @@ -36,18 +35,14 @@ public class OrganizationDomainServiceTests Txt = "btw+6789" } }; + sutProvider.GetDependency().GetManyByNextRunDateAsync(default) .ReturnsForAnyArgs(domains); await sutProvider.Sut.ValidateOrganizationsDomainAsync(); - await sutProvider.GetDependency().ReceivedWithAnyArgs(2) - .ResolveAsync(default, default); - await sutProvider.GetDependency().ReceivedWithAnyArgs(2) - .ReplaceAsync(default); - await sutProvider.GetDependency().ReceivedWithAnyArgs(2) - .LogOrganizationDomainEventAsync(default, EventType.OrganizationDomain_NotVerified, - EventSystemUser.DomainVerification); + await sutProvider.GetDependency().ReceivedWithAnyArgs(2) + .SystemVerifyOrganizationDomainAsync(default); } [Theory, BitAutoData] @@ -81,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/OrganizationServiceTests.cs b/test/Core.Test/AdminConsole/Services/OrganizationServiceTests.cs index 1193b2de8..147162c66 100644 --- a/test/Core.Test/AdminConsole/Services/OrganizationServiceTests.cs +++ b/test/Core.Test/AdminConsole/Services/OrganizationServiceTests.cs @@ -7,7 +7,6 @@ using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Services; using Bit.Core.Auth.Entities; using Bit.Core.Auth.Enums; -using Bit.Core.Auth.Models; using Bit.Core.Auth.Models.Business.Tokenables; using Bit.Core.Auth.Models.Data; using Bit.Core.Auth.Repositories; @@ -44,6 +43,8 @@ using Xunit; using Organization = Bit.Core.AdminConsole.Entities.Organization; using OrganizationUser = Bit.Core.Entities.OrganizationUser; +#nullable enable + namespace Bit.Core.Test.Services; [SutProviderCustomize] @@ -78,8 +79,9 @@ public class OrganizationServiceTests .Returns(existingUsers); organizationUserRepository.GetCountByOrganizationIdAsync(org.Id) .Returns(existingUsers.Count); - organizationUserRepository.GetManyByOrganizationAsync(org.Id, OrganizationUserType.Owner) - .Returns(existingUsers.Select(u => new OrganizationUser { Status = OrganizationUserStatusType.Confirmed, Type = OrganizationUserType.Owner, Id = u.Id }).ToList()); + sutProvider.GetDependency() + .HasConfirmedOwnersExceptAsync(org.Id, Arg.Any>()) + .Returns(true); sutProvider.GetDependency().ManageUsers(org.Id).Returns(true); // Mock tokenable factory to return a token that expires in 5 days @@ -148,8 +150,9 @@ public class OrganizationServiceTests var organizationUserRepository = sutProvider.GetDependency(); - organizationUserRepository.GetManyByOrganizationAsync(org.Id, OrganizationUserType.Owner) - .Returns(existingUsers.Select(u => new OrganizationUser { Status = OrganizationUserStatusType.Confirmed, Type = OrganizationUserType.Owner, Id = u.Id }).ToList()); + sutProvider.GetDependency() + .HasConfirmedOwnersExceptAsync(org.Id, Arg.Any>()) + .Returns(true); SetupOrgUserRepositoryCreateManyAsyncMock(organizationUserRepository); @@ -709,8 +712,9 @@ OrganizationUserInvite invite, SutProvider sutProvider) var currentContext = sutProvider.GetDependency(); organizationRepository.GetByIdAsync(organization.Id).Returns(organization); - organizationUserRepository.GetManyByOrganizationAsync(organization.Id, OrganizationUserType.Owner) - .Returns(new[] { invitor }); + sutProvider.GetDependency() + .HasConfirmedOwnersExceptAsync(organization.Id, Arg.Any>()) + .Returns(true); SetupOrgUserRepositoryCreateManyAsyncMock(organizationUserRepository); @@ -738,8 +742,9 @@ OrganizationUserInvite invite, SutProvider sutProvider) var currentContext = sutProvider.GetDependency(); organizationRepository.GetByIdAsync(organization.Id).Returns(organization); - organizationUserRepository.GetManyByOrganizationAsync(organization.Id, OrganizationUserType.Owner) - .Returns(new[] { invitor }); + sutProvider.GetDependency() + .HasConfirmedOwnersExceptAsync(organization.Id, Arg.Any>()) + .Returns(true); SetupOrgUserRepositoryCreateManyAsyncMock(organizationUserRepository); @@ -818,8 +823,9 @@ OrganizationUserInvite invite, SutProvider sutProvider) var currentContext = sutProvider.GetDependency(); organizationRepository.GetByIdAsync(organization.Id).Returns(organization); - organizationUserRepository.GetManyByOrganizationAsync(organization.Id, OrganizationUserType.Owner) - .Returns(new[] { invitor }); + sutProvider.GetDependency() + .HasConfirmedOwnersExceptAsync(organization.Id, Arg.Any>()) + .Returns(true); SetupOrgUserRepositoryCreateManyAsyncMock(organizationUserRepository); @@ -836,7 +842,6 @@ OrganizationUserInvite invite, SutProvider sutProvider) ), OrganizationCustomize, BitAutoData] public async Task InviteUser_Passes(Organization organization, OrganizationUserInvite invite, string externalId, OrganizationUser invitor, - [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner, SutProvider sutProvider) { // This method is only used to invite 1 user at a time @@ -852,8 +857,6 @@ OrganizationUserInvite invite, SutProvider sutProvider) var organizationUserRepository = sutProvider.GetDependency(); organizationRepository.GetByIdAsync(organization.Id).Returns(organization); - organizationUserRepository.GetManyByOrganizationAsync(organization.Id, OrganizationUserType.Owner) - .Returns(new[] { owner }); // Mock tokenable factory to return a token that expires in 5 days sutProvider.GetDependency() @@ -865,6 +868,10 @@ OrganizationUserInvite invite, SutProvider sutProvider) } ); + sutProvider.GetDependency() + .HasConfirmedOwnersExceptAsync(organization.Id, Arg.Any>()) + .Returns(true); + SetupOrgUserRepositoryCreateManyAsyncMock(organizationUserRepository); SetupOrgUserRepositoryCreateAsyncMock(organizationUserRepository); @@ -906,7 +913,6 @@ OrganizationUserInvite invite, SutProvider sutProvider) ), OrganizationCustomize, BitAutoData] public async Task InviteUser_UserAlreadyInvited_Throws(Organization organization, OrganizationUserInvite invite, string externalId, OrganizationUser invitor, - [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner, SutProvider sutProvider) { // This method is only used to invite 1 user at a time @@ -927,8 +933,6 @@ OrganizationUserInvite invite, SutProvider sutProvider) var organizationUserRepository = sutProvider.GetDependency(); organizationRepository.GetByIdAsync(organization.Id).Returns(organization); - organizationUserRepository.GetManyByOrganizationAsync(organization.Id, OrganizationUserType.Owner) - .Returns(new[] { owner }); // Mock tokenable factory to return a token that expires in 5 days sutProvider.GetDependency() @@ -940,6 +944,10 @@ OrganizationUserInvite invite, SutProvider sutProvider) } ); + sutProvider.GetDependency() + .HasConfirmedOwnersExceptAsync(organization.Id, Arg.Any>()) + .Returns(true); + SetupOrgUserRepositoryCreateManyAsyncMock(organizationUserRepository); SetupOrgUserRepositoryCreateAsyncMock(organizationUserRepository); @@ -988,7 +996,6 @@ OrganizationUserInvite invite, SutProvider sutProvider) ), OrganizationCustomize, BitAutoData] public async Task InviteUsers_Passes(Organization organization, IEnumerable<(OrganizationUserInvite invite, string externalId)> invites, OrganizationUser invitor, - [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner, SutProvider sutProvider) { // Setup FakeDataProtectorTokenFactory for creating new tokens - this must come first in order to avoid resetting mocks @@ -1001,8 +1008,6 @@ OrganizationUserInvite invite, SutProvider sutProvider) var organizationUserRepository = sutProvider.GetDependency(); organizationRepository.GetByIdAsync(organization.Id).Returns(organization); - organizationUserRepository.GetManyByOrganizationAsync(organization.Id, OrganizationUserType.Owner) - .Returns(new[] { owner }); // Mock tokenable factory to return a token that expires in 5 days sutProvider.GetDependency() @@ -1014,6 +1019,10 @@ OrganizationUserInvite invite, SutProvider sutProvider) } ); + sutProvider.GetDependency() + .HasConfirmedOwnersExceptAsync(organization.Id, Arg.Any>()) + .Returns(true); + SetupOrgUserRepositoryCreateManyAsyncMock(organizationUserRepository); SetupOrgUserRepositoryCreateAsyncMock(organizationUserRepository); @@ -1035,7 +1044,6 @@ OrganizationUserInvite invite, SutProvider sutProvider) ), OrganizationCustomize, BitAutoData] public async Task InviteUsers_WithEventSystemUser_Passes(Organization organization, EventSystemUser eventSystemUser, IEnumerable<(OrganizationUserInvite invite, string externalId)> invites, OrganizationUser invitor, - [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner, SutProvider sutProvider) { // Setup FakeDataProtectorTokenFactory for creating new tokens - this must come first in order to avoid resetting mocks @@ -1053,8 +1061,9 @@ OrganizationUserInvite invite, SutProvider sutProvider) var currentContext = sutProvider.GetDependency(); organizationRepository.GetByIdAsync(organization.Id).Returns(organization); - organizationUserRepository.GetManyByOrganizationAsync(organization.Id, OrganizationUserType.Owner) - .Returns(new[] { owner }); + sutProvider.GetDependency() + .HasConfirmedOwnersExceptAsync(organization.Id, Arg.Any>()) + .Returns(true); SetupOrgUserRepositoryCreateAsyncMock(organizationUserRepository); SetupOrgUserRepositoryCreateManyAsyncMock(organizationUserRepository); @@ -1182,208 +1191,17 @@ OrganizationUserInvite invite, SutProvider sutProvider) sutProvider.GetDependency().ManageUsers(organization.Id).Returns(true); } - [Theory, BitAutoData] - public async Task RemoveUser_InvalidUser(OrganizationUser organizationUser, OrganizationUser deletingUser, - SutProvider sutProvider) - { - var organizationUserRepository = sutProvider.GetDependency(); - - organizationUserRepository.GetByIdAsync(organizationUser.Id).Returns(organizationUser); - - var exception = await Assert.ThrowsAsync( - () => sutProvider.Sut.RemoveUserAsync(Guid.NewGuid(), organizationUser.Id, deletingUser.UserId)); - Assert.Contains("User not valid.", exception.Message); - } - - [Theory, BitAutoData] - public async Task RemoveUser_RemoveYourself(OrganizationUser deletingUser, SutProvider sutProvider) - { - var organizationUserRepository = sutProvider.GetDependency(); - - organizationUserRepository.GetByIdAsync(deletingUser.Id).Returns(deletingUser); - - var exception = await Assert.ThrowsAsync( - () => sutProvider.Sut.RemoveUserAsync(deletingUser.OrganizationId, deletingUser.Id, deletingUser.UserId)); - Assert.Contains("You cannot remove yourself.", exception.Message); - } - - [Theory, BitAutoData] - public async Task RemoveUser_NonOwnerRemoveOwner( - [OrganizationUser(type: OrganizationUserType.Owner)] OrganizationUser organizationUser, - [OrganizationUser(type: OrganizationUserType.Admin)] OrganizationUser deletingUser, - SutProvider sutProvider) - { - var organizationUserRepository = sutProvider.GetDependency(); - var currentContext = sutProvider.GetDependency(); - - organizationUser.OrganizationId = deletingUser.OrganizationId; - organizationUserRepository.GetByIdAsync(organizationUser.Id).Returns(organizationUser); - currentContext.OrganizationAdmin(deletingUser.OrganizationId).Returns(true); - - var exception = await Assert.ThrowsAsync( - () => sutProvider.Sut.RemoveUserAsync(deletingUser.OrganizationId, organizationUser.Id, deletingUser.UserId)); - Assert.Contains("Only owners can delete other owners.", exception.Message); - } - - [Theory, BitAutoData] - public async Task RemoveUser_LastOwner( - [OrganizationUser(type: OrganizationUserType.Owner)] OrganizationUser organizationUser, - OrganizationUser deletingUser, - SutProvider sutProvider) - { - var organizationUserRepository = sutProvider.GetDependency(); - - organizationUser.OrganizationId = deletingUser.OrganizationId; - organizationUserRepository.GetByIdAsync(organizationUser.Id).Returns(organizationUser); - organizationUserRepository.GetManyByOrganizationAsync(deletingUser.OrganizationId, OrganizationUserType.Owner) - .Returns(new[] { organizationUser }); - - var exception = await Assert.ThrowsAsync( - () => sutProvider.Sut.RemoveUserAsync(deletingUser.OrganizationId, organizationUser.Id, null)); - Assert.Contains("Organization must have at least one confirmed owner.", exception.Message); - } - - [Theory, BitAutoData] - public async Task RemoveUser_Success( - OrganizationUser organizationUser, - [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser deletingUser, - SutProvider sutProvider) - { - var organizationUserRepository = sutProvider.GetDependency(); - var currentContext = sutProvider.GetDependency(); - - organizationUser.OrganizationId = deletingUser.OrganizationId; - organizationUserRepository.GetByIdAsync(organizationUser.Id).Returns(organizationUser); - organizationUserRepository.GetByIdAsync(deletingUser.Id).Returns(deletingUser); - organizationUserRepository.GetManyByOrganizationAsync(deletingUser.OrganizationId, OrganizationUserType.Owner) - .Returns(new[] { deletingUser, organizationUser }); - currentContext.OrganizationOwner(deletingUser.OrganizationId).Returns(true); - - await sutProvider.Sut.RemoveUserAsync(deletingUser.OrganizationId, organizationUser.Id, deletingUser.UserId); - - await sutProvider.GetDependency().Received(1).LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Removed); - } - - [Theory, BitAutoData] - public async Task RemoveUser_WithEventSystemUser_Success( - OrganizationUser organizationUser, - [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser deletingUser, EventSystemUser eventSystemUser, - SutProvider sutProvider) - { - var organizationUserRepository = sutProvider.GetDependency(); - var currentContext = sutProvider.GetDependency(); - - organizationUser.OrganizationId = deletingUser.OrganizationId; - organizationUserRepository.GetByIdAsync(organizationUser.Id).Returns(organizationUser); - organizationUserRepository.GetByIdAsync(deletingUser.Id).Returns(deletingUser); - organizationUserRepository.GetManyByOrganizationAsync(deletingUser.OrganizationId, OrganizationUserType.Owner) - .Returns(new[] { deletingUser, organizationUser }); - currentContext.OrganizationOwner(deletingUser.OrganizationId).Returns(true); - - await sutProvider.Sut.RemoveUserAsync(deletingUser.OrganizationId, organizationUser.Id, eventSystemUser); - - await sutProvider.GetDependency().Received(1).LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Removed, eventSystemUser); - } - - [Theory, BitAutoData] - public async Task RemoveUsers_FilterInvalid(OrganizationUser organizationUser, OrganizationUser deletingUser, - SutProvider sutProvider) - { - var organizationUserRepository = sutProvider.GetDependency(); - var organizationUsers = new[] { organizationUser }; - var organizationUserIds = organizationUsers.Select(u => u.Id); - organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(organizationUsers); - - var exception = await Assert.ThrowsAsync( - () => sutProvider.Sut.RemoveUsersAsync(deletingUser.OrganizationId, organizationUserIds, deletingUser.UserId)); - Assert.Contains("Users invalid.", exception.Message); - } - - [Theory, BitAutoData] - public async Task RemoveUsers_RemoveYourself( - [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser orgUser, - OrganizationUser deletingUser, - SutProvider sutProvider) - { - var organizationUserRepository = sutProvider.GetDependency(); - var organizationUsers = new[] { deletingUser }; - var organizationUserIds = organizationUsers.Select(u => u.Id); - organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(organizationUsers); - organizationUserRepository.GetManyByOrganizationAsync(default, default).ReturnsForAnyArgs(new[] { orgUser }); - - var result = await sutProvider.Sut.RemoveUsersAsync(deletingUser.OrganizationId, organizationUserIds, deletingUser.UserId); - Assert.Contains("You cannot remove yourself.", result[0].Item2); - } - - [Theory, BitAutoData] - public async Task RemoveUsers_NonOwnerRemoveOwner( - [OrganizationUser(type: OrganizationUserType.Admin)] OrganizationUser deletingUser, - [OrganizationUser(type: OrganizationUserType.Owner)] OrganizationUser orgUser1, - [OrganizationUser(OrganizationUserStatusType.Confirmed)] OrganizationUser orgUser2, - SutProvider sutProvider) - { - var organizationUserRepository = sutProvider.GetDependency(); - - orgUser1.OrganizationId = orgUser2.OrganizationId = deletingUser.OrganizationId; - var organizationUsers = new[] { orgUser1 }; - var organizationUserIds = organizationUsers.Select(u => u.Id); - organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(organizationUsers); - organizationUserRepository.GetManyByOrganizationAsync(default, default).ReturnsForAnyArgs(new[] { orgUser2 }); - - var result = await sutProvider.Sut.RemoveUsersAsync(deletingUser.OrganizationId, organizationUserIds, deletingUser.UserId); - Assert.Contains("Only owners can delete other owners.", result[0].Item2); - } - - [Theory, BitAutoData] - public async Task RemoveUsers_LastOwner( - [OrganizationUser(status: OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser orgUser, - SutProvider sutProvider) - { - var organizationUserRepository = sutProvider.GetDependency(); - - var organizationUsers = new[] { orgUser }; - var organizationUserIds = organizationUsers.Select(u => u.Id); - organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(organizationUsers); - organizationUserRepository.GetManyByOrganizationAsync(orgUser.OrganizationId, OrganizationUserType.Owner).Returns(organizationUsers); - - var exception = await Assert.ThrowsAsync( - () => sutProvider.Sut.RemoveUsersAsync(orgUser.OrganizationId, organizationUserIds, null)); - Assert.Contains("Organization must have at least one confirmed owner.", exception.Message); - } - - [Theory, BitAutoData] - public async Task RemoveUsers_Success( - [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser deletingUser, - [OrganizationUser(type: OrganizationUserType.Owner)] OrganizationUser orgUser1, OrganizationUser orgUser2, - SutProvider sutProvider) - { - var organizationUserRepository = sutProvider.GetDependency(); - var currentContext = sutProvider.GetDependency(); - - orgUser1.OrganizationId = orgUser2.OrganizationId = deletingUser.OrganizationId; - var organizationUsers = new[] { orgUser1, orgUser2 }; - var organizationUserIds = organizationUsers.Select(u => u.Id); - organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(organizationUsers); - organizationUserRepository.GetByIdAsync(deletingUser.Id).Returns(deletingUser); - organizationUserRepository.GetManyByOrganizationAsync(deletingUser.OrganizationId, OrganizationUserType.Owner) - .Returns(new[] { deletingUser, orgUser1 }); - currentContext.OrganizationOwner(deletingUser.OrganizationId).Returns(true); - - await sutProvider.Sut.RemoveUsersAsync(deletingUser.OrganizationId, organizationUserIds, deletingUser.UserId); - } - [Theory, BitAutoData] public async Task ConfirmUser_InvalidStatus(OrganizationUser confirmingUser, [OrganizationUser(OrganizationUserStatusType.Invited)] OrganizationUser orgUser, string key, SutProvider sutProvider) { var organizationUserRepository = sutProvider.GetDependency(); - var userService = Substitute.For(); organizationUserRepository.GetByIdAsync(orgUser.Id).Returns(orgUser); var exception = await Assert.ThrowsAsync( - () => sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id, userService)); + () => sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id)); Assert.Contains("User not valid.", exception.Message); } @@ -1393,12 +1211,11 @@ OrganizationUserInvite invite, SutProvider sutProvider) SutProvider sutProvider) { var organizationUserRepository = sutProvider.GetDependency(); - var userService = Substitute.For(); organizationUserRepository.GetByIdAsync(orgUser.Id).Returns(orgUser); var exception = await Assert.ThrowsAsync( - () => sutProvider.Sut.ConfirmUserAsync(confirmingUser.OrganizationId, orgUser.Id, key, confirmingUser.Id, userService)); + () => sutProvider.Sut.ConfirmUserAsync(confirmingUser.OrganizationId, orgUser.Id, key, confirmingUser.Id)); Assert.Contains("User not valid.", exception.Message); } @@ -1411,7 +1228,6 @@ OrganizationUserInvite invite, SutProvider sutProvider) { var organizationUserRepository = sutProvider.GetDependency(); var organizationRepository = sutProvider.GetDependency(); - var userService = Substitute.For(); var userRepository = sutProvider.GetDependency(); org.PlanType = PlanType.Free; @@ -1424,7 +1240,7 @@ OrganizationUserInvite invite, SutProvider sutProvider) userRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { user }); var exception = await Assert.ThrowsAsync( - () => sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id, userService)); + () => sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id)); Assert.Contains("User can only be an admin of one free organization.", exception.Message); } @@ -1465,7 +1281,6 @@ OrganizationUserInvite invite, SutProvider sutProvider) { var organizationUserRepository = sutProvider.GetDependency(); var organizationRepository = sutProvider.GetDependency(); - var userService = Substitute.For(); var userRepository = sutProvider.GetDependency(); org.PlanType = planType; @@ -1478,7 +1293,7 @@ OrganizationUserInvite invite, SutProvider sutProvider) organizationRepository.GetByIdAsync(org.Id).Returns(org); userRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { user }); - await sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id, userService); + await sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id); await sutProvider.GetDependency().Received(1).LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_Confirmed); await sutProvider.GetDependency().Received(1).SendOrganizationConfirmedEmailAsync(org.DisplayName(), user.Email); @@ -1496,7 +1311,6 @@ OrganizationUserInvite invite, SutProvider sutProvider) var organizationRepository = sutProvider.GetDependency(); var userRepository = sutProvider.GetDependency(); var policyService = sutProvider.GetDependency(); - var userService = Substitute.For(); org.PlanType = PlanType.EnterpriseAnnually; orgUser.Status = OrganizationUserStatusType.Accepted; @@ -1510,7 +1324,7 @@ OrganizationUserInvite invite, SutProvider sutProvider) policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.SingleOrg).Returns(new[] { singleOrgPolicy }); var exception = await Assert.ThrowsAsync( - () => sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id, userService)); + () => sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id)); Assert.Contains("Cannot confirm this member to the organization until they leave or remove all other organizations.", exception.Message); } @@ -1524,7 +1338,6 @@ OrganizationUserInvite invite, SutProvider sutProvider) var organizationRepository = sutProvider.GetDependency(); var userRepository = sutProvider.GetDependency(); var policyService = sutProvider.GetDependency(); - var userService = Substitute.For(); org.PlanType = PlanType.EnterpriseAnnually; orgUser.Status = OrganizationUserStatusType.Accepted; @@ -1538,7 +1351,7 @@ OrganizationUserInvite invite, SutProvider sutProvider) policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.SingleOrg).Returns(new[] { singleOrgPolicy }); var exception = await Assert.ThrowsAsync( - () => sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id, userService)); + () => sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id)); Assert.Contains("Cannot confirm this member to the organization because they are in another organization which forbids it.", exception.Message); } @@ -1554,7 +1367,6 @@ OrganizationUserInvite invite, SutProvider sutProvider) var organizationUserRepository = sutProvider.GetDependency(); var organizationRepository = sutProvider.GetDependency(); var userRepository = sutProvider.GetDependency(); - var userService = Substitute.For(); org.PlanType = PlanType.EnterpriseAnnually; orgUser.Type = userType; @@ -1567,7 +1379,7 @@ OrganizationUserInvite invite, SutProvider sutProvider) organizationRepository.GetByIdAsync(org.Id).Returns(org); userRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { user }); - await sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id, userService); + await sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id); await sutProvider.GetDependency().Received(1).LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_Confirmed); await sutProvider.GetDependency().Received(1).SendOrganizationConfirmedEmailAsync(org.DisplayName(), user.Email, true); @@ -1585,7 +1397,7 @@ OrganizationUserInvite invite, SutProvider sutProvider) var organizationRepository = sutProvider.GetDependency(); var userRepository = sutProvider.GetDependency(); var policyService = sutProvider.GetDependency(); - var userService = Substitute.For(); + var twoFactorIsEnabledQuery = sutProvider.GetDependency(); org.PlanType = PlanType.EnterpriseAnnually; orgUser.OrganizationId = confirmingUser.OrganizationId = org.Id; @@ -1596,9 +1408,11 @@ OrganizationUserInvite invite, SutProvider sutProvider) userRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { user }); twoFactorPolicy.OrganizationId = org.Id; policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication).Returns(new[] { twoFactorPolicy }); + twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(Arg.Is>(ids => ids.Contains(user.Id))) + .Returns(new List<(Guid userId, bool twoFactorIsEnabled)>() { (user.Id, false) }); var exception = await Assert.ThrowsAsync( - () => sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id, userService)); + () => sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id)); Assert.Contains("User does not have two-step login enabled.", exception.Message); } @@ -1612,7 +1426,7 @@ OrganizationUserInvite invite, SutProvider sutProvider) var organizationRepository = sutProvider.GetDependency(); var userRepository = sutProvider.GetDependency(); var policyService = sutProvider.GetDependency(); - var userService = Substitute.For(); + var twoFactorIsEnabledQuery = sutProvider.GetDependency(); org.PlanType = PlanType.EnterpriseAnnually; orgUser.OrganizationId = confirmingUser.OrganizationId = org.Id; @@ -1622,71 +1436,10 @@ OrganizationUserInvite invite, SutProvider sutProvider) userRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { user }); twoFactorPolicy.OrganizationId = org.Id; policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication).Returns(new[] { twoFactorPolicy }); - userService.TwoFactorIsEnabledAsync(user).Returns(true); - - await sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id, userService); - } - - [Theory, BitAutoData] - public async Task ConfirmUser_vNext_TwoFactorPolicy_NotEnabled_Throws(Organization org, OrganizationUser confirmingUser, - [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, UserWithCalculatedPremium user, - OrganizationUser orgUserAnotherOrg, - [OrganizationUserPolicyDetails(PolicyType.TwoFactorAuthentication)] OrganizationUserPolicyDetails twoFactorPolicy, - string key, SutProvider sutProvider) - { - sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.MembersTwoFAQueryOptimization).Returns(true); - - var organizationUserRepository = sutProvider.GetDependency(); - var organizationRepository = sutProvider.GetDependency(); - var userRepository = sutProvider.GetDependency(); - var policyService = sutProvider.GetDependency(); - var userService = Substitute.For(); - var twoFactorIsEnabledQuery = sutProvider.GetDependency(); - - org.PlanType = PlanType.EnterpriseAnnually; - orgUser.OrganizationId = confirmingUser.OrganizationId = org.Id; - orgUser.UserId = orgUserAnotherOrg.UserId = user.Id; - organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { orgUser }); - organizationUserRepository.GetManyByManyUsersAsync(default).ReturnsForAnyArgs(new[] { orgUserAnotherOrg }); - organizationRepository.GetByIdAsync(org.Id).Returns(org); - userRepository.GetManyWithCalculatedPremiumAsync(default).ReturnsForAnyArgs(new[] { user }); - twoFactorPolicy.OrganizationId = org.Id; - policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication).Returns(new[] { twoFactorPolicy }); - twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(Arg.Is>(ids => ids.Contains(user.Id))) - .Returns(new List<(Guid userId, bool twoFactorIsEnabled)>() { (user.Id, false) }); - - var exception = await Assert.ThrowsAsync( - () => sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id, userService)); - Assert.Contains("User does not have two-step login enabled.", exception.Message); - } - - [Theory, BitAutoData] - public async Task ConfirmUser_vNext_TwoFactorPolicy_Enabled_Success(Organization org, OrganizationUser confirmingUser, - [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, UserWithCalculatedPremium user, - [OrganizationUserPolicyDetails(PolicyType.TwoFactorAuthentication)] OrganizationUserPolicyDetails twoFactorPolicy, - string key, SutProvider sutProvider) - { - sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.MembersTwoFAQueryOptimization).Returns(true); - - var organizationUserRepository = sutProvider.GetDependency(); - var organizationRepository = sutProvider.GetDependency(); - var userRepository = sutProvider.GetDependency(); - var policyService = sutProvider.GetDependency(); - var userService = Substitute.For(); - var twoFactorIsEnabledQuery = sutProvider.GetDependency(); - - org.PlanType = PlanType.EnterpriseAnnually; - orgUser.OrganizationId = confirmingUser.OrganizationId = org.Id; - orgUser.UserId = user.Id; - organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { orgUser }); - organizationRepository.GetByIdAsync(org.Id).Returns(org); - userRepository.GetManyWithCalculatedPremiumAsync(default).ReturnsForAnyArgs(new[] { user }); - twoFactorPolicy.OrganizationId = org.Id; - policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication).Returns(new[] { twoFactorPolicy }); twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(Arg.Is>(ids => ids.Contains(user.Id))) .Returns(new List<(Guid userId, bool twoFactorIsEnabled)>() { (user.Id, true) }); - await sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id, userService); + await sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id); } [Theory, BitAutoData] @@ -1704,52 +1457,6 @@ OrganizationUserInvite invite, SutProvider sutProvider) var organizationRepository = sutProvider.GetDependency(); var userRepository = sutProvider.GetDependency(); var policyService = sutProvider.GetDependency(); - var userService = Substitute.For(); - - org.PlanType = PlanType.EnterpriseAnnually; - orgUser1.OrganizationId = orgUser2.OrganizationId = orgUser3.OrganizationId = confirmingUser.OrganizationId = org.Id; - orgUser1.UserId = user1.Id; - orgUser2.UserId = user2.Id; - orgUser3.UserId = user3.Id; - anotherOrgUser.UserId = user3.Id; - var orgUsers = new[] { orgUser1, orgUser2, orgUser3 }; - organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(orgUsers); - organizationRepository.GetByIdAsync(org.Id).Returns(org); - userRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { user1, user2, user3 }); - twoFactorPolicy.OrganizationId = org.Id; - policyService.GetPoliciesApplicableToUserAsync(Arg.Any(), PolicyType.TwoFactorAuthentication).Returns(new[] { twoFactorPolicy }); - userService.TwoFactorIsEnabledAsync(user1).Returns(true); - userService.TwoFactorIsEnabledAsync(user2).Returns(false); - userService.TwoFactorIsEnabledAsync(user3).Returns(true); - singleOrgPolicy.OrganizationId = org.Id; - policyService.GetPoliciesApplicableToUserAsync(user3.Id, PolicyType.SingleOrg) - .Returns(new[] { singleOrgPolicy }); - organizationUserRepository.GetManyByManyUsersAsync(default) - .ReturnsForAnyArgs(new[] { orgUser1, orgUser2, orgUser3, anotherOrgUser }); - - var keys = orgUsers.ToDictionary(ou => ou.Id, _ => key); - var result = await sutProvider.Sut.ConfirmUsersAsync(confirmingUser.OrganizationId, keys, confirmingUser.Id, userService); - Assert.Contains("", result[0].Item2); - Assert.Contains("User does not have two-step login enabled.", result[1].Item2); - Assert.Contains("Cannot confirm this member to the organization until they leave or remove all other organizations.", result[2].Item2); - } - - [Theory, BitAutoData] - public async Task ConfirmUsers_vNext_Success(Organization org, - OrganizationUser confirmingUser, - [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser1, - [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser2, - [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser3, - OrganizationUser anotherOrgUser, UserWithCalculatedPremium user1, UserWithCalculatedPremium user2, UserWithCalculatedPremium user3, - [OrganizationUserPolicyDetails(PolicyType.TwoFactorAuthentication)] OrganizationUserPolicyDetails twoFactorPolicy, - [OrganizationUserPolicyDetails(PolicyType.SingleOrg)] OrganizationUserPolicyDetails singleOrgPolicy, - string key, SutProvider sutProvider) - { - var organizationUserRepository = sutProvider.GetDependency(); - var organizationRepository = sutProvider.GetDependency(); - var userRepository = sutProvider.GetDependency(); - var policyService = sutProvider.GetDependency(); - var userService = Substitute.For(); var twoFactorIsEnabledQuery = sutProvider.GetDependency(); org.PlanType = PlanType.EnterpriseAnnually; @@ -1761,7 +1468,7 @@ OrganizationUserInvite invite, SutProvider sutProvider) var orgUsers = new[] { orgUser1, orgUser2, orgUser3 }; organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(orgUsers); organizationRepository.GetByIdAsync(org.Id).Returns(org); - userRepository.GetManyWithCalculatedPremiumAsync(default).ReturnsForAnyArgs(new[] { user1, user2, user3 }); + userRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { user1, user2, user3 }); twoFactorPolicy.OrganizationId = org.Id; policyService.GetPoliciesApplicableToUserAsync(Arg.Any(), PolicyType.TwoFactorAuthentication).Returns(new[] { twoFactorPolicy }); twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(Arg.Is>(ids => ids.Contains(user1.Id) && ids.Contains(user2.Id) && ids.Contains(user3.Id))) @@ -1778,7 +1485,7 @@ OrganizationUserInvite invite, SutProvider sutProvider) .ReturnsForAnyArgs(new[] { orgUser1, orgUser2, orgUser3, anotherOrgUser }); var keys = orgUsers.ToDictionary(ou => ou.Id, _ => key); - var result = await sutProvider.Sut.ConfirmUsersAsync_vNext(confirmingUser.OrganizationId, keys, confirmingUser.Id); + var result = await sutProvider.Sut.ConfirmUsersAsync(confirmingUser.OrganizationId, keys, confirmingUser.Id); Assert.Contains("", result[0].Item2); Assert.Contains("User does not have two-step login enabled.", result[1].Item2); Assert.Contains("Cannot confirm this member to the organization until they leave or remove all other organizations.", result[2].Item2); @@ -1971,17 +1678,24 @@ OrganizationUserInvite invite, SutProvider sutProvider) await applicationCacheService.DidNotReceiveWithAnyArgs().DeleteOrganizationAbilityAsync(default); } - private void RestoreRevokeUser_Setup(Organization organization, OrganizationUser restoringUser, - OrganizationUser organizationUser, SutProvider sutProvider, - OrganizationUserType restoringUserType = OrganizationUserType.Owner) + private void RestoreRevokeUser_Setup( + Organization organization, + OrganizationUser? requestingOrganizationUser, + OrganizationUser targetOrganizationUser, + SutProvider sutProvider) { + if (requestingOrganizationUser != null) + { + requestingOrganizationUser.OrganizationId = organization.Id; + } + targetOrganizationUser.OrganizationId = organization.Id; + sutProvider.GetDependency().GetByIdAsync(organization.Id).Returns(organization); - sutProvider.GetDependency().GetByIdAsync(organizationUser.OrganizationId).Returns(organization); - sutProvider.GetDependency().OrganizationOwner(organization.Id).Returns(true); - sutProvider.GetDependency().ManageUsers(organization.Id).Returns(true); - var organizationUserRepository = sutProvider.GetDependency(); - organizationUserRepository.GetManyByOrganizationAsync(organizationUser.OrganizationId, restoringUserType) - .Returns(new[] { restoringUser }); + sutProvider.GetDependency().OrganizationOwner(organization.Id).Returns(requestingOrganizationUser != null && requestingOrganizationUser.Type is OrganizationUserType.Owner); + sutProvider.GetDependency().ManageUsers(organization.Id).Returns(requestingOrganizationUser != null && (requestingOrganizationUser.Type is OrganizationUserType.Owner or OrganizationUserType.Admin)); + sutProvider.GetDependency() + .HasConfirmedOwnersExceptAsync(organization.Id, Arg.Any>()) + .Returns(true); } [Theory, BitAutoData] @@ -2000,10 +1714,9 @@ OrganizationUserInvite invite, SutProvider sutProvider) } [Theory, BitAutoData] - public async Task RevokeUser_WithEventSystemUser_Success(Organization organization, [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner, - [OrganizationUser] OrganizationUser organizationUser, EventSystemUser eventSystemUser, SutProvider sutProvider) + public async Task RevokeUser_WithEventSystemUser_Success(Organization organization, [OrganizationUser] OrganizationUser organizationUser, EventSystemUser eventSystemUser, SutProvider sutProvider) { - RestoreRevokeUser_Setup(organization, owner, organizationUser, sutProvider); + RestoreRevokeUser_Setup(organization, null, organizationUser, sutProvider); var organizationUserRepository = sutProvider.GetDependency(); var eventService = sutProvider.GetDependency(); @@ -2019,11 +1732,10 @@ OrganizationUserInvite invite, SutProvider sutProvider) [OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser, SutProvider sutProvider) { RestoreRevokeUser_Setup(organization, owner, organizationUser, sutProvider); - var userService = Substitute.For(); var organizationUserRepository = sutProvider.GetDependency(); var eventService = sutProvider.GetDependency(); - await sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id, userService); + await sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id); await organizationUserRepository.Received().RestoreAsync(organizationUser.Id, OrganizationUserStatusType.Invited); await eventService.Received() @@ -2031,15 +1743,13 @@ OrganizationUserInvite invite, SutProvider sutProvider) } [Theory, BitAutoData] - public async Task RestoreUser_WithEventSystemUser_Success(Organization organization, [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner, - [OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser, EventSystemUser eventSystemUser, SutProvider sutProvider) + public async Task RestoreUser_WithEventSystemUser_Success(Organization organization, [OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser, EventSystemUser eventSystemUser, SutProvider sutProvider) { - RestoreRevokeUser_Setup(organization, owner, organizationUser, sutProvider); - var userService = Substitute.For(); + RestoreRevokeUser_Setup(organization, null, organizationUser, sutProvider); var organizationUserRepository = sutProvider.GetDependency(); var eventService = sutProvider.GetDependency(); - await sutProvider.Sut.RestoreUserAsync(organizationUser, eventSystemUser, userService); + await sutProvider.Sut.RestoreUserAsync(organizationUser, eventSystemUser); await organizationUserRepository.Received().RestoreAsync(organizationUser.Id, OrganizationUserStatusType.Invited); await eventService.Received() @@ -2052,12 +1762,11 @@ OrganizationUserInvite invite, SutProvider sutProvider) { organizationUser.UserId = owner.Id; RestoreRevokeUser_Setup(organization, owner, organizationUser, sutProvider); - var userService = Substitute.For(); var organizationUserRepository = sutProvider.GetDependency(); var eventService = sutProvider.GetDependency(); var exception = await Assert.ThrowsAsync( - () => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id, userService)); + () => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id)); Assert.Contains("you cannot restore yourself", exception.Message.ToLowerInvariant()); @@ -2069,17 +1778,17 @@ OrganizationUserInvite invite, SutProvider sutProvider) [Theory] [BitAutoData(OrganizationUserType.Admin)] [BitAutoData(OrganizationUserType.Custom)] - public async Task RestoreUser_AdminRestoreOwner_Fails(OrganizationUserType restoringUserType, Organization organization, [OrganizationUser(OrganizationUserStatusType.Confirmed)] OrganizationUser restoringUser, + public async Task RestoreUser_AdminRestoreOwner_Fails(OrganizationUserType restoringUserType, + Organization organization, [OrganizationUser(OrganizationUserStatusType.Confirmed)] OrganizationUser restoringUser, [OrganizationUser(OrganizationUserStatusType.Revoked, OrganizationUserType.Owner)] OrganizationUser organizationUser, SutProvider sutProvider) { restoringUser.Type = restoringUserType; - RestoreRevokeUser_Setup(organization, restoringUser, organizationUser, sutProvider, OrganizationUserType.Admin); - var userService = Substitute.For(); + RestoreRevokeUser_Setup(organization, restoringUser, organizationUser, sutProvider); var organizationUserRepository = sutProvider.GetDependency(); var eventService = sutProvider.GetDependency(); var exception = await Assert.ThrowsAsync( - () => sutProvider.Sut.RestoreUserAsync(organizationUser, restoringUser.Id, userService)); + () => sutProvider.Sut.RestoreUserAsync(organizationUser, restoringUser.Id)); Assert.Contains("only owners can restore other owners", exception.Message.ToLowerInvariant()); @@ -2097,12 +1806,11 @@ OrganizationUserInvite invite, SutProvider sutProvider) { organizationUser.Status = userStatus; RestoreRevokeUser_Setup(organization, owner, organizationUser, sutProvider); - var userService = Substitute.For(); var organizationUserRepository = sutProvider.GetDependency(); var eventService = sutProvider.GetDependency(); var exception = await Assert.ThrowsAsync( - () => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id, userService)); + () => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id)); Assert.Contains("already active", exception.Message.ToLowerInvariant()); @@ -2111,37 +1819,6 @@ OrganizationUserInvite invite, SutProvider sutProvider) .LogOrganizationUserEventAsync(Arg.Any(), Arg.Any(), Arg.Any()); } - [Theory, BitAutoData] - public async Task RestoreUser_WithSingleOrgPolicyEnabled_Fails( - Organization organization, - [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner, - [OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser, - [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser secondOrganizationUser, - SutProvider sutProvider) - { - organizationUser.Email = null; // this is required to mock that the user as had already been confirmed before the revoke - secondOrganizationUser.UserId = organizationUser.UserId; - RestoreRevokeUser_Setup(organization, owner, organizationUser, sutProvider); - var userService = Substitute.For(); - var organizationUserRepository = sutProvider.GetDependency(); - var eventService = sutProvider.GetDependency(); - - organizationUserRepository.GetManyByUserAsync(organizationUser.UserId.Value).Returns(new[] { organizationUser, secondOrganizationUser }); - sutProvider.GetDependency() - .GetPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.SingleOrg, Arg.Any()) - .Returns(new[] { new OrganizationUserPolicyDetails { OrganizationId = organizationUser.OrganizationId, PolicyType = PolicyType.SingleOrg } }); - - var exception = await Assert.ThrowsAsync( - () => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id, userService)); - - Assert.Contains("you cannot restore this user until " + - "they leave or remove all other organizations.", exception.Message.ToLowerInvariant()); - - await organizationUserRepository.DidNotReceiveWithAnyArgs().RestoreAsync(Arg.Any(), Arg.Any()); - await eventService.DidNotReceiveWithAnyArgs() - .LogOrganizationUserEventAsync(Arg.Any(), Arg.Any(), Arg.Any()); - } - [Theory, BitAutoData] public async Task RestoreUser_WithOtherOrganizationSingleOrgPolicyEnabled_Fails( Organization organization, @@ -2151,7 +1828,6 @@ OrganizationUserInvite invite, SutProvider sutProvider) { organizationUser.Email = null; // this is required to mock that the user as had already been confirmed before the revoke RestoreRevokeUser_Setup(organization, owner, organizationUser, sutProvider); - var userService = Substitute.For(); var organizationUserRepository = sutProvider.GetDependency(); var eventService = sutProvider.GetDependency(); @@ -2160,7 +1836,7 @@ OrganizationUserInvite invite, SutProvider sutProvider) .Returns(true); var exception = await Assert.ThrowsAsync( - () => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id, userService)); + () => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id)); Assert.Contains("you cannot restore this user because they are a member of " + "another organization which forbids it", exception.Message.ToLowerInvariant()); @@ -2178,20 +1854,21 @@ OrganizationUserInvite invite, SutProvider sutProvider) SutProvider sutProvider) { organizationUser.Email = null; + + sutProvider.GetDependency() + .TwoFactorIsEnabledAsync(Arg.Is>(i => i.Contains(organizationUser.UserId.Value))) + .Returns(new List<(Guid userId, bool twoFactorIsEnabled)>() { (organizationUser.UserId.Value, false) }); + + RestoreRevokeUser_Setup(organization, owner, organizationUser, sutProvider); + var organizationUserRepository = sutProvider.GetDependency(); + var eventService = sutProvider.GetDependency(); + sutProvider.GetDependency() .GetPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.TwoFactorAuthentication, Arg.Any()) .Returns(new[] { new OrganizationUserPolicyDetails { OrganizationId = organizationUser.OrganizationId, PolicyType = PolicyType.TwoFactorAuthentication } }); - RestoreRevokeUser_Setup(organization, owner, organizationUser, sutProvider); - var userService = Substitute.For(); - var organizationUserRepository = sutProvider.GetDependency(); - var eventService = sutProvider.GetDependency(); - - - userService.TwoFactorIsEnabledAsync(Arg.Any()).Returns(false); - var exception = await Assert.ThrowsAsync( - () => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id, userService)); + () => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id)); Assert.Contains("you cannot restore this user until they enable " + "two-step login on their user account.", exception.Message.ToLowerInvariant()); @@ -2210,16 +1887,17 @@ OrganizationUserInvite invite, SutProvider sutProvider) { organizationUser.Email = null; // this is required to mock that the user as had already been confirmed before the revoke RestoreRevokeUser_Setup(organization, owner, organizationUser, sutProvider); - var userService = Substitute.For(); var organizationUserRepository = sutProvider.GetDependency(); var eventService = sutProvider.GetDependency(); sutProvider.GetDependency() .GetPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.TwoFactorAuthentication, Arg.Any()) .Returns(new[] { new OrganizationUserPolicyDetails { OrganizationId = organizationUser.OrganizationId, PolicyType = PolicyType.TwoFactorAuthentication } }); - userService.TwoFactorIsEnabledAsync(Arg.Any()).Returns(true); + sutProvider.GetDependency() + .TwoFactorIsEnabledAsync(Arg.Is>(i => i.Contains(organizationUser.UserId.Value))) + .Returns(new List<(Guid userId, bool twoFactorIsEnabled)>() { (organizationUser.UserId.Value, true) }); - await sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id, userService); + await sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id); await organizationUserRepository.Received().RestoreAsync(organizationUser.Id, OrganizationUserStatusType.Confirmed); await eventService.Received() @@ -2227,19 +1905,16 @@ OrganizationUserInvite invite, SutProvider sutProvider) } [Theory, BitAutoData] - public async Task RestoreUser_vNext_WithSingleOrgPolicyEnabled_Fails( + public async Task RestoreUser_WithSingleOrgPolicyEnabled_Fails( Organization organization, [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner, [OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser, [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser secondOrganizationUser, SutProvider sutProvider) { - sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.MembersTwoFAQueryOptimization).Returns(true); - organizationUser.Email = null; // this is required to mock that the user as had already been confirmed before the revoke secondOrganizationUser.UserId = organizationUser.UserId; RestoreRevokeUser_Setup(organization, owner, organizationUser, sutProvider); - var userService = Substitute.For(); var organizationUserRepository = sutProvider.GetDependency(); var eventService = sutProvider.GetDependency(); @@ -2252,7 +1927,7 @@ OrganizationUserInvite invite, SutProvider sutProvider) }); var exception = await Assert.ThrowsAsync( - () => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id, userService)); + () => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id)); Assert.Contains("you cannot restore this user until " + "they leave or remove all other organizations.", exception.Message.ToLowerInvariant()); @@ -2270,12 +1945,9 @@ OrganizationUserInvite invite, SutProvider sutProvider) [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser secondOrganizationUser, SutProvider sutProvider) { - sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.MembersTwoFAQueryOptimization).Returns(true); - organizationUser.Email = null; // this is required to mock that the user as had already been confirmed before the revoke secondOrganizationUser.UserId = organizationUser.UserId; RestoreRevokeUser_Setup(organization, owner, organizationUser, sutProvider); - var userService = Substitute.For(); var organizationUserRepository = sutProvider.GetDependency(); var eventService = sutProvider.GetDependency(); var twoFactorIsEnabledQuery = sutProvider.GetDependency(); @@ -2289,7 +1961,7 @@ OrganizationUserInvite invite, SutProvider sutProvider) .Returns(true); var exception = await Assert.ThrowsAsync( - () => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id, userService)); + () => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id)); Assert.Contains("you cannot restore this user because they are a member of " + "another organization which forbids it", exception.Message.ToLowerInvariant()); @@ -2306,20 +1978,18 @@ OrganizationUserInvite invite, SutProvider sutProvider) [OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser, SutProvider sutProvider) { - sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.MembersTwoFAQueryOptimization).Returns(true); - organizationUser.Email = null; + + RestoreRevokeUser_Setup(organization, owner, organizationUser, sutProvider); + var organizationUserRepository = sutProvider.GetDependency(); + var eventService = sutProvider.GetDependency(); + sutProvider.GetDependency() .GetPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.TwoFactorAuthentication, Arg.Any()) .Returns(new[] { new OrganizationUserPolicyDetails { OrganizationId = organizationUser.OrganizationId, PolicyType = PolicyType.TwoFactorAuthentication } }); - RestoreRevokeUser_Setup(organization, owner, organizationUser, sutProvider); - var userService = Substitute.For(); - var organizationUserRepository = sutProvider.GetDependency(); - var eventService = sutProvider.GetDependency(); - var exception = await Assert.ThrowsAsync( - () => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id, userService)); + () => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id)); Assert.Contains("you cannot restore this user until they enable " + "two-step login on their user account.", exception.Message.ToLowerInvariant()); @@ -2336,11 +2006,8 @@ OrganizationUserInvite invite, SutProvider sutProvider) [OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser, SutProvider sutProvider) { - sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.MembersTwoFAQueryOptimization).Returns(true); - organizationUser.Email = null; // this is required to mock that the user as had already been confirmed before the revoke RestoreRevokeUser_Setup(organization, owner, organizationUser, sutProvider); - var userService = Substitute.For(); var organizationUserRepository = sutProvider.GetDependency(); var eventService = sutProvider.GetDependency(); var twoFactorIsEnabledQuery = sutProvider.GetDependency(); @@ -2353,65 +2020,13 @@ OrganizationUserInvite invite, SutProvider sutProvider) .TwoFactorIsEnabledAsync(Arg.Is>(i => i.Contains(organizationUser.UserId.Value))) .Returns(new List<(Guid userId, bool twoFactorIsEnabled)>() { (organizationUser.UserId.Value, true) }); - await sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id, userService); + await sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id); await organizationUserRepository.Received().RestoreAsync(organizationUser.Id, OrganizationUserStatusType.Confirmed); await eventService.Received() .LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Restored); } - [Theory, BitAutoData] - public async Task HasConfirmedOwnersExcept_WithConfirmedOwner_ReturnsTrue(Organization organization, [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner, SutProvider sutProvider) - { - sutProvider.GetDependency() - .GetManyByOrganizationAsync(organization.Id, OrganizationUserType.Owner) - .Returns(new List { owner }); - - var result = await sutProvider.Sut.HasConfirmedOwnersExceptAsync(organization.Id, new List(), true); - - Assert.True(result); - } - - [Theory, BitAutoData] - public async Task HasConfirmedOwnersExcept_ExcludingConfirmedOwner_ReturnsFalse(Organization organization, [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner, SutProvider sutProvider) - { - sutProvider.GetDependency() - .GetManyByOrganizationAsync(organization.Id, OrganizationUserType.Owner) - .Returns(new List { owner }); - - var result = await sutProvider.Sut.HasConfirmedOwnersExceptAsync(organization.Id, new List { owner.Id }, true); - - Assert.False(result); - } - - [Theory, BitAutoData] - public async Task HasConfirmedOwnersExcept_WithInvitedOwner_ReturnsFalse(Organization organization, [OrganizationUser(OrganizationUserStatusType.Invited, OrganizationUserType.Owner)] OrganizationUser owner, SutProvider sutProvider) - { - sutProvider.GetDependency() - .GetManyByOrganizationAsync(organization.Id, OrganizationUserType.Owner) - .Returns(new List { owner }); - - var result = await sutProvider.Sut.HasConfirmedOwnersExceptAsync(organization.Id, new List(), true); - - Assert.False(result); - } - - [Theory] - [BitAutoData(true)] - [BitAutoData(false)] - public async Task HasConfirmedOwnersExcept_WithConfirmedProviderUser_IncludeProviderTrue_ReturnsTrue(bool includeProvider, Organization organization, ProviderUser providerUser, SutProvider sutProvider) - { - providerUser.Status = ProviderUserStatusType.Confirmed; - - sutProvider.GetDependency() - .GetManyByOrganizationAsync(organization.Id, ProviderUserStatusType.Confirmed) - .Returns(new List { providerUser }); - - var result = await sutProvider.Sut.HasConfirmedOwnersExceptAsync(organization.Id, new List(), includeProvider); - - Assert.Equal(includeProvider, result); - } - [Theory] [BitAutoData(PlanType.TeamsAnnually)] [BitAutoData(PlanType.TeamsMonthly)] @@ -2635,61 +2250,6 @@ OrganizationUserInvite invite, SutProvider sutProvider) Assert.Contains("custom users can only grant the same custom permissions that they have.", exception.Message.ToLowerInvariant()); } - [Theory, BitAutoData] - public async Task HasConfirmedOwnersExceptAsync_WithConfirmedOwners_ReturnsTrue( - Guid organizationId, - IEnumerable organizationUsersId, - ICollection owners, - SutProvider sutProvider) - { - sutProvider.GetDependency() - .GetManyByOrganizationAsync(organizationId, OrganizationUserType.Owner) - .Returns(owners); - - var result = await sutProvider.Sut.HasConfirmedOwnersExceptAsync(organizationId, organizationUsersId); - - Assert.True(result); - } - - [Theory, BitAutoData] - public async Task HasConfirmedOwnersExceptAsync_WithConfirmedProviders_ReturnsTrue( - Guid organizationId, - IEnumerable organizationUsersId, - ICollection providerUsers, - SutProvider sutProvider) - { - sutProvider.GetDependency() - .GetManyByOrganizationAsync(organizationId, OrganizationUserType.Owner) - .Returns(new List()); - - sutProvider.GetDependency() - .GetManyByOrganizationAsync(organizationId, ProviderUserStatusType.Confirmed) - .Returns(providerUsers); - - var result = await sutProvider.Sut.HasConfirmedOwnersExceptAsync(organizationId, organizationUsersId); - - Assert.True(result); - } - - [Theory, BitAutoData] - public async Task HasConfirmedOwnersExceptAsync_WithNoConfirmedOwnersOrProviders_ReturnsFalse( - Guid organizationId, - IEnumerable organizationUsersId, - SutProvider sutProvider) - { - sutProvider.GetDependency() - .GetManyByOrganizationAsync(organizationId, OrganizationUserType.Owner) - .Returns(new List()); - - sutProvider.GetDependency() - .GetManyByOrganizationAsync(organizationId, ProviderUserStatusType.Confirmed) - .Returns(new List()); - - var result = await sutProvider.Sut.HasConfirmedOwnersExceptAsync(organizationId, organizationUsersId); - - Assert.False(result); - } - [Theory] [BitAutoData(OrganizationUserType.Owner)] [BitAutoData(OrganizationUserType.Admin)] diff --git a/test/Core.Test/AdminConsole/Services/PolicyServiceTests.cs b/test/Core.Test/AdminConsole/Services/PolicyServiceTests.cs index fd7597a74..da3f2b267 100644 --- a/test/Core.Test/AdminConsole/Services/PolicyServiceTests.cs +++ b/test/Core.Test/AdminConsole/Services/PolicyServiceTests.cs @@ -1,12 +1,15 @@ 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; using Bit.Core.Auth.Entities; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Models.Data; using Bit.Core.Auth.Repositories; +using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Models.Data.Organizations.OrganizationUsers; @@ -32,8 +35,6 @@ public class PolicyServiceTests var badRequestException = await Assert.ThrowsAsync( () => sutProvider.Sut.SaveAsync(policy, - Substitute.For(), - Substitute.For(), Guid.NewGuid())); Assert.Contains("Organization not found", badRequestException.Message, StringComparison.OrdinalIgnoreCase); @@ -60,8 +61,6 @@ public class PolicyServiceTests var badRequestException = await Assert.ThrowsAsync( () => sutProvider.Sut.SaveAsync(policy, - Substitute.For(), - Substitute.For(), Guid.NewGuid())); Assert.Contains("cannot use policies", badRequestException.Message, StringComparison.OrdinalIgnoreCase); @@ -93,8 +92,6 @@ public class PolicyServiceTests var badRequestException = await Assert.ThrowsAsync( () => sutProvider.Sut.SaveAsync(policy, - Substitute.For(), - Substitute.For(), Guid.NewGuid())); Assert.Contains("Single Sign-On Authentication policy is enabled.", badRequestException.Message, StringComparison.OrdinalIgnoreCase); @@ -125,8 +122,6 @@ public class PolicyServiceTests var badRequestException = await Assert.ThrowsAsync( () => sutProvider.Sut.SaveAsync(policy, - Substitute.For(), - Substitute.For(), Guid.NewGuid())); Assert.Contains("Maximum Vault Timeout policy is enabled.", badRequestException.Message, StringComparison.OrdinalIgnoreCase); @@ -163,8 +158,6 @@ public class PolicyServiceTests var badRequestException = await Assert.ThrowsAsync( () => sutProvider.Sut.SaveAsync(policy, - Substitute.For(), - Substitute.For(), Guid.NewGuid())); Assert.Contains("Key Connector is enabled.", badRequestException.Message, StringComparison.OrdinalIgnoreCase); @@ -192,8 +185,6 @@ public class PolicyServiceTests var badRequestException = await Assert.ThrowsAsync( () => sutProvider.Sut.SaveAsync(policy, - Substitute.For(), - Substitute.For(), Guid.NewGuid())); Assert.Contains("Single Organization policy not enabled.", badRequestException.Message, StringComparison.OrdinalIgnoreCase); @@ -226,7 +217,7 @@ public class PolicyServiceTests var utcNow = DateTime.UtcNow; - await sutProvider.Sut.SaveAsync(policy, Substitute.For(), Substitute.For(), Guid.NewGuid()); + await sutProvider.Sut.SaveAsync(policy, Guid.NewGuid()); await sutProvider.GetDependency().Received() .LogPolicyEventAsync(policy, EventType.Policy_Updated); @@ -256,8 +247,6 @@ public class PolicyServiceTests var badRequestException = await Assert.ThrowsAsync( () => sutProvider.Sut.SaveAsync(policy, - Substitute.For(), - Substitute.For(), Guid.NewGuid())); Assert.Contains("Single Organization policy not enabled.", badRequestException.Message, StringComparison.OrdinalIgnoreCase); @@ -348,34 +337,38 @@ public class PolicyServiceTests orgUserDetailAdmin }); - var userService = Substitute.For(); - var organizationService = Substitute.For(); + sutProvider.GetDependency() + .TwoFactorIsEnabledAsync(Arg.Any>()) + .Returns(new List<(OrganizationUserUserDetails user, bool hasTwoFactor)>() + { + (orgUserDetailUserInvited, false), + (orgUserDetailUserAcceptedWith2FA, true), + (orgUserDetailUserAcceptedWithout2FA, false), + (orgUserDetailAdmin, false), + }); - userService.TwoFactorIsEnabledAsync(orgUserDetailUserInvited).Returns(false); - userService.TwoFactorIsEnabledAsync(orgUserDetailUserAcceptedWith2FA).Returns(true); - userService.TwoFactorIsEnabledAsync(orgUserDetailUserAcceptedWithout2FA).Returns(false); - userService.TwoFactorIsEnabledAsync(orgUserDetailAdmin).Returns(false); + var removeOrganizationUserCommand = sutProvider.GetDependency(); var utcNow = DateTime.UtcNow; var savingUserId = Guid.NewGuid(); - await sutProvider.Sut.SaveAsync(policy, userService, organizationService, savingUserId); + await sutProvider.Sut.SaveAsync(policy, savingUserId); - await organizationService.Received() + await removeOrganizationUserCommand.Received() .RemoveUserAsync(policy.OrganizationId, orgUserDetailUserAcceptedWithout2FA.Id, savingUserId); await sutProvider.GetDependency().Received() .SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(organization.DisplayName(), orgUserDetailUserAcceptedWithout2FA.Email); - await organizationService.DidNotReceive() + await removeOrganizationUserCommand.DidNotReceive() .RemoveUserAsync(policy.OrganizationId, orgUserDetailUserInvited.Id, savingUserId); await sutProvider.GetDependency().DidNotReceive() .SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(organization.DisplayName(), orgUserDetailUserInvited.Email); - await organizationService.DidNotReceive() + await removeOrganizationUserCommand.DidNotReceive() .RemoveUserAsync(policy.OrganizationId, orgUserDetailUserAcceptedWith2FA.Id, savingUserId); await sutProvider.GetDependency().DidNotReceive() .SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(organization.DisplayName(), orgUserDetailUserAcceptedWith2FA.Email); - await organizationService.DidNotReceive() + await removeOrganizationUserCommand.DidNotReceive() .RemoveUserAsync(policy.OrganizationId, orgUserDetailAdmin.Id, savingUserId); await sutProvider.GetDependency().DidNotReceive() .SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(organization.DisplayName(), orgUserDetailAdmin.Email); @@ -456,21 +449,28 @@ public class PolicyServiceTests orgUserDetailAdmin }); - var userService = Substitute.For(); - var organizationService = Substitute.For(); + sutProvider.GetDependency() + .TwoFactorIsEnabledAsync(Arg.Is>(ids => + ids.Contains(orgUserDetailUserWith2FANoMP.UserId.Value) + && ids.Contains(orgUserDetailUserWithout2FA.UserId.Value) + && ids.Contains(orgUserDetailAdmin.UserId.Value))) + .Returns(new List<(Guid userId, bool hasTwoFactor)>() + { + (orgUserDetailUserWith2FANoMP.UserId.Value, true), + (orgUserDetailUserWithout2FA.UserId.Value, false), + (orgUserDetailAdmin.UserId.Value, false), + }); - userService.TwoFactorIsEnabledAsync(orgUserDetailUserWith2FANoMP).Returns(true); - userService.TwoFactorIsEnabledAsync(orgUserDetailUserWithout2FA).Returns(false); - userService.TwoFactorIsEnabledAsync(orgUserDetailAdmin).Returns(false); + var removeOrganizationUserCommand = sutProvider.GetDependency(); var savingUserId = Guid.NewGuid(); var badRequestException = await Assert.ThrowsAsync( - () => sutProvider.Sut.SaveAsync(policy, userService, organizationService, savingUserId)); + () => sutProvider.Sut.SaveAsync(policy, savingUserId)); Assert.Contains("Policy could not be enabled. Non-compliant members will lose access to their accounts. Identify members without two-step login from the policies column in the members page.", badRequestException.Message, StringComparison.OrdinalIgnoreCase); - await organizationService.DidNotReceiveWithAnyArgs() + await removeOrganizationUserCommand.DidNotReceiveWithAnyArgs() .RemoveUserAsync(organizationId: default, organizationUserId: default, deletingUserId: default); await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() @@ -526,17 +526,18 @@ public class PolicyServiceTests orgUserDetail, }); - var userService = Substitute.For(); - var organizationService = Substitute.For(); - - userService.TwoFactorIsEnabledAsync(orgUserDetail) - .Returns(false); + sutProvider.GetDependency() + .TwoFactorIsEnabledAsync(Arg.Is>(ids => ids.Contains(orgUserDetail.UserId.Value))) + .Returns(new List<(Guid userId, bool hasTwoFactor)>() + { + (orgUserDetail.UserId.Value, false), + }); var utcNow = DateTime.UtcNow; var savingUserId = Guid.NewGuid(); - await sutProvider.Sut.SaveAsync(policy, userService, organizationService, savingUserId); + await sutProvider.Sut.SaveAsync(policy, savingUserId); await sutProvider.GetDependency().Received() .LogPolicyEventAsync(policy, EventType.Policy_Updated); @@ -579,8 +580,6 @@ public class PolicyServiceTests var badRequestException = await Assert.ThrowsAsync( () => sutProvider.Sut.SaveAsync(policy, - Substitute.For(), - Substitute.For(), Guid.NewGuid())); Assert.Contains("Trusted device encryption is on and requires this policy.", badRequestException.Message, StringComparison.OrdinalIgnoreCase); @@ -616,8 +615,6 @@ public class PolicyServiceTests var badRequestException = await Assert.ThrowsAsync( () => sutProvider.Sut.SaveAsync(policy, - Substitute.For(), - Substitute.For(), Guid.NewGuid())); Assert.Contains("Trusted device encryption is on and requires this policy.", badRequestException.Message, StringComparison.OrdinalIgnoreCase); @@ -650,8 +647,6 @@ public class PolicyServiceTests var badRequestException = await Assert.ThrowsAsync( () => sutProvider.Sut.SaveAsync(policy, - Substitute.For(), - Substitute.For(), Guid.NewGuid())); Assert.Contains("Single Organization policy not enabled.", badRequestException.Message, StringComparison.OrdinalIgnoreCase); @@ -684,8 +679,6 @@ public class PolicyServiceTests var badRequestException = await Assert.ThrowsAsync( () => sutProvider.Sut.SaveAsync(policy, - Substitute.For(), - Substitute.For(), Guid.NewGuid())); Assert.Contains("Account recovery policy is enabled.", badRequestException.Message, StringComparison.OrdinalIgnoreCase); @@ -823,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/Services/SsoConfigServiceTests.cs b/test/Core.Test/Auth/Services/SsoConfigServiceTests.cs index 9c63c6b01..e397c838c 100644 --- a/test/Core.Test/Auth/Services/SsoConfigServiceTests.cs +++ b/test/Core.Test/Auth/Services/SsoConfigServiceTests.cs @@ -11,7 +11,6 @@ using Bit.Core.Auth.Services; using Bit.Core.Exceptions; using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Repositories; -using Bit.Core.Services; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; @@ -342,16 +341,12 @@ public class SsoConfigServiceTests await sutProvider.GetDependency().Received(1) .SaveAsync( Arg.Is(t => t.Type == PolicyType.SingleOrg), - Arg.Any(), - Arg.Any(), null ); await sutProvider.GetDependency().Received(1) .SaveAsync( Arg.Is(t => t.Type == PolicyType.ResetPassword && t.GetDataModel().AutoEnrollEnabled), - Arg.Any(), - Arg.Any(), null ); 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/Core.Test/Billing/Services/PaymentHistoryServiceTests.cs b/test/Core.Test/Billing/Services/PaymentHistoryServiceTests.cs index 34bbb368b..c9278e448 100644 --- a/test/Core.Test/Billing/Services/PaymentHistoryServiceTests.cs +++ b/test/Core.Test/Billing/Services/PaymentHistoryServiceTests.cs @@ -29,7 +29,7 @@ public class PaymentHistoryServiceTests var result = await paymentHistoryService.GetInvoiceHistoryAsync(subscriber); // Assert - Assert.NotNull(result); + Assert.NotEmpty(result); Assert.Single(result); await stripeAdapter.Received(1).InvoiceListAsync(Arg.Any()); } @@ -47,7 +47,7 @@ public class PaymentHistoryServiceTests var result = await paymentHistoryService.GetInvoiceHistoryAsync(null); // Assert - Assert.Null(result); + Assert.Empty(result); } [Fact] @@ -66,7 +66,7 @@ public class PaymentHistoryServiceTests var result = await paymentHistoryService.GetTransactionHistoryAsync(subscriber); // Assert - Assert.NotNull(result); + Assert.NotEmpty(result); Assert.Single(result); await transactionRepository.Received(1).GetManyByOrganizationIdAsync(subscriber.Id, Arg.Any(), Arg.Any()); } @@ -84,6 +84,6 @@ public class PaymentHistoryServiceTests var result = await paymentHistoryService.GetTransactionHistoryAsync(null); // Assert - Assert.Null(result); + Assert.Empty(result); } } diff --git a/test/Core.Test/Core.Test.csproj b/test/Core.Test/Core.Test.csproj index a7aaa2302..4858afe54 100644 --- a/test/Core.Test/Core.Test.csproj +++ b/test/Core.Test/Core.Test.csproj @@ -17,7 +17,7 @@ - + diff --git a/test/Core.Test/NotificationCenter/Authorization/NotificationAuthorizationHandlerTest.cs b/test/Core.Test/NotificationCenter/Authorization/NotificationAuthorizationHandlerTest.cs new file mode 100644 index 000000000..9985d279b --- /dev/null +++ b/test/Core.Test/NotificationCenter/Authorization/NotificationAuthorizationHandlerTest.cs @@ -0,0 +1,419 @@ +#nullable enable +using Bit.Core.Context; +using Bit.Core.Test.NotificationCenter.AutoFixture; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; + +namespace Bit.Core.Test.NotificationCenter.Authorization; + +using System.Security.Claims; +using Bit.Core.NotificationCenter.Authorization; +using Bit.Core.NotificationCenter.Entities; +using Microsoft.AspNetCore.Authorization; +using NSubstitute; +using Xunit; + +[SutProviderCustomize] +[NotificationCustomize] +public class NotificationAuthorizationHandlerTests +{ + private static void SetupUserPermission(SutProvider sutProvider, + Guid? userId = null, Guid? organizationId = null, bool canAccessReports = false) + { + sutProvider.GetDependency().UserId.Returns(userId); + sutProvider.GetDependency().GetOrganization(organizationId.GetValueOrDefault(Guid.NewGuid())) + .Returns(new CurrentContextOrganization()); + sutProvider.GetDependency().AccessReports(organizationId.GetValueOrDefault(Guid.NewGuid())) + .Returns(canAccessReports); + } + + [Theory] + [BitAutoData] + public async Task HandleAsync_UnsupportedNotificationOperationRequirement_Throws( + SutProvider sutProvider, + Notification notification, ClaimsPrincipal claimsPrincipal) + { + SetupUserPermission(sutProvider, Guid.NewGuid()); + var requirement = new NotificationOperationsRequirement("UnsupportedOperation"); + var context = new AuthorizationHandlerContext(new List { requirement }, + claimsPrincipal, notification); + + await Assert.ThrowsAsync(() => sutProvider.Sut.HandleAsync(context)); + } + + [Theory] + [BitAutoData(nameof(NotificationOperations.Read))] + [BitAutoData(nameof(NotificationOperations.Create))] + [BitAutoData(nameof(NotificationOperations.Update))] + public async Task HandleAsync_NotLoggedIn_Unauthorized( + string requirementName, + SutProvider sutProvider, + Notification notification, ClaimsPrincipal claimsPrincipal) + { + SetupUserPermission(sutProvider, userId: null); + var requirement = new NotificationOperationsRequirement(requirementName); + var context = new AuthorizationHandlerContext(new List { requirement }, + claimsPrincipal, notification); + + await sutProvider.Sut.HandleAsync(context); + + Assert.False(context.HasSucceeded); + } + + [Theory] + [BitAutoData(nameof(NotificationOperations.Read))] + [BitAutoData(nameof(NotificationOperations.Create))] + [BitAutoData(nameof(NotificationOperations.Update))] + public async Task HandleAsync_ResourceEmpty_Unauthorized( + string requirementName, + SutProvider sutProvider, + ClaimsPrincipal claimsPrincipal) + { + SetupUserPermission(sutProvider, Guid.NewGuid()); + var requirement = new NotificationOperationsRequirement(requirementName); + var context = new AuthorizationHandlerContext(new List { requirement }, + claimsPrincipal, null); + + await sutProvider.Sut.HandleAsync(context); + + Assert.False(context.HasSucceeded); + } + + [Theory] + [BitAutoData] + [NotificationCustomize(global: true)] + public async Task HandleAsync_ReadRequirementGlobalNotification_Authorized( + SutProvider sutProvider, + Notification notification, ClaimsPrincipal claimsPrincipal) + { + SetupUserPermission(sutProvider, Guid.NewGuid()); + + var requirement = NotificationOperations.Read; + var context = new AuthorizationHandlerContext(new List { requirement }, + claimsPrincipal, notification); + + await sutProvider.Sut.HandleAsync(context); + + Assert.True(context.HasSucceeded); + } + + [Theory] + [BitAutoData(false)] + [BitAutoData(true)] + [NotificationCustomize(global: false)] + public async Task HandleAsync_ReadRequirementUserNotMatching_Unauthorized( + bool hasOrganizationId, + SutProvider sutProvider, + Notification notification, ClaimsPrincipal claimsPrincipal) + { + SetupUserPermission(sutProvider, Guid.NewGuid(), notification.OrganizationId); + + if (!hasOrganizationId) + { + notification.OrganizationId = null; + } + + var requirement = NotificationOperations.Read; + var context = new AuthorizationHandlerContext(new List { requirement }, + claimsPrincipal, notification); + + await sutProvider.Sut.HandleAsync(context); + + Assert.False(context.HasSucceeded); + } + + [Theory] + [BitAutoData] + [BitAutoData(false)] + [BitAutoData(true)] + [NotificationCustomize(global: false)] + public async Task HandleAsync_ReadRequirementOrganizationNotMatching_Unauthorized( + bool hasUserId, + SutProvider sutProvider, + Notification notification, ClaimsPrincipal claimsPrincipal) + { + SetupUserPermission(sutProvider, notification.UserId, Guid.NewGuid()); + + if (!hasUserId) + { + notification.UserId = null; + } + + var requirement = NotificationOperations.Read; + var context = new AuthorizationHandlerContext(new List { requirement }, + claimsPrincipal, notification); + + await sutProvider.Sut.HandleAsync(context); + + Assert.False(context.HasSucceeded); + } + + [Theory] + [BitAutoData(false, true)] + [BitAutoData(true, false)] + [BitAutoData(true, true)] + [NotificationCustomize(global: false)] + public async Task HandleAsync_ReadRequirement_Authorized( + bool hasUserId, + bool hasOrganizationId, + SutProvider sutProvider, + Notification notification, ClaimsPrincipal claimsPrincipal) + { + SetupUserPermission(sutProvider, notification.UserId, notification.OrganizationId); + + if (!hasUserId) + { + notification.UserId = null; + } + + if (!hasOrganizationId) + { + notification.OrganizationId = null; + } + + var requirement = NotificationOperations.Read; + var context = new AuthorizationHandlerContext(new List { requirement }, + claimsPrincipal, notification); + + await sutProvider.Sut.HandleAsync(context); + + Assert.True(context.HasSucceeded); + } + + [Theory] + [BitAutoData] + [NotificationCustomize(global: true)] + public async Task HandleAsync_CreateRequirementGlobalNotification_Unauthorized( + SutProvider sutProvider, + Notification notification, ClaimsPrincipal claimsPrincipal) + { + SetupUserPermission(sutProvider, Guid.NewGuid()); + var requirement = NotificationOperations.Create; + var context = new AuthorizationHandlerContext(new List { requirement }, + claimsPrincipal, notification); + + await sutProvider.Sut.HandleAsync(context); + + Assert.False(context.HasSucceeded); + } + + [Theory] + [BitAutoData] + [NotificationCustomize(global: false)] + public async Task HandleAsync_CreateRequirementUserNotMatching_Unauthorized( + SutProvider sutProvider, + Notification notification, ClaimsPrincipal claimsPrincipal) + { + SetupUserPermission(sutProvider, Guid.NewGuid(), notification.OrganizationId); + + notification.OrganizationId = null; + + var requirement = NotificationOperations.Create; + var context = new AuthorizationHandlerContext(new List { requirement }, + claimsPrincipal, notification); + + await sutProvider.Sut.HandleAsync(context); + + Assert.False(context.HasSucceeded); + } + + [Theory] + [BitAutoData] + [NotificationCustomize(global: false)] + public async Task HandleAsync_CreateRequirementOrganizationNotMatching_Unauthorized( + SutProvider sutProvider, + Notification notification, ClaimsPrincipal claimsPrincipal) + { + SetupUserPermission(sutProvider, notification.UserId, Guid.NewGuid()); + + var requirement = NotificationOperations.Create; + var context = new AuthorizationHandlerContext(new List { requirement }, + claimsPrincipal, notification); + + await sutProvider.Sut.HandleAsync(context); + + Assert.False(context.HasSucceeded); + } + + [Theory] + [BitAutoData] + [NotificationCustomize(global: false)] + public async Task HandleAsync_CreateRequirementOrganizationUserNoAccessReportsPermission_Unauthorized( + SutProvider sutProvider, + Notification notification, ClaimsPrincipal claimsPrincipal) + { + SetupUserPermission(sutProvider, notification.UserId, notification.OrganizationId, canAccessReports: false); + + var requirement = NotificationOperations.Create; + var context = new AuthorizationHandlerContext(new List { requirement }, + claimsPrincipal, notification); + + await sutProvider.Sut.HandleAsync(context); + + Assert.False(context.HasSucceeded); + } + + [Theory] + [BitAutoData] + [NotificationCustomize(global: false)] + public async Task HandleAsync_CreateRequirementUserNotPartOfOrganization_Authorized( + SutProvider sutProvider, + Notification notification, ClaimsPrincipal claimsPrincipal) + { + SetupUserPermission(sutProvider, notification.UserId); + + notification.OrganizationId = null; + + var requirement = NotificationOperations.Create; + var context = new AuthorizationHandlerContext(new List { requirement }, + claimsPrincipal, notification); + + await sutProvider.Sut.HandleAsync(context); + + Assert.True(context.HasSucceeded); + } + + [Theory] + [BitAutoData(false)] + [BitAutoData(true)] + [NotificationCustomize(global: false)] + public async Task HandleAsync_CreateRequirementOrganizationUserCanAccessReports_Authorized( + bool hasUserId, + SutProvider sutProvider, + Notification notification, ClaimsPrincipal claimsPrincipal) + { + SetupUserPermission(sutProvider, notification.UserId, notification.OrganizationId, true); + + if (!hasUserId) + { + notification.UserId = null; + } + + var requirement = NotificationOperations.Create; + var context = new AuthorizationHandlerContext(new List { requirement }, + claimsPrincipal, notification); + + await sutProvider.Sut.HandleAsync(context); + + Assert.True(context.HasSucceeded); + } + + // TODO + [Theory] + [BitAutoData] + [NotificationCustomize(global: true)] + public async Task HandleAsync_UpdateRequirementGlobalNotification_Unauthorized( + SutProvider sutProvider, + Notification notification, ClaimsPrincipal claimsPrincipal) + { + SetupUserPermission(sutProvider, Guid.NewGuid()); + var requirement = NotificationOperations.Update; + var context = new AuthorizationHandlerContext(new List { requirement }, + claimsPrincipal, notification); + + await sutProvider.Sut.HandleAsync(context); + + Assert.False(context.HasSucceeded); + } + + [Theory] + [BitAutoData] + [NotificationCustomize(global: false)] + public async Task HandleAsync_UpdateRequirementUserNotMatching_Unauthorized( + SutProvider sutProvider, + Notification notification, ClaimsPrincipal claimsPrincipal) + { + SetupUserPermission(sutProvider, Guid.NewGuid(), notification.OrganizationId); + + notification.OrganizationId = null; + + var requirement = NotificationOperations.Update; + var context = new AuthorizationHandlerContext(new List { requirement }, + claimsPrincipal, notification); + + await sutProvider.Sut.HandleAsync(context); + + Assert.False(context.HasSucceeded); + } + + [Theory] + [BitAutoData] + [NotificationCustomize(global: false)] + public async Task HandleAsync_UpdateRequirementOrganizationNotMatching_Unauthorized( + SutProvider sutProvider, + Notification notification, ClaimsPrincipal claimsPrincipal) + { + SetupUserPermission(sutProvider, notification.UserId, Guid.NewGuid()); + + var requirement = NotificationOperations.Update; + var context = new AuthorizationHandlerContext(new List { requirement }, + claimsPrincipal, notification); + + await sutProvider.Sut.HandleAsync(context); + + Assert.False(context.HasSucceeded); + } + + [Theory] + [BitAutoData] + [NotificationCustomize(global: false)] + public async Task HandleAsync_UpdateRequirementOrganizationUserNoAccessReportsPermission_Unauthorized( + SutProvider sutProvider, + Notification notification, ClaimsPrincipal claimsPrincipal) + { + SetupUserPermission(sutProvider, notification.UserId, notification.OrganizationId, canAccessReports: false); + + var requirement = NotificationOperations.Update; + var context = new AuthorizationHandlerContext(new List { requirement }, + claimsPrincipal, notification); + + await sutProvider.Sut.HandleAsync(context); + + Assert.False(context.HasSucceeded); + } + + [Theory] + [BitAutoData] + [NotificationCustomize(global: false)] + public async Task HandleAsync_UpdateRequirementUserNotPartOfOrganization_Authorized( + SutProvider sutProvider, + Notification notification, ClaimsPrincipal claimsPrincipal) + { + SetupUserPermission(sutProvider, notification.UserId); + + notification.OrganizationId = null; + + var requirement = NotificationOperations.Update; + var context = new AuthorizationHandlerContext(new List { requirement }, + claimsPrincipal, notification); + + await sutProvider.Sut.HandleAsync(context); + + Assert.True(context.HasSucceeded); + } + + [Theory] + [BitAutoData(false)] + [BitAutoData(true)] + [NotificationCustomize(global: false)] + public async Task HandleAsync_UpdateRequirementOrganizationUserCanAccessReports_Authorized( + bool hasUserId, + SutProvider sutProvider, + Notification notification, ClaimsPrincipal claimsPrincipal) + { + SetupUserPermission(sutProvider, notification.UserId, notification.OrganizationId, true); + + if (!hasUserId) + { + notification.UserId = null; + } + + var requirement = NotificationOperations.Update; + var context = new AuthorizationHandlerContext(new List { requirement }, + claimsPrincipal, notification); + + await sutProvider.Sut.HandleAsync(context); + + Assert.True(context.HasSucceeded); + } +} diff --git a/test/Core.Test/NotificationCenter/Authorization/NotificationStatusAuthorizationHandlerTest.cs b/test/Core.Test/NotificationCenter/Authorization/NotificationStatusAuthorizationHandlerTest.cs new file mode 100644 index 000000000..a43c40ea7 --- /dev/null +++ b/test/Core.Test/NotificationCenter/Authorization/NotificationStatusAuthorizationHandlerTest.cs @@ -0,0 +1,179 @@ +#nullable enable +using Bit.Core.Context; +using Bit.Core.Test.NotificationCenter.AutoFixture; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; + +namespace Bit.Core.Test.NotificationCenter.Authorization; + +using System.Security.Claims; +using Bit.Core.NotificationCenter.Authorization; +using Bit.Core.NotificationCenter.Entities; +using Microsoft.AspNetCore.Authorization; +using NSubstitute; +using Xunit; + +[SutProviderCustomize] +[NotificationStatusCustomize] +public class NotificationStatusAuthorizationHandlerTests +{ + private static void SetupUserPermission(SutProvider sutProvider, + Guid? userId = null) + { + sutProvider.GetDependency().UserId.Returns(userId); + } + + [Theory] + [BitAutoData] + public async Task HandleAsync_UnsupportedNotificationOperationRequirement_Throws( + SutProvider sutProvider, + NotificationStatus notificationStatus, ClaimsPrincipal claimsPrincipal) + { + SetupUserPermission(sutProvider, Guid.NewGuid()); + var requirement = new NotificationStatusOperationsRequirement("UnsupportedOperation"); + var context = new AuthorizationHandlerContext(new List { requirement }, + claimsPrincipal, notificationStatus); + + await Assert.ThrowsAsync(() => sutProvider.Sut.HandleAsync(context)); + } + + [Theory] + [BitAutoData(nameof(NotificationStatusOperations.Read))] + [BitAutoData(nameof(NotificationStatusOperations.Create))] + [BitAutoData(nameof(NotificationStatusOperations.Update))] + public async Task HandleAsync_NotLoggedIn_Unauthorized( + string requirementName, + SutProvider sutProvider, + NotificationStatus notificationStatus, ClaimsPrincipal claimsPrincipal) + { + SetupUserPermission(sutProvider, userId: null); + var requirement = new NotificationStatusOperationsRequirement(requirementName); + var context = new AuthorizationHandlerContext(new List { requirement }, + claimsPrincipal, notificationStatus); + + await sutProvider.Sut.HandleAsync(context); + + Assert.False(context.HasSucceeded); + } + + [Theory] + [BitAutoData(nameof(NotificationStatusOperations.Read))] + [BitAutoData(nameof(NotificationStatusOperations.Create))] + [BitAutoData(nameof(NotificationStatusOperations.Update))] + public async Task HandleAsync_ResourceEmpty_Unauthorized( + string requirementName, + SutProvider sutProvider, + ClaimsPrincipal claimsPrincipal) + { + SetupUserPermission(sutProvider, Guid.NewGuid()); + var requirement = new NotificationStatusOperationsRequirement(requirementName); + var context = new AuthorizationHandlerContext(new List { requirement }, + claimsPrincipal, null); + + await sutProvider.Sut.HandleAsync(context); + + Assert.False(context.HasSucceeded); + } + + [Theory] + [BitAutoData] + public async Task HandleAsync_ReadRequirementUserNotMatching_Unauthorized( + SutProvider sutProvider, + NotificationStatus notificationStatus, ClaimsPrincipal claimsPrincipal) + { + SetupUserPermission(sutProvider, Guid.NewGuid()); + + var requirement = NotificationStatusOperations.Read; + var context = new AuthorizationHandlerContext(new List { requirement }, + claimsPrincipal, notificationStatus); + + await sutProvider.Sut.HandleAsync(context); + + Assert.False(context.HasSucceeded); + } + + [Theory] + [BitAutoData] + public async Task HandleAsync_ReadRequirement_Authorized( + SutProvider sutProvider, + NotificationStatus notificationStatus, ClaimsPrincipal claimsPrincipal) + { + SetupUserPermission(sutProvider, notificationStatus.UserId); + + var requirement = NotificationStatusOperations.Read; + var context = new AuthorizationHandlerContext(new List { requirement }, + claimsPrincipal, notificationStatus); + + await sutProvider.Sut.HandleAsync(context); + + Assert.True(context.HasSucceeded); + } + + [Theory] + [BitAutoData] + public async Task HandleAsync_CreateRequirementUserNotMatching_Unauthorized( + SutProvider sutProvider, + NotificationStatus notificationStatus, ClaimsPrincipal claimsPrincipal) + { + SetupUserPermission(sutProvider, Guid.NewGuid()); + + var requirement = NotificationStatusOperations.Create; + var context = new AuthorizationHandlerContext(new List { requirement }, + claimsPrincipal, notificationStatus); + + await sutProvider.Sut.HandleAsync(context); + + Assert.False(context.HasSucceeded); + } + + [Theory] + [BitAutoData] + public async Task HandleAsync_CreateRequirement_Authorized( + SutProvider sutProvider, + NotificationStatus notificationStatus, ClaimsPrincipal claimsPrincipal) + { + SetupUserPermission(sutProvider, notificationStatus.UserId); + + var requirement = NotificationStatusOperations.Create; + var context = new AuthorizationHandlerContext(new List { requirement }, + claimsPrincipal, notificationStatus); + + await sutProvider.Sut.HandleAsync(context); + + Assert.True(context.HasSucceeded); + } + + [Theory] + [BitAutoData] + public async Task HandleAsync_UpdateRequirementUserNotMatching_Unauthorized( + SutProvider sutProvider, + NotificationStatus notificationStatus, ClaimsPrincipal claimsPrincipal) + { + SetupUserPermission(sutProvider, Guid.NewGuid()); + + var requirement = NotificationStatusOperations.Update; + var context = new AuthorizationHandlerContext(new List { requirement }, + claimsPrincipal, notificationStatus); + + await sutProvider.Sut.HandleAsync(context); + + Assert.False(context.HasSucceeded); + } + + [Theory] + [BitAutoData] + public async Task HandleAsync_UpdateRequirement_Authorized( + SutProvider sutProvider, + NotificationStatus notificationStatus, ClaimsPrincipal claimsPrincipal) + { + SetupUserPermission(sutProvider, notificationStatus.UserId); + + var requirement = NotificationStatusOperations.Update; + var context = new AuthorizationHandlerContext(new List { requirement }, + claimsPrincipal, notificationStatus); + + await sutProvider.Sut.HandleAsync(context); + + Assert.True(context.HasSucceeded); + } +} diff --git a/test/Core.Test/NotificationCenter/AutoFixture/NotificationFixtures.cs b/test/Core.Test/NotificationCenter/AutoFixture/NotificationFixtures.cs new file mode 100644 index 000000000..f14a0746a --- /dev/null +++ b/test/Core.Test/NotificationCenter/AutoFixture/NotificationFixtures.cs @@ -0,0 +1,31 @@ +#nullable enable +using AutoFixture; +using Bit.Core.NotificationCenter.Entities; +using Bit.Test.Common.AutoFixture.Attributes; + +namespace Bit.Core.Test.NotificationCenter.AutoFixture; + +public class NotificationCustomization(bool global) : ICustomization +{ + public void Customize(IFixture fixture) + { + fixture.Customize(composer => + { + var postprocessComposer = composer.With(n => n.Id, Guid.NewGuid()) + .With(n => n.Global, global); + + postprocessComposer = global + ? postprocessComposer.Without(n => n.UserId) + : postprocessComposer.With(n => n.UserId, Guid.NewGuid()); + + return global + ? postprocessComposer.Without(n => n.OrganizationId) + : postprocessComposer.With(n => n.OrganizationId, Guid.NewGuid()); + }); + } +} + +public class NotificationCustomizeAttribute(bool global = true) : BitCustomizeAttribute +{ + public override ICustomization GetCustomization() => new NotificationCustomization(global); +} diff --git a/test/Core.Test/NotificationCenter/AutoFixture/NotificationStatusDetailsFixtures.cs b/test/Core.Test/NotificationCenter/AutoFixture/NotificationStatusDetailsFixtures.cs new file mode 100644 index 000000000..1e1d066d1 --- /dev/null +++ b/test/Core.Test/NotificationCenter/AutoFixture/NotificationStatusDetailsFixtures.cs @@ -0,0 +1,21 @@ +#nullable enable +using AutoFixture; +using Bit.Core.NotificationCenter.Models.Data; +using Bit.Test.Common.AutoFixture.Attributes; + +namespace Bit.Core.Test.NotificationCenter.AutoFixture; + +public class NotificationStatusDetailsCustomization : ICustomization +{ + public void Customize(IFixture fixture) + { + fixture.Customize(composer => composer.With(n => n.Id, Guid.NewGuid()) + .With(n => n.UserId, Guid.NewGuid()) + .With(n => n.OrganizationId, Guid.NewGuid())); + } +} + +public class NotificationStatusDetailsCustomizeAttribute : BitCustomizeAttribute +{ + public override ICustomization GetCustomization() => new NotificationStatusDetailsCustomization(); +} diff --git a/test/Core.Test/NotificationCenter/AutoFixture/NotificationStatusFixtures.cs b/test/Core.Test/NotificationCenter/AutoFixture/NotificationStatusFixtures.cs new file mode 100644 index 000000000..40eccb342 --- /dev/null +++ b/test/Core.Test/NotificationCenter/AutoFixture/NotificationStatusFixtures.cs @@ -0,0 +1,20 @@ +#nullable enable +using AutoFixture; +using Bit.Core.NotificationCenter.Entities; +using Bit.Test.Common.AutoFixture.Attributes; + +namespace Bit.Core.Test.NotificationCenter.AutoFixture; + +public class NotificationStatusCustomization : ICustomization +{ + public void Customize(IFixture fixture) + { + fixture.Customize(composer => composer.With(ns => ns.NotificationId, Guid.NewGuid()) + .With(ns => ns.UserId, Guid.NewGuid())); + } +} + +public class NotificationStatusCustomizeAttribute : BitCustomizeAttribute +{ + public override ICustomization GetCustomization() => new NotificationStatusCustomization(); +} diff --git a/test/Core.Test/NotificationCenter/Commands/CreateNotificationCommandTest.cs b/test/Core.Test/NotificationCenter/Commands/CreateNotificationCommandTest.cs new file mode 100644 index 000000000..4f5842d1c --- /dev/null +++ b/test/Core.Test/NotificationCenter/Commands/CreateNotificationCommandTest.cs @@ -0,0 +1,59 @@ +#nullable enable +using System.Security.Claims; +using Bit.Core.Exceptions; +using Bit.Core.NotificationCenter.Authorization; +using Bit.Core.NotificationCenter.Commands; +using Bit.Core.NotificationCenter.Entities; +using Bit.Core.NotificationCenter.Repositories; +using Bit.Core.Test.NotificationCenter.AutoFixture; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Microsoft.AspNetCore.Authorization; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.NotificationCenter.Commands; + +[SutProviderCustomize] +[NotificationCustomize] +public class CreateNotificationCommandTest +{ + private static void Setup(SutProvider sutProvider, + Notification notification, bool authorized = false) + { + sutProvider.GetDependency() + .CreateAsync(notification) + .Returns(notification); + sutProvider.GetDependency() + .AuthorizeAsync(Arg.Any(), notification, + Arg.Is>(reqs => + reqs.Contains(NotificationOperations.Create))) + .Returns(authorized ? AuthorizationResult.Success() : AuthorizationResult.Failed()); + } + + [Theory] + [BitAutoData] + public async Task CreateAsync_AuthorizationFailed_NotFoundException( + SutProvider sutProvider, + Notification notification) + { + Setup(sutProvider, notification, authorized: false); + + await Assert.ThrowsAsync(() => sutProvider.Sut.CreateAsync(notification)); + } + + [Theory] + [BitAutoData] + public async Task CreateAsync_Authorized_NotificationCreated( + SutProvider sutProvider, + Notification notification) + { + Setup(sutProvider, notification, true); + + var newNotification = await sutProvider.Sut.CreateAsync(notification); + + Assert.Equal(notification, newNotification); + Assert.Equal(DateTime.UtcNow, notification.CreationDate, TimeSpan.FromMinutes(1)); + Assert.Equal(notification.CreationDate, notification.RevisionDate); + } +} diff --git a/test/Core.Test/NotificationCenter/Commands/CreateNotificationStatusCommandTest.cs b/test/Core.Test/NotificationCenter/Commands/CreateNotificationStatusCommandTest.cs new file mode 100644 index 000000000..8dc852492 --- /dev/null +++ b/test/Core.Test/NotificationCenter/Commands/CreateNotificationStatusCommandTest.cs @@ -0,0 +1,89 @@ +#nullable enable +using System.Security.Claims; +using Bit.Core.Exceptions; +using Bit.Core.NotificationCenter.Authorization; +using Bit.Core.NotificationCenter.Commands; +using Bit.Core.NotificationCenter.Entities; +using Bit.Core.NotificationCenter.Repositories; +using Bit.Core.Test.NotificationCenter.AutoFixture; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Microsoft.AspNetCore.Authorization; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.NotificationCenter.Commands; + +[SutProviderCustomize] +[NotificationCustomize] +[NotificationStatusCustomize] +public class CreateNotificationStatusCommandTest +{ + private static void Setup(SutProvider sutProvider, + Notification? notification, NotificationStatus notificationStatus, + bool authorizedNotification = false, bool authorizedCreate = false) + { + sutProvider.GetDependency() + .GetByIdAsync(notificationStatus.NotificationId) + .Returns(notification); + sutProvider.GetDependency() + .CreateAsync(notificationStatus) + .Returns(notificationStatus); + sutProvider.GetDependency() + .AuthorizeAsync(Arg.Any(), notification ?? Arg.Any(), + Arg.Is>(reqs => + reqs.Contains(NotificationOperations.Read))) + .Returns(authorizedNotification ? AuthorizationResult.Success() : AuthorizationResult.Failed()); + sutProvider.GetDependency() + .AuthorizeAsync(Arg.Any(), notificationStatus, + Arg.Is>(reqs => + reqs.Contains(NotificationStatusOperations.Create))) + .Returns(authorizedCreate ? AuthorizationResult.Success() : AuthorizationResult.Failed()); + } + + [Theory] + [BitAutoData] + public async Task CreateAsync_NotificationNotFound_NotFoundException( + SutProvider sutProvider, + NotificationStatus notificationStatus) + { + Setup(sutProvider, notification: null, notificationStatus, true, true); + + await Assert.ThrowsAsync(() => sutProvider.Sut.CreateAsync(notificationStatus)); + } + + [Theory] + [BitAutoData] + public async Task CreateAsync_NotificationReadNotAuthorized_NotFoundException( + SutProvider sutProvider, + Notification notification, NotificationStatus notificationStatus) + { + Setup(sutProvider, notification, notificationStatus, authorizedNotification: false, true); + + await Assert.ThrowsAsync(() => sutProvider.Sut.CreateAsync(notificationStatus)); + } + + [Theory] + [BitAutoData] + public async Task CreateAsync_CreateNotAuthorized_NotFoundException( + SutProvider sutProvider, + Notification notification, NotificationStatus notificationStatus) + { + Setup(sutProvider, notification, notificationStatus, true, authorizedCreate: false); + + await Assert.ThrowsAsync(() => sutProvider.Sut.CreateAsync(notificationStatus)); + } + + [Theory] + [BitAutoData] + public async Task CreateAsync_NotificationFoundAuthorized_NotificationStatusCreated( + SutProvider sutProvider, + Notification notification, NotificationStatus notificationStatus) + { + Setup(sutProvider, notification, notificationStatus, true, true); + + var newNotificationStatus = await sutProvider.Sut.CreateAsync(notificationStatus); + + Assert.Equal(notificationStatus, newNotificationStatus); + } +} diff --git a/test/Core.Test/NotificationCenter/Commands/MarkNotificationDeletedCommandTest.cs b/test/Core.Test/NotificationCenter/Commands/MarkNotificationDeletedCommandTest.cs new file mode 100644 index 000000000..a5bb20423 --- /dev/null +++ b/test/Core.Test/NotificationCenter/Commands/MarkNotificationDeletedCommandTest.cs @@ -0,0 +1,151 @@ +#nullable enable +using System.Security.Claims; +using Bit.Core.Context; +using Bit.Core.Exceptions; +using Bit.Core.NotificationCenter.Authorization; +using Bit.Core.NotificationCenter.Commands; +using Bit.Core.NotificationCenter.Entities; +using Bit.Core.NotificationCenter.Repositories; +using Bit.Core.Test.NotificationCenter.AutoFixture; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Microsoft.AspNetCore.Authorization; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.NotificationCenter.Commands; + +[SutProviderCustomize] +[NotificationCustomize] +[NotificationStatusCustomize] +public class MarkNotificationDeletedCommandTest +{ + private static void Setup(SutProvider sutProvider, + Guid notificationId, Guid? userId, Notification? notification, NotificationStatus? notificationStatus, + bool authorizedNotification = false, bool authorizedCreate = false, bool authorizedUpdate = false) + { + sutProvider.GetDependency().UserId.Returns(userId); + sutProvider.GetDependency() + .GetByIdAsync(notificationId) + .Returns(notification); + sutProvider.GetDependency() + .GetByNotificationIdAndUserIdAsync(notificationId, userId ?? Arg.Any()) + .Returns(notificationStatus); + sutProvider.GetDependency() + .CreateAsync(Arg.Any()); + sutProvider.GetDependency() + .UpdateAsync(notificationStatus ?? Arg.Any()); + sutProvider.GetDependency() + .AuthorizeAsync(Arg.Any(), notification ?? Arg.Any(), + Arg.Is>(reqs => + reqs.Contains(NotificationOperations.Read))) + .Returns(authorizedNotification ? AuthorizationResult.Success() : AuthorizationResult.Failed()); + sutProvider.GetDependency() + .AuthorizeAsync(Arg.Any(), notificationStatus ?? Arg.Any(), + Arg.Is>(reqs => + reqs.Contains(NotificationStatusOperations.Create))) + .Returns(authorizedCreate ? AuthorizationResult.Success() : AuthorizationResult.Failed()); + sutProvider.GetDependency() + .AuthorizeAsync(Arg.Any(), notificationStatus ?? Arg.Any(), + Arg.Is>(reqs => + reqs.Contains(NotificationStatusOperations.Update))) + .Returns(authorizedUpdate ? AuthorizationResult.Success() : AuthorizationResult.Failed()); + + sutProvider.GetDependency().ClearReceivedCalls(); + } + + [Theory] + [BitAutoData] + public async Task MarkDeletedAsync_NotLoggedIn_NotFoundException( + SutProvider sutProvider, + Guid notificationId, Notification notification, NotificationStatus notificationStatus) + { + Setup(sutProvider, notificationId, userId: null, notification, notificationStatus, true, true, true); + + await Assert.ThrowsAsync(() => sutProvider.Sut.MarkDeletedAsync(notificationId)); + } + + [Theory] + [BitAutoData] + public async Task MarkDeletedAsync_NotificationNotFound_NotFoundException( + SutProvider sutProvider, + Guid notificationId, Guid userId, NotificationStatus notificationStatus) + { + Setup(sutProvider, notificationId, userId, notification: null, notificationStatus, true, true, true); + + await Assert.ThrowsAsync(() => sutProvider.Sut.MarkDeletedAsync(notificationId)); + } + + [Theory] + [BitAutoData] + public async Task MarkDeletedAsync_ReadRequirementNotificationNotAuthorized_NotFoundException( + SutProvider sutProvider, + Guid notificationId, Guid userId, Notification notification, NotificationStatus notificationStatus) + { + Setup(sutProvider, notificationId, userId, notification, notificationStatus, authorizedNotification: false, + true, true); + + await Assert.ThrowsAsync(() => sutProvider.Sut.MarkDeletedAsync(notificationId)); + } + + [Theory] + [BitAutoData] + public async Task MarkDeletedAsync_CreateRequirementNotAuthorized_NotFoundException( + SutProvider sutProvider, + Guid notificationId, Guid userId, Notification notification) + { + Setup(sutProvider, notificationId, userId, notification, notificationStatus: null, true, + authorizedCreate: false, true); + + await Assert.ThrowsAsync(() => sutProvider.Sut.MarkDeletedAsync(notificationId)); + } + + [Theory] + [BitAutoData] + public async Task MarkDeletedAsync_UpdateRequirementNotAuthorized_NotFoundException( + SutProvider sutProvider, + Guid notificationId, Guid userId, Notification notification, NotificationStatus notificationStatus) + { + Setup(sutProvider, notificationId, userId, notification, notificationStatus, true, true, + authorizedUpdate: false); + + await Assert.ThrowsAsync(() => sutProvider.Sut.MarkDeletedAsync(notificationId)); + } + + [Theory] + [BitAutoData] + public async Task MarkDeletedAsync_NotificationStatusNotFoundCreateAuthorized_NotificationStatusCreated( + SutProvider sutProvider, + Guid notificationId, Guid userId, Notification notification) + { + Setup(sutProvider, notificationId, userId, notification, notificationStatus: null, true, true, true); + + await sutProvider.Sut.MarkDeletedAsync(notificationId); + + await sutProvider.GetDependency().Received(1) + .CreateAsync(Arg.Is(ns => + ns.NotificationId == notificationId && ns.UserId == userId && !ns.ReadDate.HasValue && + ns.DeletedDate.HasValue && DateTime.UtcNow - ns.DeletedDate.Value < TimeSpan.FromMinutes(1))); + } + + [Theory] + [BitAutoData] + public async Task MarkDeletedAsync_NotificationStatusFoundCreateAuthorized_NotificationStatusUpdated( + SutProvider sutProvider, + Guid notificationId, Guid userId, Notification notification, NotificationStatus notificationStatus) + { + var deletedDate = notificationStatus.DeletedDate; + + Setup(sutProvider, notificationId, userId, notification, notificationStatus, true, true, true); + + await sutProvider.Sut.MarkDeletedAsync(notificationId); + + await sutProvider.GetDependency().Received(1) + .UpdateAsync(Arg.Is(ns => + ns.Equals(notificationStatus) && + ns.NotificationId == notificationStatus.NotificationId && ns.UserId == notificationStatus.UserId && + ns.ReadDate == notificationStatus.ReadDate && ns.DeletedDate != deletedDate && + ns.DeletedDate.HasValue && + DateTime.UtcNow - ns.DeletedDate.Value < TimeSpan.FromMinutes(1))); + } +} diff --git a/test/Core.Test/NotificationCenter/Commands/MarkNotificationReadCommandTest.cs b/test/Core.Test/NotificationCenter/Commands/MarkNotificationReadCommandTest.cs new file mode 100644 index 000000000..f80234c07 --- /dev/null +++ b/test/Core.Test/NotificationCenter/Commands/MarkNotificationReadCommandTest.cs @@ -0,0 +1,151 @@ +#nullable enable +using System.Security.Claims; +using Bit.Core.Context; +using Bit.Core.Exceptions; +using Bit.Core.NotificationCenter.Authorization; +using Bit.Core.NotificationCenter.Commands; +using Bit.Core.NotificationCenter.Entities; +using Bit.Core.NotificationCenter.Repositories; +using Bit.Core.Test.NotificationCenter.AutoFixture; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Microsoft.AspNetCore.Authorization; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.NotificationCenter.Commands; + +[SutProviderCustomize] +[NotificationCustomize] +[NotificationStatusCustomize] +public class MarkNotificationReadCommandTest +{ + private static void Setup(SutProvider sutProvider, + Guid notificationId, Guid? userId, Notification? notification, NotificationStatus? notificationStatus, + bool authorizedNotification = false, bool authorizedCreate = false, bool authorizedUpdate = false) + { + sutProvider.GetDependency().UserId.Returns(userId); + sutProvider.GetDependency() + .GetByIdAsync(notificationId) + .Returns(notification); + sutProvider.GetDependency() + .GetByNotificationIdAndUserIdAsync(notificationId, userId ?? Arg.Any()) + .Returns(notificationStatus); + sutProvider.GetDependency() + .CreateAsync(Arg.Any()); + sutProvider.GetDependency() + .UpdateAsync(notificationStatus ?? Arg.Any()); + sutProvider.GetDependency() + .AuthorizeAsync(Arg.Any(), notification ?? Arg.Any(), + Arg.Is>(reqs => + reqs.Contains(NotificationOperations.Read))) + .Returns(authorizedNotification ? AuthorizationResult.Success() : AuthorizationResult.Failed()); + sutProvider.GetDependency() + .AuthorizeAsync(Arg.Any(), notificationStatus ?? Arg.Any(), + Arg.Is>(reqs => + reqs.Contains(NotificationStatusOperations.Create))) + .Returns(authorizedCreate ? AuthorizationResult.Success() : AuthorizationResult.Failed()); + sutProvider.GetDependency() + .AuthorizeAsync(Arg.Any(), notificationStatus ?? Arg.Any(), + Arg.Is>(reqs => + reqs.Contains(NotificationStatusOperations.Update))) + .Returns(authorizedUpdate ? AuthorizationResult.Success() : AuthorizationResult.Failed()); + + sutProvider.GetDependency().ClearReceivedCalls(); + } + + [Theory] + [BitAutoData] + public async Task MarkReadAsync_NotLoggedIn_NotFoundException( + SutProvider sutProvider, + Guid notificationId, Notification notification, NotificationStatus notificationStatus) + { + Setup(sutProvider, notificationId, userId: null, notification, notificationStatus, true, true, true); + + await Assert.ThrowsAsync(() => sutProvider.Sut.MarkReadAsync(notificationId)); + } + + [Theory] + [BitAutoData] + public async Task MarkReadAsync_NotificationNotFound_NotFoundException( + SutProvider sutProvider, + Guid notificationId, Guid userId, NotificationStatus notificationStatus) + { + Setup(sutProvider, notificationId, userId, notification: null, notificationStatus, true, true, true); + + await Assert.ThrowsAsync(() => sutProvider.Sut.MarkReadAsync(notificationId)); + } + + [Theory] + [BitAutoData] + public async Task MarkReadAsync_ReadRequirementNotificationNotAuthorized_NotFoundException( + SutProvider sutProvider, + Guid notificationId, Guid userId, Notification notification, NotificationStatus notificationStatus) + { + Setup(sutProvider, notificationId, userId, notification, notificationStatus, authorizedNotification: false, + true, true); + + await Assert.ThrowsAsync(() => sutProvider.Sut.MarkReadAsync(notificationId)); + } + + [Theory] + [BitAutoData] + public async Task MarkReadAsync_CreateRequirementNotAuthorized_NotFoundException( + SutProvider sutProvider, + Guid notificationId, Guid userId, Notification notification) + { + Setup(sutProvider, notificationId, userId, notification, notificationStatus: null, true, + authorizedCreate: false, true); + + await Assert.ThrowsAsync(() => sutProvider.Sut.MarkReadAsync(notificationId)); + } + + [Theory] + [BitAutoData] + public async Task MarkReadAsync_UpdateRequirementNotAuthorized_NotFoundException( + SutProvider sutProvider, + Guid notificationId, Guid userId, Notification notification, NotificationStatus notificationStatus) + { + Setup(sutProvider, notificationId, userId, notification, notificationStatus, true, true, + authorizedUpdate: false); + + await Assert.ThrowsAsync(() => sutProvider.Sut.MarkReadAsync(notificationId)); + } + + [Theory] + [BitAutoData] + public async Task MarkReadAsync_NotificationStatusNotFoundCreateAuthorized_NotificationStatusCreated( + SutProvider sutProvider, + Guid notificationId, Guid userId, Notification notification) + { + Setup(sutProvider, notificationId, userId, notification, notificationStatus: null, true, true, true); + + await sutProvider.Sut.MarkReadAsync(notificationId); + + await sutProvider.GetDependency().Received(1) + .CreateAsync(Arg.Is(ns => + ns.NotificationId == notificationId && ns.UserId == userId && !ns.DeletedDate.HasValue && + ns.ReadDate.HasValue && DateTime.UtcNow - ns.ReadDate.Value < TimeSpan.FromMinutes(1))); + } + + [Theory] + [BitAutoData] + public async Task MarkReadAsync_NotificationStatusFoundCreateAuthorized_NotificationStatusUpdated( + SutProvider sutProvider, + Guid notificationId, Guid userId, Notification notification, NotificationStatus notificationStatus) + { + var readDate = notificationStatus.ReadDate; + + Setup(sutProvider, notificationId, userId, notification, notificationStatus, true, true, true); + + await sutProvider.Sut.MarkReadAsync(notificationId); + + await sutProvider.GetDependency().Received(1) + .UpdateAsync(Arg.Is(ns => + ns.Equals(notificationStatus) && + ns.NotificationId == notificationStatus.NotificationId && ns.UserId == notificationStatus.UserId && + ns.DeletedDate == notificationStatus.DeletedDate && ns.ReadDate != readDate && + ns.ReadDate.HasValue && + DateTime.UtcNow - ns.ReadDate.Value < TimeSpan.FromMinutes(1))); + } +} diff --git a/test/Core.Test/NotificationCenter/Commands/UpdateNotificationCommandTest.cs b/test/Core.Test/NotificationCenter/Commands/UpdateNotificationCommandTest.cs new file mode 100644 index 000000000..976d1d77a --- /dev/null +++ b/test/Core.Test/NotificationCenter/Commands/UpdateNotificationCommandTest.cs @@ -0,0 +1,95 @@ +#nullable enable +using System.Security.Claims; +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.NotificationCenter.Authorization; +using Bit.Core.NotificationCenter.Commands; +using Bit.Core.NotificationCenter.Entities; +using Bit.Core.NotificationCenter.Enums; +using Bit.Core.NotificationCenter.Repositories; +using Bit.Core.Test.NotificationCenter.AutoFixture; +using Bit.Core.Utilities; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Microsoft.AspNetCore.Authorization; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.NotificationCenter.Commands; + +[SutProviderCustomize] +[NotificationCustomize] +public class UpdateNotificationCommandTest +{ + private static void Setup(SutProvider sutProvider, + Guid notificationId, Notification? notification, bool authorized = false) + { + sutProvider.GetDependency() + .GetByIdAsync(notificationId) + .Returns(notification); + sutProvider.GetDependency() + .AuthorizeAsync(Arg.Any(), notification ?? Arg.Any(), + Arg.Is>(reqs => + reqs.Contains(NotificationOperations.Update))) + .Returns(authorized ? AuthorizationResult.Success() : AuthorizationResult.Failed()); + + sutProvider.GetDependency().ClearReceivedCalls(); + } + + [Theory] + [BitAutoData] + public async Task UpdateAsync_NotificationNotFound_NotFoundException( + SutProvider sutProvider, + Notification notification) + { + Setup(sutProvider, notification.Id, notification: null, true); + + await Assert.ThrowsAsync(() => sutProvider.Sut.UpdateAsync(notification)); + } + + [Theory] + [BitAutoData] + public async Task UpdateAsync_AuthorizationFailed_NotFoundException( + SutProvider sutProvider, + Notification notification) + { + Setup(sutProvider, notification.Id, notification, authorized: false); + + await Assert.ThrowsAsync(() => sutProvider.Sut.UpdateAsync(notification)); + } + + [Theory] + [BitAutoData] + public async Task UpdateAsync_Authorized_NotificationCreated( + SutProvider sutProvider, + Notification notification) + { + notification.Priority = Priority.Medium; + notification.ClientType = ClientType.Web; + notification.Title = "Title"; + notification.Body = "Body"; + notification.RevisionDate = DateTime.UtcNow.AddMinutes(-60); + + Setup(sutProvider, notification.Id, notification, true); + + var notificationToUpdate = CoreHelpers.CloneObject(notification); + notificationToUpdate.Priority = Priority.High; + notificationToUpdate.ClientType = ClientType.Mobile; + notificationToUpdate.Title = "Updated Title"; + notificationToUpdate.Body = "Updated Body"; + notificationToUpdate.RevisionDate = DateTime.UtcNow.AddMinutes(-30); + + await sutProvider.Sut.UpdateAsync(notificationToUpdate); + + await sutProvider.GetDependency().Received(1) + .ReplaceAsync(Arg.Is(n => + // Not updated fields + n.Id == notificationToUpdate.Id && n.Global == notificationToUpdate.Global && + n.UserId == notificationToUpdate.UserId && n.OrganizationId == notificationToUpdate.OrganizationId && + n.CreationDate == notificationToUpdate.CreationDate && + // Updated fields + n.Priority == notificationToUpdate.Priority && n.ClientType == notificationToUpdate.ClientType && + n.Title == notificationToUpdate.Title && n.Body == notificationToUpdate.Body && + DateTime.UtcNow - n.RevisionDate < TimeSpan.FromMinutes(1))); + } +} diff --git a/test/Core.Test/NotificationCenter/Queries/GetNotificationStatusDetailsForUserQueryTest.cs b/test/Core.Test/NotificationCenter/Queries/GetNotificationStatusDetailsForUserQueryTest.cs new file mode 100644 index 000000000..7d9c26560 --- /dev/null +++ b/test/Core.Test/NotificationCenter/Queries/GetNotificationStatusDetailsForUserQueryTest.cs @@ -0,0 +1,55 @@ +#nullable enable +using Bit.Core.Context; +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.NotificationCenter.Models.Data; +using Bit.Core.NotificationCenter.Models.Filter; +using Bit.Core.NotificationCenter.Queries; +using Bit.Core.NotificationCenter.Repositories; +using Bit.Core.Test.NotificationCenter.AutoFixture; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.NotificationCenter.Queries; + +[SutProviderCustomize] +[NotificationStatusDetailsCustomize] +public class GetNotificationStatusDetailsForUserQueryTest +{ + private static void Setup(SutProvider sutProvider, + List notificationsStatusDetails, NotificationStatusFilter statusFilter, Guid? userId) + { + sutProvider.GetDependency().UserId.Returns(userId); + sutProvider.GetDependency().GetByUserIdAndStatusAsync( + userId.GetValueOrDefault(Guid.NewGuid()), Arg.Any(), statusFilter) + .Returns(notificationsStatusDetails); + } + + [Theory] + [BitAutoData] + public async Task GetByUserIdStatusFilterAsync_NotLoggedIn_NotFoundException( + SutProvider sutProvider, + List notificationsStatusDetails, NotificationStatusFilter notificationStatusFilter) + { + Setup(sutProvider, notificationsStatusDetails, notificationStatusFilter, userId: null); + + await Assert.ThrowsAsync(() => + sutProvider.Sut.GetByUserIdStatusFilterAsync(notificationStatusFilter)); + } + + [Theory] + [BitAutoData] + public async Task GetByUserIdStatusFilterAsync_NotificationsFound_Returned( + SutProvider sutProvider, + List notificationsStatusDetails, NotificationStatusFilter notificationStatusFilter) + { + Setup(sutProvider, notificationsStatusDetails, notificationStatusFilter, Guid.NewGuid()); + + var actualNotificationsStatusDetails = + await sutProvider.Sut.GetByUserIdStatusFilterAsync(notificationStatusFilter); + + Assert.Equal(notificationsStatusDetails, actualNotificationsStatusDetails); + } +} diff --git a/test/Core.Test/NotificationCenter/Queries/GetNotificationStatusForUserTest.cs b/test/Core.Test/NotificationCenter/Queries/GetNotificationStatusForUserTest.cs new file mode 100644 index 000000000..5ae22508b --- /dev/null +++ b/test/Core.Test/NotificationCenter/Queries/GetNotificationStatusForUserTest.cs @@ -0,0 +1,85 @@ +#nullable enable +using Bit.Core.Context; +using Bit.Core.Exceptions; +using Bit.Core.NotificationCenter.Queries; +using Bit.Core.NotificationCenter.Repositories; +using Bit.Core.Test.NotificationCenter.AutoFixture; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; + +namespace Bit.Core.Test.NotificationCenter.Queries; + +using System.Security.Claims; +using Bit.Core.NotificationCenter.Authorization; +using Bit.Core.NotificationCenter.Entities; +using Microsoft.AspNetCore.Authorization; +using NSubstitute; +using Xunit; + +[SutProviderCustomize] +[NotificationStatusCustomize] +public class GetNotificationStatusForUserQueryTest +{ + private static void Setup(SutProvider sutProvider, + Guid notificationId, NotificationStatus? notificationStatus, Guid? userId, bool authorized = false) + { + sutProvider.GetDependency().UserId.Returns(userId); + sutProvider.GetDependency() + .GetByNotificationIdAndUserIdAsync(notificationId, userId.GetValueOrDefault(Guid.NewGuid())) + .Returns(notificationStatus); + sutProvider.GetDependency() + .AuthorizeAsync(Arg.Any(), notificationStatus ?? Arg.Any(), + Arg.Is>(reqs => + reqs.Contains(NotificationStatusOperations.Read))) + .Returns(authorized ? AuthorizationResult.Success() : AuthorizationResult.Failed()); + } + + [Theory] + [BitAutoData] + public async Task GetByUserIdStatusFilterAsync_UserNotLoggedIn_NotFoundException( + SutProvider sutProvider, + Guid notificationId, NotificationStatus notificationStatus) + { + Setup(sutProvider, notificationId, notificationStatus, userId: null, true); + + await Assert.ThrowsAsync(() => + sutProvider.Sut.GetByNotificationIdAndUserIdAsync(notificationId)); + } + + [Theory] + [BitAutoData] + public async Task GetByUserIdStatusFilterAsync_NotificationStatusNotFound_NotFoundException( + SutProvider sutProvider, + Guid notificationId) + { + Setup(sutProvider, notificationId, notificationStatus: null, Guid.NewGuid(), true); + + await Assert.ThrowsAsync(() => + sutProvider.Sut.GetByNotificationIdAndUserIdAsync(notificationId)); + } + + [Theory] + [BitAutoData] + public async Task GetByUserIdStatusFilterAsync_AuthorizationFailed_NotFoundException( + SutProvider sutProvider, + Guid notificationId, NotificationStatus notificationStatus) + { + Setup(sutProvider, notificationId, notificationStatus, Guid.NewGuid(), authorized: false); + + await Assert.ThrowsAsync(() => + sutProvider.Sut.GetByNotificationIdAndUserIdAsync(notificationId)); + } + + [Theory] + [BitAutoData] + public async Task GetByUserIdStatusFilterAsync_NotificationFoundAuthorized_Returned( + SutProvider sutProvider, + Guid notificationId, NotificationStatus notificationStatus) + { + Setup(sutProvider, notificationId, notificationStatus, Guid.NewGuid(), true); + + var actualNotificationStatus = await sutProvider.Sut.GetByNotificationIdAndUserIdAsync(notificationId); + + Assert.Equal(notificationStatus, actualNotificationStatus); + } +} diff --git a/test/Core.Test/NotificationHub/NotificationHubConnectionTests.cs b/test/Core.Test/NotificationHub/NotificationHubConnectionTests.cs new file mode 100644 index 000000000..0d7382b3c --- /dev/null +++ b/test/Core.Test/NotificationHub/NotificationHubConnectionTests.cs @@ -0,0 +1,205 @@ +using Bit.Core.Settings; +using Bit.Core.Utilities; +using Xunit; + +namespace Bit.Core.Test.NotificationHub; + +public class NotificationHubConnectionTests +{ + [Fact] + public void IsValid_ConnectionStringIsNull_ReturnsFalse() + { + // Arrange + var hub = new GlobalSettings.NotificationHubSettings() + { + ConnectionString = null, + HubName = "hub", + RegistrationStartDate = DateTime.UtcNow, + RegistrationEndDate = DateTime.UtcNow.AddDays(1) + }; + + // Act + var connection = NotificationHubConnection.From(hub); + + // Assert + Assert.False(connection.IsValid); + } + + [Fact] + public void IsValid_HubNameIsNull_ReturnsFalse() + { + // Arrange + var hub = new GlobalSettings.NotificationHubSettings() + { + ConnectionString = "Endpoint=sb://example.servicebus.windows.net/;", + HubName = null, + RegistrationStartDate = DateTime.UtcNow, + RegistrationEndDate = DateTime.UtcNow.AddDays(1) + }; + + // Act + var connection = NotificationHubConnection.From(hub); + + // Assert + Assert.False(connection.IsValid); + } + + [Fact] + public void IsValid_ConnectionStringAndHubNameAreNotNull_ReturnsTrue() + { + // Arrange + var hub = new GlobalSettings.NotificationHubSettings() + { + ConnectionString = "connection", + HubName = "hub", + RegistrationStartDate = DateTime.UtcNow, + RegistrationEndDate = DateTime.UtcNow.AddDays(1) + }; + + // Act + var connection = NotificationHubConnection.From(hub); + + // Assert + Assert.True(connection.IsValid); + } + + [Fact] + public void RegistrationEnabled_QueryTimeIsBeforeStartDate_ReturnsFalse() + { + // Arrange + var hub = new GlobalSettings.NotificationHubSettings() + { + ConnectionString = "connection", + HubName = "hub", + RegistrationStartDate = DateTime.UtcNow.AddDays(1), + RegistrationEndDate = DateTime.UtcNow.AddDays(2) + }; + var connection = NotificationHubConnection.From(hub); + + // Act + var result = connection.RegistrationEnabled(DateTime.UtcNow); + + // Assert + Assert.False(result); + } + + [Fact] + public void RegistrationEnabled_QueryTimeIsAfterEndDate_ReturnsFalse() + { + // Arrange + var hub = new GlobalSettings.NotificationHubSettings() + { + ConnectionString = "connection", + HubName = "hub", + RegistrationStartDate = DateTime.UtcNow, + RegistrationEndDate = DateTime.UtcNow.AddDays(1) + }; + var connection = NotificationHubConnection.From(hub); + + // Act + var result = connection.RegistrationEnabled(DateTime.UtcNow.AddDays(2)); + + // Assert + Assert.False(result); + } + + [Fact] + public void RegistrationEnabled_NullStartDate_ReturnsFalse() + { + // Arrange + var hub = new GlobalSettings.NotificationHubSettings() + { + ConnectionString = "connection", + HubName = "hub", + RegistrationStartDate = null, + RegistrationEndDate = DateTime.UtcNow.AddDays(1) + }; + var connection = NotificationHubConnection.From(hub); + + // Act + var result = connection.RegistrationEnabled(DateTime.UtcNow); + + // Assert + Assert.False(result); + } + + [Fact] + public void RegistrationEnabled_QueryTimeIsBetweenStartDateAndEndDate_ReturnsTrue() + { + // Arrange + var hub = new GlobalSettings.NotificationHubSettings() + { + ConnectionString = "connection", + HubName = "hub", + RegistrationStartDate = DateTime.UtcNow, + RegistrationEndDate = DateTime.UtcNow.AddDays(1) + }; + var connection = NotificationHubConnection.From(hub); + + // Act + var result = connection.RegistrationEnabled(DateTime.UtcNow.AddHours(1)); + + // Assert + Assert.True(result); + } + + [Fact] + public void RegistrationEnabled_CombTimeIsBeforeStartDate_ReturnsFalse() + { + // Arrange + var hub = new GlobalSettings.NotificationHubSettings() + { + ConnectionString = "connection", + HubName = "hub", + RegistrationStartDate = DateTime.UtcNow.AddDays(1), + RegistrationEndDate = DateTime.UtcNow.AddDays(2) + }; + var connection = NotificationHubConnection.From(hub); + + // Act + var result = connection.RegistrationEnabled(CoreHelpers.GenerateComb(Guid.NewGuid(), DateTime.UtcNow)); + + // Assert + Assert.False(result); + } + + [Fact] + public void RegistrationEnabled_CombTimeIsAfterEndDate_ReturnsFalse() + { + // Arrange + var hub = new GlobalSettings.NotificationHubSettings() + { + ConnectionString = "connection", + HubName = "hub", + RegistrationStartDate = DateTime.UtcNow, + RegistrationEndDate = DateTime.UtcNow.AddDays(1) + }; + var connection = NotificationHubConnection.From(hub); + + // Act + var result = connection.RegistrationEnabled(CoreHelpers.GenerateComb(Guid.NewGuid(), DateTime.UtcNow.AddDays(2))); + + // Assert + Assert.False(result); + } + + [Fact] + public void RegistrationEnabled_CombTimeIsBetweenStartDateAndEndDate_ReturnsTrue() + { + // Arrange + var hub = new GlobalSettings.NotificationHubSettings() + { + ConnectionString = "connection", + HubName = "hub", + RegistrationStartDate = DateTime.UtcNow, + RegistrationEndDate = DateTime.UtcNow.AddDays(1) + }; + var connection = NotificationHubConnection.From(hub); + + // Act + var result = connection.RegistrationEnabled(CoreHelpers.GenerateComb(Guid.NewGuid(), DateTime.UtcNow.AddHours(1))); + + // Assert + Assert.True(result); + } +} diff --git a/test/Core.Test/NotificationHub/NotificationHubPoolTests.cs b/test/Core.Test/NotificationHub/NotificationHubPoolTests.cs new file mode 100644 index 000000000..dd9afb867 --- /dev/null +++ b/test/Core.Test/NotificationHub/NotificationHubPoolTests.cs @@ -0,0 +1,156 @@ +using Bit.Core.NotificationHub; +using Bit.Core.Settings; +using Bit.Core.Utilities; +using Microsoft.Extensions.Logging; +using NSubstitute; +using Xunit; +using static Bit.Core.Settings.GlobalSettings; + +namespace Bit.Core.Test.NotificationHub; + +public class NotificationHubPoolTests +{ + [Fact] + public void NotificationHubPool_WarnsOnMissingConnectionString() + { + // Arrange + var globalSettings = new GlobalSettings() + { + NotificationHubPool = new NotificationHubPoolSettings() + { + NotificationHubs = new() { + new() { + ConnectionString = null, + HubName = "hub", + RegistrationStartDate = DateTime.UtcNow, + RegistrationEndDate = DateTime.UtcNow.AddDays(1) + } + } + } + }; + var logger = Substitute.For>(); + + // Act + var sut = new NotificationHubPool(logger, globalSettings); + + // Assert + logger.Received().Log(LogLevel.Warning, Arg.Any(), + Arg.Is(o => o.ToString() == "Invalid notification hub settings: hub"), + null, + Arg.Any>()); + } + + [Fact] + public void NotificationHubPool_WarnsOnMissingHubName() + { + // Arrange + var globalSettings = new GlobalSettings() + { + NotificationHubPool = new NotificationHubPoolSettings() + { + NotificationHubs = new() { + new() { + ConnectionString = "connection", + HubName = null, + RegistrationStartDate = DateTime.UtcNow, + RegistrationEndDate = DateTime.UtcNow.AddDays(1) + } + } + } + }; + var logger = Substitute.For>(); + + // Act + var sut = new NotificationHubPool(logger, globalSettings); + + // Assert + logger.Received().Log(LogLevel.Warning, Arg.Any(), + Arg.Is(o => o.ToString() == "Invalid notification hub settings: hub name missing"), + null, + Arg.Any>()); + } + + [Fact] + public void NotificationHubPool_ClientFor_ThrowsOnNoValidHubs() + { + // Arrange + var globalSettings = new GlobalSettings() + { + NotificationHubPool = new NotificationHubPoolSettings() + { + NotificationHubs = new() { + new() { + ConnectionString = "connection", + HubName = "hub", + RegistrationStartDate = null, + RegistrationEndDate = null, + } + } + } + }; + var logger = Substitute.For>(); + var sut = new NotificationHubPool(logger, globalSettings); + + // Act + Action act = () => sut.ClientFor(Guid.NewGuid()); + + // Assert + Assert.Throws(act); + } + + [Fact] + public void NotificationHubPool_ClientFor_ReturnsClient() + { + // Arrange + var globalSettings = new GlobalSettings() + { + NotificationHubPool = new NotificationHubPoolSettings() + { + NotificationHubs = new() { + new() { + ConnectionString = "Endpoint=sb://example.servicebus.windows.net/;SharedAccessKey=example///example=", + HubName = "hub", + RegistrationStartDate = DateTime.UtcNow.AddMinutes(-1), + RegistrationEndDate = DateTime.UtcNow.AddDays(1), + } + } + } + }; + var logger = Substitute.For>(); + var sut = new NotificationHubPool(logger, globalSettings); + + // Act + var client = sut.ClientFor(CoreHelpers.GenerateComb(Guid.NewGuid(), DateTime.UtcNow)); + + // Assert + Assert.NotNull(client); + } + + [Fact] + public void NotificationHubPool_AllClients_ReturnsProxy() + { + // Arrange + var globalSettings = new GlobalSettings() + { + NotificationHubPool = new NotificationHubPoolSettings() + { + NotificationHubs = new() { + new() { + ConnectionString = "connection", + HubName = "hub", + RegistrationStartDate = DateTime.UtcNow, + RegistrationEndDate = DateTime.UtcNow.AddDays(1), + } + } + } + }; + var logger = Substitute.For>(); + var sut = new NotificationHubPool(logger, globalSettings); + + // Act + var proxy = sut.AllClients; + + // Assert + Assert.NotNull(proxy); + } +} diff --git a/test/Core.Test/NotificationHub/NotificationHubProxyTests.cs b/test/Core.Test/NotificationHub/NotificationHubProxyTests.cs new file mode 100644 index 000000000..b2e9c4f9f --- /dev/null +++ b/test/Core.Test/NotificationHub/NotificationHubProxyTests.cs @@ -0,0 +1,40 @@ +using AutoFixture; +using Bit.Core.NotificationHub; +using Bit.Test.Common.AutoFixture; +using Microsoft.Azure.NotificationHubs; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.NotificationHub; + +public class NotificationHubProxyTests +{ + private readonly IEnumerable _clients; + public NotificationHubProxyTests() + { + _clients = new Fixture().WithAutoNSubstitutions().CreateMany(); + } + + public static IEnumerable ClientMethods = + [ + [ + (NotificationHubClientProxy c) => c.SendTemplateNotificationAsync(new Dictionary() { { "key", "value" } }, "tag"), + (INotificationHubClient c) => c.SendTemplateNotificationAsync(Arg.Is>((a) => a.Keys.Count == 1 && a.ContainsKey("key") && a["key"] == "value"), "tag"), + ], + ]; + + [Theory] + [MemberData(nameof(ClientMethods))] + public async void CallsAllClients(Func proxyMethod, Func clientMethod) + { + var clients = _clients.ToArray(); + var proxy = new NotificationHubClientProxy(clients); + + await proxyMethod(proxy); + + foreach (var client in clients) + { + await clientMethod(client.Received()); + } + } +} diff --git a/test/Core.Test/Services/NotificationHubPushNotificationServiceTests.cs b/test/Core.Test/NotificationHub/NotificationHubPushNotificationServiceTests.cs similarity index 81% rename from test/Core.Test/Services/NotificationHubPushNotificationServiceTests.cs rename to test/Core.Test/NotificationHub/NotificationHubPushNotificationServiceTests.cs index 82594445a..ea9ce5413 100644 --- a/test/Core.Test/Services/NotificationHubPushNotificationServiceTests.cs +++ b/test/Core.Test/NotificationHub/NotificationHubPushNotificationServiceTests.cs @@ -1,32 +1,32 @@ -using Bit.Core.Repositories; +using Bit.Core.NotificationHub; +using Bit.Core.Repositories; using Bit.Core.Services; -using Bit.Core.Settings; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using NSubstitute; using Xunit; -namespace Bit.Core.Test.Services; +namespace Bit.Core.Test.NotificationHub; public class NotificationHubPushNotificationServiceTests { private readonly NotificationHubPushNotificationService _sut; private readonly IInstallationDeviceRepository _installationDeviceRepository; - private readonly GlobalSettings _globalSettings; + private readonly INotificationHubPool _notificationHubPool; private readonly IHttpContextAccessor _httpContextAccessor; private readonly ILogger _logger; public NotificationHubPushNotificationServiceTests() { _installationDeviceRepository = Substitute.For(); - _globalSettings = new GlobalSettings(); _httpContextAccessor = Substitute.For(); + _notificationHubPool = Substitute.For(); _logger = Substitute.For>(); _sut = new NotificationHubPushNotificationService( _installationDeviceRepository, - _globalSettings, + _notificationHubPool, _httpContextAccessor, _logger ); diff --git a/test/Core.Test/Services/NotificationHubPushRegistrationServiceTests.cs b/test/Core.Test/NotificationHub/NotificationHubPushRegistrationServiceTests.cs similarity index 82% rename from test/Core.Test/Services/NotificationHubPushRegistrationServiceTests.cs rename to test/Core.Test/NotificationHub/NotificationHubPushRegistrationServiceTests.cs index a8dd536b8..c5851f279 100644 --- a/test/Core.Test/Services/NotificationHubPushRegistrationServiceTests.cs +++ b/test/Core.Test/NotificationHub/NotificationHubPushRegistrationServiceTests.cs @@ -1,11 +1,11 @@ -using Bit.Core.Repositories; -using Bit.Core.Services; +using Bit.Core.NotificationHub; +using Bit.Core.Repositories; using Bit.Core.Settings; using Microsoft.Extensions.Logging; using NSubstitute; using Xunit; -namespace Bit.Core.Test.Services; +namespace Bit.Core.Test.NotificationHub; public class NotificationHubPushRegistrationServiceTests { @@ -15,6 +15,7 @@ public class NotificationHubPushRegistrationServiceTests private readonly IServiceProvider _serviceProvider; private readonly ILogger _logger; private readonly GlobalSettings _globalSettings; + private readonly INotificationHubPool _notificationHubPool; public NotificationHubPushRegistrationServiceTests() { @@ -22,10 +23,12 @@ public class NotificationHubPushRegistrationServiceTests _serviceProvider = Substitute.For(); _logger = Substitute.For>(); _globalSettings = new GlobalSettings(); + _notificationHubPool = Substitute.For(); _sut = new NotificationHubPushRegistrationService( _installationDeviceRepository, _globalSettings, + _notificationHubPool, _serviceProvider, _logger ); diff --git a/test/Core.Test/Services/MultiServicePushNotificationServiceTests.cs b/test/Core.Test/Services/MultiServicePushNotificationServiceTests.cs index b1876f1dd..68d6c50a7 100644 --- a/test/Core.Test/Services/MultiServicePushNotificationServiceTests.cs +++ b/test/Core.Test/Services/MultiServicePushNotificationServiceTests.cs @@ -1,10 +1,10 @@ -using Bit.Core.Repositories; +using AutoFixture; using Bit.Core.Services; -using Bit.Core.Settings; -using Microsoft.AspNetCore.Http; +using Bit.Test.Common.AutoFixture; using Microsoft.Extensions.Logging; using NSubstitute; using Xunit; +using GlobalSettingsCustomization = Bit.Test.Common.AutoFixture.GlobalSettings; namespace Bit.Core.Test.Services; @@ -12,35 +12,26 @@ public class MultiServicePushNotificationServiceTests { private readonly MultiServicePushNotificationService _sut; - private readonly IHttpClientFactory _httpFactory; - private readonly IDeviceRepository _deviceRepository; - private readonly IInstallationDeviceRepository _installationDeviceRepository; - private readonly GlobalSettings _globalSettings; - private readonly IHttpContextAccessor _httpContextAccessor; private readonly ILogger _logger; private readonly ILogger _relayLogger; private readonly ILogger _hubLogger; + private readonly IEnumerable _services; + private readonly Settings.GlobalSettings _globalSettings; public MultiServicePushNotificationServiceTests() { - _httpFactory = Substitute.For(); - _deviceRepository = Substitute.For(); - _installationDeviceRepository = Substitute.For(); - _globalSettings = new GlobalSettings(); - _httpContextAccessor = Substitute.For(); _logger = Substitute.For>(); _relayLogger = Substitute.For>(); _hubLogger = Substitute.For>(); + var fixture = new Fixture().WithAutoNSubstitutions().Customize(new GlobalSettingsCustomization()); + _services = fixture.CreateMany(); + _globalSettings = fixture.Create(); + _sut = new MultiServicePushNotificationService( - _httpFactory, - _deviceRepository, - _installationDeviceRepository, - _globalSettings, - _httpContextAccessor, + _services, _logger, - _relayLogger, - _hubLogger + _globalSettings ); } diff --git a/test/Core.Test/Services/UserServiceTests.cs b/test/Core.Test/Services/UserServiceTests.cs index 098d4d279..aa2c0a5cc 100644 --- a/test/Core.Test/Services/UserServiceTests.cs +++ b/test/Core.Test/Services/UserServiceTests.cs @@ -1,5 +1,6 @@ using System.Text.Json; using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Services; using Bit.Core.Auth.Enums; @@ -26,7 +27,6 @@ using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using NSubstitute; -using NSubstitute.ReceivedExtensions; using Xunit; namespace Bit.Core.Test.Services; @@ -263,7 +263,8 @@ public class UserServiceTests sutProvider.GetDependency(), new FakeDataProtectorTokenFactory(), sutProvider.GetDependency(), - sutProvider.GetDependency() + sutProvider.GetDependency(), + sutProvider.GetDependency() ); var actualIsVerified = await sut.VerifySecretAsync(user, secret); @@ -280,45 +281,69 @@ public class UserServiceTests } [Theory, BitAutoData] - public async Task IsManagedByAnyOrganizationAsync_WithManagingEnabledOrganization_ReturnsTrue( - SutProvider sutProvider, Guid userId, Organization organization) + public async Task IsManagedByAnyOrganizationAsync_WithAccountDeprovisioningDisabled_ReturnsFalse( + SutProvider sutProvider, Guid userId) { - organization.Enabled = true; - organization.UseSso = true; - - sutProvider.GetDependency() - .GetByClaimedUserDomainAsync(userId) - .Returns(organization); - - var result = await sutProvider.Sut.IsManagedByAnyOrganizationAsync(userId); - Assert.True(result); - } - - [Theory, BitAutoData] - public async Task IsManagedByAnyOrganizationAsync_WithManagingDisabledOrganization_ReturnsFalse( - SutProvider sutProvider, Guid userId, Organization organization) - { - organization.Enabled = false; - organization.UseSso = true; - - sutProvider.GetDependency() - .GetByClaimedUserDomainAsync(userId) - .Returns(organization); + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.AccountDeprovisioning) + .Returns(false); var result = await sutProvider.Sut.IsManagedByAnyOrganizationAsync(userId); Assert.False(result); } [Theory, BitAutoData] - public async Task IsManagedByAnyOrganizationAsync_WithOrganizationUseSsoFalse_ReturnsFalse( + public async Task IsManagedByAnyOrganizationAsync_WithAccountDeprovisioningEnabled_WithManagingEnabledOrganization_ReturnsTrue( + SutProvider sutProvider, Guid userId, Organization organization) + { + organization.Enabled = true; + organization.UseSso = true; + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.AccountDeprovisioning) + .Returns(true); + + sutProvider.GetDependency() + .GetByVerifiedUserEmailDomainAsync(userId) + .Returns(new[] { organization }); + + var result = await sutProvider.Sut.IsManagedByAnyOrganizationAsync(userId); + Assert.True(result); + } + + [Theory, BitAutoData] + public async Task IsManagedByAnyOrganizationAsync_WithAccountDeprovisioningEnabled_WithManagingDisabledOrganization_ReturnsFalse( + SutProvider sutProvider, Guid userId, Organization organization) + { + organization.Enabled = false; + organization.UseSso = true; + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.AccountDeprovisioning) + .Returns(true); + + sutProvider.GetDependency() + .GetByVerifiedUserEmailDomainAsync(userId) + .Returns(new[] { organization }); + + var result = await sutProvider.Sut.IsManagedByAnyOrganizationAsync(userId); + Assert.False(result); + } + + [Theory, BitAutoData] + public async Task IsManagedByAnyOrganizationAsync_WithAccountDeprovisioningEnabled_WithOrganizationUseSsoFalse_ReturnsFalse( SutProvider sutProvider, Guid userId, Organization organization) { organization.Enabled = true; organization.UseSso = false; + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.AccountDeprovisioning) + .Returns(true); + sutProvider.GetDependency() - .GetByClaimedUserDomainAsync(userId) - .Returns(organization); + .GetByVerifiedUserEmailDomainAsync(userId) + .Returns(new[] { organization }); var result = await sutProvider.Sut.IsManagedByAnyOrganizationAsync(userId); Assert.False(result); diff --git a/test/Core.Test/Utilities/CoreHelpersTests.cs b/test/Core.Test/Utilities/CoreHelpersTests.cs index af1156798..2cce276fc 100644 --- a/test/Core.Test/Utilities/CoreHelpersTests.cs +++ b/test/Core.Test/Utilities/CoreHelpersTests.cs @@ -34,33 +34,30 @@ public class CoreHelpersTests // the comb are working properly } - public static IEnumerable GenerateCombCases = new[] - { - new object[] - { + public static IEnumerable GuidSeedCases = [ + [ Guid.Parse("a58db474-43d8-42f1-b4ee-0c17647cd0c0"), // Input Guid new DateTime(2022, 3, 12, 12, 12, 0, DateTimeKind.Utc), // Input Time - Guid.Parse("a58db474-43d8-42f1-b4ee-ae5600c90cc1"), // Expected Comb - }, - new object[] - { + ], + [ Guid.Parse("f776e6ee-511f-4352-bb28-88513002bdeb"), new DateTime(2021, 5, 10, 10, 52, 0, DateTimeKind.Utc), - Guid.Parse("f776e6ee-511f-4352-bb28-ad2400b313c1"), - }, - new object[] - { + ], + [ Guid.Parse("51a25fc7-3cad-497d-8e2f-8d77011648a1"), new DateTime(1999, 2, 26, 16, 53, 13, DateTimeKind.Utc), - Guid.Parse("51a25fc7-3cad-497d-8e2f-8d77011649cd"), - }, - new object[] - { + ], + [ Guid.Parse("bfb8f353-3b32-4a9e-bef6-24fe0b54bfb0"), new DateTime(2024, 10, 20, 1, 32, 16, DateTimeKind.Utc), - Guid.Parse("bfb8f353-3b32-4a9e-bef6-b20f00195780"), - } - }; + ] + ]; + public static IEnumerable GenerateCombCases = GuidSeedCases.Zip([ + Guid.Parse("a58db474-43d8-42f1-b4ee-ae5600c90cc1"), // Expected Comb for each Guid Seed case + Guid.Parse("f776e6ee-511f-4352-bb28-ad2400b313c1"), + Guid.Parse("51a25fc7-3cad-497d-8e2f-8d77011649cd"), + Guid.Parse("bfb8f353-3b32-4a9e-bef6-b20f00195780"), + ]).Select((zip) => new object[] { zip.Item1[0], zip.Item1[1], zip.Item2 }); [Theory] [MemberData(nameof(GenerateCombCases))] @@ -71,6 +68,31 @@ public class CoreHelpersTests Assert.Equal(expectedComb, comb); } + [Theory] + [MemberData(nameof(GuidSeedCases))] + public void DateFromComb_WithComb_Success(Guid inputGuid, DateTime inputTime) + { + var comb = CoreHelpers.GenerateComb(inputGuid, inputTime); + var inverseComb = CoreHelpers.DateFromComb(comb); + + Assert.Equal(inputTime, inverseComb, TimeSpan.FromMilliseconds(4)); + } + + [Theory] + [InlineData("00000000-0000-0000-0000-000000000000", 1, 0)] + [InlineData("00000000-0000-0000-0000-000000000001", 1, 0)] + [InlineData("00000000-0000-0000-0000-000000000000", 500, 430)] + [InlineData("00000000-0000-0000-0000-000000000001", 500, 430)] + [InlineData("10000000-0000-0000-0000-000000000001", 500, 454)] + [InlineData("00000000-0000-0100-0000-000000000001", 500, 19)] + public void BinForComb_Success(string guidString, int nbins, int expectedBin) + { + var guid = Guid.Parse(guidString); + var bin = CoreHelpers.BinForComb(guid, nbins); + + Assert.Equal(expectedBin, bin); + } + /* [Fact] public void ToGuidIdArrayTVP_Success() 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 fac271b14..703faed48 100644 --- a/test/Identity.IntegrationTest/RequestValidation/ResourceOwnerPasswordValidatorTests.cs +++ b/test/Identity.IntegrationTest/RequestValidation/ResourceOwnerPasswordValidatorTests.cs @@ -4,11 +4,15 @@ using Bit.Core.Auth.Enums; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Repositories; +using Bit.Core.Services; +using Bit.Identity.IdentityServer.RequestValidators; using Bit.Identity.Models.Request.Accounts; using Bit.IntegrationTestCommon.Factories; using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.Helpers; +using Duende.IdentityServer.Validation; using Microsoft.AspNetCore.Identity; +using NSubstitute; using Xunit; namespace Bit.Identity.IntegrationTest.RequestValidation; @@ -21,6 +25,7 @@ public class ResourceOwnerPasswordValidatorTests : IClassFixture _userManager; private readonly IAuthRequestRepository _authRequestRepository; + private readonly IDeviceService _deviceService; public ResourceOwnerPasswordValidatorTests(IdentityApplicationFactory factory) { @@ -28,13 +33,13 @@ public class ResourceOwnerPasswordValidatorTests : IClassFixture>(); _authRequestRepository = _factory.GetService(); - + _deviceService = _factory.GetService(); } [Fact] public async Task ValidateAsync_Success() { - // Arrange + // Arrange await EnsureUserCreatedAsync(); // Act @@ -53,7 +58,7 @@ public class ResourceOwnerPasswordValidatorTests : IClassFixture - /// I would have liked to spy into the IUserService but by spying into the IUserService it - /// creates a Singleton that is not available to the UserManager thus causing the + /// I would have liked to spy into the IUserService but by spying into the IUserService it + /// creates a Singleton that is not available to the UserManager thus causing the /// RegisterAsync() to create a the user in a different UserStore than the one the - /// UserManager has access to. This is an assumption made from observing the behavior while - /// writing theses tests. I could be wrong. - /// + /// UserManager has access to (This is an assumption made from observing the behavior while + /// writing theses tests, I could be wrong). + /// /// For the time being, verifying that the user is not null confirms that the failure is due to /// a bad password. /// @@ -102,10 +107,11 @@ public class ResourceOwnerPasswordValidatorTests : IClassFixture context.SetAuthEmail(DefaultUsername)); // Assert - Assert.NotNull(await _userManager.FindByEmailAsync(DefaultUsername)); - var body = await AssertHelper.AssertResponseTypeIs(context); var root = body.RootElement; @@ -200,8 +204,8 @@ public class ResourceOwnerPasswordValidatorTests : IClassFixture(sub => + { + sub.SaveDeviceAsync(Arg.Any(), Arg.Any()) + .Returns(null as Device); + }); + + // Add User + await factory.RegisterAsync(new RegisterRequestModel + { + Email = DefaultUsername, + MasterPasswordHash = DefaultPassword + }); + var userManager = factory.GetService>(); + await factory.RegisterAsync(new RegisterRequestModel + { + Email = DefaultUsername, + MasterPasswordHash = DefaultPassword + }); + var user = await userManager.FindByEmailAsync(DefaultUsername); + Assert.NotNull(user); + + // Act + var context = await factory.Server.PostAsync("/connect/token", + GetFormUrlEncodedContent(), + context => context.SetAuthEmail(DefaultUsername)); + + // Assert + var body = await AssertHelper.AssertResponseTypeIs(context); + var root = body.RootElement; + + var errorModel = AssertHelper.AssertJsonProperty(root, "ErrorModel", JsonValueKind.Object); + var errorMessage = AssertHelper.AssertJsonProperty(errorModel, "Message", JsonValueKind.String).GetString(); + Assert.Equal("No device information provided.", errorMessage); + } + private async Task EnsureUserCreatedAsync(IdentityApplicationFactory factory = null) { factory ??= _factory; diff --git a/test/Identity.Test/IdentityServer/BaseRequestValidatorTests.cs b/test/Identity.Test/IdentityServer/BaseRequestValidatorTests.cs index c1d34e1b0..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; @@ -29,22 +28,17 @@ namespace Bit.Identity.Test.IdentityServer; public class BaseRequestValidatorTests { private UserManager _userManager; - private readonly IDeviceRepository _deviceRepository; - private readonly IDeviceService _deviceService; private readonly IUserService _userService; private readonly IEventService _eventService; - private readonly IOrganizationDuoWebTokenProvider _organizationDuoWebTokenProvider; - private readonly ITemporaryDuoWebV4SDKService _duoWebV4SDKService; - private readonly IOrganizationRepository _organizationRepository; + private readonly IDeviceValidator _deviceValidator; + 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; @@ -53,45 +47,35 @@ public class BaseRequestValidatorTests public BaseRequestValidatorTests() { - _deviceRepository = Substitute.For(); - _deviceService = Substitute.For(); + _userManager = SubstituteUserManager(); _userService = Substitute.For(); _eventService = Substitute.For(); - _organizationDuoWebTokenProvider = Substitute.For(); - _duoWebV4SDKService = Substitute.For(); - _organizationRepository = Substitute.For(); + _deviceValidator = 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, - _deviceRepository, - _deviceService, _userService, _eventService, - _organizationDuoWebTokenProvider, - _duoWebV4SDKService, - _organizationRepository, + _deviceValidator, + _twoFactorAuthenticationValidator, _organizationUserRepository, - _applicationCacheService, _mailService, _logger, _currentContext, _globalSettings, _userRepository, _policyService, - _tokenDataFactory, _featureService, _ssoConfigRepository, _userDecryptionOptionsBuilder); @@ -119,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); @@ -130,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] @@ -157,7 +141,7 @@ public class BaseRequestValidatorTests /* Logic path ValidateAsync -> UpdateFailedAuthDetailsAsync -> _mailService.SendFailedLoginAttemptsEmailAsync - |-> BuildErrorResultAsync -> _eventService.LogUserEventAsync + |-> BuildErrorResultAsync -> _eventService.LogUserEventAsync |-> SetErrorResult */ [Theory, BitAutoData] @@ -205,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; @@ -228,10 +215,14 @@ public class BaseRequestValidatorTests public async Task ValidateAsync_ClientCredentialsGrantType_ShouldSucceed( [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest, CustomValidatorRequestContext requestContext, - GrantValidationResult grantResult) + GrantValidationResult grantResult, + Device device) { // 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; @@ -239,19 +230,14 @@ 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.Raw["DeviceIdentifier"] = "DeviceIdentifier"; - context.ValidatedTokenRequest.Raw["DeviceType"] = "Android"; // This needs to be an actual Type - context.ValidatedTokenRequest.Raw["DeviceName"] = "DeviceName"; - context.ValidatedTokenRequest.Raw["DevicePushToken"] = "DevicePushToken"; + context.ValidatedTokenRequest.GrantType = "client_credentials"; // This || AuthCode will allow process to continue to get device + _deviceValidator.SaveDeviceAsync(Arg.Any(), Arg.Any()) + .Returns(device); // Act await _sut.ValidateAsync(context); // Assert - await _mailService.Received(1).SendNewDeviceLoggedInEmail( - context.CustomValidatorRequestContext.User.Email, "Android", Arg.Any(), Arg.Any() - ); Assert.False(context.GrantResult.IsError); } @@ -262,7 +248,8 @@ public class BaseRequestValidatorTests public async Task ValidateAsync_ClientCredentialsGrantType_ExistingDevice_ShouldSucceed( [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest, CustomValidatorRequestContext requestContext, - GrantValidationResult grantResult) + GrantValidationResult grantResult, + Device device) { // Arrange var context = CreateContext(tokenRequest, requestContext, grantResult); @@ -273,14 +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.Raw["DeviceIdentifier"] = "DeviceIdentifier"; - context.ValidatedTokenRequest.Raw["DeviceType"] = "Android"; // This needs to be an actual Type - context.ValidatedTokenRequest.Raw["DeviceName"] = "DeviceName"; - context.ValidatedTokenRequest.Raw["DevicePushToken"] = "DevicePushToken"; + context.ValidatedTokenRequest.GrantType = "client_credentials"; // This || AuthCode will allow process to continue to get device - _deviceRepository.GetByIdentifierAsync("DeviceIdentifier", Arg.Any()) - .Returns(new Device() { Identifier = "DeviceIdentifier" }); + _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); @@ -316,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); @@ -340,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); @@ -351,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 new file mode 100644 index 000000000..2db792c93 --- /dev/null +++ b/test/Identity.Test/IdentityServer/DeviceValidatorTests.cs @@ -0,0 +1,247 @@ +using Bit.Core.Context; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Bit.Core.Settings; +using Bit.Identity.IdentityServer.RequestValidators; +using Bit.Test.Common.AutoFixture.Attributes; +using Duende.IdentityServer.Validation; +using NSubstitute; +using Xunit; +using AuthFixtures = Bit.Identity.Test.AutoFixture; + +namespace Bit.Identity.Test.IdentityServer; + +public class DeviceValidatorTests +{ + private readonly IDeviceService _deviceService; + private readonly IDeviceRepository _deviceRepository; + private readonly GlobalSettings _globalSettings; + private readonly IMailService _mailService; + private readonly ICurrentContext _currentContext; + private readonly DeviceValidator _sut; + + public DeviceValidatorTests() + { + _deviceService = Substitute.For(); + _deviceRepository = Substitute.For(); + _globalSettings = new GlobalSettings(); + _mailService = Substitute.For(); + _currentContext = Substitute.For(); + _sut = new DeviceValidator( + _deviceService, + _deviceRepository, + _globalSettings, + _mailService, + _currentContext); + } + + [Theory] + [BitAutoData] + public async void SaveDeviceAsync_DeviceNull_ShouldReturnNull( + [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request, + User user) + { + // Arrange + request.Raw["DeviceIdentifier"] = null; + + // Act + var device = await _sut.SaveDeviceAsync(user, request); + + // Assert + Assert.Null(device); + await _mailService.DidNotReceive().SendNewDeviceLoggedInEmail( + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); + } + + [Theory] + [BitAutoData] + public async void SaveDeviceAsync_UserIsNull_ShouldReturnNull( + [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request) + { + // Arrange + request = AddValidDeviceToRequest(request); + + // Act + var device = await _sut.SaveDeviceAsync(null, request); + + // Assert + Assert.Null(device); + await _mailService.DidNotReceive().SendNewDeviceLoggedInEmail( + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); + } + + [Theory] + [BitAutoData] + public async void SaveDeviceAsync_ExistingUser_NewDevice_ReturnsDevice_SendsEmail( + [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request, + User user) + { + // Arrange + request = AddValidDeviceToRequest(request); + + user.CreationDate = DateTime.UtcNow - TimeSpan.FromMinutes(11); + _globalSettings.DisableEmailNewDevice = false; + + // Act + var device = await _sut.SaveDeviceAsync(user, request); + + // Assert + Assert.NotNull(device); + Assert.Equal(user.Id, device.UserId); + Assert.Equal("DeviceIdentifier", device.Identifier); + Assert.Equal(DeviceType.Android, device.Type); + await _mailService.Received(1).SendNewDeviceLoggedInEmail( + user.Email, "Android", Arg.Any(), Arg.Any()); + } + + [Theory] + [BitAutoData] + public async void SaveDeviceAsync_ExistingUser_NewDevice_ReturnsDevice_SendEmailFalse( + [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request, + User user) + { + // Arrange + request = AddValidDeviceToRequest(request); + + user.CreationDate = DateTime.UtcNow - TimeSpan.FromMinutes(11); + _globalSettings.DisableEmailNewDevice = true; + + // Act + var device = await _sut.SaveDeviceAsync(user, request); + + // Assert + Assert.NotNull(device); + Assert.Equal(user.Id, device.UserId); + Assert.Equal("DeviceIdentifier", device.Identifier); + Assert.Equal(DeviceType.Android, device.Type); + await _mailService.DidNotReceive().SendNewDeviceLoggedInEmail( + user.Email, "Android", Arg.Any(), Arg.Any()); + } + + [Theory] + [BitAutoData] + public async void SaveDeviceAsync_DeviceIsKnown_ShouldReturnDevice( + [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request, + User user, + Device device) + { + // Arrange + request = AddValidDeviceToRequest(request); + + device.UserId = user.Id; + device.Identifier = "DeviceIdentifier"; + device.Type = DeviceType.Android; + device.Name = "DeviceName"; + device.PushToken = "DevicePushToken"; + _deviceRepository.GetByIdentifierAsync(device.Identifier, user.Id).Returns(device); + + // Act + var resultDevice = await _sut.SaveDeviceAsync(user, request); + + // Assert + Assert.Equal(device, resultDevice); + await _mailService.DidNotReceive().SendNewDeviceLoggedInEmail( + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); + } + + [Theory] + [BitAutoData] + public async void SaveDeviceAsync_NewUser_DeviceUnknown_ShouldSaveDevice_NoEmail( + [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request, + User user) + { + // Arrange + request = AddValidDeviceToRequest(request); + user.CreationDate = DateTime.UtcNow; + _deviceRepository.GetByIdentifierAsync(Arg.Any(), Arg.Any()).Returns(null as Device); + + // Act + var device = await _sut.SaveDeviceAsync(user, request); + + // Assert + Assert.NotNull(device); + Assert.Equal(user.Id, device.UserId); + Assert.Equal("DeviceIdentifier", device.Identifier); + Assert.Equal(DeviceType.Android, device.Type); + await _deviceService.Received(1).SaveAsync(device); + await _mailService.DidNotReceive().SendNewDeviceLoggedInEmail( + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); + } + + [Theory] + [BitAutoData] + public async void KnownDeviceAsync_UserNull_ReturnsFalse( + [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request) + { + // Arrange + request = AddValidDeviceToRequest(request); + + // Act + var result = await _sut.KnownDeviceAsync(null, request); + + // Assert + Assert.False(result); + } + + [Theory] + [BitAutoData] + public async void KnownDeviceAsync_DeviceNull_ReturnsFalse( + [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request, + User user) + { + // Arrange + // Device raw data is null which will cause the device to be null + + // Act + var result = await _sut.KnownDeviceAsync(user, request); + + // Assert + Assert.False(result); + } + + [Theory] + [BitAutoData] + public async void KnownDeviceAsync_DeviceNotInDatabase_ReturnsFalse( + [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request, + User user) + { + // Arrange + request = AddValidDeviceToRequest(request); + _deviceRepository.GetByIdentifierAsync(Arg.Any(), Arg.Any()) + .Returns(null as Device); + // Act + var result = await _sut.KnownDeviceAsync(user, request); + + // Assert + Assert.False(result); + } + + [Theory] + [BitAutoData] + public async void KnownDeviceAsync_UserAndDeviceValid_ReturnsTrue( + [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request, + User user, + Device device) + { + // Arrange + request = AddValidDeviceToRequest(request); + _deviceRepository.GetByIdentifierAsync(Arg.Any(), Arg.Any()) + .Returns(device); + // Act + var result = await _sut.KnownDeviceAsync(user, request); + + // Assert + Assert.True(result); + } + + private ValidatedTokenRequest AddValidDeviceToRequest(ValidatedTokenRequest request) + { + request.Raw["DeviceIdentifier"] = "DeviceIdentifier"; + request.Raw["DeviceType"] = "Android"; + request.Raw["DeviceName"] = "DeviceName"; + request.Raw["DevicePushToken"] = "DevicePushToken"; + return request; + } +} 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 e525d0de7..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; @@ -51,43 +48,33 @@ IBaseRequestValidatorTestWrapper public bool isValid { get; set; } public BaseRequestValidatorTestWrapper( UserManager userManager, - IDeviceRepository deviceRepository, - IDeviceService deviceService, 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, IUserRepository userRepository, IPolicyService policyService, - IDataProtectorTokenFactory tokenDataFactory, IFeatureService featureService, ISsoConfigRepository ssoConfigRepository, IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder) : - base( + base( userManager, - deviceRepository, - deviceService, userService, eventService, - organizationDuoWebTokenProvider, - duoWebV4SDKService, - organizationRepository, + deviceValidator, + twoFactorAuthenticationValidator, organizationUserRepository, - applicationCacheService, mailService, logger, currentContext, globalSettings, userRepository, policyService, - tokenDataFactory, featureService, ssoConfigRepository, userDecryptionOptionsBuilder) @@ -100,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/AdminConsole/Repositories/OrganizationRepositoryTests.cs b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationRepositoryTests.cs index eac71e9c2..f6dc4a989 100644 --- a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationRepositoryTests.cs +++ b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationRepositoryTests.cs @@ -97,13 +97,160 @@ public class OrganizationRepositoryTests ResetPasswordKey = "resetpasswordkey1", }); - var user1Response = await organizationRepository.GetByClaimedUserDomainAsync(user1.Id); - var user2Response = await organizationRepository.GetByClaimedUserDomainAsync(user2.Id); - var user3Response = await organizationRepository.GetByClaimedUserDomainAsync(user3.Id); + var user1Response = await organizationRepository.GetByVerifiedUserEmailDomainAsync(user1.Id); + var user2Response = await organizationRepository.GetByVerifiedUserEmailDomainAsync(user2.Id); + var user3Response = await organizationRepository.GetByVerifiedUserEmailDomainAsync(user3.Id); - Assert.NotNull(user1Response); - Assert.Equal(organization.Id, user1Response.Id); - Assert.Null(user2Response); - Assert.Null(user3Response); + Assert.NotEmpty(user1Response); + Assert.Equal(organization.Id, user1Response.First().Id); + Assert.Empty(user2Response); + Assert.Empty(user3Response); + } + + [DatabaseTheory, DatabaseData] + public async Task GetByVerifiedUserEmailDomainAsync_WithUnverifiedDomains_ReturnsEmpty( + IUserRepository userRepository, + IOrganizationRepository organizationRepository, + IOrganizationUserRepository organizationUserRepository, + IOrganizationDomainRepository organizationDomainRepository) + { + var id = Guid.NewGuid(); + var domainName = $"{id}.example.com"; + + var user = await userRepository.CreateAsync(new User + { + Name = "Test User", + Email = $"test+{id}@{domainName}", + ApiKey = "TEST", + SecurityStamp = "stamp", + Kdf = KdfType.PBKDF2_SHA256, + KdfIterations = 1, + KdfMemory = 2, + KdfParallelism = 3 + }); + + var organization = await organizationRepository.CreateAsync(new Organization + { + Name = $"Test Org {id}", + BillingEmail = user.Email, + Plan = "Test", + PrivateKey = "privatekey", + }); + + var organizationDomain = new OrganizationDomain + { + OrganizationId = organization.Id, + DomainName = domainName, + Txt = "btw+12345", + }; + organizationDomain.SetNextRunDate(12); + organizationDomain.SetJobRunCount(); + await organizationDomainRepository.CreateAsync(organizationDomain); + + await organizationUserRepository.CreateAsync(new OrganizationUser + { + OrganizationId = organization.Id, + UserId = user.Id, + Status = OrganizationUserStatusType.Confirmed, + ResetPasswordKey = "resetpasswordkey", + }); + + var result = await organizationRepository.GetByVerifiedUserEmailDomainAsync(user.Id); + + Assert.Empty(result); + } + + [DatabaseTheory, DatabaseData] + public async Task GetByVerifiedUserEmailDomainAsync_WithMultipleVerifiedDomains_ReturnsAllMatchingOrganizations( + IUserRepository userRepository, + IOrganizationRepository organizationRepository, + IOrganizationUserRepository organizationUserRepository, + IOrganizationDomainRepository organizationDomainRepository) + { + var id = Guid.NewGuid(); + var domainName = $"{id}.example.com"; + + var user = await userRepository.CreateAsync(new User + { + Name = "Test User", + Email = $"test+{id}@{domainName}", + ApiKey = "TEST", + SecurityStamp = "stamp", + Kdf = KdfType.PBKDF2_SHA256, + KdfIterations = 1, + KdfMemory = 2, + KdfParallelism = 3 + }); + + var organization1 = await organizationRepository.CreateAsync(new Organization + { + Name = $"Test Org 1 {id}", + BillingEmail = user.Email, + Plan = "Test", + PrivateKey = "privatekey1", + }); + + var organization2 = await organizationRepository.CreateAsync(new Organization + { + Name = $"Test Org 2 {id}", + BillingEmail = user.Email, + Plan = "Test", + PrivateKey = "privatekey2", + }); + + var organizationDomain1 = new OrganizationDomain + { + OrganizationId = organization1.Id, + DomainName = domainName, + Txt = "btw+12345", + }; + organizationDomain1.SetNextRunDate(12); + organizationDomain1.SetJobRunCount(); + organizationDomain1.SetVerifiedDate(); + await organizationDomainRepository.CreateAsync(organizationDomain1); + + var organizationDomain2 = new OrganizationDomain + { + OrganizationId = organization2.Id, + DomainName = domainName, + Txt = "btw+67890", + }; + organizationDomain2.SetNextRunDate(12); + organizationDomain2.SetJobRunCount(); + organizationDomain2.SetVerifiedDate(); + await organizationDomainRepository.CreateAsync(organizationDomain2); + + await organizationUserRepository.CreateAsync(new OrganizationUser + { + OrganizationId = organization1.Id, + UserId = user.Id, + Status = OrganizationUserStatusType.Confirmed, + ResetPasswordKey = "resetpasswordkey1", + }); + + await organizationUserRepository.CreateAsync(new OrganizationUser + { + OrganizationId = organization2.Id, + UserId = user.Id, + Status = OrganizationUserStatusType.Confirmed, + ResetPasswordKey = "resetpasswordkey2", + }); + + var result = await organizationRepository.GetByVerifiedUserEmailDomainAsync(user.Id); + + Assert.Equal(2, result.Count); + Assert.Contains(result, org => org.Id == organization1.Id); + Assert.Contains(result, org => org.Id == organization2.Id); + } + + [DatabaseTheory, DatabaseData] + public async Task GetByVerifiedUserEmailDomainAsync_WithNonExistentUser_ReturnsEmpty( + IOrganizationRepository organizationRepository) + { + var nonExistentUserId = Guid.NewGuid(); + + var result = await organizationRepository.GetByVerifiedUserEmailDomainAsync(nonExistentUserId); + + Assert.Empty(result); } } diff --git a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepositoryTests.cs b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepositoryTests.cs index 3b102c788..dba511074 100644 --- a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepositoryTests.cs +++ b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepositoryTests.cs @@ -253,6 +253,9 @@ public class OrganizationUserRepositoryTests Assert.Equal(orgUser1.Permissions, result.Permissions); Assert.Equal(organization.SmSeats, result.SmSeats); Assert.Equal(organization.SmServiceAccounts, result.SmServiceAccounts); + Assert.Equal(organization.LimitCollectionCreation, result.LimitCollectionCreation); + Assert.Equal(organization.LimitCollectionDeletion, result.LimitCollectionDeletion); + // Deprecated: https://bitwarden.atlassian.net/browse/PM-10863 Assert.Equal(organization.LimitCollectionCreationDeletion, result.LimitCollectionCreationDeletion); Assert.Equal(organization.AllowAdminAccessToAllCollectionItems, result.AllowAdminAccessToAllCollectionItems); } 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 e0fcc0e5e..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. /// @@ -145,7 +155,10 @@ public abstract class WebApplicationFactoryBase : WebApplicationFactory // Email Verification { "globalSettings:enableEmailVerification", "true" }, { "globalSettings:disableUserRegistration", "false" }, - { "globalSettings:launchDarkly:flagValues:email-verification", "true" } + { "globalSettings:launchDarkly:flagValues:email-verification", "true" }, + + // New Device Verification + { "globalSettings:disableEmailNewDevice", "false" }, }); }); 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/DbMigrator.cs b/util/Migrator/DbMigrator.cs index 11b80fac7..b9584326e 100644 --- a/util/Migrator/DbMigrator.cs +++ b/util/Migrator/DbMigrator.cs @@ -14,13 +14,15 @@ public class DbMigrator private readonly string _connectionString; private readonly ILogger _logger; private readonly bool _skipDatabasePreparation; + private readonly bool _noTransactionMigration; public DbMigrator(string connectionString, ILogger logger = null, - bool skipDatabasePreparation = false) + bool skipDatabasePreparation = false, bool noTransactionMigration = false) { _connectionString = connectionString; _logger = logger ?? CreateLogger(); _skipDatabasePreparation = skipDatabasePreparation; + _noTransactionMigration = noTransactionMigration; } public bool MigrateMsSqlDatabaseWithRetries(bool enableLogging = true, @@ -30,6 +32,7 @@ public class DbMigrator CancellationToken cancellationToken = default) { var attempt = 1; + while (attempt < 10) { try @@ -69,6 +72,7 @@ public class DbMigrator using (var connection = new SqlConnection(masterConnectionString)) { var databaseName = new SqlConnectionStringBuilder(_connectionString).InitialCatalog; + if (string.IsNullOrWhiteSpace(databaseName)) { databaseName = "vault"; @@ -105,10 +109,10 @@ public class DbMigrator } private bool MigrateDatabase(bool enableLogging = true, - bool repeatable = false, - string folderName = MigratorConstants.DefaultMigrationsFolderName, - bool dryRun = false, - CancellationToken cancellationToken = default) + bool repeatable = false, + string folderName = MigratorConstants.DefaultMigrationsFolderName, + bool dryRun = false, + CancellationToken cancellationToken = default) { if (enableLogging) { @@ -121,8 +125,17 @@ public class DbMigrator .SqlDatabase(_connectionString) .WithScriptsAndCodeEmbeddedInAssembly(Assembly.GetExecutingAssembly(), s => s.Contains($".{folderName}.") && !s.Contains(".Archive.")) - .WithTransaction() - .WithExecutionTimeout(new TimeSpan(0, 5, 0)); + .WithExecutionTimeout(TimeSpan.FromMinutes(5)); + + if (_noTransactionMigration) + { + builder = builder.WithoutTransaction() + .WithExecutionTimeout(TimeSpan.FromMinutes(60)); + } + else + { + builder = builder.WithTransaction(); + } if (repeatable) { @@ -144,6 +157,7 @@ public class DbMigrator { var scriptsToExec = upgrader.GetScriptsToExecute(); var stringBuilder = new StringBuilder("Scripts that will be applied:"); + foreach (var script in scriptsToExec) { stringBuilder.AppendLine(script.Name); diff --git a/util/Migrator/DbScripts/2024-09-25_00_AddLimitCollectionCreationColumn.sql b/util/Migrator/DbScripts/2024-09-25_00_AddLimitCollectionCreationColumn.sql new file mode 100644 index 000000000..9da6bbdc9 --- /dev/null +++ b/util/Migrator/DbScripts/2024-09-25_00_AddLimitCollectionCreationColumn.sql @@ -0,0 +1,486 @@ +-- Add Columns +IF COL_LENGTH('[dbo].[Organization]', 'LimitCollectionCreation') IS NULL +BEGIN + ALTER TABLE + [dbo].[Organization] + ADD + [LimitCollectionCreation] BIT NOT NULL CONSTRAINT [DF_Organization_LimitCollectionCreation] DEFAULT (0) +END +GO + +IF COL_LENGTH('[dbo].[Organization]', 'LimitCollectionDeletion') IS NULL +BEGIN + ALTER TABLE + [dbo].[Organization] + ADD + [LimitCollectionDeletion] BIT NOT NULL CONSTRAINT [DF_Organization_LimitCollectionDeletion] DEFAULT (0) +END +GO + +-- Refresh Views +CREATE OR ALTER VIEW [dbo].[ProviderUserProviderOrganizationDetailsView] +AS +SELECT + PU.[UserId], + PO.[OrganizationId], + O.[Name], + O.[Enabled], + O.[UsePolicies], + O.[UseSso], + O.[UseKeyConnector], + O.[UseScim], + O.[UseGroups], + O.[UseDirectory], + O.[UseEvents], + O.[UseTotp], + O.[Use2fa], + O.[UseApi], + O.[UseResetPassword], + O.[SelfHost], + O.[UsersGetPremium], + O.[UseCustomPermissions], + O.[Seats], + O.[MaxCollections], + O.[MaxStorageGb], + O.[Identifier], + PO.[Key], + O.[PublicKey], + O.[PrivateKey], + PU.[Status], + PU.[Type], + PO.[ProviderId], + PU.[Id] ProviderUserId, + P.[Name] ProviderName, + O.[PlanType], + O.[LimitCollectionCreationDeletion], -- Deprecated https://bitwarden.atlassian.net/browse/PM-10863 + O.[LimitCollectionCreation], + O.[LimitCollectionDeletion], + O.[AllowAdminAccessToAllCollectionItems] +FROM + [dbo].[ProviderUser] PU +INNER JOIN + [dbo].[ProviderOrganization] PO ON PO.[ProviderId] = PU.[ProviderId] +INNER JOIN + [dbo].[Organization] O ON O.[Id] = PO.[OrganizationId] +INNER JOIN + [dbo].[Provider] P ON P.[Id] = PU.[ProviderId] +GO + +CREATE OR ALTER VIEW [dbo].[OrganizationUserOrganizationDetailsView] +AS +SELECT + OU.[UserId], + OU.[OrganizationId], + OU.[Id] OrganizationUserId, + O.[Name], + O.[Enabled], + O.[PlanType], + O.[UsePolicies], + O.[UseSso], + O.[UseKeyConnector], + O.[UseScim], + O.[UseGroups], + O.[UseDirectory], + O.[UseEvents], + O.[UseTotp], + O.[Use2fa], + O.[UseApi], + O.[UseResetPassword], + O.[SelfHost], + O.[UsersGetPremium], + O.[UseCustomPermissions], + O.[UseSecretsManager], + O.[Seats], + O.[MaxCollections], + O.[MaxStorageGb], + O.[Identifier], + OU.[Key], + OU.[ResetPasswordKey], + O.[PublicKey], + O.[PrivateKey], + OU.[Status], + OU.[Type], + SU.[ExternalId] SsoExternalId, + OU.[Permissions], + PO.[ProviderId], + P.[Name] ProviderName, + P.[Type] ProviderType, + SS.[Data] SsoConfig, + OS.[FriendlyName] FamilySponsorshipFriendlyName, + OS.[LastSyncDate] FamilySponsorshipLastSyncDate, + OS.[ToDelete] FamilySponsorshipToDelete, + OS.[ValidUntil] FamilySponsorshipValidUntil, + OU.[AccessSecretsManager], + O.[UsePasswordManager], + O.[SmSeats], + O.[SmServiceAccounts], + O.[LimitCollectionCreationDeletion], -- Deprecated https://bitwarden.atlassian.net/browse/PM-10863 + O.[LimitCollectionCreation], + O.[LimitCollectionDeletion], + O.[AllowAdminAccessToAllCollectionItems] +FROM + [dbo].[OrganizationUser] OU +LEFT JOIN + [dbo].[Organization] O ON O.[Id] = OU.[OrganizationId] +LEFT JOIN + [dbo].[SsoUser] SU ON SU.[UserId] = OU.[UserId] AND SU.[OrganizationId] = OU.[OrganizationId] +LEFT JOIN + [dbo].[ProviderOrganization] PO ON PO.[OrganizationId] = O.[Id] +LEFT JOIN + [dbo].[Provider] P ON P.[Id] = PO.[ProviderId] +LEFT JOIN + [dbo].[SsoConfig] SS ON SS.[OrganizationId] = OU.[OrganizationId] +LEFT JOIN + [dbo].[OrganizationSponsorship] OS ON OS.[SponsoringOrganizationUserID] = OU.[Id] +GO + +IF OBJECT_ID('[dbo].[OrganizationView]') IS NOT NULL + BEGIN + EXECUTE sp_refreshview N'[dbo].[OrganizationView]'; + END +GO + +-- Refresh Stored Procedures +CREATE OR ALTER PROCEDURE [dbo].[Organization_Create] + @Id UNIQUEIDENTIFIER OUTPUT, + @Identifier NVARCHAR(50), + @Name NVARCHAR(50), + @BusinessName NVARCHAR(50), + @BusinessAddress1 NVARCHAR(50), + @BusinessAddress2 NVARCHAR(50), + @BusinessAddress3 NVARCHAR(50), + @BusinessCountry VARCHAR(2), + @BusinessTaxNumber NVARCHAR(30), + @BillingEmail NVARCHAR(256), + @Plan NVARCHAR(50), + @PlanType TINYINT, + @Seats INT, + @MaxCollections SMALLINT, + @UsePolicies BIT, + @UseSso BIT, + @UseGroups BIT, + @UseDirectory BIT, + @UseEvents BIT, + @UseTotp BIT, + @Use2fa BIT, + @UseApi BIT, + @UseResetPassword BIT, + @SelfHost BIT, + @UsersGetPremium BIT, + @Storage BIGINT, + @MaxStorageGb SMALLINT, + @Gateway TINYINT, + @GatewayCustomerId VARCHAR(50), + @GatewaySubscriptionId VARCHAR(50), + @ReferenceData VARCHAR(MAX), + @Enabled BIT, + @LicenseKey VARCHAR(100), + @PublicKey VARCHAR(MAX), + @PrivateKey VARCHAR(MAX), + @TwoFactorProviders NVARCHAR(MAX), + @ExpirationDate DATETIME2(7), + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7), + @OwnersNotifiedOfAutoscaling DATETIME2(7), + @MaxAutoscaleSeats INT, + @UseKeyConnector BIT = 0, + @UseScim BIT = 0, + @UseCustomPermissions BIT = 0, + @UseSecretsManager BIT = 0, + @Status TINYINT = 0, + @UsePasswordManager BIT = 1, + @SmSeats INT = null, + @SmServiceAccounts INT = null, + @MaxAutoscaleSmSeats INT= null, + @MaxAutoscaleSmServiceAccounts INT = null, + @SecretsManagerBeta BIT = 0, + @LimitCollectionCreationDeletion BIT = NULL, -- Deprecated https://bitwarden.atlassian.net/browse/PM-10863 + @LimitCollectionCreation BIT = NULL, + @LimitCollectionDeletion BIT = NULL, + @AllowAdminAccessToAllCollectionItems BIT = 0 +AS +BEGIN + SET NOCOUNT ON + + SET @LimitCollectionCreation = COALESCE(@LimitCollectionCreation, @LimitCollectionCreationDeletion, 0); + SET @LimitCollectionDeletion = COALESCE(@LimitCollectionDeletion, @LimitCollectionCreationDeletion, 0); + + INSERT INTO [dbo].[Organization] + ( + [Id], + [Identifier], + [Name], + [BusinessName], + [BusinessAddress1], + [BusinessAddress2], + [BusinessAddress3], + [BusinessCountry], + [BusinessTaxNumber], + [BillingEmail], + [Plan], + [PlanType], + [Seats], + [MaxCollections], + [UsePolicies], + [UseSso], + [UseGroups], + [UseDirectory], + [UseEvents], + [UseTotp], + [Use2fa], + [UseApi], + [UseResetPassword], + [SelfHost], + [UsersGetPremium], + [Storage], + [MaxStorageGb], + [Gateway], + [GatewayCustomerId], + [GatewaySubscriptionId], + [ReferenceData], + [Enabled], + [LicenseKey], + [PublicKey], + [PrivateKey], + [TwoFactorProviders], + [ExpirationDate], + [CreationDate], + [RevisionDate], + [OwnersNotifiedOfAutoscaling], + [MaxAutoscaleSeats], + [UseKeyConnector], + [UseScim], + [UseCustomPermissions], + [UseSecretsManager], + [Status], + [UsePasswordManager], + [SmSeats], + [SmServiceAccounts], + [MaxAutoscaleSmSeats], + [MaxAutoscaleSmServiceAccounts], + [SecretsManagerBeta], + [LimitCollectionCreationDeletion], -- Deprecated https://bitwarden.atlassian.net/browse/PM-10863 + [LimitCollectionCreation], + [LimitCollectionDeletion], + [AllowAdminAccessToAllCollectionItems] + ) + VALUES + ( + @Id, + @Identifier, + @Name, + @BusinessName, + @BusinessAddress1, + @BusinessAddress2, + @BusinessAddress3, + @BusinessCountry, + @BusinessTaxNumber, + @BillingEmail, + @Plan, + @PlanType, + @Seats, + @MaxCollections, + @UsePolicies, + @UseSso, + @UseGroups, + @UseDirectory, + @UseEvents, + @UseTotp, + @Use2fa, + @UseApi, + @UseResetPassword, + @SelfHost, + @UsersGetPremium, + @Storage, + @MaxStorageGb, + @Gateway, + @GatewayCustomerId, + @GatewaySubscriptionId, + @ReferenceData, + @Enabled, + @LicenseKey, + @PublicKey, + @PrivateKey, + @TwoFactorProviders, + @ExpirationDate, + @CreationDate, + @RevisionDate, + @OwnersNotifiedOfAutoscaling, + @MaxAutoscaleSeats, + @UseKeyConnector, + @UseScim, + @UseCustomPermissions, + @UseSecretsManager, + @Status, + @UsePasswordManager, + @SmSeats, + @SmServiceAccounts, + @MaxAutoscaleSmSeats, + @MaxAutoscaleSmServiceAccounts, + @SecretsManagerBeta, + COALESCE(@LimitCollectionCreation, @LimitCollectionDeletion, 0), -- Deprecated https://bitwarden.atlassian.net/browse/PM-10863 + @LimitCollectionCreation, + @LimitCollectionDeletion, + @AllowAdminAccessToAllCollectionItems + ) +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[Organization_ReadAbilities] +AS +BEGIN + SET NOCOUNT ON + + SELECT + [Id], + [UseEvents], + [Use2fa], + CASE + WHEN [Use2fa] = 1 AND [TwoFactorProviders] IS NOT NULL AND [TwoFactorProviders] != '{}' THEN + 1 + ELSE + 0 + END AS [Using2fa], + [UsersGetPremium], + [UseCustomPermissions], + [UseSso], + [UseKeyConnector], + [UseScim], + [UseResetPassword], + [UsePolicies], + [Enabled], + [LimitCollectionCreationDeletion], -- Deprecated https://bitwarden.atlassian.net/browse/PM-10863 + [LimitCollectionCreation], + [LimitCollectionDeletion], + [AllowAdminAccessToAllCollectionItems] + FROM + [dbo].[Organization] +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[Organization_Update] + @Id UNIQUEIDENTIFIER, + @Identifier NVARCHAR(50), + @Name NVARCHAR(50), + @BusinessName NVARCHAR(50), + @BusinessAddress1 NVARCHAR(50), + @BusinessAddress2 NVARCHAR(50), + @BusinessAddress3 NVARCHAR(50), + @BusinessCountry VARCHAR(2), + @BusinessTaxNumber NVARCHAR(30), + @BillingEmail NVARCHAR(256), + @Plan NVARCHAR(50), + @PlanType TINYINT, + @Seats INT, + @MaxCollections SMALLINT, + @UsePolicies BIT, + @UseSso BIT, + @UseGroups BIT, + @UseDirectory BIT, + @UseEvents BIT, + @UseTotp BIT, + @Use2fa BIT, + @UseApi BIT, + @UseResetPassword BIT, + @SelfHost BIT, + @UsersGetPremium BIT, + @Storage BIGINT, + @MaxStorageGb SMALLINT, + @Gateway TINYINT, + @GatewayCustomerId VARCHAR(50), + @GatewaySubscriptionId VARCHAR(50), + @ReferenceData VARCHAR(MAX), + @Enabled BIT, + @LicenseKey VARCHAR(100), + @PublicKey VARCHAR(MAX), + @PrivateKey VARCHAR(MAX), + @TwoFactorProviders NVARCHAR(MAX), + @ExpirationDate DATETIME2(7), + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7), + @OwnersNotifiedOfAutoscaling DATETIME2(7), + @MaxAutoscaleSeats INT, + @UseKeyConnector BIT = 0, + @UseScim BIT = 0, + @UseCustomPermissions BIT = 0, + @UseSecretsManager BIT = 0, + @Status TINYINT = 0, + @UsePasswordManager BIT = 1, + @SmSeats INT = null, + @SmServiceAccounts INT = null, + @MaxAutoscaleSmSeats INT = null, + @MaxAutoscaleSmServiceAccounts INT = null, + @SecretsManagerBeta BIT = 0, + @LimitCollectionCreationDeletion BIT = null, -- Deprecated https://bitwarden.atlassian.net/browse/PM-10863 + @LimitCollectionCreation BIT = null, + @LimitCollectionDeletion BIT = null, + @AllowAdminAccessToAllCollectionItems BIT = 0 +AS +BEGIN + SET NOCOUNT ON + + SET @LimitCollectionCreation = COALESCE(@LimitCollectionCreation, @LimitCollectionCreationDeletion, 0); + SET @LimitCollectionDeletion = COALESCE(@LimitCollectionDeletion, @LimitCollectionCreationDeletion, 0); + + UPDATE + [dbo].[Organization] + SET + [Identifier] = @Identifier, + [Name] = @Name, + [BusinessName] = @BusinessName, + [BusinessAddress1] = @BusinessAddress1, + [BusinessAddress2] = @BusinessAddress2, + [BusinessAddress3] = @BusinessAddress3, + [BusinessCountry] = @BusinessCountry, + [BusinessTaxNumber] = @BusinessTaxNumber, + [BillingEmail] = @BillingEmail, + [Plan] = @Plan, + [PlanType] = @PlanType, + [Seats] = @Seats, + [MaxCollections] = @MaxCollections, + [UsePolicies] = @UsePolicies, + [UseSso] = @UseSso, + [UseGroups] = @UseGroups, + [UseDirectory] = @UseDirectory, + [UseEvents] = @UseEvents, + [UseTotp] = @UseTotp, + [Use2fa] = @Use2fa, + [UseApi] = @UseApi, + [UseResetPassword] = @UseResetPassword, + [SelfHost] = @SelfHost, + [UsersGetPremium] = @UsersGetPremium, + [Storage] = @Storage, + [MaxStorageGb] = @MaxStorageGb, + [Gateway] = @Gateway, + [GatewayCustomerId] = @GatewayCustomerId, + [GatewaySubscriptionId] = @GatewaySubscriptionId, + [ReferenceData] = @ReferenceData, + [Enabled] = @Enabled, + [LicenseKey] = @LicenseKey, + [PublicKey] = @PublicKey, + [PrivateKey] = @PrivateKey, + [TwoFactorProviders] = @TwoFactorProviders, + [ExpirationDate] = @ExpirationDate, + [CreationDate] = @CreationDate, + [RevisionDate] = @RevisionDate, + [OwnersNotifiedOfAutoscaling] = @OwnersNotifiedOfAutoscaling, + [MaxAutoscaleSeats] = @MaxAutoscaleSeats, + [UseKeyConnector] = @UseKeyConnector, + [UseScim] = @UseScim, + [UseCustomPermissions] = @UseCustomPermissions, + [UseSecretsManager] = @UseSecretsManager, + [Status] = @Status, + [UsePasswordManager] = @UsePasswordManager, + [SmSeats] = @SmSeats, + [SmServiceAccounts] = @SmServiceAccounts, + [MaxAutoscaleSmSeats] = @MaxAutoscaleSmSeats, + [MaxAutoscaleSmServiceAccounts] = @MaxAutoscaleSmServiceAccounts, + [SecretsManagerBeta] = @SecretsManagerBeta, + [LimitCollectionCreationDeletion] = COALESCE(@LimitCollectionCreation, @LimitCollectionDeletion, 0), -- Deprecated https://bitwarden.atlassian.net/browse/PM-10863 + [LimitCollectionCreation] = @LimitCollectionCreation, + [LimitCollectionDeletion] = @LimitCollectionDeletion, + [AllowAdminAccessToAllCollectionItems] = @AllowAdminAccessToAllCollectionItems + WHERE + [Id] = @Id +END +GO diff --git a/util/Migrator/DbScripts/2024-09-25_01_SyncLimitCollectionCreationColumn.sql b/util/Migrator/DbScripts/2024-09-25_01_SyncLimitCollectionCreationColumn.sql new file mode 100644 index 000000000..d46ac3e99 --- /dev/null +++ b/util/Migrator/DbScripts/2024-09-25_01_SyncLimitCollectionCreationColumn.sql @@ -0,0 +1,8 @@ +-- Sync existing data +UPDATE [dbo].[Organization] +SET + [LimitCollectionCreation] = 1, + [LimitCollectionDeletion] = 1 +WHERE [LimitCollectionCreationDeletion] = 1 +GO + diff --git a/util/Migrator/DbScripts/2024-09-26_00_AddVerifiedOrganizationDomainSsoDetails_ReadByEmail.sql b/util/Migrator/DbScripts/2024-09-26_00_AddVerifiedOrganizationDomainSsoDetails_ReadByEmail.sql new file mode 100644 index 000000000..e36ea1f46 --- /dev/null +++ b/util/Migrator/DbScripts/2024-09-26_00_AddVerifiedOrganizationDomainSsoDetails_ReadByEmail.sql @@ -0,0 +1,24 @@ +CREATE OR ALTER PROCEDURE [dbo].[VerifiedOrganizationDomainSsoDetails_ReadByEmail] + @Email NVARCHAR(256) +AS +BEGIN + SET NOCOUNT ON + + DECLARE @Domain NVARCHAR(256) + +SELECT @Domain = SUBSTRING(@Email, CHARINDEX( '@', @Email) + 1, LEN(@Email)) + +SELECT + O.Id AS OrganizationId, + O.Name AS OrganizationName, + O.Identifier AS OrganizationIdentifier, + OD.DomainName +FROM [dbo].[OrganizationView] O + INNER JOIN [dbo].[OrganizationDomainView] OD ON O.Id = OD.OrganizationId + LEFT JOIN [dbo].[Ssoconfig] S ON O.Id = S.OrganizationId +WHERE OD.DomainName = @Domain + AND O.Enabled = 1 + AND OD.VerifiedDate IS NOT NULL + AND S.Enabled = 1 +END +GO diff --git a/util/Migrator/DbScripts/2024-10-03_00_NotificationStatusDetailsView.sql b/util/Migrator/DbScripts/2024-10-03_00_NotificationStatusDetailsView.sql new file mode 100644 index 000000000..5d9e4fec2 --- /dev/null +++ b/util/Migrator/DbScripts/2024-10-03_00_NotificationStatusDetailsView.sql @@ -0,0 +1,61 @@ +-- View NotificationStatusDetailsView + +IF EXISTS(SELECT * + FROM sys.views + WHERE [Name] = 'NotificationStatusDetailsView') +BEGIN +DROP VIEW [dbo].[NotificationStatusDetailsView] +END +GO + +CREATE VIEW [dbo].[NotificationStatusDetailsView] +AS +SELECT + N.*, + NS.UserId AS NotificationStatusUserId, + NS.ReadDate, + NS.DeletedDate +FROM + [dbo].[Notification] AS N +LEFT JOIN + [dbo].[NotificationStatus] as NS +ON + N.[Id] = NS.[NotificationId] +GO + +-- Stored Procedure Notification_ReadByUserIdAndStatus + +CREATE OR ALTER PROCEDURE [dbo].[Notification_ReadByUserIdAndStatus] + @UserId UNIQUEIDENTIFIER, + @ClientType TINYINT, + @Read BIT, + @Deleted BIT +AS +BEGIN + SET NOCOUNT ON + + SELECT n.* + FROM [dbo].[NotificationStatusDetailsView] n + LEFT JOIN [dbo].[OrganizationUserView] ou ON n.[OrganizationId] = ou.[OrganizationId] + AND ou.[UserId] = @UserId + WHERE (n.[NotificationStatusUserId] IS NULL OR n.[NotificationStatusUserId] = @UserId) + AND [ClientType] IN (0, CASE WHEN @ClientType != 0 THEN @ClientType END) + AND ([Global] = 1 + OR (n.[UserId] = @UserId + AND (n.[OrganizationId] IS NULL + OR ou.[OrganizationId] IS NOT NULL)) + OR (n.[UserId] IS NULL + AND ou.[OrganizationId] IS NOT NULL)) + AND ((@Read IS NULL AND @Deleted IS NULL) + OR (n.[NotificationStatusUserId] IS NOT NULL + AND ((@Read IS NULL + OR IIF((@Read = 1 AND n.[ReadDate] IS NOT NULL) OR + (@Read = 0 AND n.[ReadDate] IS NULL), + 1, 0) = 1) + OR (@Deleted IS NULL + OR IIF((@Deleted = 1 AND n.[DeletedDate] IS NOT NULL) OR + (@Deleted = 0 AND n.[DeletedDate] IS NULL), + 1, 0) = 1)))) + ORDER BY [Priority] DESC, n.[CreationDate] DESC +END +GO diff --git a/util/Migrator/DbScripts/2024-10-04_01_AddClientOrganizationMigrationRecordTable.sql b/util/Migrator/DbScripts/2024-10-04_01_AddClientOrganizationMigrationRecordTable.sql new file mode 100644 index 000000000..15c44b37d --- /dev/null +++ b/util/Migrator/DbScripts/2024-10-04_01_AddClientOrganizationMigrationRecordTable.sql @@ -0,0 +1,175 @@ +-- Table +IF OBJECT_ID('[dbo].[ClientOrganizationMigrationRecord]') IS NULL +BEGIN + CREATE TABLE [dbo].[ClientOrganizationMigrationRecord] ( + [Id] UNIQUEIDENTIFIER NOT NULL, + [OrganizationId] UNIQUEIDENTIFIER NOT NULL, + [ProviderId] UNIQUEIDENTIFIER NOT NULL, + [PlanType] TINYINT NOT NULL, + [Seats] SMALLINT NOT NULL, + [MaxStorageGb] SMALLINT NULL, + [GatewayCustomerId] VARCHAR(50) NOT NULL, + [GatewaySubscriptionId] VARCHAR(50) NOT NULL, + [ExpirationDate] DATETIME2(7) NULL, + [MaxAutoscaleSeats] INT NULL, + [Status] TINYINT NOT NULL, + CONSTRAINT [PK_ClientOrganizationMigrationRecord] PRIMARY KEY CLUSTERED ([Id] ASC), + CONSTRAINT [PK_OrganizationIdProviderId] UNIQUE ([ProviderId], [OrganizationId]) + ); +END +GO + +-- View +CREATE OR AlTER VIEW [dbo].[ClientOrganizationMigrationRecordView] +AS +SELECT + * +FROM + [dbo].[ClientOrganizationMigrationRecord] +GO + +-- Stored Procedures: Create +CREATE OR ALTER PROCEDURE [dbo].[ClientOrganizationMigrationRecord_Create] + @Id UNIQUEIDENTIFIER OUTPUT, + @OrganizationId UNIQUEIDENTIFIER, + @ProviderId UNIQUEIDENTIFIER, + @PlanType TINYINT, + @Seats SMALLINT, + @MaxStorageGb SMALLINT, + @GatewayCustomerId VARCHAR(50), + @GatewaySubscriptionId VARCHAR(50), + @ExpirationDate DATETIME2(7), + @MaxAutoscaleSeats INT, + @Status TINYINT +AS +BEGIN + SET NOCOUNT ON + + INSERT INTO [dbo].[ClientOrganizationMigrationRecord] + ( + [Id], + [OrganizationId], + [ProviderId], + [PlanType], + [Seats], + [MaxStorageGb], + [GatewayCustomerId], + [GatewaySubscriptionId], + [ExpirationDate], + [MaxAutoscaleSeats], + [Status] + ) + VALUES + ( + @Id, + @OrganizationId, + @ProviderId, + @PlanType, + @Seats, + @MaxStorageGb, + @GatewayCustomerId, + @GatewaySubscriptionId, + @ExpirationDate, + @MaxAutoscaleSeats, + @Status + ) +END +GO + +-- Stored Procedures: DeleteById +CREATE OR ALTER PROCEDURE [dbo].[ClientOrganizationMigrationRecord_DeleteById] + @Id UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + DELETE + FROM + [dbo].[ClientOrganizationMigrationRecord] + WHERE + [Id] = @Id +END +GO + +-- Stored Procedures: ReadById +CREATE OR ALTER PROCEDURE [dbo].[ClientOrganizationMigrationRecord_ReadById] + @Id UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT + * + FROM + [dbo].[ClientOrganizationMigrationRecordView] + WHERE + [Id] = @Id +END +GO + +-- Stored Procedures: ReadByOrganizationId +CREATE OR ALTER PROCEDURE [dbo].[ClientOrganizationMigrationRecord_ReadByOrganizationId] + @OrganizationId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT + * + FROM + [dbo].[ClientOrganizationMigrationRecordView] + WHERE + [OrganizationId] = @OrganizationId +END +GO + +-- Stored Procedures: ReadByProviderId +CREATE OR ALTER PROCEDURE [dbo].[ClientOrganizationMigrationRecord_ReadByProviderId] + @ProviderId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT + * + FROM + [dbo].[ClientOrganizationMigrationRecordView] + WHERE + [ProviderId] = @ProviderId +END +GO + +-- Stored Procedures: Update +CREATE OR ALTER PROCEDURE [dbo].[ClientOrganizationMigrationRecord_Update] + @Id UNIQUEIDENTIFIER OUTPUT, + @OrganizationId UNIQUEIDENTIFIER, + @ProviderId UNIQUEIDENTIFIER, + @PlanType TINYINT, + @Seats SMALLINT, + @MaxStorageGb SMALLINT, + @GatewayCustomerId VARCHAR(50), + @GatewaySubscriptionId VARCHAR(50), + @ExpirationDate DATETIME2(7), + @MaxAutoscaleSeats INT, + @Status TINYINT +AS +BEGIN + SET NOCOUNT ON + + UPDATE + [dbo].[ClientOrganizationMigrationRecord] + SET + [OrganizationId] = @OrganizationId, + [ProviderId] = @ProviderId, + [PlanType] = @PlanType, + [Seats] = @Seats, + [MaxStorageGb] = @MaxStorageGb, + [GatewayCustomerId] = @GatewayCustomerId, + [GatewaySubscriptionId] = @GatewaySubscriptionId, + [ExpirationDate] = @ExpirationDate, + [MaxAutoscaleSeats] = @MaxAutoscaleSeats, + [Status] = @Status + WHERE + [Id] = @Id +END +GO diff --git a/util/Migrator/DbScripts/2024-10-18-00_CollectionCipher_ReadByUserId.sql b/util/Migrator/DbScripts/2024-10-18-00_CollectionCipher_ReadByUserId.sql new file mode 100644 index 000000000..154740229 --- /dev/null +++ b/util/Migrator/DbScripts/2024-10-18-00_CollectionCipher_ReadByUserId.sql @@ -0,0 +1,39 @@ +CREATE OR ALTER PROCEDURE [dbo].[CollectionCipher_ReadByUserId] + @UserId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT + CC.* + FROM + [dbo].[CollectionCipher] CC + INNER JOIN + [dbo].[Collection] S ON S.[Id] = CC.[CollectionId] + INNER JOIN + [dbo].[OrganizationUser] OU ON OU.[OrganizationId] = S.[OrganizationId] AND OU.[UserId] = @UserId + INNER JOIN + [dbo].[CollectionUser] CU ON CU.[CollectionId] = S.[Id] AND CU.[OrganizationUserId] = OU.[Id] + WHERE + OU.[Status] = 2 + + UNION ALL + + SELECT + CC.* + FROM + [dbo].[CollectionCipher] CC + INNER JOIN + [dbo].[Collection] S ON S.[Id] = CC.[CollectionId] + INNER JOIN + [dbo].[OrganizationUser] OU ON OU.[OrganizationId] = S.[OrganizationId] AND OU.[UserId] = @UserId + INNER JOIN + [dbo].[GroupUser] GU ON GU.[OrganizationUserId] = OU.[Id] + INNER JOIN + [dbo].[CollectionGroup] CG ON CG.[CollectionId] = CC.[CollectionId] AND CG.[GroupId] = GU.[GroupId] + LEFT JOIN + [dbo].[CollectionUser] CU ON CU.[CollectionId] = S.[Id] AND CU.[OrganizationUserId] = OU.[Id] + WHERE + OU.[Status] = 2 + AND CU.[CollectionId] IS NULL +END 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/MsSqlMigratorUtility/Program.cs b/util/MsSqlMigratorUtility/Program.cs index 03e4716e0..056cb696f 100644 --- a/util/MsSqlMigratorUtility/Program.cs +++ b/util/MsSqlMigratorUtility/Program.cs @@ -17,13 +17,15 @@ internal class Program [Option('f', "folder", Description = "Folder name of database scripts")] string folderName = MigratorConstants.DefaultMigrationsFolderName, [Option('d', "dry-run", Description = "Print the scripts that will be applied without actually executing them")] - bool dryRun = false - ) => MigrateDatabase(databaseConnectionString, repeatable, folderName, dryRun); + bool dryRun = false, + [Option("no-transaction", Description = "Run without adding transaction per script or all scripts")] + bool noTransactionMigration = false + ) => MigrateDatabase(databaseConnectionString, repeatable, folderName, dryRun, noTransactionMigration); private static bool MigrateDatabase(string databaseConnectionString, - bool repeatable = false, string folderName = "", bool dryRun = false) + bool repeatable = false, string folderName = "", bool dryRun = false, bool noTransactionMigration = false) { - var migrator = new DbMigrator(databaseConnectionString); + var migrator = new DbMigrator(databaseConnectionString, noTransactionMigration: noTransactionMigration); bool success; if (!string.IsNullOrWhiteSpace(folderName)) { diff --git a/util/MySqlMigrations/Migrations/20240925201836_SplitOrganizationLimitCollectionCreationDeletionColumn.Designer.cs b/util/MySqlMigrations/Migrations/20240925201836_SplitOrganizationLimitCollectionCreationDeletionColumn.Designer.cs new file mode 100644 index 000000000..093cdb075 --- /dev/null +++ b/util/MySqlMigrations/Migrations/20240925201836_SplitOrganizationLimitCollectionCreationDeletionColumn.Designer.cs @@ -0,0 +1,2798 @@ +// +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("20240925201836_SplitOrganizationLimitCollectionCreationDeletionColumn")] + partial class SplitOrganizationLimitCollectionCreationDeletionColumn + { + /// + 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.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("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/20240925201836_SplitOrganizationLimitCollectionCreationDeletionColumn.cs b/util/MySqlMigrations/Migrations/20240925201836_SplitOrganizationLimitCollectionCreationDeletionColumn.cs new file mode 100644 index 000000000..5a3fef524 --- /dev/null +++ b/util/MySqlMigrations/Migrations/20240925201836_SplitOrganizationLimitCollectionCreationDeletionColumn.cs @@ -0,0 +1,57 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.MySqlMigrations.Migrations; + +/// +public partial class SplitOrganizationLimitCollectionCreationDeletionColumn : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "LimitCollectionCreationDeletion", + table: "Organization", + type: "tinyint(1)", + nullable: false, + oldClrType: typeof(bool), + oldType: "tinyint(1)", + oldDefaultValue: true); + + migrationBuilder.AddColumn( + name: "LimitCollectionCreation", + table: "Organization", + type: "tinyint(1)", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "LimitCollectionDeletion", + table: "Organization", + type: "tinyint(1)", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "LimitCollectionCreation", + table: "Organization"); + + migrationBuilder.DropColumn( + name: "LimitCollectionDeletion", + table: "Organization"); + + migrationBuilder.AlterColumn( + name: "LimitCollectionCreationDeletion", + table: "Organization", + type: "tinyint(1)", + nullable: false, + defaultValue: true, + oldClrType: typeof(bool), + oldType: "tinyint(1)"); + } +} diff --git a/util/MySqlMigrations/Migrations/20240925202356_SyncOrganizationLimitCollectionCreationDeletionColumn.Designer.cs b/util/MySqlMigrations/Migrations/20240925202356_SyncOrganizationLimitCollectionCreationDeletionColumn.Designer.cs new file mode 100644 index 000000000..4739e019a --- /dev/null +++ b/util/MySqlMigrations/Migrations/20240925202356_SyncOrganizationLimitCollectionCreationDeletionColumn.Designer.cs @@ -0,0 +1,2798 @@ +// +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("20240925202356_SyncOrganizationLimitCollectionCreationDeletionColumn")] + partial class SyncOrganizationLimitCollectionCreationDeletionColumn + { + /// + 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.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("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/20240925202356_SyncOrganizationLimitCollectionCreationDeletionColumn.cs b/util/MySqlMigrations/Migrations/20240925202356_SyncOrganizationLimitCollectionCreationDeletionColumn.cs new file mode 100644 index 000000000..7bb07e5d1 --- /dev/null +++ b/util/MySqlMigrations/Migrations/20240925202356_SyncOrganizationLimitCollectionCreationDeletionColumn.cs @@ -0,0 +1,27 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.MySqlMigrations.Migrations; + +/// +public partial class SyncOrganizationLimitCollectionCreationDeletionColumn : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql( + @" + UPDATE Organization + SET + LimitCollectionCreation = LimitCollectionCreationDeletion, + LimitCollectionDeletion = LimitCollectionCreationDeletion; + "); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + + } +} diff --git a/util/MySqlMigrations/Migrations/20241004154527_AddClientOrganizationMigrationRecordTable.Designer.cs b/util/MySqlMigrations/Migrations/20241004154527_AddClientOrganizationMigrationRecordTable.Designer.cs new file mode 100644 index 000000000..51865431c --- /dev/null +++ b/util/MySqlMigrations/Migrations/20241004154527_AddClientOrganizationMigrationRecordTable.Designer.cs @@ -0,0 +1,2845 @@ +// +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("20241004154527_AddClientOrganizationMigrationRecordTable")] + partial class AddClientOrganizationMigrationRecordTable + { + /// + 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("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/20241004154527_AddClientOrganizationMigrationRecordTable.cs b/util/MySqlMigrations/Migrations/20241004154527_AddClientOrganizationMigrationRecordTable.cs new file mode 100644 index 000000000..10047c75f --- /dev/null +++ b/util/MySqlMigrations/Migrations/20241004154527_AddClientOrganizationMigrationRecordTable.cs @@ -0,0 +1,50 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.MySqlMigrations.Migrations; + +/// +public partial class AddClientOrganizationMigrationRecordTable : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "ClientOrganizationMigrationRecord", + columns: table => new + { + Id = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + OrganizationId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + ProviderId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + PlanType = table.Column(type: "tinyint unsigned", nullable: false), + Seats = table.Column(type: "int", nullable: false), + MaxStorageGb = table.Column(type: "smallint", nullable: true), + GatewayCustomerId = table.Column(type: "varchar(50)", maxLength: 50, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + GatewaySubscriptionId = table.Column(type: "varchar(50)", maxLength: 50, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + ExpirationDate = table.Column(type: "datetime(6)", nullable: true), + MaxAutoscaleSeats = table.Column(type: "int", nullable: true), + Status = table.Column(type: "tinyint unsigned", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ClientOrganizationMigrationRecord", x => x.Id); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateIndex( + name: "IX_ClientOrganizationMigrationRecord_ProviderId_OrganizationId", + table: "ClientOrganizationMigrationRecord", + columns: new[] { "ProviderId", "OrganizationId" }, + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "ClientOrganizationMigrationRecord"); + } +} diff --git a/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs index f713f5712..ef7212f17 100644 --- a/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -88,9 +88,14 @@ namespace Bit.MySqlMigrations.Migrations .HasMaxLength(100) .HasColumnType("varchar(100)"); + b.Property("LimitCollectionCreation") + .HasColumnType("tinyint(1)"); + b.Property("LimitCollectionCreationDeletion") - .HasColumnType("tinyint(1)") - .HasDefaultValue(true); + .HasColumnType("tinyint(1)"); + + b.Property("LimitCollectionDeletion") + .HasColumnType("tinyint(1)"); b.Property("MaxAutoscaleSeats") .HasColumnType("int"); @@ -682,6 +687,53 @@ namespace Bit.MySqlMigrations.Migrations 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") diff --git a/util/PostgresMigrations/Migrations/20240925201832_SplitOrganizationLimitCollectionCreationDeletionColumn.Designer.cs b/util/PostgresMigrations/Migrations/20240925201832_SplitOrganizationLimitCollectionCreationDeletionColumn.Designer.cs new file mode 100644 index 000000000..dd8c67d3c --- /dev/null +++ b/util/PostgresMigrations/Migrations/20240925201832_SplitOrganizationLimitCollectionCreationDeletionColumn.Designer.cs @@ -0,0 +1,2804 @@ +// +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("20240925201832_SplitOrganizationLimitCollectionCreationDeletionColumn")] + partial class SplitOrganizationLimitCollectionCreationDeletionColumn + { + /// + 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.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("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/20240925201832_SplitOrganizationLimitCollectionCreationDeletionColumn.cs b/util/PostgresMigrations/Migrations/20240925201832_SplitOrganizationLimitCollectionCreationDeletionColumn.cs new file mode 100644 index 000000000..5d91747fa --- /dev/null +++ b/util/PostgresMigrations/Migrations/20240925201832_SplitOrganizationLimitCollectionCreationDeletionColumn.cs @@ -0,0 +1,57 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.PostgresMigrations.Migrations; + +/// +public partial class SplitOrganizationLimitCollectionCreationDeletionColumn : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "LimitCollectionCreationDeletion", + table: "Organization", + type: "boolean", + nullable: false, + oldClrType: typeof(bool), + oldType: "boolean", + oldDefaultValue: true); + + migrationBuilder.AddColumn( + name: "LimitCollectionCreation", + table: "Organization", + type: "boolean", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "LimitCollectionDeletion", + table: "Organization", + type: "boolean", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "LimitCollectionCreation", + table: "Organization"); + + migrationBuilder.DropColumn( + name: "LimitCollectionDeletion", + table: "Organization"); + + migrationBuilder.AlterColumn( + name: "LimitCollectionCreationDeletion", + table: "Organization", + type: "boolean", + nullable: false, + defaultValue: true, + oldClrType: typeof(bool), + oldType: "boolean"); + } +} diff --git a/util/PostgresMigrations/Migrations/20240925202400_SyncOrganizationLimitCollectionCreationDeletionColumn.Designer.cs b/util/PostgresMigrations/Migrations/20240925202400_SyncOrganizationLimitCollectionCreationDeletionColumn.Designer.cs new file mode 100644 index 000000000..5d5976501 --- /dev/null +++ b/util/PostgresMigrations/Migrations/20240925202400_SyncOrganizationLimitCollectionCreationDeletionColumn.Designer.cs @@ -0,0 +1,2804 @@ +// +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("20240925202400_SyncOrganizationLimitCollectionCreationDeletionColumn")] + partial class SyncOrganizationLimitCollectionCreationDeletionColumn + { + /// + 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.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("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/20240925202400_SyncOrganizationLimitCollectionCreationDeletionColumn.cs b/util/PostgresMigrations/Migrations/20240925202400_SyncOrganizationLimitCollectionCreationDeletionColumn.cs new file mode 100644 index 000000000..eec50a109 --- /dev/null +++ b/util/PostgresMigrations/Migrations/20240925202400_SyncOrganizationLimitCollectionCreationDeletionColumn.cs @@ -0,0 +1,31 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.PostgresMigrations.Migrations; + +/// +public partial class SyncOrganizationLimitCollectionCreationDeletionColumn : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + // Postgres is particular about the casing of entities. It wants to + // lowercase everything by default, and convert casings + // automatically. Quoting the entity names here provides explicit & + // correct casing. + migrationBuilder.Sql( + @" + UPDATE ""Organization"" + SET + ""LimitCollectionCreation"" = ""LimitCollectionCreationDeletion"", + ""LimitCollectionDeletion"" = ""LimitCollectionCreationDeletion""; + "); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + + } +} diff --git a/util/PostgresMigrations/Migrations/20241004154531_AddClientOrganizationMigrationRecordTable.Designer.cs b/util/PostgresMigrations/Migrations/20241004154531_AddClientOrganizationMigrationRecordTable.Designer.cs new file mode 100644 index 000000000..252ad0bba --- /dev/null +++ b/util/PostgresMigrations/Migrations/20241004154531_AddClientOrganizationMigrationRecordTable.Designer.cs @@ -0,0 +1,2851 @@ +// +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("20241004154531_AddClientOrganizationMigrationRecordTable")] + partial class AddClientOrganizationMigrationRecordTable + { + /// + 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("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/20241004154531_AddClientOrganizationMigrationRecordTable.cs b/util/PostgresMigrations/Migrations/20241004154531_AddClientOrganizationMigrationRecordTable.cs new file mode 100644 index 000000000..b024dda2f --- /dev/null +++ b/util/PostgresMigrations/Migrations/20241004154531_AddClientOrganizationMigrationRecordTable.cs @@ -0,0 +1,47 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.PostgresMigrations.Migrations; + +/// +public partial class AddClientOrganizationMigrationRecordTable : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "ClientOrganizationMigrationRecord", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + OrganizationId = table.Column(type: "uuid", nullable: false), + ProviderId = table.Column(type: "uuid", nullable: false), + PlanType = table.Column(type: "smallint", nullable: false), + Seats = table.Column(type: "integer", nullable: false), + MaxStorageGb = table.Column(type: "smallint", nullable: true), + GatewayCustomerId = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), + GatewaySubscriptionId = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), + ExpirationDate = table.Column(type: "timestamp with time zone", nullable: true), + MaxAutoscaleSeats = table.Column(type: "integer", nullable: true), + Status = table.Column(type: "smallint", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ClientOrganizationMigrationRecord", x => x.Id); + }); + + migrationBuilder.CreateIndex( + name: "IX_ClientOrganizationMigrationRecord_ProviderId_OrganizationId", + table: "ClientOrganizationMigrationRecord", + columns: new[] { "ProviderId", "OrganizationId" }, + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "ClientOrganizationMigrationRecord"); + } +} diff --git a/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs index 840f79072..a50b72568 100644 --- a/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -90,9 +90,14 @@ namespace Bit.PostgresMigrations.Migrations .HasMaxLength(100) .HasColumnType("character varying(100)"); + b.Property("LimitCollectionCreation") + .HasColumnType("boolean"); + b.Property("LimitCollectionCreationDeletion") - .HasColumnType("boolean") - .HasDefaultValue(true); + .HasColumnType("boolean"); + + b.Property("LimitCollectionDeletion") + .HasColumnType("boolean"); b.Property("MaxAutoscaleSeats") .HasColumnType("integer"); @@ -687,6 +692,53 @@ namespace Bit.PostgresMigrations.Migrations 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") 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/20240925201828_SplitOrganizationLimitCollectionCreationDeletionColumn.Designer.cs b/util/SqliteMigrations/Migrations/20240925201828_SplitOrganizationLimitCollectionCreationDeletionColumn.Designer.cs new file mode 100644 index 000000000..25b81c538 --- /dev/null +++ b/util/SqliteMigrations/Migrations/20240925201828_SplitOrganizationLimitCollectionCreationDeletionColumn.Designer.cs @@ -0,0 +1,2787 @@ +// +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("20240925201828_SplitOrganizationLimitCollectionCreationDeletionColumn")] + partial class SplitOrganizationLimitCollectionCreationDeletionColumn + { + /// + 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.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("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/20240925201828_SplitOrganizationLimitCollectionCreationDeletionColumn.cs b/util/SqliteMigrations/Migrations/20240925201828_SplitOrganizationLimitCollectionCreationDeletionColumn.cs new file mode 100644 index 000000000..ebdffb705 --- /dev/null +++ b/util/SqliteMigrations/Migrations/20240925201828_SplitOrganizationLimitCollectionCreationDeletionColumn.cs @@ -0,0 +1,57 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.SqliteMigrations.Migrations; + +/// +public partial class SplitOrganizationLimitCollectionCreationDeletionColumn : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "LimitCollectionCreationDeletion", + table: "Organization", + type: "INTEGER", + nullable: false, + oldClrType: typeof(bool), + oldType: "INTEGER", + oldDefaultValue: true); + + migrationBuilder.AddColumn( + name: "LimitCollectionCreation", + table: "Organization", + type: "INTEGER", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "LimitCollectionDeletion", + table: "Organization", + type: "INTEGER", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "LimitCollectionCreation", + table: "Organization"); + + migrationBuilder.DropColumn( + name: "LimitCollectionDeletion", + table: "Organization"); + + migrationBuilder.AlterColumn( + name: "LimitCollectionCreationDeletion", + table: "Organization", + type: "INTEGER", + nullable: false, + defaultValue: true, + oldClrType: typeof(bool), + oldType: "INTEGER"); + } +} diff --git a/util/SqliteMigrations/Migrations/20240925202404_SyncOrganizationLimitCollectionCreationDeletionColumn.Designer.cs b/util/SqliteMigrations/Migrations/20240925202404_SyncOrganizationLimitCollectionCreationDeletionColumn.Designer.cs new file mode 100644 index 000000000..3b63a10c9 --- /dev/null +++ b/util/SqliteMigrations/Migrations/20240925202404_SyncOrganizationLimitCollectionCreationDeletionColumn.Designer.cs @@ -0,0 +1,2787 @@ +// +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("20240925202404_SyncOrganizationLimitCollectionCreationDeletionColumn")] + partial class SyncOrganizationLimitCollectionCreationDeletionColumn + { + /// + 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.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("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/20240925202404_SyncOrganizationLimitCollectionCreationDeletionColumn.cs b/util/SqliteMigrations/Migrations/20240925202404_SyncOrganizationLimitCollectionCreationDeletionColumn.cs new file mode 100644 index 000000000..079a2867b --- /dev/null +++ b/util/SqliteMigrations/Migrations/20240925202404_SyncOrganizationLimitCollectionCreationDeletionColumn.cs @@ -0,0 +1,27 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.SqliteMigrations.Migrations; + +/// +public partial class SyncOrganizationLimitCollectionCreationDeletionColumn : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql( + @" + UPDATE Organization + SET + LimitCollectionCreation = LimitCollectionCreationDeletion, + LimitCollectionDeletion = LimitCollectionCreationDeletion; + "); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + + } +} diff --git a/util/SqliteMigrations/Migrations/20241004154523_AddClientOrganizationMigrationRecordTable.Designer.cs b/util/SqliteMigrations/Migrations/20241004154523_AddClientOrganizationMigrationRecordTable.Designer.cs new file mode 100644 index 000000000..588ed0741 --- /dev/null +++ b/util/SqliteMigrations/Migrations/20241004154523_AddClientOrganizationMigrationRecordTable.Designer.cs @@ -0,0 +1,2834 @@ +// +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("20241004154523_AddClientOrganizationMigrationRecordTable")] + partial class AddClientOrganizationMigrationRecordTable + { + /// + 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("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/20241004154523_AddClientOrganizationMigrationRecordTable.cs b/util/SqliteMigrations/Migrations/20241004154523_AddClientOrganizationMigrationRecordTable.cs new file mode 100644 index 000000000..fce8ae17d --- /dev/null +++ b/util/SqliteMigrations/Migrations/20241004154523_AddClientOrganizationMigrationRecordTable.cs @@ -0,0 +1,47 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.SqliteMigrations.Migrations; + +/// +public partial class AddClientOrganizationMigrationRecordTable : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "ClientOrganizationMigrationRecord", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + OrganizationId = table.Column(type: "TEXT", nullable: false), + ProviderId = table.Column(type: "TEXT", nullable: false), + PlanType = table.Column(type: "INTEGER", nullable: false), + Seats = table.Column(type: "INTEGER", nullable: false), + MaxStorageGb = table.Column(type: "INTEGER", nullable: true), + GatewayCustomerId = table.Column(type: "TEXT", maxLength: 50, nullable: false), + GatewaySubscriptionId = table.Column(type: "TEXT", maxLength: 50, nullable: false), + ExpirationDate = table.Column(type: "TEXT", nullable: true), + MaxAutoscaleSeats = table.Column(type: "INTEGER", nullable: true), + Status = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ClientOrganizationMigrationRecord", x => x.Id); + }); + + migrationBuilder.CreateIndex( + name: "IX_ClientOrganizationMigrationRecord_ProviderId_OrganizationId", + table: "ClientOrganizationMigrationRecord", + columns: new[] { "ProviderId", "OrganizationId" }, + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "ClientOrganizationMigrationRecord"); + } +} diff --git a/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs index e57f72108..997363135 100644 --- a/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -83,9 +83,14 @@ namespace Bit.SqliteMigrations.Migrations .HasMaxLength(100) .HasColumnType("TEXT"); + b.Property("LimitCollectionCreation") + .HasColumnType("INTEGER"); + b.Property("LimitCollectionCreationDeletion") - .HasColumnType("INTEGER") - .HasDefaultValue(true); + .HasColumnType("INTEGER"); + + b.Property("LimitCollectionDeletion") + .HasColumnType("INTEGER"); b.Property("MaxAutoscaleSeats") .HasColumnType("INTEGER"); @@ -671,6 +676,53 @@ namespace Bit.SqliteMigrations.Migrations 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")