mirror of
https://github.com/bitwarden/server.git
synced 2024-12-01 13:43:23 +01:00
Merge branch 'main' into ac/pm-10338/leave-endpoint-to-log-organizationuser_left
This commit is contained in:
commit
dc1acb5355
@ -3,7 +3,7 @@
|
|||||||
"isRoot": true,
|
"isRoot": true,
|
||||||
"tools": {
|
"tools": {
|
||||||
"swashbuckle.aspnetcore.cli": {
|
"swashbuckle.aspnetcore.cli": {
|
||||||
"version": "6.8.1",
|
"version": "6.9.0",
|
||||||
"commands": ["swagger"]
|
"commands": ["swagger"]
|
||||||
},
|
},
|
||||||
"dotnet-ef": {
|
"dotnet-ef": {
|
||||||
|
@ -29,7 +29,7 @@ jobs:
|
|||||||
secrets: "github-pat-bitwarden-devops-bot-repo-scope"
|
secrets: "github-pat-bitwarden-devops-bot-repo-scope"
|
||||||
|
|
||||||
- name: Check out branch
|
- name: Check out branch
|
||||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
with:
|
with:
|
||||||
token: ${{ steps.retrieve-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
|
token: ${{ steps.retrieve-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
|
||||||
|
|
||||||
@ -53,7 +53,7 @@ jobs:
|
|||||||
if: ${{ needs.setup.outputs.copy_finalization_scripts == 'true' }}
|
if: ${{ needs.setup.outputs.copy_finalization_scripts == 'true' }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@ -107,7 +107,7 @@ jobs:
|
|||||||
devops-alerts-slack-webhook-url"
|
devops-alerts-slack-webhook-url"
|
||||||
|
|
||||||
- name: Import GPG keys
|
- 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:
|
with:
|
||||||
gpg_private_key: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key }}
|
gpg_private_key: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key }}
|
||||||
passphrase: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key-passphrase }}
|
passphrase: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key-passphrase }}
|
||||||
|
24
.github/workflows/build.yml
vendored
24
.github/workflows/build.yml
vendored
@ -18,10 +18,10 @@ jobs:
|
|||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: Check out repo
|
- name: Check out repo
|
||||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
|
||||||
- name: Set up .NET
|
- name: Set up .NET
|
||||||
uses: actions/setup-dotnet@6bd8b7f7774af54e05809fcc5431931b3eb1ddee # v4.0.1
|
uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0
|
||||||
|
|
||||||
- name: Verify format
|
- name: Verify format
|
||||||
run: dotnet format --verify-no-changes
|
run: dotnet format --verify-no-changes
|
||||||
@ -67,13 +67,13 @@ jobs:
|
|||||||
node: true
|
node: true
|
||||||
steps:
|
steps:
|
||||||
- name: Check out repo
|
- name: Check out repo
|
||||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
|
||||||
- name: Set up .NET
|
- name: Set up .NET
|
||||||
uses: actions/setup-dotnet@6bd8b7f7774af54e05809fcc5431931b3eb1ddee # v4.0.1
|
uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0
|
||||||
|
|
||||||
- name: Set up Node
|
- name: Set up Node
|
||||||
uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4
|
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
|
||||||
with:
|
with:
|
||||||
cache: "npm"
|
cache: "npm"
|
||||||
cache-dependency-path: "**/package-lock.json"
|
cache-dependency-path: "**/package-lock.json"
|
||||||
@ -172,7 +172,7 @@ jobs:
|
|||||||
dotnet: true
|
dotnet: true
|
||||||
steps:
|
steps:
|
||||||
- name: Check out repo
|
- name: Check out repo
|
||||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
|
||||||
- name: Check branch to publish
|
- name: Check branch to publish
|
||||||
env:
|
env:
|
||||||
@ -274,14 +274,14 @@ jobs:
|
|||||||
|
|
||||||
- name: Scan Docker image
|
- name: Scan Docker image
|
||||||
id: container-scan
|
id: container-scan
|
||||||
uses: anchore/scan-action@49e50b215b647c5ec97abb66f69af73c46a4ca08 # v5.0.1
|
uses: anchore/scan-action@5ed195cc06065322983cae4bb31e2a751feb86fd # v5.2.0
|
||||||
with:
|
with:
|
||||||
image: ${{ steps.image-tags.outputs.primary_tag }}
|
image: ${{ steps.image-tags.outputs.primary_tag }}
|
||||||
fail-build: false
|
fail-build: false
|
||||||
output-format: sarif
|
output-format: sarif
|
||||||
|
|
||||||
- name: Upload Grype results to GitHub
|
- name: Upload Grype results to GitHub
|
||||||
uses: github/codeql-action/upload-sarif@f779452ac5af1c261dce0346a8f964149f49322b # v3.26.13
|
uses: github/codeql-action/upload-sarif@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0
|
||||||
with:
|
with:
|
||||||
sarif_file: ${{ steps.container-scan.outputs.sarif }}
|
sarif_file: ${{ steps.container-scan.outputs.sarif }}
|
||||||
|
|
||||||
@ -291,10 +291,10 @@ jobs:
|
|||||||
needs: build-docker
|
needs: build-docker
|
||||||
steps:
|
steps:
|
||||||
- name: Check out repo
|
- name: Check out repo
|
||||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
|
||||||
- name: Set up .NET
|
- 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
|
- name: Log in to Azure - production subscription
|
||||||
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
|
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
|
||||||
@ -466,10 +466,10 @@ jobs:
|
|||||||
- win-x64
|
- win-x64
|
||||||
steps:
|
steps:
|
||||||
- name: Check out repo
|
- name: Check out repo
|
||||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
|
||||||
- name: Set up .NET
|
- name: Set up .NET
|
||||||
uses: actions/setup-dotnet@6bd8b7f7774af54e05809fcc5431931b3eb1ddee # v4.0.1
|
uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0
|
||||||
|
|
||||||
- name: Print environment
|
- name: Print environment
|
||||||
run: |
|
run: |
|
||||||
|
@ -12,7 +12,7 @@ jobs:
|
|||||||
config-exists: ${{ steps.validate-config.outputs.config-exists }}
|
config-exists: ${{ steps.validate-config.outputs.config-exists }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout PR
|
- name: Checkout PR
|
||||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
|
||||||
- name: Validate config exists in path
|
- name: Validate config exists in path
|
||||||
id: validate-config
|
id: validate-config
|
||||||
|
2
.github/workflows/cleanup-rc-branch.yml
vendored
2
.github/workflows/cleanup-rc-branch.yml
vendored
@ -23,7 +23,7 @@ jobs:
|
|||||||
secrets: "github-pat-bitwarden-devops-bot-repo-scope"
|
secrets: "github-pat-bitwarden-devops-bot-repo-scope"
|
||||||
|
|
||||||
- name: Checkout main
|
- name: Checkout main
|
||||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
with:
|
with:
|
||||||
ref: main
|
ref: main
|
||||||
token: ${{ steps.retrieve-bot-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
|
token: ${{ steps.retrieve-bot-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
|
||||||
|
2
.github/workflows/code-references.yml
vendored
2
.github/workflows/code-references.yml
vendored
@ -33,7 +33,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check out repository
|
- name: Check out repository
|
||||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
|
||||||
- name: Collect
|
- name: Collect
|
||||||
id: collect
|
id: collect
|
||||||
|
2
.github/workflows/protect-files.yml
vendored
2
.github/workflows/protect-files.yml
vendored
@ -28,7 +28,7 @@ jobs:
|
|||||||
label: "DB-migrations-changed"
|
label: "DB-migrations-changed"
|
||||||
steps:
|
steps:
|
||||||
- name: Check out repo
|
- name: Check out repo
|
||||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
with:
|
with:
|
||||||
fetch-depth: 2
|
fetch-depth: 2
|
||||||
|
|
||||||
|
2
.github/workflows/publish.yml
vendored
2
.github/workflows/publish.yml
vendored
@ -98,7 +98,7 @@ jobs:
|
|||||||
echo "Github Release Option: $RELEASE_OPTION"
|
echo "Github Release Option: $RELEASE_OPTION"
|
||||||
|
|
||||||
- name: Check out repo
|
- name: Check out repo
|
||||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
|
||||||
- name: Set up project name
|
- name: Set up project name
|
||||||
id: setup
|
id: setup
|
||||||
|
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@ -36,7 +36,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Check out repo
|
- name: Check out repo
|
||||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
|
||||||
- name: Check release version
|
- name: Check release version
|
||||||
id: version
|
id: version
|
||||||
|
58
.github/workflows/repository-management.yml
vendored
58
.github/workflows/repository-management.yml
vendored
@ -27,7 +27,7 @@ jobs:
|
|||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: Check out target ref
|
- name: Check out target ref
|
||||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
with:
|
with:
|
||||||
ref: ${{ inputs.target_ref }}
|
ref: ${{ inputs.target_ref }}
|
||||||
|
|
||||||
@ -62,7 +62,7 @@ jobs:
|
|||||||
version: ${{ inputs.version_number_override }}
|
version: ${{ inputs.version_number_override }}
|
||||||
|
|
||||||
- name: Check out branch
|
- name: Check out branch
|
||||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
with:
|
with:
|
||||||
ref: main
|
ref: main
|
||||||
|
|
||||||
@ -135,13 +135,61 @@ jobs:
|
|||||||
git config --local user.email "actions@github.com"
|
git config --local user.email "actions@github.com"
|
||||||
git config --local user.name "Github Actions"
|
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
|
- name: Commit files
|
||||||
run: git commit -m "Bumped version to ${{ steps.set-final-version-output.outputs.version }}" -a
|
run: git commit -m "Bumped version to ${{ steps.set-final-version-output.outputs.version }}" -a
|
||||||
|
|
||||||
- name: Push changes
|
- name: Push changes
|
||||||
|
run: git push
|
||||||
|
|
||||||
|
- name: Generate GH App token
|
||||||
|
uses: actions/create-github-app-token@5d869da34e18e7287c1daad50e0b8ea0f506ce69 # v1.11.0
|
||||||
|
id: app-token
|
||||||
|
with:
|
||||||
|
app-id: ${{ secrets.BW_GHAPP_ID }}
|
||||||
|
private-key: ${{ secrets.BW_GHAPP_KEY }}
|
||||||
|
owner: ${{ github.repository_owner }}
|
||||||
|
|
||||||
|
- name: Create version PR
|
||||||
|
id: create-pr
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
||||||
|
PR_BRANCH: ${{ steps.create-branch.outputs.name }}
|
||||||
|
TITLE: "Bump version to ${{ steps.set-final-version-output.outputs.version }}"
|
||||||
run: |
|
run: |
|
||||||
git pull -pt
|
PR_URL=$(gh pr create --title "$TITLE" \
|
||||||
git push
|
--base "main" \
|
||||||
|
--head "$PR_BRANCH" \
|
||||||
|
--label "version update" \
|
||||||
|
--label "automated pr" \
|
||||||
|
--body "
|
||||||
|
## Type of change
|
||||||
|
- [ ] Bug fix
|
||||||
|
- [ ] New feature development
|
||||||
|
- [ ] Tech debt (refactoring, code cleanup, dependency upgrades, etc)
|
||||||
|
- [ ] Build/deploy pipeline (DevOps)
|
||||||
|
- [X] Other
|
||||||
|
## Objective
|
||||||
|
Automated version bump to ${{ steps.set-final-version-output.outputs.version }}")
|
||||||
|
echo "pr_number=${PR_URL##*/}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Approve PR
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
PR_NUMBER: ${{ steps.create-pr.outputs.pr_number }}
|
||||||
|
run: gh pr review $PR_NUMBER --approve
|
||||||
|
|
||||||
|
- name: Merge PR
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
||||||
|
PR_NUMBER: ${{ steps.create-pr.outputs.pr_number }}
|
||||||
|
run: gh pr merge $PR_NUMBER --squash --auto --delete-branch
|
||||||
|
|
||||||
|
|
||||||
cherry_pick:
|
cherry_pick:
|
||||||
@ -150,7 +198,7 @@ jobs:
|
|||||||
needs: bump_version
|
needs: bump_version
|
||||||
steps:
|
steps:
|
||||||
- name: Check out main branch
|
- name: Check out main branch
|
||||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
with:
|
with:
|
||||||
ref: main
|
ref: main
|
||||||
|
|
||||||
|
10
.github/workflows/scan.yml
vendored
10
.github/workflows/scan.yml
vendored
@ -26,7 +26,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check out repo
|
- name: Check out repo
|
||||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.event.pull_request.head.sha }}
|
ref: ${{ github.event.pull_request.head.sha }}
|
||||||
|
|
||||||
@ -46,7 +46,7 @@ jobs:
|
|||||||
--output-path . ${{ env.INCREMENTAL }}
|
--output-path . ${{ env.INCREMENTAL }}
|
||||||
|
|
||||||
- name: Upload Checkmarx results to GitHub
|
- name: Upload Checkmarx results to GitHub
|
||||||
uses: github/codeql-action/upload-sarif@f779452ac5af1c261dce0346a8f964149f49322b # v3.26.13
|
uses: github/codeql-action/upload-sarif@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0
|
||||||
with:
|
with:
|
||||||
sarif_file: cx_result.sarif
|
sarif_file: cx_result.sarif
|
||||||
|
|
||||||
@ -60,19 +60,19 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Set up JDK 17
|
- name: Set up JDK 17
|
||||||
uses: actions/setup-java@b36c23c0d998641eff861008f374ee103c25ac73 # v4.4.0
|
uses: actions/setup-java@8df1039502a15bceb9433410b1a100fbe190c53b # v4.5.0
|
||||||
with:
|
with:
|
||||||
java-version: 17
|
java-version: 17
|
||||||
distribution: "zulu"
|
distribution: "zulu"
|
||||||
|
|
||||||
- name: Check out repo
|
- name: Check out repo
|
||||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
ref: ${{ github.event.pull_request.head.sha }}
|
ref: ${{ github.event.pull_request.head.sha }}
|
||||||
|
|
||||||
- name: Set up .NET
|
- name: Set up .NET
|
||||||
uses: actions/setup-dotnet@6bd8b7f7774af54e05809fcc5431931b3eb1ddee # v4.0.1
|
uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0
|
||||||
|
|
||||||
- name: Install SonarCloud scanner
|
- name: Install SonarCloud scanner
|
||||||
run: dotnet tool install dotnet-sonarscanner -g
|
run: dotnet tool install dotnet-sonarscanner -g
|
||||||
|
8
.github/workflows/test-database.yml
vendored
8
.github/workflows/test-database.yml
vendored
@ -35,10 +35,10 @@ jobs:
|
|||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: Check out repo
|
- name: Check out repo
|
||||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
|
||||||
- name: Set up .NET
|
- name: Set up .NET
|
||||||
uses: actions/setup-dotnet@6bd8b7f7774af54e05809fcc5431931b3eb1ddee # v4.0.1
|
uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0
|
||||||
|
|
||||||
- name: Restore tools
|
- name: Restore tools
|
||||||
run: dotnet tool restore
|
run: dotnet tool restore
|
||||||
@ -146,10 +146,10 @@ jobs:
|
|||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: Check out repo
|
- name: Check out repo
|
||||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
|
||||||
- name: Set up .NET
|
- name: Set up .NET
|
||||||
uses: actions/setup-dotnet@6bd8b7f7774af54e05809fcc5431931b3eb1ddee # v4.0.1
|
uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0
|
||||||
|
|
||||||
- name: Print environment
|
- name: Print environment
|
||||||
run: |
|
run: |
|
||||||
|
4
.github/workflows/test.yml
vendored
4
.github/workflows/test.yml
vendored
@ -46,10 +46,10 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check out repo
|
- name: Check out repo
|
||||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
|
||||||
- name: Set up .NET
|
- name: Set up .NET
|
||||||
uses: actions/setup-dotnet@6bd8b7f7774af54e05809fcc5431931b3eb1ddee # v4.0.1
|
uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0
|
||||||
|
|
||||||
- name: Print environment
|
- name: Print environment
|
||||||
run: |
|
run: |
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
|
||||||
<Version>2024.10.1</Version>
|
<Version>2024.11.0</Version>
|
||||||
|
|
||||||
<RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace>
|
<RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
@ -40,6 +40,36 @@ public class CreateProviderCommand : ICreateProviderCommand
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async Task CreateMspAsync(Provider provider, string ownerEmail, int teamsMinimumSeats, int enterpriseMinimumSeats)
|
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<Guid> CreateProviderAsync(Provider provider, string ownerEmail)
|
||||||
{
|
{
|
||||||
var owner = await _userRepository.GetByEmailAsync(ownerEmail);
|
var owner = await _userRepository.GetByEmailAsync(ownerEmail);
|
||||||
if (owner == null)
|
if (owner == null)
|
||||||
@ -64,27 +94,10 @@ public class CreateProviderCommand : ICreateProviderCommand
|
|||||||
Status = ProviderUserStatusType.Confirmed,
|
Status = ProviderUserStatusType.Confirmed,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isConsolidatedBillingEnabled)
|
|
||||||
{
|
|
||||||
var providerPlans = new List<ProviderPlan>
|
|
||||||
{
|
|
||||||
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 _providerUserRepository.CreateAsync(providerUser);
|
||||||
await _providerService.SendProviderSetupInviteEmailAsync(provider, owner.Email);
|
await _providerService.SendProviderSetupInviteEmailAsync(provider, owner.Email);
|
||||||
}
|
|
||||||
|
|
||||||
public async Task CreateResellerAsync(Provider provider)
|
return provider.Id;
|
||||||
{
|
|
||||||
await ProviderRepositoryCreateAsync(provider, ProviderStatusType.Created);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task ProviderRepositoryCreateAsync(Provider provider, ProviderStatusType status)
|
private async Task ProviderRepositoryCreateAsync(Provider provider, ProviderStatusType status)
|
||||||
@ -95,9 +108,9 @@ public class CreateProviderCommand : ICreateProviderCommand
|
|||||||
await _providerRepository.CreateAsync(provider);
|
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,
|
ProviderId = providerId,
|
||||||
PlanType = planType,
|
PlanType = planType,
|
||||||
@ -105,5 +118,6 @@ public class CreateProviderCommand : ICreateProviderCommand
|
|||||||
PurchasedSeats = 0,
|
PurchasedSeats = 0,
|
||||||
AllocatedSeats = 0
|
AllocatedSeats = 0
|
||||||
};
|
};
|
||||||
|
await _providerPlanRepository.CreateAsync(plan);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,7 @@ using Bit.Core.Billing.Enums;
|
|||||||
using Bit.Core.Billing.Extensions;
|
using Bit.Core.Billing.Extensions;
|
||||||
using Bit.Core.Billing.Repositories;
|
using Bit.Core.Billing.Repositories;
|
||||||
using Bit.Core.Billing.Services;
|
using Bit.Core.Billing.Services;
|
||||||
|
using Bit.Core.Billing.Services.Contracts;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
@ -379,42 +380,23 @@ public class ProviderBillingService(
|
|||||||
|
|
||||||
var subscriptionItemOptionsList = new List<SubscriptionItemOptions>();
|
var subscriptionItemOptionsList = new List<SubscriptionItemOptions>();
|
||||||
|
|
||||||
var teamsProviderPlan =
|
foreach (var providerPlan in providerPlans)
|
||||||
providerPlans.SingleOrDefault(providerPlan => providerPlan.PlanType == PlanType.TeamsMonthly);
|
|
||||||
|
|
||||||
if (teamsProviderPlan == null || !teamsProviderPlan.IsConfigured())
|
|
||||||
{
|
{
|
||||||
logger.LogError("Cannot start subscription for provider ({ProviderID}) that has no configured Teams plan", provider.Id);
|
var plan = StaticStore.GetPlan(providerPlan.PlanType);
|
||||||
|
|
||||||
|
if (!providerPlan.IsConfigured())
|
||||||
|
{
|
||||||
|
logger.LogError("Cannot start subscription for provider ({ProviderID}) that has no configured {ProviderName} plan", provider.Id, plan.Name);
|
||||||
throw new BillingException();
|
throw new BillingException();
|
||||||
}
|
}
|
||||||
|
|
||||||
var teamsPlan = StaticStore.GetPlan(PlanType.TeamsMonthly);
|
|
||||||
|
|
||||||
subscriptionItemOptionsList.Add(new SubscriptionItemOptions
|
subscriptionItemOptionsList.Add(new SubscriptionItemOptions
|
||||||
{
|
{
|
||||||
Price = teamsPlan.PasswordManager.StripeProviderPortalSeatPlanId,
|
Price = plan.PasswordManager.StripeProviderPortalSeatPlanId,
|
||||||
Quantity = teamsProviderPlan.SeatMinimum
|
Quantity = providerPlan.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
|
var subscriptionCreateOptions = new SubscriptionCreateOptions
|
||||||
{
|
{
|
||||||
AutomaticTax = new SubscriptionAutomaticTaxOptions
|
AutomaticTax = new SubscriptionAutomaticTaxOptions
|
||||||
@ -456,144 +438,142 @@ public class ProviderBillingService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task UpdateSeatMinimums(
|
public async Task ChangePlan(ChangeProviderPlanCommand command)
|
||||||
Provider provider,
|
|
||||||
int enterpriseSeatMinimum,
|
|
||||||
int teamsSeatMinimum)
|
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(provider);
|
var plan = await providerPlanRepository.GetByIdAsync(command.ProviderPlanId);
|
||||||
|
|
||||||
if (enterpriseSeatMinimum < 0 || teamsSeatMinimum < 0)
|
if (plan == null)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Provider plan not found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (plan.PlanType == command.NewPlan)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var oldPlanConfiguration = StaticStore.GetPlan(plan.PlanType);
|
||||||
|
|
||||||
|
plan.PlanType = command.NewPlan;
|
||||||
|
await providerPlanRepository.ReplaceAsync(plan);
|
||||||
|
|
||||||
|
Subscription subscription;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
subscription = await stripeAdapter.ProviderSubscriptionGetAsync(command.GatewaySubscriptionId, plan.ProviderId);
|
||||||
|
}
|
||||||
|
catch (InvalidOperationException)
|
||||||
|
{
|
||||||
|
throw new ConflictException("Subscription not found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var oldSubscriptionItem = subscription.Items.SingleOrDefault(x =>
|
||||||
|
x.Price.Id == oldPlanConfiguration.PasswordManager.StripeProviderPortalSeatPlanId);
|
||||||
|
|
||||||
|
var updateOptions = new SubscriptionUpdateOptions
|
||||||
|
{
|
||||||
|
Items =
|
||||||
|
[
|
||||||
|
new SubscriptionItemOptions
|
||||||
|
{
|
||||||
|
Price = StaticStore.GetPlan(command.NewPlan).PasswordManager.StripeProviderPortalSeatPlanId,
|
||||||
|
Quantity = oldSubscriptionItem!.Quantity
|
||||||
|
},
|
||||||
|
new SubscriptionItemOptions
|
||||||
|
{
|
||||||
|
Id = oldSubscriptionItem.Id,
|
||||||
|
Deleted = true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
await stripeAdapter.SubscriptionUpdateAsync(command.GatewaySubscriptionId, updateOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateSeatMinimums(UpdateProviderSeatMinimumsCommand command)
|
||||||
|
{
|
||||||
|
if (command.Configuration.Any(x => x.SeatsMinimum < 0))
|
||||||
{
|
{
|
||||||
throw new BadRequestException("Provider seat minimums must be at least 0.");
|
throw new BadRequestException("Provider seat minimums must be at least 0.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var subscription = await stripeAdapter.SubscriptionGetAsync(provider.GatewaySubscriptionId);
|
Subscription subscription;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
subscription = await stripeAdapter.ProviderSubscriptionGetAsync(command.GatewaySubscriptionId, command.Id);
|
||||||
|
}
|
||||||
|
catch (InvalidOperationException)
|
||||||
|
{
|
||||||
|
throw new ConflictException("Subscription not found.");
|
||||||
|
}
|
||||||
|
|
||||||
var subscriptionItemOptionsList = new List<SubscriptionItemOptions>();
|
var subscriptionItemOptionsList = new List<SubscriptionItemOptions>();
|
||||||
|
|
||||||
var providerPlans = await providerPlanRepository.GetByProviderId(provider.Id);
|
var providerPlans = await providerPlanRepository.GetByProviderId(command.Id);
|
||||||
|
|
||||||
var enterpriseProviderPlan =
|
foreach (var newPlanConfiguration in command.Configuration)
|
||||||
providerPlans.Single(providerPlan => providerPlan.PlanType == PlanType.EnterpriseMonthly);
|
|
||||||
|
|
||||||
if (enterpriseProviderPlan.SeatMinimum != enterpriseSeatMinimum)
|
|
||||||
{
|
{
|
||||||
var enterprisePriceId = StaticStore.GetPlan(PlanType.EnterpriseMonthly).PasswordManager
|
var providerPlan =
|
||||||
|
providerPlans.Single(providerPlan => providerPlan.PlanType == newPlanConfiguration.Plan);
|
||||||
|
|
||||||
|
if (providerPlan.SeatMinimum != newPlanConfiguration.SeatsMinimum)
|
||||||
|
{
|
||||||
|
var priceId = StaticStore.GetPlan(newPlanConfiguration.Plan).PasswordManager
|
||||||
.StripeProviderPortalSeatPlanId;
|
.StripeProviderPortalSeatPlanId;
|
||||||
|
var subscriptionItem = subscription.Items.First(item => item.Price.Id == priceId);
|
||||||
|
|
||||||
var enterpriseSubscriptionItem = subscription.Items.First(item => item.Price.Id == enterprisePriceId);
|
if (providerPlan.PurchasedSeats == 0)
|
||||||
|
|
||||||
if (enterpriseProviderPlan.PurchasedSeats == 0)
|
|
||||||
{
|
{
|
||||||
if (enterpriseProviderPlan.AllocatedSeats > enterpriseSeatMinimum)
|
if (providerPlan.AllocatedSeats > newPlanConfiguration.SeatsMinimum)
|
||||||
{
|
{
|
||||||
enterpriseProviderPlan.PurchasedSeats =
|
providerPlan.PurchasedSeats = providerPlan.AllocatedSeats - newPlanConfiguration.SeatsMinimum;
|
||||||
enterpriseProviderPlan.AllocatedSeats - enterpriseSeatMinimum;
|
|
||||||
|
|
||||||
subscriptionItemOptionsList.Add(new SubscriptionItemOptions
|
subscriptionItemOptionsList.Add(new SubscriptionItemOptions
|
||||||
{
|
{
|
||||||
Id = enterpriseSubscriptionItem.Id,
|
Id = subscriptionItem.Id,
|
||||||
Price = enterprisePriceId,
|
Price = priceId,
|
||||||
Quantity = enterpriseProviderPlan.AllocatedSeats
|
Quantity = providerPlan.AllocatedSeats
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
subscriptionItemOptionsList.Add(new SubscriptionItemOptions
|
subscriptionItemOptionsList.Add(new SubscriptionItemOptions
|
||||||
{
|
{
|
||||||
Id = enterpriseSubscriptionItem.Id,
|
Id = subscriptionItem.Id,
|
||||||
Price = enterprisePriceId,
|
Price = priceId,
|
||||||
Quantity = enterpriseSeatMinimum
|
Quantity = newPlanConfiguration.SeatsMinimum
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var totalEnterpriseSeats = enterpriseProviderPlan.SeatMinimum + enterpriseProviderPlan.PurchasedSeats;
|
var totalSeats = providerPlan.SeatMinimum + providerPlan.PurchasedSeats;
|
||||||
|
|
||||||
if (enterpriseSeatMinimum <= totalEnterpriseSeats)
|
if (newPlanConfiguration.SeatsMinimum <= totalSeats)
|
||||||
{
|
{
|
||||||
enterpriseProviderPlan.PurchasedSeats = totalEnterpriseSeats - enterpriseSeatMinimum;
|
providerPlan.PurchasedSeats = totalSeats - newPlanConfiguration.SeatsMinimum;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
enterpriseProviderPlan.PurchasedSeats = 0;
|
providerPlan.PurchasedSeats = 0;
|
||||||
subscriptionItemOptionsList.Add(new SubscriptionItemOptions
|
subscriptionItemOptionsList.Add(new SubscriptionItemOptions
|
||||||
{
|
{
|
||||||
Id = enterpriseSubscriptionItem.Id,
|
Id = subscriptionItem.Id,
|
||||||
Price = enterprisePriceId,
|
Price = priceId,
|
||||||
Quantity = enterpriseSeatMinimum
|
Quantity = newPlanConfiguration.SeatsMinimum
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enterpriseProviderPlan.SeatMinimum = enterpriseSeatMinimum;
|
providerPlan.SeatMinimum = newPlanConfiguration.SeatsMinimum;
|
||||||
|
|
||||||
await providerPlanRepository.ReplaceAsync(enterpriseProviderPlan);
|
await providerPlanRepository.ReplaceAsync(providerPlan);
|
||||||
}
|
}
|
||||||
|
|
||||||
var teamsProviderPlan =
|
|
||||||
providerPlans.Single(providerPlan => providerPlan.PlanType == PlanType.TeamsMonthly);
|
|
||||||
|
|
||||||
if (teamsProviderPlan.SeatMinimum != teamsSeatMinimum)
|
|
||||||
{
|
|
||||||
var teamsPriceId = StaticStore.GetPlan(PlanType.TeamsMonthly).PasswordManager
|
|
||||||
.StripeProviderPortalSeatPlanId;
|
|
||||||
|
|
||||||
var teamsSubscriptionItem = subscription.Items.First(item => item.Price.Id == teamsPriceId);
|
|
||||||
|
|
||||||
if (teamsProviderPlan.PurchasedSeats == 0)
|
|
||||||
{
|
|
||||||
if (teamsProviderPlan.AllocatedSeats > teamsSeatMinimum)
|
|
||||||
{
|
|
||||||
teamsProviderPlan.PurchasedSeats = teamsProviderPlan.AllocatedSeats - teamsSeatMinimum;
|
|
||||||
|
|
||||||
subscriptionItemOptionsList.Add(new SubscriptionItemOptions
|
|
||||||
{
|
|
||||||
Id = teamsSubscriptionItem.Id,
|
|
||||||
Price = teamsPriceId,
|
|
||||||
Quantity = teamsProviderPlan.AllocatedSeats
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
subscriptionItemOptionsList.Add(new SubscriptionItemOptions
|
|
||||||
{
|
|
||||||
Id = teamsSubscriptionItem.Id,
|
|
||||||
Price = teamsPriceId,
|
|
||||||
Quantity = teamsSeatMinimum
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var totalTeamsSeats = teamsProviderPlan.SeatMinimum + teamsProviderPlan.PurchasedSeats;
|
|
||||||
|
|
||||||
if (teamsSeatMinimum <= totalTeamsSeats)
|
|
||||||
{
|
|
||||||
teamsProviderPlan.PurchasedSeats = totalTeamsSeats - teamsSeatMinimum;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
teamsProviderPlan.PurchasedSeats = 0;
|
|
||||||
subscriptionItemOptionsList.Add(new SubscriptionItemOptions
|
|
||||||
{
|
|
||||||
Id = teamsSubscriptionItem.Id,
|
|
||||||
Price = teamsPriceId,
|
|
||||||
Quantity = teamsSeatMinimum
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
teamsProviderPlan.SeatMinimum = teamsSeatMinimum;
|
|
||||||
|
|
||||||
await providerPlanRepository.ReplaceAsync(teamsProviderPlan);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (subscriptionItemOptionsList.Count > 0)
|
if (subscriptionItemOptionsList.Count > 0)
|
||||||
{
|
{
|
||||||
await stripeAdapter.SubscriptionUpdateAsync(provider.GatewaySubscriptionId,
|
await stripeAdapter.SubscriptionUpdateAsync(command.GatewaySubscriptionId,
|
||||||
new SubscriptionUpdateOptions { Items = subscriptionItemOptionsList });
|
new SubscriptionUpdateOptions { Items = subscriptionItemOptionsList });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ using Bit.Core.AdminConsole.Entities.Provider;
|
|||||||
using Bit.Core.AdminConsole.Enums.Provider;
|
using Bit.Core.AdminConsole.Enums.Provider;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
using Bit.Core.AdminConsole.Services;
|
using Bit.Core.AdminConsole.Services;
|
||||||
|
using Bit.Core.Billing.Enums;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
@ -19,23 +20,30 @@ public class CreateProviderCommandTests
|
|||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task CreateMspAsync_UserIdIsInvalid_Throws(Provider provider, SutProvider<CreateProviderCommand> sutProvider)
|
public async Task CreateMspAsync_UserIdIsInvalid_Throws(Provider provider, SutProvider<CreateProviderCommand> sutProvider)
|
||||||
{
|
{
|
||||||
|
// Arrange
|
||||||
provider.Type = ProviderType.Msp;
|
provider.Type = ProviderType.Msp;
|
||||||
|
|
||||||
|
// Act
|
||||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||||
() => sutProvider.Sut.CreateMspAsync(provider, default, default, default));
|
() => sutProvider.Sut.CreateMspAsync(provider, default, default, default));
|
||||||
|
|
||||||
|
// Assert
|
||||||
Assert.Contains("Invalid owner.", exception.Message);
|
Assert.Contains("Invalid owner.", exception.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task CreateMspAsync_Success(Provider provider, User user, SutProvider<CreateProviderCommand> sutProvider)
|
public async Task CreateMspAsync_Success(Provider provider, User user, SutProvider<CreateProviderCommand> sutProvider)
|
||||||
{
|
{
|
||||||
|
// Arrange
|
||||||
provider.Type = ProviderType.Msp;
|
provider.Type = ProviderType.Msp;
|
||||||
|
|
||||||
var userRepository = sutProvider.GetDependency<IUserRepository>();
|
var userRepository = sutProvider.GetDependency<IUserRepository>();
|
||||||
userRepository.GetByEmailAsync(user.Email).Returns(user);
|
userRepository.GetByEmailAsync(user.Email).Returns(user);
|
||||||
|
|
||||||
|
// Act
|
||||||
await sutProvider.Sut.CreateMspAsync(provider, user.Email, default, default);
|
await sutProvider.Sut.CreateMspAsync(provider, user.Email, default, default);
|
||||||
|
|
||||||
|
// Assert
|
||||||
await sutProvider.GetDependency<IProviderRepository>().ReceivedWithAnyArgs().CreateAsync(default);
|
await sutProvider.GetDependency<IProviderRepository>().ReceivedWithAnyArgs().CreateAsync(default);
|
||||||
await sutProvider.GetDependency<IProviderService>().Received(1).SendProviderSetupInviteEmailAsync(provider, user.Email);
|
await sutProvider.GetDependency<IProviderService>().Received(1).SendProviderSetupInviteEmailAsync(provider, user.Email);
|
||||||
}
|
}
|
||||||
@ -43,11 +51,52 @@ public class CreateProviderCommandTests
|
|||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task CreateResellerAsync_Success(Provider provider, SutProvider<CreateProviderCommand> sutProvider)
|
public async Task CreateResellerAsync_Success(Provider provider, SutProvider<CreateProviderCommand> sutProvider)
|
||||||
{
|
{
|
||||||
|
// Arrange
|
||||||
provider.Type = ProviderType.Reseller;
|
provider.Type = ProviderType.Reseller;
|
||||||
|
|
||||||
|
// Act
|
||||||
await sutProvider.Sut.CreateResellerAsync(provider);
|
await sutProvider.Sut.CreateResellerAsync(provider);
|
||||||
|
|
||||||
|
// Assert
|
||||||
await sutProvider.GetDependency<IProviderRepository>().ReceivedWithAnyArgs().CreateAsync(default);
|
await sutProvider.GetDependency<IProviderRepository>().ReceivedWithAnyArgs().CreateAsync(default);
|
||||||
await sutProvider.GetDependency<IProviderService>().DidNotReceiveWithAnyArgs().SendProviderSetupInviteEmailAsync(default, default);
|
await sutProvider.GetDependency<IProviderService>().DidNotReceiveWithAnyArgs().SendProviderSetupInviteEmailAsync(default, default);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task CreateMultiOrganizationEnterpriseAsync_Success(
|
||||||
|
Provider provider,
|
||||||
|
User user,
|
||||||
|
PlanType plan,
|
||||||
|
int minimumSeats,
|
||||||
|
SutProvider<CreateProviderCommand> sutProvider)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
provider.Type = ProviderType.MultiOrganizationEnterprise;
|
||||||
|
|
||||||
|
var userRepository = sutProvider.GetDependency<IUserRepository>();
|
||||||
|
userRepository.GetByEmailAsync(user.Email).Returns(user);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await sutProvider.Sut.CreateMultiOrganizationEnterpriseAsync(provider, user.Email, plan, minimumSeats);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await sutProvider.GetDependency<IProviderRepository>().ReceivedWithAnyArgs().CreateAsync(provider);
|
||||||
|
await sutProvider.GetDependency<IProviderService>().Received(1).SendProviderSetupInviteEmailAsync(provider, user.Email);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task CreateMultiOrganizationEnterpriseAsync_UserIdIsInvalid_Throws(
|
||||||
|
Provider provider,
|
||||||
|
SutProvider<CreateProviderCommand> sutProvider)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
provider.Type = ProviderType.Msp;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||||
|
() => sutProvider.Sut.CreateMultiOrganizationEnterpriseAsync(provider, default, default, default));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Contains("Invalid owner.", exception.Message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,7 @@ using Bit.Core.Billing.Entities;
|
|||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
using Bit.Core.Billing.Repositories;
|
using Bit.Core.Billing.Repositories;
|
||||||
using Bit.Core.Billing.Services;
|
using Bit.Core.Billing.Services;
|
||||||
|
using Bit.Core.Billing.Services.Contracts;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
@ -1011,26 +1012,192 @@ public class ProviderBillingServiceTests
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region UpdateSeatMinimums
|
#region ChangePlan
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task UpdateSeatMinimums_NullProvider_ThrowsArgumentNullException(
|
public async Task ChangePlan_NullProviderPlan_ThrowsBadRequestException(
|
||||||
SutProvider<ProviderBillingService> sutProvider) =>
|
ChangeProviderPlanCommand command,
|
||||||
await Assert.ThrowsAsync<ArgumentNullException>(() => sutProvider.Sut.UpdateSeatMinimums(null, 0, 0));
|
SutProvider<ProviderBillingService> sutProvider)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var providerPlanRepository = sutProvider.GetDependency<IProviderPlanRepository>();
|
||||||
|
providerPlanRepository.GetByIdAsync(Arg.Any<Guid>()).Returns((ProviderPlan)null);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var actual = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.ChangePlan(command));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("Provider plan not found.", actual.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task ChangePlan_ProviderNotFound_DoesNothing(
|
||||||
|
ChangeProviderPlanCommand command,
|
||||||
|
SutProvider<ProviderBillingService> sutProvider)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var providerPlanRepository = sutProvider.GetDependency<IProviderPlanRepository>();
|
||||||
|
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||||
|
var existingPlan = new ProviderPlan
|
||||||
|
{
|
||||||
|
Id = command.ProviderPlanId,
|
||||||
|
PlanType = command.NewPlan,
|
||||||
|
PurchasedSeats = 0,
|
||||||
|
AllocatedSeats = 0,
|
||||||
|
SeatMinimum = 0
|
||||||
|
};
|
||||||
|
providerPlanRepository
|
||||||
|
.GetByIdAsync(Arg.Is<Guid>(p => p == command.ProviderPlanId))
|
||||||
|
.Returns(existingPlan);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await sutProvider.Sut.ChangePlan(command);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await providerPlanRepository.Received(0).ReplaceAsync(Arg.Any<ProviderPlan>());
|
||||||
|
await stripeAdapter.Received(0).SubscriptionUpdateAsync(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task ChangePlan_SameProviderPlan_DoesNothing(
|
||||||
|
ChangeProviderPlanCommand command,
|
||||||
|
SutProvider<ProviderBillingService> sutProvider)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var providerPlanRepository = sutProvider.GetDependency<IProviderPlanRepository>();
|
||||||
|
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||||
|
var existingPlan = new ProviderPlan
|
||||||
|
{
|
||||||
|
Id = command.ProviderPlanId,
|
||||||
|
PlanType = command.NewPlan,
|
||||||
|
PurchasedSeats = 0,
|
||||||
|
AllocatedSeats = 0,
|
||||||
|
SeatMinimum = 0
|
||||||
|
};
|
||||||
|
providerPlanRepository
|
||||||
|
.GetByIdAsync(Arg.Is<Guid>(p => p == command.ProviderPlanId))
|
||||||
|
.Returns(existingPlan);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await sutProvider.Sut.ChangePlan(command);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await providerPlanRepository.Received(0).ReplaceAsync(Arg.Any<ProviderPlan>());
|
||||||
|
await stripeAdapter.Received(0).SubscriptionUpdateAsync(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task ChangePlan_UpdatesSubscriptionCorrectly(
|
||||||
|
Guid providerPlanId,
|
||||||
|
Provider provider,
|
||||||
|
SutProvider<ProviderBillingService> sutProvider)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var providerPlanRepository = sutProvider.GetDependency<IProviderPlanRepository>();
|
||||||
|
var existingPlan = new ProviderPlan
|
||||||
|
{
|
||||||
|
Id = providerPlanId,
|
||||||
|
ProviderId = provider.Id,
|
||||||
|
PlanType = PlanType.EnterpriseAnnually,
|
||||||
|
PurchasedSeats = 2,
|
||||||
|
AllocatedSeats = 10,
|
||||||
|
SeatMinimum = 8
|
||||||
|
};
|
||||||
|
providerPlanRepository
|
||||||
|
.GetByIdAsync(Arg.Is<Guid>(p => p == providerPlanId))
|
||||||
|
.Returns(existingPlan);
|
||||||
|
|
||||||
|
var providerRepository = sutProvider.GetDependency<IProviderRepository>();
|
||||||
|
providerRepository.GetByIdAsync(Arg.Is(existingPlan.ProviderId)).Returns(provider);
|
||||||
|
|
||||||
|
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||||
|
stripeAdapter.ProviderSubscriptionGetAsync(
|
||||||
|
Arg.Is(provider.GatewaySubscriptionId),
|
||||||
|
Arg.Is(provider.Id))
|
||||||
|
.Returns(new Subscription
|
||||||
|
{
|
||||||
|
Id = provider.GatewaySubscriptionId,
|
||||||
|
Items = new StripeList<SubscriptionItem>
|
||||||
|
{
|
||||||
|
Data =
|
||||||
|
[
|
||||||
|
new SubscriptionItem
|
||||||
|
{
|
||||||
|
Id = "si_ent_annual",
|
||||||
|
Price = new Price
|
||||||
|
{
|
||||||
|
Id = StaticStore.GetPlan(PlanType.EnterpriseAnnually).PasswordManager
|
||||||
|
.StripeProviderPortalSeatPlanId
|
||||||
|
},
|
||||||
|
Quantity = 10
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var command =
|
||||||
|
new ChangeProviderPlanCommand(providerPlanId, PlanType.EnterpriseMonthly, provider.GatewaySubscriptionId);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await sutProvider.Sut.ChangePlan(command);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await providerPlanRepository.Received(1)
|
||||||
|
.ReplaceAsync(Arg.Is<ProviderPlan>(p => p.PlanType == PlanType.EnterpriseMonthly));
|
||||||
|
|
||||||
|
await stripeAdapter.Received(1)
|
||||||
|
.SubscriptionUpdateAsync(
|
||||||
|
Arg.Is(provider.GatewaySubscriptionId),
|
||||||
|
Arg.Is<SubscriptionUpdateOptions>(p =>
|
||||||
|
p.Items.Count(si => si.Id == "si_ent_annual" && si.Deleted == true) == 1));
|
||||||
|
|
||||||
|
var newPlanCfg = StaticStore.GetPlan(command.NewPlan);
|
||||||
|
await stripeAdapter.Received(1)
|
||||||
|
.SubscriptionUpdateAsync(
|
||||||
|
Arg.Is(provider.GatewaySubscriptionId),
|
||||||
|
Arg.Is<SubscriptionUpdateOptions>(p =>
|
||||||
|
p.Items.Count(si =>
|
||||||
|
si.Price == newPlanCfg.PasswordManager.StripeProviderPortalSeatPlanId &&
|
||||||
|
si.Deleted == default &&
|
||||||
|
si.Quantity == 10) == 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region UpdateSeatMinimums
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task UpdateSeatMinimums_NegativeSeatMinimum_ThrowsBadRequestException(
|
public async Task UpdateSeatMinimums_NegativeSeatMinimum_ThrowsBadRequestException(
|
||||||
Provider provider,
|
Provider provider,
|
||||||
SutProvider<ProviderBillingService> sutProvider) =>
|
SutProvider<ProviderBillingService> sutProvider)
|
||||||
await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSeatMinimums(provider, -10, 100));
|
{
|
||||||
|
// Arrange
|
||||||
|
var providerRepository = sutProvider.GetDependency<IProviderRepository>();
|
||||||
|
providerRepository.GetByIdAsync(provider.Id).Returns(provider);
|
||||||
|
var command = new UpdateProviderSeatMinimumsCommand(
|
||||||
|
provider.Id,
|
||||||
|
provider.GatewaySubscriptionId,
|
||||||
|
[
|
||||||
|
(PlanType.TeamsMonthly, -10),
|
||||||
|
(PlanType.EnterpriseMonthly, 50)
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var actual = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSeatMinimums(command));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("Provider seat minimums must be at least 0.", actual.Message);
|
||||||
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task UpdateSeatMinimums_NoPurchasedSeats_AllocatedHigherThanIncomingMinimum_UpdatesPurchasedSeats_SyncsStripeWithNewSeatMinimum(
|
public async Task UpdateSeatMinimums_NoPurchasedSeats_AllocatedHigherThanIncomingMinimum_UpdatesPurchasedSeats_SyncsStripeWithNewSeatMinimum(
|
||||||
Provider provider,
|
Provider provider,
|
||||||
SutProvider<ProviderBillingService> sutProvider)
|
SutProvider<ProviderBillingService> sutProvider)
|
||||||
{
|
{
|
||||||
|
// Arrange
|
||||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||||
var providerPlanRepository = sutProvider.GetDependency<IProviderPlanRepository>();
|
var providerPlanRepository = sutProvider.GetDependency<IProviderPlanRepository>();
|
||||||
|
var providerRepository = sutProvider.GetDependency<IProviderRepository>();
|
||||||
|
|
||||||
const string enterpriseLineItemId = "enterprise_line_item_id";
|
const string enterpriseLineItemId = "enterprise_line_item_id";
|
||||||
const string teamsLineItemId = "teams_line_item_id";
|
const string teamsLineItemId = "teams_line_item_id";
|
||||||
@ -1058,7 +1225,9 @@ public class ProviderBillingServiceTests
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
stripeAdapter.SubscriptionGetAsync(provider.GatewaySubscriptionId).Returns(subscription);
|
stripeAdapter.ProviderSubscriptionGetAsync(
|
||||||
|
provider.GatewaySubscriptionId,
|
||||||
|
provider.Id).Returns(subscription);
|
||||||
|
|
||||||
var providerPlans = new List<ProviderPlan>
|
var providerPlans = new List<ProviderPlan>
|
||||||
{
|
{
|
||||||
@ -1066,10 +1235,21 @@ public class ProviderBillingServiceTests
|
|||||||
new() { PlanType = PlanType.TeamsMonthly, SeatMinimum = 30, PurchasedSeats = 0, AllocatedSeats = 25 }
|
new() { PlanType = PlanType.TeamsMonthly, SeatMinimum = 30, PurchasedSeats = 0, AllocatedSeats = 25 }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
providerRepository.GetByIdAsync(provider.Id).Returns(provider);
|
||||||
providerPlanRepository.GetByProviderId(provider.Id).Returns(providerPlans);
|
providerPlanRepository.GetByProviderId(provider.Id).Returns(providerPlans);
|
||||||
|
|
||||||
await sutProvider.Sut.UpdateSeatMinimums(provider, 30, 20);
|
var command = new UpdateProviderSeatMinimumsCommand(
|
||||||
|
provider.Id,
|
||||||
|
provider.GatewaySubscriptionId,
|
||||||
|
[
|
||||||
|
(PlanType.EnterpriseMonthly, 30),
|
||||||
|
(PlanType.TeamsMonthly, 20)
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await sutProvider.Sut.UpdateSeatMinimums(command);
|
||||||
|
|
||||||
|
// Assert
|
||||||
await providerPlanRepository.Received(1).ReplaceAsync(Arg.Is<ProviderPlan>(
|
await providerPlanRepository.Received(1).ReplaceAsync(Arg.Is<ProviderPlan>(
|
||||||
providerPlan => providerPlan.PlanType == PlanType.EnterpriseMonthly && providerPlan.SeatMinimum == 30));
|
providerPlan => providerPlan.PlanType == PlanType.EnterpriseMonthly && providerPlan.SeatMinimum == 30));
|
||||||
|
|
||||||
@ -1091,8 +1271,11 @@ public class ProviderBillingServiceTests
|
|||||||
Provider provider,
|
Provider provider,
|
||||||
SutProvider<ProviderBillingService> sutProvider)
|
SutProvider<ProviderBillingService> sutProvider)
|
||||||
{
|
{
|
||||||
|
// Arrange
|
||||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||||
var providerPlanRepository = sutProvider.GetDependency<IProviderPlanRepository>();
|
var providerPlanRepository = sutProvider.GetDependency<IProviderPlanRepository>();
|
||||||
|
var providerRepository = sutProvider.GetDependency<IProviderRepository>();
|
||||||
|
providerRepository.GetByIdAsync(provider.Id).Returns(provider);
|
||||||
|
|
||||||
const string enterpriseLineItemId = "enterprise_line_item_id";
|
const string enterpriseLineItemId = "enterprise_line_item_id";
|
||||||
const string teamsLineItemId = "teams_line_item_id";
|
const string teamsLineItemId = "teams_line_item_id";
|
||||||
@ -1120,7 +1303,7 @@ public class ProviderBillingServiceTests
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
stripeAdapter.SubscriptionGetAsync(provider.GatewaySubscriptionId).Returns(subscription);
|
stripeAdapter.ProviderSubscriptionGetAsync(provider.GatewaySubscriptionId, provider.Id).Returns(subscription);
|
||||||
|
|
||||||
var providerPlans = new List<ProviderPlan>
|
var providerPlans = new List<ProviderPlan>
|
||||||
{
|
{
|
||||||
@ -1130,8 +1313,18 @@ public class ProviderBillingServiceTests
|
|||||||
|
|
||||||
providerPlanRepository.GetByProviderId(provider.Id).Returns(providerPlans);
|
providerPlanRepository.GetByProviderId(provider.Id).Returns(providerPlans);
|
||||||
|
|
||||||
await sutProvider.Sut.UpdateSeatMinimums(provider, 70, 50);
|
var command = new UpdateProviderSeatMinimumsCommand(
|
||||||
|
provider.Id,
|
||||||
|
provider.GatewaySubscriptionId,
|
||||||
|
[
|
||||||
|
(PlanType.EnterpriseMonthly, 70),
|
||||||
|
(PlanType.TeamsMonthly, 50)
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await sutProvider.Sut.UpdateSeatMinimums(command);
|
||||||
|
|
||||||
|
// Assert
|
||||||
await providerPlanRepository.Received(1).ReplaceAsync(Arg.Is<ProviderPlan>(
|
await providerPlanRepository.Received(1).ReplaceAsync(Arg.Is<ProviderPlan>(
|
||||||
providerPlan => providerPlan.PlanType == PlanType.EnterpriseMonthly && providerPlan.SeatMinimum == 70));
|
providerPlan => providerPlan.PlanType == PlanType.EnterpriseMonthly && providerPlan.SeatMinimum == 70));
|
||||||
|
|
||||||
@ -1153,8 +1346,11 @@ public class ProviderBillingServiceTests
|
|||||||
Provider provider,
|
Provider provider,
|
||||||
SutProvider<ProviderBillingService> sutProvider)
|
SutProvider<ProviderBillingService> sutProvider)
|
||||||
{
|
{
|
||||||
|
// Arrange
|
||||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||||
var providerPlanRepository = sutProvider.GetDependency<IProviderPlanRepository>();
|
var providerPlanRepository = sutProvider.GetDependency<IProviderPlanRepository>();
|
||||||
|
var providerRepository = sutProvider.GetDependency<IProviderRepository>();
|
||||||
|
providerRepository.GetByIdAsync(provider.Id).Returns(provider);
|
||||||
|
|
||||||
const string enterpriseLineItemId = "enterprise_line_item_id";
|
const string enterpriseLineItemId = "enterprise_line_item_id";
|
||||||
const string teamsLineItemId = "teams_line_item_id";
|
const string teamsLineItemId = "teams_line_item_id";
|
||||||
@ -1182,7 +1378,7 @@ public class ProviderBillingServiceTests
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
stripeAdapter.SubscriptionGetAsync(provider.GatewaySubscriptionId).Returns(subscription);
|
stripeAdapter.ProviderSubscriptionGetAsync(provider.GatewaySubscriptionId, provider.Id).Returns(subscription);
|
||||||
|
|
||||||
var providerPlans = new List<ProviderPlan>
|
var providerPlans = new List<ProviderPlan>
|
||||||
{
|
{
|
||||||
@ -1192,8 +1388,18 @@ public class ProviderBillingServiceTests
|
|||||||
|
|
||||||
providerPlanRepository.GetByProviderId(provider.Id).Returns(providerPlans);
|
providerPlanRepository.GetByProviderId(provider.Id).Returns(providerPlans);
|
||||||
|
|
||||||
await sutProvider.Sut.UpdateSeatMinimums(provider, 60, 60);
|
var command = new UpdateProviderSeatMinimumsCommand(
|
||||||
|
provider.Id,
|
||||||
|
provider.GatewaySubscriptionId,
|
||||||
|
[
|
||||||
|
(PlanType.EnterpriseMonthly, 60),
|
||||||
|
(PlanType.TeamsMonthly, 60)
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await sutProvider.Sut.UpdateSeatMinimums(command);
|
||||||
|
|
||||||
|
// Assert
|
||||||
await providerPlanRepository.Received(1).ReplaceAsync(Arg.Is<ProviderPlan>(
|
await providerPlanRepository.Received(1).ReplaceAsync(Arg.Is<ProviderPlan>(
|
||||||
providerPlan => providerPlan.PlanType == PlanType.EnterpriseMonthly && providerPlan.SeatMinimum == 60 && providerPlan.PurchasedSeats == 10));
|
providerPlan => providerPlan.PlanType == PlanType.EnterpriseMonthly && providerPlan.SeatMinimum == 60 && providerPlan.PurchasedSeats == 10));
|
||||||
|
|
||||||
@ -1209,8 +1415,11 @@ public class ProviderBillingServiceTests
|
|||||||
Provider provider,
|
Provider provider,
|
||||||
SutProvider<ProviderBillingService> sutProvider)
|
SutProvider<ProviderBillingService> sutProvider)
|
||||||
{
|
{
|
||||||
|
// Arrange
|
||||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||||
var providerPlanRepository = sutProvider.GetDependency<IProviderPlanRepository>();
|
var providerPlanRepository = sutProvider.GetDependency<IProviderPlanRepository>();
|
||||||
|
var providerRepository = sutProvider.GetDependency<IProviderRepository>();
|
||||||
|
providerRepository.GetByIdAsync(provider.Id).Returns(provider);
|
||||||
|
|
||||||
const string enterpriseLineItemId = "enterprise_line_item_id";
|
const string enterpriseLineItemId = "enterprise_line_item_id";
|
||||||
const string teamsLineItemId = "teams_line_item_id";
|
const string teamsLineItemId = "teams_line_item_id";
|
||||||
@ -1238,7 +1447,7 @@ public class ProviderBillingServiceTests
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
stripeAdapter.SubscriptionGetAsync(provider.GatewaySubscriptionId).Returns(subscription);
|
stripeAdapter.ProviderSubscriptionGetAsync(provider.GatewaySubscriptionId, provider.Id).Returns(subscription);
|
||||||
|
|
||||||
var providerPlans = new List<ProviderPlan>
|
var providerPlans = new List<ProviderPlan>
|
||||||
{
|
{
|
||||||
@ -1248,8 +1457,18 @@ public class ProviderBillingServiceTests
|
|||||||
|
|
||||||
providerPlanRepository.GetByProviderId(provider.Id).Returns(providerPlans);
|
providerPlanRepository.GetByProviderId(provider.Id).Returns(providerPlans);
|
||||||
|
|
||||||
await sutProvider.Sut.UpdateSeatMinimums(provider, 80, 80);
|
var command = new UpdateProviderSeatMinimumsCommand(
|
||||||
|
provider.Id,
|
||||||
|
provider.GatewaySubscriptionId,
|
||||||
|
[
|
||||||
|
(PlanType.EnterpriseMonthly, 80),
|
||||||
|
(PlanType.TeamsMonthly, 80)
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await sutProvider.Sut.UpdateSeatMinimums(command);
|
||||||
|
|
||||||
|
// Assert
|
||||||
await providerPlanRepository.Received(1).ReplaceAsync(Arg.Is<ProviderPlan>(
|
await providerPlanRepository.Received(1).ReplaceAsync(Arg.Is<ProviderPlan>(
|
||||||
providerPlan => providerPlan.PlanType == PlanType.EnterpriseMonthly && providerPlan.SeatMinimum == 80 && providerPlan.PurchasedSeats == 0));
|
providerPlan => providerPlan.PlanType == PlanType.EnterpriseMonthly && providerPlan.SeatMinimum == 80 && providerPlan.PurchasedSeats == 0));
|
||||||
|
|
||||||
@ -1271,8 +1490,11 @@ public class ProviderBillingServiceTests
|
|||||||
Provider provider,
|
Provider provider,
|
||||||
SutProvider<ProviderBillingService> sutProvider)
|
SutProvider<ProviderBillingService> sutProvider)
|
||||||
{
|
{
|
||||||
|
// Arrange
|
||||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||||
var providerPlanRepository = sutProvider.GetDependency<IProviderPlanRepository>();
|
var providerPlanRepository = sutProvider.GetDependency<IProviderPlanRepository>();
|
||||||
|
var providerRepository = sutProvider.GetDependency<IProviderRepository>();
|
||||||
|
providerRepository.GetByIdAsync(provider.Id).Returns(provider);
|
||||||
|
|
||||||
const string enterpriseLineItemId = "enterprise_line_item_id";
|
const string enterpriseLineItemId = "enterprise_line_item_id";
|
||||||
const string teamsLineItemId = "teams_line_item_id";
|
const string teamsLineItemId = "teams_line_item_id";
|
||||||
@ -1300,7 +1522,7 @@ public class ProviderBillingServiceTests
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
stripeAdapter.SubscriptionGetAsync(provider.GatewaySubscriptionId).Returns(subscription);
|
stripeAdapter.ProviderSubscriptionGetAsync(provider.GatewaySubscriptionId, provider.Id).Returns(subscription);
|
||||||
|
|
||||||
var providerPlans = new List<ProviderPlan>
|
var providerPlans = new List<ProviderPlan>
|
||||||
{
|
{
|
||||||
@ -1310,8 +1532,18 @@ public class ProviderBillingServiceTests
|
|||||||
|
|
||||||
providerPlanRepository.GetByProviderId(provider.Id).Returns(providerPlans);
|
providerPlanRepository.GetByProviderId(provider.Id).Returns(providerPlans);
|
||||||
|
|
||||||
await sutProvider.Sut.UpdateSeatMinimums(provider, 70, 30);
|
var command = new UpdateProviderSeatMinimumsCommand(
|
||||||
|
provider.Id,
|
||||||
|
provider.GatewaySubscriptionId,
|
||||||
|
[
|
||||||
|
(PlanType.EnterpriseMonthly, 70),
|
||||||
|
(PlanType.TeamsMonthly, 30)
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await sutProvider.Sut.UpdateSeatMinimums(command);
|
||||||
|
|
||||||
|
// Assert
|
||||||
await providerPlanRepository.Received(1).ReplaceAsync(Arg.Is<ProviderPlan>(
|
await providerPlanRepository.Received(1).ReplaceAsync(Arg.Is<ProviderPlan>(
|
||||||
providerPlan => providerPlan.PlanType == PlanType.EnterpriseMonthly && providerPlan.SeatMinimum == 70));
|
providerPlan => providerPlan.PlanType == PlanType.EnterpriseMonthly && providerPlan.SeatMinimum == 70));
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.8" />
|
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.10" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(MicrosoftNetTestSdkVersion)" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(MicrosoftNetTestSdkVersion)" />
|
||||||
<PackageReference Include="NSubstitute" Version="$(NSubstituteVersion)" />
|
<PackageReference Include="NSubstitute" Version="$(NSubstituteVersion)" />
|
||||||
<PackageReference Include="xunit" Version="$(XUnitVersion)" />
|
<PackageReference Include="xunit" Version="$(XUnitVersion)" />
|
||||||
|
@ -14,6 +14,7 @@ using Bit.Core.Billing.Enums;
|
|||||||
using Bit.Core.Billing.Extensions;
|
using Bit.Core.Billing.Extensions;
|
||||||
using Bit.Core.Billing.Repositories;
|
using Bit.Core.Billing.Repositories;
|
||||||
using Bit.Core.Billing.Services;
|
using Bit.Core.Billing.Services;
|
||||||
|
using Bit.Core.Billing.Services.Contracts;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
@ -107,9 +108,15 @@ public class ProvidersController : Controller
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public IActionResult Create(int teamsMinimumSeats, int enterpriseMinimumSeats, string ownerEmail = null)
|
public IActionResult Create()
|
||||||
{
|
{
|
||||||
return View(new CreateProviderModel
|
return View(new CreateProviderModel());
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("providers/create/msp")]
|
||||||
|
public IActionResult CreateMsp(int teamsMinimumSeats, int enterpriseMinimumSeats, string ownerEmail = null)
|
||||||
|
{
|
||||||
|
return View(new CreateMspProviderModel
|
||||||
{
|
{
|
||||||
OwnerEmail = ownerEmail,
|
OwnerEmail = ownerEmail,
|
||||||
TeamsMonthlySeatMinimum = teamsMinimumSeats,
|
TeamsMonthlySeatMinimum = teamsMinimumSeats,
|
||||||
@ -117,10 +124,50 @@ public class ProvidersController : Controller
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("providers/create/reseller")]
|
||||||
|
public IActionResult CreateReseller()
|
||||||
|
{
|
||||||
|
return View(new CreateResellerProviderModel());
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("providers/create/multi-organization-enterprise")]
|
||||||
|
public IActionResult CreateMultiOrganizationEnterprise(int enterpriseMinimumSeats, string ownerEmail = null)
|
||||||
|
{
|
||||||
|
if (!_featureService.IsEnabled(FeatureFlagKeys.PM12275_MultiOrganizationEnterprises))
|
||||||
|
{
|
||||||
|
return RedirectToAction("Create");
|
||||||
|
}
|
||||||
|
|
||||||
|
return View(new CreateMultiOrganizationEnterpriseProviderModel
|
||||||
|
{
|
||||||
|
OwnerEmail = ownerEmail,
|
||||||
|
EnterpriseSeatMinimum = enterpriseMinimumSeats
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
[ValidateAntiForgeryToken]
|
[ValidateAntiForgeryToken]
|
||||||
[RequirePermission(Permission.Provider_Create)]
|
[RequirePermission(Permission.Provider_Create)]
|
||||||
public async Task<IActionResult> 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<IActionResult> CreateMsp(CreateMspProviderModel model)
|
||||||
{
|
{
|
||||||
if (!ModelState.IsValid)
|
if (!ModelState.IsValid)
|
||||||
{
|
{
|
||||||
@ -128,20 +175,52 @@ public class ProvidersController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
var provider = model.ToProvider();
|
var provider = model.ToProvider();
|
||||||
switch (provider.Type)
|
|
||||||
{
|
|
||||||
case ProviderType.Msp:
|
|
||||||
await _createProviderCommand.CreateMspAsync(
|
await _createProviderCommand.CreateMspAsync(
|
||||||
provider,
|
provider,
|
||||||
model.OwnerEmail,
|
model.OwnerEmail,
|
||||||
model.TeamsMonthlySeatMinimum,
|
model.TeamsMonthlySeatMinimum,
|
||||||
model.EnterpriseMonthlySeatMinimum);
|
model.EnterpriseMonthlySeatMinimum);
|
||||||
break;
|
|
||||||
case ProviderType.Reseller:
|
return RedirectToAction("Edit", new { id = provider.Id });
|
||||||
await _createProviderCommand.CreateResellerAsync(provider);
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpPost("providers/create/reseller")]
|
||||||
|
[ValidateAntiForgeryToken]
|
||||||
|
[RequirePermission(Permission.Provider_Create)]
|
||||||
|
public async Task<IActionResult> CreateReseller(CreateResellerProviderModel model)
|
||||||
|
{
|
||||||
|
if (!ModelState.IsValid)
|
||||||
|
{
|
||||||
|
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<IActionResult> 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 });
|
return RedirectToAction("Edit", new { id = provider.Id });
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -212,26 +291,40 @@ public class ProvidersController : Controller
|
|||||||
|
|
||||||
var providerPlans = await _providerPlanRepository.GetByProviderId(id);
|
var providerPlans = await _providerPlanRepository.GetByProviderId(id);
|
||||||
|
|
||||||
if (providerPlans.Count == 0)
|
switch (provider.Type)
|
||||||
{
|
{
|
||||||
var newProviderPlans = new List<ProviderPlan>
|
case ProviderType.Msp:
|
||||||
|
var updateMspSeatMinimumsCommand = new UpdateProviderSeatMinimumsCommand(
|
||||||
|
provider.Id,
|
||||||
|
provider.GatewaySubscriptionId,
|
||||||
|
[
|
||||||
|
(Plan: PlanType.TeamsMonthly, SeatsMinimum: model.TeamsMonthlySeatMinimum),
|
||||||
|
(Plan: PlanType.EnterpriseMonthly, SeatsMinimum: model.EnterpriseMonthlySeatMinimum)
|
||||||
|
]);
|
||||||
|
await _providerBillingService.UpdateSeatMinimums(updateMspSeatMinimumsCommand);
|
||||||
|
break;
|
||||||
|
case ProviderType.MultiOrganizationEnterprise:
|
||||||
{
|
{
|
||||||
new () { ProviderId = provider.Id, PlanType = PlanType.TeamsMonthly, SeatMinimum = model.TeamsMonthlySeatMinimum, PurchasedSeats = 0, AllocatedSeats = 0 },
|
var existingMoePlan = providerPlans.Single();
|
||||||
new () { ProviderId = provider.Id, PlanType = PlanType.EnterpriseMonthly, SeatMinimum = model.EnterpriseMonthlySeatMinimum, PurchasedSeats = 0, AllocatedSeats = 0 }
|
|
||||||
};
|
|
||||||
|
|
||||||
foreach (var newProviderPlan in newProviderPlans)
|
// 1. Change the plan and take over any old values.
|
||||||
{
|
var changeMoePlanCommand = new ChangeProviderPlanCommand(
|
||||||
await _providerPlanRepository.CreateAsync(newProviderPlan);
|
existingMoePlan.Id,
|
||||||
|
model.Plan!.Value,
|
||||||
|
provider.GatewaySubscriptionId);
|
||||||
|
await _providerBillingService.ChangePlan(changeMoePlanCommand);
|
||||||
|
|
||||||
|
// 2. Update the seat minimums.
|
||||||
|
var updateMoeSeatMinimumsCommand = new UpdateProviderSeatMinimumsCommand(
|
||||||
|
provider.Id,
|
||||||
|
provider.GatewaySubscriptionId,
|
||||||
|
[
|
||||||
|
(Plan: model.Plan!.Value, SeatsMinimum: model.EnterpriseMinimumSeats!.Value)
|
||||||
|
]);
|
||||||
|
await _providerBillingService.UpdateSeatMinimums(updateMoeSeatMinimumsCommand);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
await _providerBillingService.UpdateSeatMinimums(
|
|
||||||
provider,
|
|
||||||
model.EnterpriseMonthlySeatMinimum,
|
|
||||||
model.TeamsMonthlySeatMinimum);
|
|
||||||
}
|
|
||||||
|
|
||||||
return RedirectToAction("Edit", new { id });
|
return RedirectToAction("Edit", new { id });
|
||||||
}
|
}
|
||||||
|
45
src/Admin/AdminConsole/Models/CreateMspProviderModel.cs
Normal file
45
src/Admin/AdminConsole/Models/CreateMspProviderModel.cs
Normal file
@ -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<ValidationResult> Validate(ValidationContext validationContext)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(OwnerEmail))
|
||||||
|
{
|
||||||
|
var ownerEmailDisplayName = nameof(OwnerEmail).GetDisplayAttribute<CreateMspProviderModel>()?.GetName() ?? nameof(OwnerEmail);
|
||||||
|
yield return new ValidationResult($"The {ownerEmailDisplayName} field is required.");
|
||||||
|
}
|
||||||
|
if (TeamsMonthlySeatMinimum < 0)
|
||||||
|
{
|
||||||
|
var teamsMinimumSeatsDisplayName = nameof(TeamsMonthlySeatMinimum).GetDisplayAttribute<CreateMspProviderModel>()?.GetName() ?? nameof(TeamsMonthlySeatMinimum);
|
||||||
|
yield return new ValidationResult($"The {teamsMinimumSeatsDisplayName} field can not be negative.");
|
||||||
|
}
|
||||||
|
if (EnterpriseMonthlySeatMinimum < 0)
|
||||||
|
{
|
||||||
|
var enterpriseMinimumSeatsDisplayName = nameof(EnterpriseMonthlySeatMinimum).GetDisplayAttribute<CreateMspProviderModel>()?.GetName() ?? nameof(EnterpriseMonthlySeatMinimum);
|
||||||
|
yield return new ValidationResult($"The {enterpriseMinimumSeatsDisplayName} field can not be negative.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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<ValidationResult> Validate(ValidationContext validationContext)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(OwnerEmail))
|
||||||
|
{
|
||||||
|
var ownerEmailDisplayName = nameof(OwnerEmail).GetDisplayAttribute<CreateMultiOrganizationEnterpriseProviderModel>()?.GetName() ?? nameof(OwnerEmail);
|
||||||
|
yield return new ValidationResult($"The {ownerEmailDisplayName} field is required.");
|
||||||
|
}
|
||||||
|
if (EnterpriseSeatMinimum < 0)
|
||||||
|
{
|
||||||
|
var enterpriseSeatMinimumDisplayName = nameof(EnterpriseSeatMinimum).GetDisplayAttribute<CreateMultiOrganizationEnterpriseProviderModel>()?.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<CreateMultiOrganizationEnterpriseProviderModel>()?.GetName() ?? nameof(Plan);
|
||||||
|
yield return new ValidationResult($"The {planDisplayName} field must be set to Enterprise Annually or Enterprise Monthly.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,84 +1,8 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using Bit.Core.AdminConsole.Enums.Provider;
|
||||||
using Bit.Core.AdminConsole.Entities.Provider;
|
|
||||||
using Bit.Core.AdminConsole.Enums.Provider;
|
|
||||||
using Bit.SharedWeb.Utilities;
|
|
||||||
|
|
||||||
namespace Bit.Admin.AdminConsole.Models;
|
namespace Bit.Admin.AdminConsole.Models;
|
||||||
|
|
||||||
public class CreateProviderModel : IValidatableObject
|
public class CreateProviderModel
|
||||||
{
|
{
|
||||||
public CreateProviderModel() { }
|
|
||||||
|
|
||||||
[Display(Name = "Provider Type")]
|
|
||||||
public ProviderType Type { get; set; }
|
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<ValidationResult> Validate(ValidationContext validationContext)
|
|
||||||
{
|
|
||||||
switch (Type)
|
|
||||||
{
|
|
||||||
case ProviderType.Msp:
|
|
||||||
if (string.IsNullOrWhiteSpace(OwnerEmail))
|
|
||||||
{
|
|
||||||
var ownerEmailDisplayName = nameof(OwnerEmail).GetDisplayAttribute<CreateProviderModel>()?.GetName() ?? nameof(OwnerEmail);
|
|
||||||
yield return new ValidationResult($"The {ownerEmailDisplayName} field is required.");
|
|
||||||
}
|
|
||||||
if (TeamsMonthlySeatMinimum < 0)
|
|
||||||
{
|
|
||||||
var teamsMinimumSeatsDisplayName = nameof(TeamsMonthlySeatMinimum).GetDisplayAttribute<CreateProviderModel>()?.GetName() ?? nameof(TeamsMonthlySeatMinimum);
|
|
||||||
yield return new ValidationResult($"The {teamsMinimumSeatsDisplayName} field can not be negative.");
|
|
||||||
}
|
|
||||||
if (EnterpriseMonthlySeatMinimum < 0)
|
|
||||||
{
|
|
||||||
var enterpriseMinimumSeatsDisplayName = nameof(EnterpriseMonthlySeatMinimum).GetDisplayAttribute<CreateProviderModel>()?.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<CreateProviderModel>()?.GetName() ?? nameof(Name);
|
|
||||||
yield return new ValidationResult($"The {nameDisplayName} field is required.");
|
|
||||||
}
|
|
||||||
if (string.IsNullOrWhiteSpace(BusinessName))
|
|
||||||
{
|
|
||||||
var businessNameDisplayName = nameof(BusinessName).GetDisplayAttribute<CreateProviderModel>()?.GetName() ?? nameof(BusinessName);
|
|
||||||
yield return new ValidationResult($"The {businessNameDisplayName} field is required.");
|
|
||||||
}
|
|
||||||
if (string.IsNullOrWhiteSpace(BillingEmail))
|
|
||||||
{
|
|
||||||
var billingEmailDisplayName = nameof(BillingEmail).GetDisplayAttribute<CreateProviderModel>()?.GetName() ?? nameof(BillingEmail);
|
|
||||||
yield return new ValidationResult($"The {billingEmailDisplayName} field is required.");
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
48
src/Admin/AdminConsole/Models/CreateResellerProviderModel.cs
Normal file
48
src/Admin/AdminConsole/Models/CreateResellerProviderModel.cs
Normal file
@ -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<ValidationResult> Validate(ValidationContext validationContext)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(Name))
|
||||||
|
{
|
||||||
|
var nameDisplayName = nameof(Name).GetDisplayAttribute<CreateProviderModel>()?.GetName() ?? nameof(Name);
|
||||||
|
yield return new ValidationResult($"The {nameDisplayName} field is required.");
|
||||||
|
}
|
||||||
|
if (string.IsNullOrWhiteSpace(BusinessName))
|
||||||
|
{
|
||||||
|
var businessNameDisplayName = nameof(BusinessName).GetDisplayAttribute<CreateProviderModel>()?.GetName() ?? nameof(BusinessName);
|
||||||
|
yield return new ValidationResult($"The {businessNameDisplayName} field is required.");
|
||||||
|
}
|
||||||
|
if (string.IsNullOrWhiteSpace(BillingEmail))
|
||||||
|
{
|
||||||
|
var billingEmailDisplayName = nameof(BillingEmail).GetDisplayAttribute<CreateProviderModel>()?.GetName() ?? nameof(BillingEmail);
|
||||||
|
yield return new ValidationResult($"The {billingEmailDisplayName} field is required.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -33,6 +33,13 @@ public class ProviderEditModel : ProviderViewModel, IValidatableObject
|
|||||||
GatewayCustomerUrl = gatewayCustomerUrl;
|
GatewayCustomerUrl = gatewayCustomerUrl;
|
||||||
GatewaySubscriptionUrl = gatewaySubscriptionUrl;
|
GatewaySubscriptionUrl = gatewaySubscriptionUrl;
|
||||||
Type = provider.Type;
|
Type = provider.Type;
|
||||||
|
|
||||||
|
if (Type == ProviderType.MultiOrganizationEnterprise)
|
||||||
|
{
|
||||||
|
var plan = providerPlans.SingleOrDefault();
|
||||||
|
EnterpriseMinimumSeats = plan?.SeatMinimum ?? 0;
|
||||||
|
Plan = plan?.PlanType;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[Display(Name = "Billing Email")]
|
[Display(Name = "Billing Email")]
|
||||||
@ -58,13 +65,24 @@ public class ProviderEditModel : ProviderViewModel, IValidatableObject
|
|||||||
[Display(Name = "Provider Type")]
|
[Display(Name = "Provider Type")]
|
||||||
public ProviderType Type { get; set; }
|
public ProviderType Type { get; set; }
|
||||||
|
|
||||||
|
[Display(Name = "Plan")]
|
||||||
|
public PlanType? Plan { get; set; }
|
||||||
|
|
||||||
|
[Display(Name = "Enterprise Seats Minimum")]
|
||||||
|
public int? EnterpriseMinimumSeats { get; set; }
|
||||||
|
|
||||||
public virtual Provider ToProvider(Provider existingProvider)
|
public virtual Provider ToProvider(Provider existingProvider)
|
||||||
{
|
{
|
||||||
existingProvider.BillingEmail = BillingEmail?.ToLowerInvariant().Trim();
|
existingProvider.BillingEmail = BillingEmail?.ToLowerInvariant().Trim();
|
||||||
existingProvider.BillingPhone = BillingPhone?.ToLowerInvariant().Trim();
|
existingProvider.BillingPhone = BillingPhone?.ToLowerInvariant().Trim();
|
||||||
|
switch (Type)
|
||||||
|
{
|
||||||
|
case ProviderType.Msp:
|
||||||
existingProvider.Gateway = Gateway;
|
existingProvider.Gateway = Gateway;
|
||||||
existingProvider.GatewayCustomerId = GatewayCustomerId;
|
existingProvider.GatewayCustomerId = GatewayCustomerId;
|
||||||
existingProvider.GatewaySubscriptionId = GatewaySubscriptionId;
|
existingProvider.GatewaySubscriptionId = GatewaySubscriptionId;
|
||||||
|
break;
|
||||||
|
}
|
||||||
return existingProvider;
|
return existingProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -82,6 +100,23 @@ public class ProviderEditModel : ProviderViewModel, IValidatableObject
|
|||||||
yield return new ValidationResult($"The {billingEmailDisplayName} field is required.");
|
yield return new ValidationResult($"The {billingEmailDisplayName} field is required.");
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case ProviderType.MultiOrganizationEnterprise:
|
||||||
|
if (Plan == null)
|
||||||
|
{
|
||||||
|
var displayName = nameof(Plan).GetDisplayAttribute<CreateProviderModel>()?.GetName() ?? nameof(Plan);
|
||||||
|
yield return new ValidationResult($"The {displayName} field is required.");
|
||||||
|
}
|
||||||
|
if (EnterpriseMinimumSeats == null)
|
||||||
|
{
|
||||||
|
var displayName = nameof(EnterpriseMinimumSeats).GetDisplayAttribute<CreateProviderModel>()?.GetName() ?? nameof(EnterpriseMinimumSeats);
|
||||||
|
yield return new ValidationResult($"The {displayName} field is required.");
|
||||||
|
}
|
||||||
|
if (EnterpriseMinimumSeats < 0)
|
||||||
|
{
|
||||||
|
var displayName = nameof(EnterpriseMinimumSeats).GetDisplayAttribute<CreateProviderModel>()?.GetName() ?? nameof(EnterpriseMinimumSeats);
|
||||||
|
yield return new ValidationResult($"The {displayName} field cannot be less than 0.");
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,80 +1,48 @@
|
|||||||
@using Bit.SharedWeb.Utilities
|
@using Bit.SharedWeb.Utilities
|
||||||
@using Bit.Core.AdminConsole.Enums.Provider
|
@using Bit.Core.AdminConsole.Enums.Provider
|
||||||
@using Bit.Core
|
@using Bit.Core
|
||||||
|
|
||||||
@model CreateProviderModel
|
@model CreateProviderModel
|
||||||
|
|
||||||
@inject Bit.Core.Services.IFeatureService FeatureService
|
@inject Bit.Core.Services.IFeatureService FeatureService
|
||||||
|
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = "Create Provider";
|
ViewData["Title"] = "Create Provider";
|
||||||
}
|
|
||||||
|
|
||||||
@section Scripts {
|
var providerTypes = Enum.GetValues<ProviderType>()
|
||||||
<script>
|
.OrderBy(x => x.GetDisplayAttribute().Order)
|
||||||
function toggleProviderTypeInfo(value) {
|
.ToList();
|
||||||
document.querySelectorAll('[id^="info-"]').forEach(el => { el.classList.add('d-none'); });
|
|
||||||
document.getElementById('info-' + value).classList.remove('d-none');
|
if (!FeatureService.IsEnabled(FeatureFlagKeys.PM12275_MultiOrganizationEnterprises))
|
||||||
|
{
|
||||||
|
providerTypes.Remove(ProviderType.MultiOrganizationEnterprise);
|
||||||
}
|
}
|
||||||
</script>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
<h1>Create Provider</h1>
|
<h1>Create Provider</h1>
|
||||||
|
<form method="post" asp-action="Create">
|
||||||
<form method="post">
|
|
||||||
<div asp-validation-summary="All" class="alert alert-danger"></div>
|
<div asp-validation-summary="All" class="alert alert-danger"></div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label asp-for="Type" class="h2"></label>
|
<label asp-for="Type" class="h2"></label>
|
||||||
@foreach(ProviderType providerType in Enum.GetValues(typeof(ProviderType)))
|
@foreach (var providerType in providerTypes)
|
||||||
{
|
{
|
||||||
var providerTypeValue = (int)providerType;
|
var providerTypeValue = (int)providerType;
|
||||||
<div class="form-check">
|
|
||||||
@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}" })
|
|
||||||
<br/>
|
|
||||||
@Html.LabelFor(m => m.Type, providerType.GetDisplayAttribute()?.GetDescription(), new { @class = "form-check-label small text-muted ml-3 align-top", @for = $"providerType-{providerTypeValue}" })
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="@($"info-{(int)ProviderType.Msp}")" class="form-group @(Model.Type != ProviderType.Msp ? "d-none" : string.Empty)">
|
|
||||||
<h2>MSP Info</h2>
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label asp-for="OwnerEmail"></label>
|
|
||||||
<input type="text" class="form-control" asp-for="OwnerEmail">
|
|
||||||
</div>
|
|
||||||
@if (FeatureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling))
|
|
||||||
{
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm">
|
<div class="col">
|
||||||
<div class="form-group">
|
<div class="form-check">
|
||||||
<label asp-for="TeamsMonthlySeatMinimum"></label>
|
@Html.RadioButtonFor(m => m.Type, providerType, new { id = $"providerType-{providerTypeValue}", @class = "form-check-input" })
|
||||||
<input type="number" class="form-control" asp-for="TeamsMonthlySeatMinimum">
|
@Html.LabelFor(m => m.Type, providerType.GetDisplayAttribute()?.GetName(), new { @class = "form-check-label align-middle", @for = $"providerType-{providerTypeValue}" })
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm">
|
</div>
|
||||||
<div class="form-group">
|
<div class="row">
|
||||||
<label asp-for="EnterpriseMonthlySeatMinimum"></label>
|
<div class="col">
|
||||||
<input type="number" class="form-control" asp-for="EnterpriseMonthlySeatMinimum">
|
@Html.LabelFor(m => m.Type, providerType.GetDisplayAttribute()?.GetDescription(), new { @class = "form-check-label small text-muted align-top", @for = $"providerType-{providerTypeValue}" })
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary mb-2">Next</button>
|
||||||
<div id="@($"info-{(int)ProviderType.Reseller}")" class="form-group @(Model.Type != ProviderType.Reseller ? "d-none" : string.Empty)">
|
|
||||||
<h2>Reseller Info</h2>
|
|
||||||
<div class="form-group">
|
|
||||||
<label asp-for="Name"></label>
|
|
||||||
<input type="text" class="form-control" asp-for="Name">
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label asp-for="BusinessName"></label>
|
|
||||||
<input type="text" class="form-control" asp-for="BusinessName">
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label asp-for="BillingEmail"></label>
|
|
||||||
<input type="text" class="form-control" asp-for="BillingEmail">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button type="submit" class="btn btn-primary mb-2">Create Provider</button>
|
|
||||||
</form>
|
</form>
|
||||||
|
39
src/Admin/AdminConsole/Views/Providers/CreateMsp.cshtml
Normal file
39
src/Admin/AdminConsole/Views/Providers/CreateMsp.cshtml
Normal file
@ -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";
|
||||||
|
}
|
||||||
|
|
||||||
|
<h1>Create Managed Service Provider</h1>
|
||||||
|
<div>
|
||||||
|
<form class="form-group" method="post" asp-action="CreateMsp">
|
||||||
|
<div asp-validation-summary="All" class="alert alert-danger"></div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="OwnerEmail"></label>
|
||||||
|
<input type="text" class="form-control" asp-for="OwnerEmail">
|
||||||
|
</div>
|
||||||
|
@if (FeatureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling))
|
||||||
|
{
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm">
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="TeamsMonthlySeatMinimum"></label>
|
||||||
|
<input type="number" class="form-control" asp-for="TeamsMonthlySeatMinimum">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm">
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="EnterpriseMonthlySeatMinimum"></label>
|
||||||
|
<input type="number" class="form-control" asp-for="EnterpriseMonthlySeatMinimum">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<button type="submit" class="btn btn-primary mb-2">Create Provider</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
@ -0,0 +1,43 @@
|
|||||||
|
@using Bit.Core.Billing.Enums
|
||||||
|
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||||
|
|
||||||
|
@model CreateMultiOrganizationEnterpriseProviderModel
|
||||||
|
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "Create Multi-organization Enterprise Provider";
|
||||||
|
}
|
||||||
|
|
||||||
|
<h1>Create Multi-organization Enterprise Provider</h1>
|
||||||
|
<div>
|
||||||
|
<form class="form-group" method="post" asp-action="CreateMultiOrganizationEnterprise">
|
||||||
|
<div asp-validation-summary="All" class="alert alert-danger"></div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="OwnerEmail"></label>
|
||||||
|
<input type="text" class="form-control" asp-for="OwnerEmail">
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm">
|
||||||
|
<div class="form-group">
|
||||||
|
@{
|
||||||
|
var multiOrgPlans = new List<PlanType>
|
||||||
|
{
|
||||||
|
PlanType.EnterpriseAnnually,
|
||||||
|
PlanType.EnterpriseMonthly
|
||||||
|
};
|
||||||
|
}
|
||||||
|
<label asp-for="Plan"></label>
|
||||||
|
<select class="form-control" asp-for="Plan" asp-items="Html.GetEnumSelectList(multiOrgPlans)">
|
||||||
|
<option value="">--</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm">
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="EnterpriseSeatMinimum"></label>
|
||||||
|
<input type="number" class="form-control" asp-for="EnterpriseSeatMinimum">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary mb-2">Create Provider</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
25
src/Admin/AdminConsole/Views/Providers/CreateReseller.cshtml
Normal file
25
src/Admin/AdminConsole/Views/Providers/CreateReseller.cshtml
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
@model CreateResellerProviderModel
|
||||||
|
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "Create Reseller Provider";
|
||||||
|
}
|
||||||
|
|
||||||
|
<h1>Create Reseller Provider</h1>
|
||||||
|
<div>
|
||||||
|
<form class="form-group" method="post" asp-action="CreateReseller">
|
||||||
|
<div asp-validation-summary="All" class="alert alert-danger"></div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="Name"></label>
|
||||||
|
<input type="text" class="form-control" asp-for="Name">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="BusinessName"></label>
|
||||||
|
<input type="text" class="form-control" asp-for="BusinessName">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="BillingEmail"></label>
|
||||||
|
<input type="text" class="form-control" asp-for="BillingEmail">
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary mb-2">Create Provider</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
@ -1,6 +1,9 @@
|
|||||||
@using Bit.Admin.Enums;
|
@using Bit.Admin.Enums;
|
||||||
@using Bit.Core
|
@using Bit.Core
|
||||||
|
@using Bit.Core.AdminConsole.Enums.Provider
|
||||||
|
@using Bit.Core.Billing.Enums
|
||||||
@using Bit.Core.Billing.Extensions
|
@using Bit.Core.Billing.Extensions
|
||||||
|
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||||
@inject Bit.Admin.Services.IAccessControlService AccessControlService
|
@inject Bit.Admin.Services.IAccessControlService AccessControlService
|
||||||
@inject Bit.Core.Services.IFeatureService FeatureService
|
@inject Bit.Core.Services.IFeatureService FeatureService
|
||||||
|
|
||||||
@ -46,6 +49,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@if (FeatureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling) && Model.Provider.IsBillable())
|
@if (FeatureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling) && Model.Provider.IsBillable())
|
||||||
|
{
|
||||||
|
switch (Model.Provider.Type)
|
||||||
|
{
|
||||||
|
case ProviderType.Msp:
|
||||||
{
|
{
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm">
|
<div class="col-sm">
|
||||||
@ -101,6 +108,39 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case ProviderType.MultiOrganizationEnterprise:
|
||||||
|
{
|
||||||
|
@if (FeatureService.IsEnabled(FeatureFlagKeys.PM12275_MultiOrganizationEnterprises) && Model.Provider.Type == ProviderType.MultiOrganizationEnterprise)
|
||||||
|
{
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm">
|
||||||
|
<div class="form-group">
|
||||||
|
@{
|
||||||
|
var multiOrgPlans = new List<PlanType>
|
||||||
|
{
|
||||||
|
PlanType.EnterpriseAnnually,
|
||||||
|
PlanType.EnterpriseMonthly
|
||||||
|
};
|
||||||
|
}
|
||||||
|
<label asp-for="Plan"></label>
|
||||||
|
<select class="form-control" asp-for="Plan" asp-items="Html.GetEnumSelectList(multiOrgPlans)">
|
||||||
|
<option value="">--</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm">
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="EnterpriseMinimumSeats"></label>
|
||||||
|
<input type="number" class="form-control" asp-for="EnterpriseMinimumSeats">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</form>
|
</form>
|
||||||
@await Html.PartialAsync("Organizations", Model)
|
@await Html.PartialAsync("Organizations", Model)
|
||||||
|
@ -4,6 +4,7 @@ using Bit.Admin.Enums;
|
|||||||
using Bit.Admin.Models;
|
using Bit.Admin.Models;
|
||||||
using Bit.Admin.Services;
|
using Bit.Admin.Services;
|
||||||
using Bit.Admin.Utilities;
|
using Bit.Admin.Utilities;
|
||||||
|
using Bit.Core;
|
||||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
@ -24,6 +25,8 @@ public class UsersController : Controller
|
|||||||
private readonly GlobalSettings _globalSettings;
|
private readonly GlobalSettings _globalSettings;
|
||||||
private readonly IAccessControlService _accessControlService;
|
private readonly IAccessControlService _accessControlService;
|
||||||
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
||||||
|
private readonly IUserService _userService;
|
||||||
|
private readonly IFeatureService _featureService;
|
||||||
|
|
||||||
public UsersController(
|
public UsersController(
|
||||||
IUserRepository userRepository,
|
IUserRepository userRepository,
|
||||||
@ -31,7 +34,9 @@ public class UsersController : Controller
|
|||||||
IPaymentService paymentService,
|
IPaymentService paymentService,
|
||||||
GlobalSettings globalSettings,
|
GlobalSettings globalSettings,
|
||||||
IAccessControlService accessControlService,
|
IAccessControlService accessControlService,
|
||||||
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery)
|
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
||||||
|
IUserService userService,
|
||||||
|
IFeatureService featureService)
|
||||||
{
|
{
|
||||||
_userRepository = userRepository;
|
_userRepository = userRepository;
|
||||||
_cipherRepository = cipherRepository;
|
_cipherRepository = cipherRepository;
|
||||||
@ -39,6 +44,8 @@ public class UsersController : Controller
|
|||||||
_globalSettings = globalSettings;
|
_globalSettings = globalSettings;
|
||||||
_accessControlService = accessControlService;
|
_accessControlService = accessControlService;
|
||||||
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
|
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
|
||||||
|
_userService = userService;
|
||||||
|
_featureService = featureService;
|
||||||
}
|
}
|
||||||
|
|
||||||
[RequirePermission(Permission.User_List_View)]
|
[RequirePermission(Permission.User_List_View)]
|
||||||
@ -82,8 +89,8 @@ public class UsersController : Controller
|
|||||||
var ciphers = await _cipherRepository.GetManyByUserIdAsync(id);
|
var ciphers = await _cipherRepository.GetManyByUserIdAsync(id);
|
||||||
|
|
||||||
var isTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user);
|
var isTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user);
|
||||||
|
var verifiedDomain = await AccountDeprovisioningEnabled(user.Id);
|
||||||
return View(UserViewModel.MapViewModel(user, isTwoFactorEnabled, ciphers));
|
return View(UserViewModel.MapViewModel(user, isTwoFactorEnabled, ciphers, verifiedDomain));
|
||||||
}
|
}
|
||||||
|
|
||||||
[SelfHosted(NotSelfHostedOnly = true)]
|
[SelfHosted(NotSelfHostedOnly = true)]
|
||||||
@ -99,7 +106,8 @@ public class UsersController : Controller
|
|||||||
var billingInfo = await _paymentService.GetBillingAsync(user);
|
var billingInfo = await _paymentService.GetBillingAsync(user);
|
||||||
var billingHistoryInfo = await _paymentService.GetBillingHistoryAsync(user);
|
var billingHistoryInfo = await _paymentService.GetBillingHistoryAsync(user);
|
||||||
var isTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(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]
|
[HttpPost]
|
||||||
@ -153,4 +161,12 @@ public class UsersController : Controller
|
|||||||
|
|
||||||
return RedirectToAction("Index");
|
return RedirectToAction("Index");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Feature flag to be removed in PM-14207
|
||||||
|
private async Task<bool?> AccountDeprovisioningEnabled(Guid userId)
|
||||||
|
{
|
||||||
|
return _featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
|
||||||
|
? await _userService.IsManagedByAnyOrganizationAsync(userId)
|
||||||
|
: null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
19
src/Admin/Enums/HtmlHelperExtensions.cs
Normal file
19
src/Admin/Enums/HtmlHelperExtensions.cs
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
|
||||||
|
using Bit.SharedWeb.Utilities;
|
||||||
|
|
||||||
|
// ReSharper disable once CheckNamespace
|
||||||
|
namespace Microsoft.AspNetCore.Mvc.Rendering;
|
||||||
|
|
||||||
|
public static class HtmlHelper
|
||||||
|
{
|
||||||
|
public static IEnumerable<SelectListItem> GetEnumSelectList<T>(this IHtmlHelper htmlHelper, IEnumerable<T> values)
|
||||||
|
where T : Enum
|
||||||
|
{
|
||||||
|
return values.Select(v => new SelectListItem
|
||||||
|
{
|
||||||
|
Text = v.GetDisplayAttribute().Name,
|
||||||
|
Value = v.ToString()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -20,9 +20,11 @@ public class UserEditModel
|
|||||||
IEnumerable<Cipher> ciphers,
|
IEnumerable<Cipher> ciphers,
|
||||||
BillingInfo billingInfo,
|
BillingInfo billingInfo,
|
||||||
BillingHistoryInfo billingHistoryInfo,
|
BillingHistoryInfo billingHistoryInfo,
|
||||||
GlobalSettings globalSettings)
|
GlobalSettings globalSettings,
|
||||||
|
bool? domainVerified
|
||||||
|
)
|
||||||
{
|
{
|
||||||
User = UserViewModel.MapViewModel(user, isTwoFactorEnabled, ciphers);
|
User = UserViewModel.MapViewModel(user, isTwoFactorEnabled, ciphers, domainVerified);
|
||||||
|
|
||||||
BillingInfo = billingInfo;
|
BillingInfo = billingInfo;
|
||||||
BillingHistoryInfo = billingHistoryInfo;
|
BillingHistoryInfo = billingHistoryInfo;
|
||||||
|
@ -14,6 +14,7 @@ public class UserViewModel
|
|||||||
public bool Premium { get; }
|
public bool Premium { get; }
|
||||||
public short? MaxStorageGb { get; }
|
public short? MaxStorageGb { get; }
|
||||||
public bool EmailVerified { get; }
|
public bool EmailVerified { get; }
|
||||||
|
public bool? DomainVerified { get; }
|
||||||
public bool TwoFactorEnabled { get; }
|
public bool TwoFactorEnabled { get; }
|
||||||
public DateTime AccountRevisionDate { get; }
|
public DateTime AccountRevisionDate { get; }
|
||||||
public DateTime RevisionDate { get; }
|
public DateTime RevisionDate { get; }
|
||||||
@ -35,6 +36,7 @@ public class UserViewModel
|
|||||||
bool premium,
|
bool premium,
|
||||||
short? maxStorageGb,
|
short? maxStorageGb,
|
||||||
bool emailVerified,
|
bool emailVerified,
|
||||||
|
bool? domainVerified,
|
||||||
bool twoFactorEnabled,
|
bool twoFactorEnabled,
|
||||||
DateTime accountRevisionDate,
|
DateTime accountRevisionDate,
|
||||||
DateTime revisionDate,
|
DateTime revisionDate,
|
||||||
@ -56,6 +58,7 @@ public class UserViewModel
|
|||||||
Premium = premium;
|
Premium = premium;
|
||||||
MaxStorageGb = maxStorageGb;
|
MaxStorageGb = maxStorageGb;
|
||||||
EmailVerified = emailVerified;
|
EmailVerified = emailVerified;
|
||||||
|
DomainVerified = domainVerified;
|
||||||
TwoFactorEnabled = twoFactorEnabled;
|
TwoFactorEnabled = twoFactorEnabled;
|
||||||
AccountRevisionDate = accountRevisionDate;
|
AccountRevisionDate = accountRevisionDate;
|
||||||
RevisionDate = revisionDate;
|
RevisionDate = revisionDate;
|
||||||
@ -73,10 +76,10 @@ public class UserViewModel
|
|||||||
public static IEnumerable<UserViewModel> MapViewModels(
|
public static IEnumerable<UserViewModel> MapViewModels(
|
||||||
IEnumerable<User> users,
|
IEnumerable<User> users,
|
||||||
IEnumerable<(Guid userId, bool twoFactorIsEnabled)> lookup) =>
|
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,
|
public static UserViewModel MapViewModel(User user,
|
||||||
IEnumerable<(Guid userId, bool twoFactorIsEnabled)> lookup) =>
|
IEnumerable<(Guid userId, bool twoFactorIsEnabled)> lookup, bool? domainVerified) =>
|
||||||
new(
|
new(
|
||||||
user.Id,
|
user.Id,
|
||||||
user.Name,
|
user.Name,
|
||||||
@ -86,6 +89,7 @@ public class UserViewModel
|
|||||||
user.Premium,
|
user.Premium,
|
||||||
user.MaxStorageGb,
|
user.MaxStorageGb,
|
||||||
user.EmailVerified,
|
user.EmailVerified,
|
||||||
|
domainVerified,
|
||||||
IsTwoFactorEnabled(user, lookup),
|
IsTwoFactorEnabled(user, lookup),
|
||||||
user.AccountRevisionDate,
|
user.AccountRevisionDate,
|
||||||
user.RevisionDate,
|
user.RevisionDate,
|
||||||
@ -100,9 +104,9 @@ public class UserViewModel
|
|||||||
Array.Empty<Cipher>());
|
Array.Empty<Cipher>());
|
||||||
|
|
||||||
public static UserViewModel MapViewModel(User user, bool isTwoFactorEnabled) =>
|
public static UserViewModel MapViewModel(User user, bool isTwoFactorEnabled) =>
|
||||||
MapViewModel(user, isTwoFactorEnabled, Array.Empty<Cipher>());
|
MapViewModel(user, isTwoFactorEnabled, Array.Empty<Cipher>(), false);
|
||||||
|
|
||||||
public static UserViewModel MapViewModel(User user, bool isTwoFactorEnabled, IEnumerable<Cipher> ciphers) =>
|
public static UserViewModel MapViewModel(User user, bool isTwoFactorEnabled, IEnumerable<Cipher> ciphers, bool? domainVerified) =>
|
||||||
new(
|
new(
|
||||||
user.Id,
|
user.Id,
|
||||||
user.Name,
|
user.Name,
|
||||||
@ -112,6 +116,7 @@ public class UserViewModel
|
|||||||
user.Premium,
|
user.Premium,
|
||||||
user.MaxStorageGb,
|
user.MaxStorageGb,
|
||||||
user.EmailVerified,
|
user.EmailVerified,
|
||||||
|
domainVerified,
|
||||||
isTwoFactorEnabled,
|
isTwoFactorEnabled,
|
||||||
user.AccountRevisionDate,
|
user.AccountRevisionDate,
|
||||||
user.RevisionDate,
|
user.RevisionDate,
|
||||||
|
@ -110,6 +110,7 @@ public static class RolePermissionMapping
|
|||||||
Permission.User_Licensing_View,
|
Permission.User_Licensing_View,
|
||||||
Permission.User_Billing_View,
|
Permission.User_Billing_View,
|
||||||
Permission.User_Billing_LaunchGateway,
|
Permission.User_Billing_LaunchGateway,
|
||||||
|
Permission.User_Delete,
|
||||||
Permission.Org_List_View,
|
Permission.Org_List_View,
|
||||||
Permission.Org_OrgInformation_View,
|
Permission.Org_OrgInformation_View,
|
||||||
Permission.Org_GeneralDetails_View,
|
Permission.Org_GeneralDetails_View,
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
@model UserViewModel
|
@model UserViewModel
|
||||||
<dl class="row">
|
<dl class="row">
|
||||||
<dt class="col-sm-4 col-lg-3">Id</dt>
|
<dt class="col-sm-4 col-lg-3">Id</dt>
|
||||||
<dd class="col-sm-8 col-lg-9"><code>@Model.Id</code></dd>
|
<dd class="col-sm-8 col-lg-9"><code>@Model.Id</code></dd>
|
||||||
@ -12,6 +12,11 @@
|
|||||||
<dt class="col-sm-4 col-lg-3">Email Verified</dt>
|
<dt class="col-sm-4 col-lg-3">Email Verified</dt>
|
||||||
<dd class="col-sm-8 col-lg-9">@(Model.EmailVerified ? "Yes" : "No")</dd>
|
<dd class="col-sm-8 col-lg-9">@(Model.EmailVerified ? "Yes" : "No")</dd>
|
||||||
|
|
||||||
|
@if(Model.DomainVerified.HasValue){
|
||||||
|
<dt class="col-sm-4 col-lg-3">Domain Verified</dt>
|
||||||
|
<dd class="col-sm-8 col-lg-9">@(Model.DomainVerified.Value == true ? "Yes" : "No")</dd>
|
||||||
|
}
|
||||||
|
|
||||||
<dt class="col-sm-4 col-lg-3">Using 2FA</dt>
|
<dt class="col-sm-4 col-lg-3">Using 2FA</dt>
|
||||||
<dd class="col-sm-8 col-lg-9">@(Model.TwoFactorEnabled ? "Yes" : "No")</dd>
|
<dd class="col-sm-8 col-lg-9">@(Model.TwoFactorEnabled ? "Yes" : "No")</dd>
|
||||||
|
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
using Bit.Api.AdminConsole.Models.Request.Organizations;
|
using Bit.Api.AdminConsole.Models.Request.Organizations;
|
||||||
using Bit.Api.AdminConsole.Models.Response.Organizations;
|
using Bit.Api.AdminConsole.Models.Response.Organizations;
|
||||||
using Bit.Api.Auth.Models.Request.Accounts;
|
|
||||||
using Bit.Api.Models.Request.Organizations;
|
using Bit.Api.Models.Request.Organizations;
|
||||||
using Bit.Api.Models.Response;
|
using Bit.Api.Models.Response;
|
||||||
using Bit.Api.Vault.AuthorizationHandlers.Collections;
|
using Bit.Api.Vault.AuthorizationHandlers.Collections;
|
||||||
@ -53,6 +52,8 @@ public class OrganizationUsersController : Controller
|
|||||||
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
||||||
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
|
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
|
||||||
private readonly IDeleteManagedOrganizationUserAccountCommand _deleteManagedOrganizationUserAccountCommand;
|
private readonly IDeleteManagedOrganizationUserAccountCommand _deleteManagedOrganizationUserAccountCommand;
|
||||||
|
private readonly IGetOrganizationUsersManagementStatusQuery _getOrganizationUsersManagementStatusQuery;
|
||||||
|
private readonly IFeatureService _featureService;
|
||||||
|
|
||||||
public OrganizationUsersController(
|
public OrganizationUsersController(
|
||||||
IOrganizationRepository organizationRepository,
|
IOrganizationRepository organizationRepository,
|
||||||
@ -73,7 +74,9 @@ public class OrganizationUsersController : Controller
|
|||||||
IOrganizationUserUserDetailsQuery organizationUserUserDetailsQuery,
|
IOrganizationUserUserDetailsQuery organizationUserUserDetailsQuery,
|
||||||
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
||||||
IRemoveOrganizationUserCommand removeOrganizationUserCommand,
|
IRemoveOrganizationUserCommand removeOrganizationUserCommand,
|
||||||
IDeleteManagedOrganizationUserAccountCommand deleteManagedOrganizationUserAccountCommand)
|
IDeleteManagedOrganizationUserAccountCommand deleteManagedOrganizationUserAccountCommand,
|
||||||
|
IGetOrganizationUsersManagementStatusQuery getOrganizationUsersManagementStatusQuery,
|
||||||
|
IFeatureService featureService)
|
||||||
{
|
{
|
||||||
_organizationRepository = organizationRepository;
|
_organizationRepository = organizationRepository;
|
||||||
_organizationUserRepository = organizationUserRepository;
|
_organizationUserRepository = organizationUserRepository;
|
||||||
@ -94,29 +97,34 @@ public class OrganizationUsersController : Controller
|
|||||||
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
|
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
|
||||||
_removeOrganizationUserCommand = removeOrganizationUserCommand;
|
_removeOrganizationUserCommand = removeOrganizationUserCommand;
|
||||||
_deleteManagedOrganizationUserAccountCommand = deleteManagedOrganizationUserAccountCommand;
|
_deleteManagedOrganizationUserAccountCommand = deleteManagedOrganizationUserAccountCommand;
|
||||||
|
_getOrganizationUsersManagementStatusQuery = getOrganizationUsersManagementStatusQuery;
|
||||||
|
_featureService = featureService;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{id}")]
|
[HttpGet("{id}")]
|
||||||
public async Task<OrganizationUserDetailsResponseModel> Get(string id, bool includeGroups = false)
|
public async Task<OrganizationUserDetailsResponseModel> Get(Guid id, bool includeGroups = false)
|
||||||
{
|
{
|
||||||
var organizationUser = await _organizationUserRepository.GetDetailsByIdWithCollectionsAsync(new Guid(id));
|
var (organizationUser, collections) = await _organizationUserRepository.GetDetailsByIdWithCollectionsAsync(id);
|
||||||
if (organizationUser == null || !await _currentContext.ManageUsers(organizationUser.Item1.OrganizationId))
|
if (organizationUser == null || !await _currentContext.ManageUsers(organizationUser.OrganizationId))
|
||||||
{
|
{
|
||||||
throw new NotFoundException();
|
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)
|
if (includeGroups)
|
||||||
{
|
{
|
||||||
response.Groups = await _groupRepository.GetManyIdsByUserIdAsync(organizationUser.Item1.Id);
|
response.Groups = await _groupRepository.GetManyIdsByUserIdAsync(organizationUser.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("mini-details")]
|
[HttpGet("mini-details")]
|
||||||
[RequireFeature(FeatureFlagKeys.Pm3478RefactorOrganizationUserApi)]
|
|
||||||
public async Task<ListResponseModel<OrganizationUserUserMiniDetailsResponseModel>> GetMiniDetails(Guid orgId)
|
public async Task<ListResponseModel<OrganizationUserUserMiniDetailsResponseModel>> GetMiniDetails(Guid orgId)
|
||||||
{
|
{
|
||||||
var authorizationResult = await _authorizationService.AuthorizeAsync(User, new OrganizationScope(orgId),
|
var authorizationResult = await _authorizationService.AuthorizeAsync(User, new OrganizationScope(orgId),
|
||||||
@ -150,11 +158,13 @@ public class OrganizationUsersController : Controller
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
var organizationUsersTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(organizationUsers);
|
var organizationUsersTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(organizationUsers);
|
||||||
|
var organizationUsersManagementStatus = await GetManagedByOrganizationStatusAsync(orgId, organizationUsers.Select(o => o.Id));
|
||||||
var responses = organizationUsers
|
var responses = organizationUsers
|
||||||
.Select(o =>
|
.Select(o =>
|
||||||
{
|
{
|
||||||
var userTwoFactorEnabled = organizationUsersTwoFactorEnabled.FirstOrDefault(u => u.user.Id == o.Id).twoFactorIsEnabled;
|
var userTwoFactorEnabled = organizationUsersTwoFactorEnabled.FirstOrDefault(u => u.user.Id == o.Id).twoFactorIsEnabled;
|
||||||
var orgUser = new OrganizationUserUserDetailsResponseModel(o, userTwoFactorEnabled);
|
var managedByOrganization = organizationUsersManagementStatus[o.Id];
|
||||||
|
var orgUser = new OrganizationUserUserDetailsResponseModel(o, userTwoFactorEnabled, managedByOrganization);
|
||||||
|
|
||||||
return orgUser;
|
return orgUser;
|
||||||
});
|
});
|
||||||
@ -534,7 +544,7 @@ public class OrganizationUsersController : Controller
|
|||||||
[RequireFeature(FeatureFlagKeys.AccountDeprovisioning)]
|
[RequireFeature(FeatureFlagKeys.AccountDeprovisioning)]
|
||||||
[HttpDelete("{id}/delete-account")]
|
[HttpDelete("{id}/delete-account")]
|
||||||
[HttpPost("{id}/delete-account")]
|
[HttpPost("{id}/delete-account")]
|
||||||
public async Task DeleteAccount(Guid orgId, Guid id, [FromBody] SecretVerificationRequestModel model)
|
public async Task DeleteAccount(Guid orgId, Guid id)
|
||||||
{
|
{
|
||||||
if (!await _currentContext.ManageUsers(orgId))
|
if (!await _currentContext.ManageUsers(orgId))
|
||||||
{
|
{
|
||||||
@ -547,19 +557,13 @@ public class OrganizationUsersController : Controller
|
|||||||
throw new UnauthorizedAccessException();
|
throw new UnauthorizedAccessException();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!await _userService.VerifySecretAsync(currentUser, model.Secret))
|
|
||||||
{
|
|
||||||
await Task.Delay(2000);
|
|
||||||
throw new BadRequestException(string.Empty, "User verification failed.");
|
|
||||||
}
|
|
||||||
|
|
||||||
await _deleteManagedOrganizationUserAccountCommand.DeleteUserAsync(orgId, id, currentUser.Id);
|
await _deleteManagedOrganizationUserAccountCommand.DeleteUserAsync(orgId, id, currentUser.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
[RequireFeature(FeatureFlagKeys.AccountDeprovisioning)]
|
[RequireFeature(FeatureFlagKeys.AccountDeprovisioning)]
|
||||||
[HttpDelete("delete-account")]
|
[HttpDelete("delete-account")]
|
||||||
[HttpPost("delete-account")]
|
[HttpPost("delete-account")]
|
||||||
public async Task<ListResponseModel<OrganizationUserBulkResponseModel>> BulkDeleteAccount(Guid orgId, [FromBody] SecureOrganizationUserBulkRequestModel model)
|
public async Task<ListResponseModel<OrganizationUserBulkResponseModel>> BulkDeleteAccount(Guid orgId, [FromBody] OrganizationUserBulkRequestModel model)
|
||||||
{
|
{
|
||||||
if (!await _currentContext.ManageUsers(orgId))
|
if (!await _currentContext.ManageUsers(orgId))
|
||||||
{
|
{
|
||||||
@ -572,12 +576,6 @@ public class OrganizationUsersController : Controller
|
|||||||
throw new UnauthorizedAccessException();
|
throw new UnauthorizedAccessException();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!await _userService.VerifySecretAsync(currentUser, model.Secret))
|
|
||||||
{
|
|
||||||
await Task.Delay(2000);
|
|
||||||
throw new BadRequestException(string.Empty, "User verification failed.");
|
|
||||||
}
|
|
||||||
|
|
||||||
var results = await _deleteManagedOrganizationUserAccountCommand.DeleteManyUsersAsync(orgId, model.Ids, currentUser.Id);
|
var results = await _deleteManagedOrganizationUserAccountCommand.DeleteManyUsersAsync(orgId, model.Ids, currentUser.Id);
|
||||||
|
|
||||||
return new ListResponseModel<OrganizationUserBulkResponseModel>(results.Select(r =>
|
return new ListResponseModel<OrganizationUserBulkResponseModel>(results.Select(r =>
|
||||||
@ -682,4 +680,15 @@ public class OrganizationUsersController : Controller
|
|||||||
return new ListResponseModel<OrganizationUserBulkResponseModel>(result.Select(r =>
|
return new ListResponseModel<OrganizationUserBulkResponseModel>(result.Select(r =>
|
||||||
new OrganizationUserBulkResponseModel(r.Item1.Id, r.Item2)));
|
new OrganizationUserBulkResponseModel(r.Item1.Id, r.Item2)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<IDictionary<Guid, bool>> GetManagedByOrganizationStatusAsync(Guid orgId, IEnumerable<Guid> userIds)
|
||||||
|
{
|
||||||
|
if (!_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning))
|
||||||
|
{
|
||||||
|
return userIds.ToDictionary(kvp => kvp, kvp => false);
|
||||||
|
}
|
||||||
|
|
||||||
|
var usersOrganizationManagementStatus = await _getOrganizationUsersManagementStatusQuery.GetUsersOrganizationManagementStatusAsync(orgId, userIds);
|
||||||
|
return usersOrganizationManagementStatus;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,7 @@ using Bit.Core.Utilities;
|
|||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.DataProtection;
|
using Microsoft.AspNetCore.DataProtection;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using AdminConsoleEntities = Bit.Core.AdminConsole.Entities;
|
||||||
|
|
||||||
namespace Bit.Api.AdminConsole.Controllers;
|
namespace Bit.Api.AdminConsole.Controllers;
|
||||||
|
|
||||||
@ -55,17 +56,16 @@ public class PoliciesController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{type}")]
|
[HttpGet("{type}")]
|
||||||
public async Task<PolicyResponseModel> Get(string orgId, int type)
|
public async Task<PolicyResponseModel> Get(Guid orgId, int type)
|
||||||
{
|
{
|
||||||
var orgIdGuid = new Guid(orgId);
|
if (!await _currentContext.ManagePolicies(orgId))
|
||||||
if (!await _currentContext.ManagePolicies(orgIdGuid))
|
|
||||||
{
|
{
|
||||||
throw new NotFoundException();
|
throw new NotFoundException();
|
||||||
}
|
}
|
||||||
var policy = await _policyRepository.GetByOrganizationIdTypeAsync(orgIdGuid, (PolicyType)type);
|
var policy = await _policyRepository.GetByOrganizationIdTypeAsync(orgId, (PolicyType)type);
|
||||||
if (policy == null)
|
if (policy == null)
|
||||||
{
|
{
|
||||||
throw new NotFoundException();
|
return new PolicyResponseModel(new AdminConsoleEntities.Policy() { Type = (PolicyType)type, Enabled = false });
|
||||||
}
|
}
|
||||||
|
|
||||||
return new PolicyResponseModel(policy);
|
return new PolicyResponseModel(policy);
|
||||||
|
@ -1,10 +0,0 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
using Bit.Api.Auth.Models.Request.Accounts;
|
|
||||||
|
|
||||||
namespace Bit.Api.AdminConsole.Models.Request.Organizations;
|
|
||||||
|
|
||||||
public class SecureOrganizationUserBulkRequestModel : SecretVerificationRequestModel
|
|
||||||
{
|
|
||||||
[Required]
|
|
||||||
public IEnumerable<Guid> Ids { get; set; }
|
|
||||||
}
|
|
@ -64,20 +64,27 @@ public class OrganizationUserResponseModel : ResponseModel
|
|||||||
|
|
||||||
public class OrganizationUserDetailsResponseModel : OrganizationUserResponseModel
|
public class OrganizationUserDetailsResponseModel : OrganizationUserResponseModel
|
||||||
{
|
{
|
||||||
public OrganizationUserDetailsResponseModel(OrganizationUser organizationUser,
|
public OrganizationUserDetailsResponseModel(
|
||||||
|
OrganizationUser organizationUser,
|
||||||
|
bool managedByOrganization,
|
||||||
IEnumerable<CollectionAccessSelection> collections)
|
IEnumerable<CollectionAccessSelection> collections)
|
||||||
: base(organizationUser, "organizationUserDetails")
|
: base(organizationUser, "organizationUserDetails")
|
||||||
{
|
{
|
||||||
|
ManagedByOrganization = managedByOrganization;
|
||||||
Collections = collections.Select(c => new SelectionReadOnlyResponseModel(c));
|
Collections = collections.Select(c => new SelectionReadOnlyResponseModel(c));
|
||||||
}
|
}
|
||||||
|
|
||||||
public OrganizationUserDetailsResponseModel(OrganizationUserUserDetails organizationUser,
|
public OrganizationUserDetailsResponseModel(OrganizationUserUserDetails organizationUser,
|
||||||
|
bool managedByOrganization,
|
||||||
IEnumerable<CollectionAccessSelection> collections)
|
IEnumerable<CollectionAccessSelection> collections)
|
||||||
: base(organizationUser, "organizationUserDetails")
|
: base(organizationUser, "organizationUserDetails")
|
||||||
{
|
{
|
||||||
|
ManagedByOrganization = managedByOrganization;
|
||||||
Collections = collections.Select(c => new SelectionReadOnlyResponseModel(c));
|
Collections = collections.Select(c => new SelectionReadOnlyResponseModel(c));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool ManagedByOrganization { get; set; }
|
||||||
|
|
||||||
public IEnumerable<SelectionReadOnlyResponseModel> Collections { get; set; }
|
public IEnumerable<SelectionReadOnlyResponseModel> Collections { get; set; }
|
||||||
|
|
||||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||||
@ -110,7 +117,7 @@ public class OrganizationUserUserMiniDetailsResponseModel : ResponseModel
|
|||||||
public class OrganizationUserUserDetailsResponseModel : OrganizationUserResponseModel
|
public class OrganizationUserUserDetailsResponseModel : OrganizationUserResponseModel
|
||||||
{
|
{
|
||||||
public OrganizationUserUserDetailsResponseModel(OrganizationUserUserDetails organizationUser,
|
public OrganizationUserUserDetailsResponseModel(OrganizationUserUserDetails organizationUser,
|
||||||
bool twoFactorEnabled, string obj = "organizationUserUserDetails")
|
bool twoFactorEnabled, bool managedByOrganization, string obj = "organizationUserUserDetails")
|
||||||
: base(organizationUser, obj)
|
: base(organizationUser, obj)
|
||||||
{
|
{
|
||||||
if (organizationUser == null)
|
if (organizationUser == null)
|
||||||
@ -127,6 +134,7 @@ public class OrganizationUserUserDetailsResponseModel : OrganizationUserResponse
|
|||||||
Groups = organizationUser.Groups;
|
Groups = organizationUser.Groups;
|
||||||
// Prevent reset password when using key connector.
|
// Prevent reset password when using key connector.
|
||||||
ResetPasswordEnrolled = ResetPasswordEnrolled && !organizationUser.UsesKeyConnector;
|
ResetPasswordEnrolled = ResetPasswordEnrolled && !organizationUser.UsesKeyConnector;
|
||||||
|
ManagedByOrganization = managedByOrganization;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
@ -134,6 +142,11 @@ public class OrganizationUserUserDetailsResponseModel : OrganizationUserResponse
|
|||||||
public string AvatarColor { get; set; }
|
public string AvatarColor { get; set; }
|
||||||
public bool TwoFactorEnabled { get; set; }
|
public bool TwoFactorEnabled { get; set; }
|
||||||
public bool SsoBound { get; set; }
|
public bool SsoBound { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
public bool ManagedByOrganization { get; set; }
|
||||||
public IEnumerable<SelectionReadOnlyResponseModel> Collections { get; set; }
|
public IEnumerable<SelectionReadOnlyResponseModel> Collections { get; set; }
|
||||||
public IEnumerable<Guid> Groups { get; set; }
|
public IEnumerable<Guid> Groups { get; set; }
|
||||||
}
|
}
|
||||||
|
@ -71,14 +71,13 @@ public class MembersController : Controller
|
|||||||
[ProducesResponseType((int)HttpStatusCode.NotFound)]
|
[ProducesResponseType((int)HttpStatusCode.NotFound)]
|
||||||
public async Task<IActionResult> Get(Guid id)
|
public async Task<IActionResult> Get(Guid id)
|
||||||
{
|
{
|
||||||
var userDetails = await _organizationUserRepository.GetDetailsByIdWithCollectionsAsync(id);
|
var (orgUser, collections) = await _organizationUserRepository.GetDetailsByIdWithCollectionsAsync(id);
|
||||||
var orgUser = userDetails?.Item1;
|
|
||||||
if (orgUser == null || orgUser.OrganizationId != _currentContext.OrganizationId)
|
if (orgUser == null || orgUser.OrganizationId != _currentContext.OrganizationId)
|
||||||
{
|
{
|
||||||
return new NotFoundResult();
|
return new NotFoundResult();
|
||||||
}
|
}
|
||||||
var response = new MemberResponseModel(orgUser, await _userService.TwoFactorIsEnabledAsync(orgUser),
|
var response = new MemberResponseModel(orgUser, await _userService.TwoFactorIsEnabledAsync(orgUser),
|
||||||
userDetails.Item2);
|
collections);
|
||||||
return new JsonResult(response);
|
return new JsonResult(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,7 +35,7 @@
|
|||||||
<PackageReference Include="AspNetCore.HealthChecks.SqlServer" Version="8.0.2" />
|
<PackageReference Include="AspNetCore.HealthChecks.SqlServer" Version="8.0.2" />
|
||||||
<PackageReference Include="AspNetCore.HealthChecks.Uris" Version="8.0.1" />
|
<PackageReference Include="AspNetCore.HealthChecks.Uris" Version="8.0.1" />
|
||||||
<PackageReference Include="Azure.Messaging.EventGrid" Version="4.25.0" />
|
<PackageReference Include="Azure.Messaging.EventGrid" Version="4.25.0" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.8.1" />
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.9.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
@ -148,6 +148,13 @@ public class AccountsController : Controller
|
|||||||
throw new BadRequestException("MasterPasswordHash", "Invalid password.");
|
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);
|
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.");
|
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,
|
var result = await _userService.ChangeEmailAsync(user, model.MasterPasswordHash, model.NewEmail,
|
||||||
model.NewMasterPasswordHash, model.Token, model.Key);
|
model.NewMasterPasswordHash, model.Token, model.Key);
|
||||||
if (result.Succeeded)
|
if (result.Succeeded)
|
||||||
@ -566,6 +580,13 @@ public class AccountsController : Controller
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
// If Account Deprovisioning is enabled, we need to check if the user is managed by any organization.
|
||||||
|
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
|
||||||
|
&& await _userService.IsManagedByAnyOrganizationAsync(user.Id))
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Cannot delete accounts owned by an organization. Contact your organization administrator for additional details.");
|
||||||
|
}
|
||||||
|
|
||||||
var result = await _userService.DeleteAsync(user);
|
var result = await _userService.DeleteAsync(user);
|
||||||
if (result.Succeeded)
|
if (result.Succeeded)
|
||||||
{
|
{
|
||||||
|
@ -26,7 +26,7 @@ public class OrganizationBillingController(
|
|||||||
[HttpGet("metadata")]
|
[HttpGet("metadata")]
|
||||||
public async Task<IResult> GetMetadataAsync([FromRoute] Guid organizationId)
|
public async Task<IResult> GetMetadataAsync([FromRoute] Guid organizationId)
|
||||||
{
|
{
|
||||||
if (!await currentContext.AccessMembersTab(organizationId))
|
if (!await currentContext.OrganizationUser(organizationId))
|
||||||
{
|
{
|
||||||
return Error.Unauthorized();
|
return Error.Unauthorized();
|
||||||
}
|
}
|
||||||
|
@ -4,10 +4,14 @@ namespace Bit.Api.Billing.Models.Responses;
|
|||||||
|
|
||||||
public record OrganizationMetadataResponse(
|
public record OrganizationMetadataResponse(
|
||||||
bool IsEligibleForSelfHost,
|
bool IsEligibleForSelfHost,
|
||||||
bool IsOnSecretsManagerStandalone)
|
bool IsManaged,
|
||||||
|
bool IsOnSecretsManagerStandalone,
|
||||||
|
bool IsSubscriptionUnpaid)
|
||||||
{
|
{
|
||||||
public static OrganizationMetadataResponse From(OrganizationMetadata metadata)
|
public static OrganizationMetadataResponse From(OrganizationMetadata metadata)
|
||||||
=> new(
|
=> new(
|
||||||
metadata.IsEligibleForSelfHost,
|
metadata.IsEligibleForSelfHost,
|
||||||
metadata.IsOnSecretsManagerStandalone);
|
metadata.IsManaged,
|
||||||
|
metadata.IsOnSecretsManagerStandalone,
|
||||||
|
metadata.IsSubscriptionUnpaid);
|
||||||
}
|
}
|
||||||
|
@ -196,8 +196,8 @@ public class DevicesController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpDelete("{id}")]
|
[HttpDelete("{id}")]
|
||||||
[HttpPost("{id}/delete")]
|
[HttpPost("{id}/deactivate")]
|
||||||
public async Task Delete(string id)
|
public async Task Deactivate(string id)
|
||||||
{
|
{
|
||||||
var device = await _deviceRepository.GetByIdAsync(new Guid(id), _userService.GetProperUserId(User).Value);
|
var device = await _deviceRepository.GetByIdAsync(new Guid(id), _userService.GetProperUserId(User).Value);
|
||||||
if (device == null)
|
if (device == null)
|
||||||
@ -205,7 +205,7 @@ public class DevicesController : Controller
|
|||||||
throw new NotFoundException();
|
throw new NotFoundException();
|
||||||
}
|
}
|
||||||
|
|
||||||
await _deviceService.DeleteAsync(device);
|
await _deviceService.DeactivateAsync(device);
|
||||||
}
|
}
|
||||||
|
|
||||||
[AllowAnonymous]
|
[AllowAnonymous]
|
||||||
|
@ -4,8 +4,10 @@ namespace Bit.Core.AdminConsole.Enums.Provider;
|
|||||||
|
|
||||||
public enum ProviderType : byte
|
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,
|
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,
|
Reseller = 1,
|
||||||
|
[Display(ShortName = "MOE", Name = "Multi-organization Enterprise", Description = "Access to multiple organizations", Order = 1)]
|
||||||
|
MultiOrganizationEnterprise = 2,
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,6 @@
|
|||||||
|
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;
|
||||||
|
|
||||||
|
public interface IOrganizationHasVerifiedDomainsQuery
|
||||||
|
{
|
||||||
|
Task<bool> HasVerifiedDomainsAsync(Guid orgId);
|
||||||
|
}
|
@ -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<bool> HasVerifiedDomainsAsync(Guid orgId) =>
|
||||||
|
(await domainRepository.GetDomainsByOrganizationIdAsync(orgId)).Any(od => od.VerifiedDate is not null);
|
||||||
|
}
|
@ -1,4 +1,7 @@
|
|||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
|
using Bit.Core.AdminConsole.Enums;
|
||||||
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;
|
||||||
|
using Bit.Core.AdminConsole.Services;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
@ -15,6 +18,9 @@ public class VerifyOrganizationDomainCommand : IVerifyOrganizationDomainCommand
|
|||||||
private readonly IDnsResolverService _dnsResolverService;
|
private readonly IDnsResolverService _dnsResolverService;
|
||||||
private readonly IEventService _eventService;
|
private readonly IEventService _eventService;
|
||||||
private readonly IGlobalSettings _globalSettings;
|
private readonly IGlobalSettings _globalSettings;
|
||||||
|
private readonly IPolicyService _policyService;
|
||||||
|
private readonly IFeatureService _featureService;
|
||||||
|
private readonly IOrganizationService _organizationService;
|
||||||
private readonly ILogger<VerifyOrganizationDomainCommand> _logger;
|
private readonly ILogger<VerifyOrganizationDomainCommand> _logger;
|
||||||
|
|
||||||
public VerifyOrganizationDomainCommand(
|
public VerifyOrganizationDomainCommand(
|
||||||
@ -22,12 +28,18 @@ public class VerifyOrganizationDomainCommand : IVerifyOrganizationDomainCommand
|
|||||||
IDnsResolverService dnsResolverService,
|
IDnsResolverService dnsResolverService,
|
||||||
IEventService eventService,
|
IEventService eventService,
|
||||||
IGlobalSettings globalSettings,
|
IGlobalSettings globalSettings,
|
||||||
|
IPolicyService policyService,
|
||||||
|
IFeatureService featureService,
|
||||||
|
IOrganizationService organizationService,
|
||||||
ILogger<VerifyOrganizationDomainCommand> logger)
|
ILogger<VerifyOrganizationDomainCommand> logger)
|
||||||
{
|
{
|
||||||
_organizationDomainRepository = organizationDomainRepository;
|
_organizationDomainRepository = organizationDomainRepository;
|
||||||
_dnsResolverService = dnsResolverService;
|
_dnsResolverService = dnsResolverService;
|
||||||
_eventService = eventService;
|
_eventService = eventService;
|
||||||
_globalSettings = globalSettings;
|
_globalSettings = globalSettings;
|
||||||
|
_policyService = policyService;
|
||||||
|
_featureService = featureService;
|
||||||
|
_organizationService = organizationService;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -102,6 +114,8 @@ public class VerifyOrganizationDomainCommand : IVerifyOrganizationDomainCommand
|
|||||||
if (await _dnsResolverService.ResolveAsync(domain.DomainName, domain.Txt))
|
if (await _dnsResolverService.ResolveAsync(domain.DomainName, domain.Txt))
|
||||||
{
|
{
|
||||||
domain.SetVerifiedDate();
|
domain.SetVerifiedDate();
|
||||||
|
|
||||||
|
await EnableSingleOrganizationPolicyAsync(domain.OrganizationId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
@ -112,4 +126,13 @@ public class VerifyOrganizationDomainCommand : IVerifyOrganizationDomainCommand
|
|||||||
|
|
||||||
return domain;
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
#nullable enable
|
#nullable enable
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Services;
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
|
||||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Authorization;
|
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Authorization;
|
||||||
@ -10,12 +9,10 @@ public class OrganizationUserUserDetailsAuthorizationHandler
|
|||||||
: AuthorizationHandler<OrganizationUserUserDetailsOperationRequirement, OrganizationScope>
|
: AuthorizationHandler<OrganizationUserUserDetailsOperationRequirement, OrganizationScope>
|
||||||
{
|
{
|
||||||
private readonly ICurrentContext _currentContext;
|
private readonly ICurrentContext _currentContext;
|
||||||
private readonly IFeatureService _featureService;
|
|
||||||
|
|
||||||
public OrganizationUserUserDetailsAuthorizationHandler(ICurrentContext currentContext, IFeatureService featureService)
|
public OrganizationUserUserDetailsAuthorizationHandler(ICurrentContext currentContext)
|
||||||
{
|
{
|
||||||
_currentContext = currentContext;
|
_currentContext = currentContext;
|
||||||
_featureService = featureService;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context,
|
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context,
|
||||||
@ -37,29 +34,6 @@ public class OrganizationUserUserDetailsAuthorizationHandler
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async Task<bool> CanReadAllAsync(Guid organizationId)
|
private async Task<bool> CanReadAllAsync(Guid organizationId)
|
||||||
{
|
|
||||||
if (_featureService.IsEnabled(FeatureFlagKeys.Pm3478RefactorOrganizationUserApi))
|
|
||||||
{
|
|
||||||
return await CanReadAllAsync_vNext(organizationId);
|
|
||||||
}
|
|
||||||
|
|
||||||
return await CanReadAllAsync_vCurrent(organizationId);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<bool> 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<bool> CanReadAllAsync_vNext(Guid organizationId)
|
|
||||||
{
|
{
|
||||||
// Admins can access this for general user management
|
// Admins can access this for general user management
|
||||||
var organization = _currentContext.GetOrganization(organizationId);
|
var organization = _currentContext.GetOrganization(organizationId);
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using Bit.Core.AdminConsole.Entities.Provider;
|
using Bit.Core.AdminConsole.Entities.Provider;
|
||||||
|
using Bit.Core.Billing.Enums;
|
||||||
|
|
||||||
namespace Bit.Core.AdminConsole.Providers.Interfaces;
|
namespace Bit.Core.AdminConsole.Providers.Interfaces;
|
||||||
|
|
||||||
@ -6,4 +7,5 @@ public interface ICreateProviderCommand
|
|||||||
{
|
{
|
||||||
Task CreateMspAsync(Provider provider, string ownerEmail, int teamsMinimumSeats, int enterpriseMinimumSeats);
|
Task CreateMspAsync(Provider provider, string ownerEmail, int teamsMinimumSeats, int enterpriseMinimumSeats);
|
||||||
Task CreateResellerAsync(Provider provider);
|
Task CreateResellerAsync(Provider provider);
|
||||||
|
Task CreateMultiOrganizationEnterpriseAsync(Provider provider, string ownerEmail, PlanType plan, int minimumSeats);
|
||||||
}
|
}
|
||||||
|
@ -22,8 +22,7 @@ public interface IOrganizationUserRepository : IRepository<OrganizationUser, Gui
|
|||||||
Task<OrganizationUser?> GetByOrganizationAsync(Guid organizationId, Guid userId);
|
Task<OrganizationUser?> GetByOrganizationAsync(Guid organizationId, Guid userId);
|
||||||
Task<Tuple<OrganizationUser?, ICollection<CollectionAccessSelection>>> GetByIdWithCollectionsAsync(Guid id);
|
Task<Tuple<OrganizationUser?, ICollection<CollectionAccessSelection>>> GetByIdWithCollectionsAsync(Guid id);
|
||||||
Task<OrganizationUserUserDetails?> GetDetailsByIdAsync(Guid id);
|
Task<OrganizationUserUserDetails?> GetDetailsByIdAsync(Guid id);
|
||||||
Task<Tuple<OrganizationUserUserDetails?, ICollection<CollectionAccessSelection>>>
|
Task<(OrganizationUserUserDetails? OrganizationUser, ICollection<CollectionAccessSelection> Collections)> GetDetailsByIdWithCollectionsAsync(Guid id);
|
||||||
GetDetailsByIdWithCollectionsAsync(Guid id);
|
|
||||||
Task<ICollection<OrganizationUserUserDetails>> GetManyDetailsByOrganizationAsync(Guid organizationId, bool includeGroups = false, bool includeCollections = false);
|
Task<ICollection<OrganizationUserUserDetails>> GetManyDetailsByOrganizationAsync(Guid organizationId, bool includeGroups = false, bool includeCollections = false);
|
||||||
Task<ICollection<OrganizationUserOrganizationDetails>> GetManyDetailsByUserAsync(Guid userId,
|
Task<ICollection<OrganizationUserOrganizationDetails>> GetManyDetailsByUserAsync(Guid userId,
|
||||||
OrganizationUserStatusType? status = null);
|
OrganizationUserStatusType? status = null);
|
||||||
|
@ -4,8 +4,4 @@ public interface IOrganizationDomainService
|
|||||||
{
|
{
|
||||||
Task ValidateOrganizationsDomainAsync();
|
Task ValidateOrganizationsDomainAsync();
|
||||||
Task OrganizationDomainMaintenanceAsync();
|
Task OrganizationDomainMaintenanceAsync();
|
||||||
/// <summary>
|
|
||||||
/// Indicates if the organization has any verified domains.
|
|
||||||
/// </summary>
|
|
||||||
Task<bool> HasVerifiedDomainsAsync(Guid orgId);
|
|
||||||
}
|
}
|
||||||
|
@ -106,12 +106,6 @@ public class OrganizationDomainService : IOrganizationDomainService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> HasVerifiedDomainsAsync(Guid orgId)
|
|
||||||
{
|
|
||||||
var orgDomains = await _domainRepository.GetDomainsByOrganizationIdAsync(orgId);
|
|
||||||
return orgDomains.Any(od => od.VerifiedDate != null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<List<string>> GetAdminEmailsAsync(Guid organizationId)
|
private async Task<List<string>> GetAdminEmailsAsync(Guid organizationId)
|
||||||
{
|
{
|
||||||
var orgUsers = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(organizationId);
|
var orgUsers = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(organizationId);
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.AdminConsole.Enums;
|
using Bit.Core.AdminConsole.Enums;
|
||||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
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.OrganizationUsers.Interfaces;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
|
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
|
||||||
@ -32,6 +33,7 @@ public class PolicyService : IPolicyService
|
|||||||
private readonly IFeatureService _featureService;
|
private readonly IFeatureService _featureService;
|
||||||
private readonly ISavePolicyCommand _savePolicyCommand;
|
private readonly ISavePolicyCommand _savePolicyCommand;
|
||||||
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
|
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
|
||||||
|
private readonly IOrganizationHasVerifiedDomainsQuery _organizationHasVerifiedDomainsQuery;
|
||||||
|
|
||||||
public PolicyService(
|
public PolicyService(
|
||||||
IApplicationCacheService applicationCacheService,
|
IApplicationCacheService applicationCacheService,
|
||||||
@ -45,7 +47,8 @@ public class PolicyService : IPolicyService
|
|||||||
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
||||||
IFeatureService featureService,
|
IFeatureService featureService,
|
||||||
ISavePolicyCommand savePolicyCommand,
|
ISavePolicyCommand savePolicyCommand,
|
||||||
IRemoveOrganizationUserCommand removeOrganizationUserCommand)
|
IRemoveOrganizationUserCommand removeOrganizationUserCommand,
|
||||||
|
IOrganizationHasVerifiedDomainsQuery organizationHasVerifiedDomainsQuery)
|
||||||
{
|
{
|
||||||
_applicationCacheService = applicationCacheService;
|
_applicationCacheService = applicationCacheService;
|
||||||
_eventService = eventService;
|
_eventService = eventService;
|
||||||
@ -59,6 +62,7 @@ public class PolicyService : IPolicyService
|
|||||||
_featureService = featureService;
|
_featureService = featureService;
|
||||||
_savePolicyCommand = savePolicyCommand;
|
_savePolicyCommand = savePolicyCommand;
|
||||||
_removeOrganizationUserCommand = removeOrganizationUserCommand;
|
_removeOrganizationUserCommand = removeOrganizationUserCommand;
|
||||||
|
_organizationHasVerifiedDomainsQuery = organizationHasVerifiedDomainsQuery;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SaveAsync(Policy policy, Guid? savingUserId)
|
public async Task SaveAsync(Policy policy, Guid? savingUserId)
|
||||||
@ -239,6 +243,7 @@ public class PolicyService : IPolicyService
|
|||||||
case PolicyType.SingleOrg:
|
case PolicyType.SingleOrg:
|
||||||
if (!policy.Enabled)
|
if (!policy.Enabled)
|
||||||
{
|
{
|
||||||
|
await HasVerifiedDomainsAsync(org);
|
||||||
await RequiredBySsoAsync(org);
|
await RequiredBySsoAsync(org);
|
||||||
await RequiredByVaultTimeoutAsync(org);
|
await RequiredByVaultTimeoutAsync(org);
|
||||||
await RequiredByKeyConnectorAsync(org);
|
await RequiredByKeyConnectorAsync(org);
|
||||||
@ -279,6 +284,15 @@ public class PolicyService : IPolicyService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task HasVerifiedDomainsAsync(Organization org)
|
||||||
|
{
|
||||||
|
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
|
||||||
|
&& await _organizationHasVerifiedDomainsQuery.HasVerifiedDomainsAsync(org.Id))
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Organization has verified domains.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async Task SetPolicyConfiguration(Policy policy)
|
private async Task SetPolicyConfiguration(Policy policy)
|
||||||
{
|
{
|
||||||
await _policyRepository.UpsertAsync(policy);
|
await _policyRepository.UpsertAsync(policy);
|
||||||
|
@ -6,6 +6,14 @@ using Bit.Core.Utilities;
|
|||||||
namespace Bit.Core.Auth.Models.Api.Request.Accounts;
|
namespace Bit.Core.Auth.Models.Api.Request.Accounts;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
public enum RegisterFinishTokenType : byte
|
||||||
|
{
|
||||||
|
EmailVerification = 1,
|
||||||
|
OrganizationInvite = 2,
|
||||||
|
OrgSponsoredFreeFamilyPlan = 3,
|
||||||
|
EmergencyAccessInvite = 4,
|
||||||
|
ProviderInvite = 5,
|
||||||
|
}
|
||||||
|
|
||||||
public class RegisterFinishRequestModel : IValidatableObject
|
public class RegisterFinishRequestModel : IValidatableObject
|
||||||
{
|
{
|
||||||
@ -36,6 +44,10 @@ public class RegisterFinishRequestModel : IValidatableObject
|
|||||||
public string? AcceptEmergencyAccessInviteToken { get; set; }
|
public string? AcceptEmergencyAccessInviteToken { get; set; }
|
||||||
public Guid? AcceptEmergencyAccessId { get; set; }
|
public Guid? AcceptEmergencyAccessId { get; set; }
|
||||||
|
|
||||||
|
public string? ProviderInviteToken { get; set; }
|
||||||
|
|
||||||
|
public Guid? ProviderUserId { get; set; }
|
||||||
|
|
||||||
public User ToUser()
|
public User ToUser()
|
||||||
{
|
{
|
||||||
var user = new User
|
var user = new User
|
||||||
@ -54,6 +66,32 @@ public class RegisterFinishRequestModel : IValidatableObject
|
|||||||
return user;
|
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<ValidationResult> Validate(ValidationContext validationContext)
|
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
|
||||||
{
|
{
|
||||||
|
@ -0,0 +1,7 @@
|
|||||||
|
using Bit.Core.Models.Mail;
|
||||||
|
|
||||||
|
namespace Bit.Core.Auth.Models.Mail;
|
||||||
|
|
||||||
|
public class CannotDeleteManagedAccountViewModel : BaseMailModel
|
||||||
|
{
|
||||||
|
}
|
@ -61,4 +61,16 @@ public interface IRegisterUserCommand
|
|||||||
public Task<IdentityResult> RegisterUserViaAcceptEmergencyAccessInviteToken(User user, string masterPasswordHash,
|
public Task<IdentityResult> RegisterUserViaAcceptEmergencyAccessInviteToken(User user, string masterPasswordHash,
|
||||||
string acceptEmergencyAccessInviteToken, Guid acceptEmergencyAccessId);
|
string acceptEmergencyAccessInviteToken, Guid acceptEmergencyAccessId);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="user">The <see cref="User"/> to create</param>
|
||||||
|
/// <param name="masterPasswordHash">The hashed master password the user entered</param>
|
||||||
|
/// <param name="providerInviteToken">The provider invite token sent to the user via email</param>
|
||||||
|
/// <param name="providerUserId">The provider user id which is used to validate the invite token</param>
|
||||||
|
/// <returns><see cref="IdentityResult"/></returns>
|
||||||
|
public Task<IdentityResult> RegisterUserViaProviderInviteToken(User user, string masterPasswordHash, string providerInviteToken, Guid providerUserId);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -32,6 +32,7 @@ public class RegisterUserCommand : IRegisterUserCommand
|
|||||||
private readonly IDataProtectorTokenFactory<OrgUserInviteTokenable> _orgUserInviteTokenDataFactory;
|
private readonly IDataProtectorTokenFactory<OrgUserInviteTokenable> _orgUserInviteTokenDataFactory;
|
||||||
private readonly IDataProtectorTokenFactory<RegistrationEmailVerificationTokenable> _registrationEmailVerificationTokenDataFactory;
|
private readonly IDataProtectorTokenFactory<RegistrationEmailVerificationTokenable> _registrationEmailVerificationTokenDataFactory;
|
||||||
private readonly IDataProtector _organizationServiceDataProtector;
|
private readonly IDataProtector _organizationServiceDataProtector;
|
||||||
|
private readonly IDataProtector _providerServiceDataProtector;
|
||||||
|
|
||||||
private readonly ICurrentContext _currentContext;
|
private readonly ICurrentContext _currentContext;
|
||||||
|
|
||||||
@ -75,6 +76,8 @@ public class RegisterUserCommand : IRegisterUserCommand
|
|||||||
|
|
||||||
_validateRedemptionTokenCommand = validateRedemptionTokenCommand;
|
_validateRedemptionTokenCommand = validateRedemptionTokenCommand;
|
||||||
_emergencyAccessInviteTokenDataFactory = emergencyAccessInviteTokenDataFactory;
|
_emergencyAccessInviteTokenDataFactory = emergencyAccessInviteTokenDataFactory;
|
||||||
|
|
||||||
|
_providerServiceDataProtector = dataProtectionProvider.CreateProtector("ProviderServiceDataProtector");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -303,6 +306,25 @@ public class RegisterUserCommand : IRegisterUserCommand
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<IdentityResult> 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()
|
private void ValidateOpenRegistrationAllowed()
|
||||||
{
|
{
|
||||||
// We validate open registration on send of initial email and here b/c a user could technically start the
|
// 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)
|
private RegistrationEmailVerificationTokenable ValidateRegistrationEmailVerificationTokenable(string emailVerificationToken, string userEmail)
|
||||||
{
|
{
|
||||||
|
@ -11,11 +11,10 @@ namespace Bit.Core.Billing.Extensions;
|
|||||||
public static class BillingExtensions
|
public static class BillingExtensions
|
||||||
{
|
{
|
||||||
public static bool IsBillable(this Provider provider) =>
|
public static bool IsBillable(this Provider provider) =>
|
||||||
provider is
|
provider.SupportsConsolidatedBilling() && provider.Status == ProviderStatusType.Billable;
|
||||||
{
|
|
||||||
Type: ProviderType.Msp,
|
public static bool SupportsConsolidatedBilling(this Provider provider)
|
||||||
Status: ProviderStatusType.Billable
|
=> provider.Type is ProviderType.Msp or ProviderType.MultiOrganizationEnterprise;
|
||||||
};
|
|
||||||
|
|
||||||
public static bool IsValidClient(this Organization organization)
|
public static bool IsValidClient(this Organization organization)
|
||||||
=> organization is
|
=> organization is
|
||||||
|
@ -8,6 +8,7 @@ using Bit.Core.Billing.Enums;
|
|||||||
using Bit.Core.Billing.Migration.Models;
|
using Bit.Core.Billing.Migration.Models;
|
||||||
using Bit.Core.Billing.Repositories;
|
using Bit.Core.Billing.Repositories;
|
||||||
using Bit.Core.Billing.Services;
|
using Bit.Core.Billing.Services;
|
||||||
|
using Bit.Core.Billing.Services.Contracts;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@ -307,7 +308,14 @@ public class ProviderMigrator(
|
|||||||
.FirstOrDefault(providerPlan => providerPlan.PlanType == PlanType.TeamsMonthly)?
|
.FirstOrDefault(providerPlan => providerPlan.PlanType == PlanType.TeamsMonthly)?
|
||||||
.SeatMinimum ?? 0;
|
.SeatMinimum ?? 0;
|
||||||
|
|
||||||
await providerBillingService.UpdateSeatMinimums(provider, enterpriseSeatMinimum, teamsSeatMinimum);
|
var updateSeatMinimumsCommand = new UpdateProviderSeatMinimumsCommand(
|
||||||
|
provider.Id,
|
||||||
|
provider.GatewaySubscriptionId,
|
||||||
|
[
|
||||||
|
(Plan: PlanType.EnterpriseMonthly, SeatsMinimum: enterpriseSeatMinimum),
|
||||||
|
(Plan: PlanType.TeamsMonthly, SeatsMinimum: teamsSeatMinimum)
|
||||||
|
]);
|
||||||
|
await providerBillingService.UpdateSeatMinimums(updateSeatMinimumsCommand);
|
||||||
|
|
||||||
logger.LogInformation(
|
logger.LogInformation(
|
||||||
"CB: Updated Stripe subscription for provider ({ProviderID}) with current seat minimums", provider.Id);
|
"CB: Updated Stripe subscription for provider ({ProviderID}) with current seat minimums", provider.Id);
|
||||||
@ -325,6 +333,8 @@ public class ProviderMigrator(
|
|||||||
|
|
||||||
var organizationCancellationCredit = organizationCustomers.Sum(customer => customer.Balance);
|
var organizationCancellationCredit = organizationCustomers.Sum(customer => customer.Balance);
|
||||||
|
|
||||||
|
if (organizationCancellationCredit != 0)
|
||||||
|
{
|
||||||
await stripeAdapter.CustomerBalanceTransactionCreate(provider.GatewayCustomerId,
|
await stripeAdapter.CustomerBalanceTransactionCreate(provider.GatewayCustomerId,
|
||||||
new CustomerBalanceTransactionCreateOptions
|
new CustomerBalanceTransactionCreateOptions
|
||||||
{
|
{
|
||||||
@ -332,6 +342,7 @@ public class ProviderMigrator(
|
|||||||
Currency = "USD",
|
Currency = "USD",
|
||||||
Description = "Unused, prorated time for client organization subscriptions."
|
Description = "Unused, prorated time for client organization subscriptions."
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
var migrationRecords = await Task.WhenAll(organizations.Select(organization =>
|
var migrationRecords = await Task.WhenAll(organizations.Select(organization =>
|
||||||
clientOrganizationMigrationRecordRepository.GetByOrganizationId(organization.Id)));
|
clientOrganizationMigrationRecordRepository.GetByOrganizationId(organization.Id)));
|
||||||
|
@ -2,9 +2,6 @@
|
|||||||
|
|
||||||
public record OrganizationMetadata(
|
public record OrganizationMetadata(
|
||||||
bool IsEligibleForSelfHost,
|
bool IsEligibleForSelfHost,
|
||||||
bool IsOnSecretsManagerStandalone)
|
bool IsManaged,
|
||||||
{
|
bool IsOnSecretsManagerStandalone,
|
||||||
public static OrganizationMetadata Default() => new(
|
bool IsSubscriptionUnpaid);
|
||||||
IsEligibleForSelfHost: false,
|
|
||||||
IsOnSecretsManagerStandalone: false);
|
|
||||||
}
|
|
||||||
|
@ -87,7 +87,9 @@ public record EnterprisePlan : Plan
|
|||||||
AdditionalStoragePricePerGb = 4;
|
AdditionalStoragePricePerGb = 4;
|
||||||
StripeStoragePlanId = "storage-gb-annually";
|
StripeStoragePlanId = "storage-gb-annually";
|
||||||
StripeSeatPlanId = "2023-enterprise-org-seat-annually";
|
StripeSeatPlanId = "2023-enterprise-org-seat-annually";
|
||||||
|
StripeProviderPortalSeatPlanId = "password-manager-provider-portal-enterprise-annually-2024";
|
||||||
SeatPrice = 72;
|
SeatPrice = 72;
|
||||||
|
ProviderPortalSeatPrice = 72;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
@ -0,0 +1,8 @@
|
|||||||
|
using Bit.Core.Billing.Enums;
|
||||||
|
|
||||||
|
namespace Bit.Core.Billing.Services.Contracts;
|
||||||
|
|
||||||
|
public record ChangeProviderPlanCommand(
|
||||||
|
Guid ProviderPlanId,
|
||||||
|
PlanType NewPlan,
|
||||||
|
string GatewaySubscriptionId);
|
@ -0,0 +1,10 @@
|
|||||||
|
using Bit.Core.Billing.Enums;
|
||||||
|
|
||||||
|
namespace Bit.Core.Billing.Services.Contracts;
|
||||||
|
|
||||||
|
/// <param name="Id">The ID of the provider to update the seat minimums for.</param>
|
||||||
|
/// <param name="Configuration">The new seat minimums for the provider.</param>
|
||||||
|
public record UpdateProviderSeatMinimumsCommand(
|
||||||
|
Guid Id,
|
||||||
|
string GatewaySubscriptionId,
|
||||||
|
IReadOnlyCollection<(PlanType Plan, int SeatsMinimum)> Configuration);
|
@ -3,6 +3,7 @@ using Bit.Core.AdminConsole.Entities.Provider;
|
|||||||
using Bit.Core.AdminConsole.Enums.Provider;
|
using Bit.Core.AdminConsole.Enums.Provider;
|
||||||
using Bit.Core.Billing.Entities;
|
using Bit.Core.Billing.Entities;
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
|
using Bit.Core.Billing.Services.Contracts;
|
||||||
using Bit.Core.Models.Business;
|
using Bit.Core.Models.Business;
|
||||||
using Stripe;
|
using Stripe;
|
||||||
|
|
||||||
@ -89,8 +90,12 @@ public interface IProviderBillingService
|
|||||||
Task<Subscription> SetupSubscription(
|
Task<Subscription> SetupSubscription(
|
||||||
Provider provider);
|
Provider provider);
|
||||||
|
|
||||||
Task UpdateSeatMinimums(
|
/// <summary>
|
||||||
Provider provider,
|
/// Changes the assigned provider plan for the provider.
|
||||||
int enterpriseSeatMinimum,
|
/// </summary>
|
||||||
int teamsSeatMinimum);
|
/// <param name="command">The command to change the provider plan.</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task ChangePlan(ChangeProviderPlanCommand command);
|
||||||
|
|
||||||
|
Task UpdateSeatMinimums(UpdateProviderSeatMinimumsCommand command);
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
|
||||||
using Bit.Core.Billing.Caches;
|
using Bit.Core.Billing.Caches;
|
||||||
using Bit.Core.Billing.Constants;
|
using Bit.Core.Billing.Constants;
|
||||||
using Bit.Core.Billing.Models;
|
using Bit.Core.Billing.Models;
|
||||||
@ -27,7 +26,6 @@ public class OrganizationBillingService(
|
|||||||
IGlobalSettings globalSettings,
|
IGlobalSettings globalSettings,
|
||||||
ILogger<OrganizationBillingService> logger,
|
ILogger<OrganizationBillingService> logger,
|
||||||
IOrganizationRepository organizationRepository,
|
IOrganizationRepository organizationRepository,
|
||||||
IProviderRepository providerRepository,
|
|
||||||
ISetupIntentCache setupIntentCache,
|
ISetupIntentCache setupIntentCache,
|
||||||
IStripeAdapter stripeAdapter,
|
IStripeAdapter stripeAdapter,
|
||||||
ISubscriberService subscriberService) : IOrganizationBillingService
|
ISubscriberService subscriberService) : IOrganizationBillingService
|
||||||
@ -64,18 +62,18 @@ public class OrganizationBillingService(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var customer = await subscriberService.GetCustomer(organization, new CustomerGetOptions
|
var customer = await subscriberService.GetCustomer(organization,
|
||||||
{
|
new CustomerGetOptions { Expand = ["discount.coupon.applies_to"] });
|
||||||
Expand = ["discount.coupon.applies_to"]
|
|
||||||
});
|
|
||||||
|
|
||||||
var subscription = await subscriberService.GetSubscription(organization);
|
var subscription = await subscriberService.GetSubscription(organization);
|
||||||
|
|
||||||
var isEligibleForSelfHost = await IsEligibleForSelfHost(organization, subscription);
|
var isEligibleForSelfHost = IsEligibleForSelfHost(organization);
|
||||||
|
var isManaged = organization.Status == OrganizationStatusType.Managed;
|
||||||
var isOnSecretsManagerStandalone = IsOnSecretsManagerStandalone(organization, customer, subscription);
|
var isOnSecretsManagerStandalone = IsOnSecretsManagerStandalone(organization, customer, subscription);
|
||||||
|
var isSubscriptionUnpaid = IsSubscriptionUnpaid(subscription);
|
||||||
|
|
||||||
return new OrganizationMetadata(isEligibleForSelfHost, isOnSecretsManagerStandalone);
|
return new OrganizationMetadata(isEligibleForSelfHost, isManaged, isOnSecretsManagerStandalone,
|
||||||
|
isSubscriptionUnpaid);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task UpdatePaymentMethod(
|
public async Task UpdatePaymentMethod(
|
||||||
@ -339,26 +337,12 @@ public class OrganizationBillingService(
|
|||||||
return await stripeAdapter.SubscriptionCreateAsync(subscriptionCreateOptions);
|
return await stripeAdapter.SubscriptionCreateAsync(subscriptionCreateOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<bool> IsEligibleForSelfHost(
|
private static bool IsEligibleForSelfHost(
|
||||||
Organization organization,
|
Organization organization)
|
||||||
Subscription? organizationSubscription)
|
|
||||||
{
|
{
|
||||||
if (organization.Status != OrganizationStatusType.Managed)
|
var eligibleSelfHostPlans = StaticStore.Plans.Where(plan => plan.HasSelfHost).Select(plan => plan.Type);
|
||||||
{
|
|
||||||
return organization.Plan.Contains("Families") ||
|
|
||||||
organization.Plan.Contains("Enterprise") && IsActive(organizationSubscription);
|
|
||||||
}
|
|
||||||
|
|
||||||
var provider = await providerRepository.GetByOrganizationIdAsync(organization.Id);
|
return eligibleSelfHostPlans.Contains(organization.PlanType);
|
||||||
|
|
||||||
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(
|
private static bool IsOnSecretsManagerStandalone(
|
||||||
@ -392,5 +376,16 @@ public class OrganizationBillingService(
|
|||||||
return subscriptionProductIds.Intersect(couponAppliesTo ?? []).Any();
|
return subscriptionProductIds.Intersect(couponAppliesTo ?? []).Any();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool IsSubscriptionUnpaid(Subscription subscription)
|
||||||
|
{
|
||||||
|
if (subscription == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return subscription.Status == "unpaid";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
|
@ -106,7 +106,6 @@ public static class FeatureFlagKeys
|
|||||||
public const string UseTreeWalkerApiForPageDetailsCollection = "use-tree-walker-api-for-page-details-collection";
|
public const string UseTreeWalkerApiForPageDetailsCollection = "use-tree-walker-api-for-page-details-collection";
|
||||||
public const string ItemShare = "item-share";
|
public const string ItemShare = "item-share";
|
||||||
public const string DuoRedirect = "duo-redirect";
|
public const string DuoRedirect = "duo-redirect";
|
||||||
public const string PM5864DollarThreshold = "PM-5864-dollar-threshold";
|
|
||||||
public const string AC2101UpdateTrialInitiationEmail = "AC-2101-update-trial-initiation-email";
|
public const string AC2101UpdateTrialInitiationEmail = "AC-2101-update-trial-initiation-email";
|
||||||
public const string EnableConsolidatedBilling = "enable-consolidated-billing";
|
public const string EnableConsolidatedBilling = "enable-consolidated-billing";
|
||||||
public const string AC1795_UpdatedSubscriptionStatusSection = "AC-1795_updated-subscription-status-section";
|
public const string AC1795_UpdatedSubscriptionStatusSection = "AC-1795_updated-subscription-status-section";
|
||||||
@ -117,7 +116,6 @@ public static class FeatureFlagKeys
|
|||||||
public const string RestrictProviderAccess = "restrict-provider-access";
|
public const string RestrictProviderAccess = "restrict-provider-access";
|
||||||
public const string PM4154BulkEncryptionService = "PM-4154-bulk-encryption-service";
|
public const string PM4154BulkEncryptionService = "PM-4154-bulk-encryption-service";
|
||||||
public const string VaultBulkManagementAction = "vault-bulk-management-action";
|
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 MemberAccessReport = "ac-2059-member-access-report";
|
||||||
public const string BlockLegacyUsers = "block-legacy-users";
|
public const string BlockLegacyUsers = "block-legacy-users";
|
||||||
public const string InlineMenuFieldQualification = "inline-menu-field-qualification";
|
public const string InlineMenuFieldQualification = "inline-menu-field-qualification";
|
||||||
@ -142,12 +140,14 @@ public static class FeatureFlagKeys
|
|||||||
public const string EnableNewCardCombinedExpiryAutofill = "enable-new-card-combined-expiry-autofill";
|
public const string EnableNewCardCombinedExpiryAutofill = "enable-new-card-combined-expiry-autofill";
|
||||||
public const string StorageReseedRefactor = "storage-reseed-refactor";
|
public const string StorageReseedRefactor = "storage-reseed-refactor";
|
||||||
public const string TrialPayment = "PM-8163-trial-payment";
|
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 RemoveServerVersionHeader = "remove-server-version-header";
|
||||||
public const string AccessIntelligence = "pm-13227-access-intelligence";
|
public const string AccessIntelligence = "pm-13227-access-intelligence";
|
||||||
public const string VerifiedSsoDomainEndpoint = "pm-12337-refactor-sso-details-endpoint";
|
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 Pm13322AddPolicyDefinitions = "pm-13322-add-policy-definitions";
|
||||||
public const string LimitCollectionCreationDeletionSplit = "pm-10863-limit-collection-creation-deletion-split";
|
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<string> GetAllKeys()
|
public static List<string> GetAllKeys()
|
||||||
{
|
{
|
||||||
@ -163,7 +163,6 @@ public static class FeatureFlagKeys
|
|||||||
return new Dictionary<string, string>()
|
return new Dictionary<string, string>()
|
||||||
{
|
{
|
||||||
{ DuoRedirect, "true" },
|
{ DuoRedirect, "true" },
|
||||||
{ BulkDeviceApproval, "true" },
|
|
||||||
{ CipherKeyEncryption, "true" },
|
{ CipherKeyEncryption, "true" },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -21,11 +21,11 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="AspNetCoreRateLimit.Redis" Version="2.0.0" />
|
<PackageReference Include="AspNetCoreRateLimit.Redis" Version="2.0.0" />
|
||||||
<PackageReference Include="AWSSDK.SimpleEmail" Version="3.7.401.24" />
|
<PackageReference Include="AWSSDK.SimpleEmail" Version="3.7.401.30" />
|
||||||
<PackageReference Include="AWSSDK.SQS" Version="3.7.400.34" />
|
<PackageReference Include="AWSSDK.SQS" Version="3.7.400.40" />
|
||||||
<PackageReference Include="Azure.Data.Tables" Version="12.9.0" />
|
<PackageReference Include="Azure.Data.Tables" Version="12.9.0" />
|
||||||
<PackageReference Include="Azure.Extensions.AspNetCore.DataProtection.Blobs" Version="1.3.4" />
|
<PackageReference Include="Azure.Extensions.AspNetCore.DataProtection.Blobs" Version="1.3.4" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.DataProtection" Version="8.0.8" />
|
<PackageReference Include="Microsoft.AspNetCore.DataProtection" Version="8.0.10" />
|
||||||
<PackageReference Include="Azure.Messaging.ServiceBus" Version="7.18.1" />
|
<PackageReference Include="Azure.Messaging.ServiceBus" Version="7.18.1" />
|
||||||
<PackageReference Include="Azure.Storage.Blobs" Version="12.21.2" />
|
<PackageReference Include="Azure.Storage.Blobs" Version="12.21.2" />
|
||||||
<PackageReference Include="Azure.Storage.Queues" Version="12.19.1" />
|
<PackageReference Include="Azure.Storage.Queues" Version="12.19.1" />
|
||||||
@ -35,22 +35,22 @@
|
|||||||
<PackageReference Include="Fido2.AspNet" Version="3.0.1" />
|
<PackageReference Include="Fido2.AspNet" Version="3.0.1" />
|
||||||
<PackageReference Include="Handlebars.Net" Version="2.1.6" />
|
<PackageReference Include="Handlebars.Net" Version="2.1.6" />
|
||||||
<PackageReference Include="MailKit" Version="4.8.0" />
|
<PackageReference Include="MailKit" Version="4.8.0" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.8" />
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.10" />
|
||||||
<PackageReference Include="Microsoft.Azure.Cosmos" Version="3.44.0" />
|
<PackageReference Include="Microsoft.Azure.Cosmos" Version="3.45.0" />
|
||||||
<PackageReference Include="Microsoft.Azure.NotificationHubs" Version="4.2.0" />
|
<PackageReference Include="Microsoft.Azure.NotificationHubs" Version="4.2.0" />
|
||||||
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.2" />
|
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.2" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Caching.Cosmos" Version="1.6.1" />
|
<PackageReference Include="Microsoft.Extensions.Caching.Cosmos" Version="1.6.1" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Caching.SqlServer" Version="8.0.8" />
|
<PackageReference Include="Microsoft.Extensions.Caching.SqlServer" Version="8.0.10" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="8.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="8.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="8.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="8.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Identity.Stores" Version="8.0.8" />
|
<PackageReference Include="Microsoft.Extensions.Identity.Stores" Version="8.0.10" />
|
||||||
<PackageReference Include="Quartz" Version="3.9.0" />
|
<PackageReference Include="Quartz" Version="3.9.0" />
|
||||||
<PackageReference Include="SendGrid" Version="9.29.3" />
|
<PackageReference Include="SendGrid" Version="9.29.3" />
|
||||||
<PackageReference Include="Serilog.AspNetCore" Version="8.0.3" />
|
<PackageReference Include="Serilog.AspNetCore" Version="8.0.3" />
|
||||||
<PackageReference Include="Serilog.Extensions.Logging" Version="8.0.0" />
|
<PackageReference Include="Serilog.Extensions.Logging" Version="8.0.0" />
|
||||||
<PackageReference Include="Serilog.Extensions.Logging.File" Version="3.0.0" />
|
<PackageReference Include="Serilog.Extensions.Logging.File" Version="3.0.0" />
|
||||||
<PackageReference Include="Sentry.Serilog" Version="3.41.4" />
|
<PackageReference Include="Sentry.Serilog" Version="3.41.4" />
|
||||||
<PackageReference Include="Duende.IdentityServer" Version="7.0.6" />
|
<PackageReference Include="Duende.IdentityServer" Version="7.0.8" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||||
<PackageReference Include="Serilog.Sinks.SyslogMessages" Version="4.0.0" />
|
<PackageReference Include="Serilog.Sinks.SyslogMessages" Version="4.0.0" />
|
||||||
<PackageReference Include="AspNetCoreRateLimit" Version="5.0.0" />
|
<PackageReference Include="AspNetCoreRateLimit" Version="5.0.0" />
|
||||||
@ -58,8 +58,8 @@
|
|||||||
<PackageReference Include="Stripe.net" Version="45.14.0" />
|
<PackageReference Include="Stripe.net" Version="45.14.0" />
|
||||||
<PackageReference Include="Otp.NET" Version="1.4.0" />
|
<PackageReference Include="Otp.NET" Version="1.4.0" />
|
||||||
<PackageReference Include="YubicoDotNetClient" Version="1.2.0" />
|
<PackageReference Include="YubicoDotNetClient" Version="1.2.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="8.0.8" />
|
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="8.0.10" />
|
||||||
<PackageReference Include="LaunchDarkly.ServerSdk" Version="8.5.2" />
|
<PackageReference Include="LaunchDarkly.ServerSdk" Version="8.6.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
@ -38,6 +38,10 @@ public class Device : ITableObject<Guid>
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string? EncryptedPrivateKey { get; set; }
|
public string? EncryptedPrivateKey { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the device is active for the user.
|
||||||
|
/// </summary>
|
||||||
|
public bool Active { get; set; } = true;
|
||||||
|
|
||||||
public void SetNewId()
|
public void SetNewId()
|
||||||
{
|
{
|
||||||
|
@ -0,0 +1,15 @@
|
|||||||
|
{{#>FullHtmlLayout}}
|
||||||
|
<table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||||
|
<tr style="margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||||
|
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: left;" valign="top" align="center">
|
||||||
|
You have requested to delete your account. This action cannot be completed because your account is owned by an organization.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||||
|
<td class="content-block last" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none; text-align: left;" valign="top" align="center">
|
||||||
|
Please contact your organization administrator for additional details.
|
||||||
|
<br style="margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;" />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
{{/FullHtmlLayout}}
|
@ -0,0 +1,6 @@
|
|||||||
|
{{#>BasicTextLayout}}
|
||||||
|
You have requested to delete your account. This action cannot be completed because your account is owned by an organization.
|
||||||
|
|
||||||
|
Please contact your organization administrator for additional details.
|
||||||
|
|
||||||
|
{{/BasicTextLayout}}
|
@ -130,6 +130,7 @@ public static class OrganizationServiceCollectionExtensions
|
|||||||
services.AddScoped<IGetOrganizationDomainByIdOrganizationIdQuery, GetOrganizationDomainByIdOrganizationIdQuery>();
|
services.AddScoped<IGetOrganizationDomainByIdOrganizationIdQuery, GetOrganizationDomainByIdOrganizationIdQuery>();
|
||||||
services.AddScoped<IGetOrganizationDomainByOrganizationIdQuery, GetOrganizationDomainByOrganizationIdQuery>();
|
services.AddScoped<IGetOrganizationDomainByOrganizationIdQuery, GetOrganizationDomainByOrganizationIdQuery>();
|
||||||
services.AddScoped<IDeleteOrganizationDomainCommand, DeleteOrganizationDomainCommand>();
|
services.AddScoped<IDeleteOrganizationDomainCommand, DeleteOrganizationDomainCommand>();
|
||||||
|
services.AddScoped<IOrganizationHasVerifiedDomainsQuery, OrganizationHasVerifiedDomainsQuery>();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void AddOrganizationAuthCommands(this IServiceCollection services)
|
private static void AddOrganizationAuthCommands(this IServiceCollection services)
|
||||||
|
@ -7,7 +7,7 @@ public interface IDeviceService
|
|||||||
{
|
{
|
||||||
Task SaveAsync(Device device);
|
Task SaveAsync(Device device);
|
||||||
Task ClearTokenAsync(Device device);
|
Task ClearTokenAsync(Device device);
|
||||||
Task DeleteAsync(Device device);
|
Task DeactivateAsync(Device device);
|
||||||
Task UpdateDevicesTrustAsync(string currentDeviceIdentifier,
|
Task UpdateDevicesTrustAsync(string currentDeviceIdentifier,
|
||||||
Guid currentUserId,
|
Guid currentUserId,
|
||||||
DeviceKeysUpdateRequestModel currentDeviceUpdate,
|
DeviceKeysUpdateRequestModel currentDeviceUpdate,
|
||||||
|
@ -18,6 +18,7 @@ public interface IMailService
|
|||||||
ProductTierType productTier,
|
ProductTierType productTier,
|
||||||
IEnumerable<ProductType> products);
|
IEnumerable<ProductType> products);
|
||||||
Task SendVerifyDeleteEmailAsync(string email, Guid userId, string token);
|
Task SendVerifyDeleteEmailAsync(string email, Guid userId, string token);
|
||||||
|
Task SendCannotDeleteManagedAccountEmailAsync(string email);
|
||||||
Task SendChangeEmailAlreadyExistsEmailAsync(string fromEmail, string toEmail);
|
Task SendChangeEmailAlreadyExistsEmailAsync(string fromEmail, string toEmail);
|
||||||
Task SendChangeEmailEmailAsync(string newEmailAddress, string token);
|
Task SendChangeEmailEmailAsync(string newEmailAddress, string token);
|
||||||
Task SendTwoFactorEmailAsync(string email, string token);
|
Task SendTwoFactorEmailAsync(string email, string token);
|
||||||
|
@ -14,6 +14,17 @@ public interface IStripeAdapter
|
|||||||
CustomerBalanceTransactionCreateOptions options);
|
CustomerBalanceTransactionCreateOptions options);
|
||||||
Task<Stripe.Subscription> SubscriptionCreateAsync(Stripe.SubscriptionCreateOptions subscriptionCreateOptions);
|
Task<Stripe.Subscription> SubscriptionCreateAsync(Stripe.SubscriptionCreateOptions subscriptionCreateOptions);
|
||||||
Task<Stripe.Subscription> SubscriptionGetAsync(string id, Stripe.SubscriptionGetOptions options = null);
|
Task<Stripe.Subscription> SubscriptionGetAsync(string id, Stripe.SubscriptionGetOptions options = null);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves a subscription object for a provider.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">The subscription ID.</param>
|
||||||
|
/// <param name="providerId">The provider ID.</param>
|
||||||
|
/// <param name="options">Additional options.</param>
|
||||||
|
/// <returns>The subscription object.</returns>
|
||||||
|
/// <exception cref="InvalidOperationException">Thrown when the subscription doesn't belong to the provider.</exception>
|
||||||
|
Task<Stripe.Subscription> ProviderSubscriptionGetAsync(string id, Guid providerId, Stripe.SubscriptionGetOptions options = null);
|
||||||
|
|
||||||
Task<List<Stripe.Subscription>> SubscriptionListAsync(StripeSubscriptionListOptions subscriptionSearchOptions);
|
Task<List<Stripe.Subscription>> SubscriptionListAsync(StripeSubscriptionListOptions subscriptionSearchOptions);
|
||||||
Task<Stripe.Subscription> SubscriptionUpdateAsync(string id, Stripe.SubscriptionUpdateOptions options = null);
|
Task<Stripe.Subscription> SubscriptionUpdateAsync(string id, Stripe.SubscriptionUpdateOptions options = null);
|
||||||
Task<Stripe.Subscription> SubscriptionCancelAsync(string Id, Stripe.SubscriptionCancelOptions options = null);
|
Task<Stripe.Subscription> SubscriptionCancelAsync(string Id, Stripe.SubscriptionCancelOptions options = null);
|
||||||
|
@ -41,9 +41,18 @@ public class DeviceService : IDeviceService
|
|||||||
await _pushRegistrationService.DeleteRegistrationAsync(device.Id.ToString());
|
await _pushRegistrationService.DeleteRegistrationAsync(device.Id.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task DeleteAsync(Device device)
|
public async Task DeactivateAsync(Device device)
|
||||||
{
|
{
|
||||||
await _deviceRepository.DeleteAsync(device);
|
// already deactivated
|
||||||
|
if (!device.Active)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
device.Active = false;
|
||||||
|
device.RevisionDate = DateTime.UtcNow;
|
||||||
|
await _deviceRepository.UpsertAsync(device);
|
||||||
|
|
||||||
await _pushRegistrationService.DeleteRegistrationAsync(device.Id.ToString());
|
await _pushRegistrationService.DeleteRegistrationAsync(device.Id.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -112,6 +112,19 @@ public class HandlebarsMailService : IMailService
|
|||||||
await _mailDeliveryService.SendEmailAsync(message);
|
await _mailDeliveryService.SendEmailAsync(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task SendCannotDeleteManagedAccountEmailAsync(string email)
|
||||||
|
{
|
||||||
|
var message = CreateDefaultMessage("Delete Your Account", email);
|
||||||
|
var model = new CannotDeleteManagedAccountViewModel
|
||||||
|
{
|
||||||
|
WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,
|
||||||
|
SiteName = _globalSettings.SiteName,
|
||||||
|
};
|
||||||
|
await AddMessageContentAsync(message, "AdminConsole.CannotDeleteManagedAccount", model);
|
||||||
|
message.Category = "CannotDeleteManagedAccount";
|
||||||
|
await _mailDeliveryService.SendEmailAsync(message);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task SendChangeEmailAlreadyExistsEmailAsync(string fromEmail, string toEmail)
|
public async Task SendChangeEmailAlreadyExistsEmailAsync(string fromEmail, string toEmail)
|
||||||
{
|
{
|
||||||
var message = CreateDefaultMessage("Your Email Change", toEmail);
|
var message = CreateDefaultMessage("Your Email Change", toEmail);
|
||||||
|
@ -79,6 +79,20 @@ public class StripeAdapter : IStripeAdapter
|
|||||||
return _subscriptionService.GetAsync(id, options);
|
return _subscriptionService.GetAsync(id, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<Subscription> ProviderSubscriptionGetAsync(
|
||||||
|
string id,
|
||||||
|
Guid providerId,
|
||||||
|
SubscriptionGetOptions options = null)
|
||||||
|
{
|
||||||
|
var subscription = await _subscriptionService.GetAsync(id, options);
|
||||||
|
if (subscription.Metadata.TryGetValue("providerId", out var value) && value == providerId.ToString())
|
||||||
|
{
|
||||||
|
return subscription;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new InvalidOperationException("Subscription does not belong to the provider.");
|
||||||
|
}
|
||||||
|
|
||||||
public Task<Stripe.Subscription> SubscriptionUpdateAsync(string id,
|
public Task<Stripe.Subscription> SubscriptionUpdateAsync(string id,
|
||||||
Stripe.SubscriptionUpdateOptions options = null)
|
Stripe.SubscriptionUpdateOptions options = null)
|
||||||
{
|
{
|
||||||
|
@ -792,19 +792,16 @@ public class StripePaymentService : IPaymentService
|
|||||||
var daysUntilDue = sub.DaysUntilDue;
|
var daysUntilDue = sub.DaysUntilDue;
|
||||||
var chargeNow = collectionMethod == "charge_automatically";
|
var chargeNow = collectionMethod == "charge_automatically";
|
||||||
var updatedItemOptions = subscriptionUpdate.UpgradeItemsOptions(sub);
|
var updatedItemOptions = subscriptionUpdate.UpgradeItemsOptions(sub);
|
||||||
var isPm5864DollarThresholdEnabled = _featureService.IsEnabled(FeatureFlagKeys.PM5864DollarThreshold);
|
|
||||||
var isAnnualPlan = sub?.Items?.Data.FirstOrDefault()?.Plan?.Interval == "year";
|
var isAnnualPlan = sub?.Items?.Data.FirstOrDefault()?.Plan?.Interval == "year";
|
||||||
|
|
||||||
var subUpdateOptions = new SubscriptionUpdateOptions
|
var subUpdateOptions = new SubscriptionUpdateOptions
|
||||||
{
|
{
|
||||||
Items = updatedItemOptions,
|
Items = updatedItemOptions,
|
||||||
ProrationBehavior = !isPm5864DollarThresholdEnabled || invoiceNow
|
ProrationBehavior = invoiceNow ? Constants.AlwaysInvoice : Constants.CreateProrations,
|
||||||
? Constants.AlwaysInvoice
|
|
||||||
: Constants.CreateProrations,
|
|
||||||
DaysUntilDue = daysUntilDue ?? 1,
|
DaysUntilDue = daysUntilDue ?? 1,
|
||||||
CollectionMethod = "send_invoice"
|
CollectionMethod = "send_invoice"
|
||||||
};
|
};
|
||||||
if (!invoiceNow && isAnnualPlan && isPm5864DollarThresholdEnabled && sub.Status.Trim() != "trialing")
|
if (!invoiceNow && isAnnualPlan && sub.Status.Trim() != "trialing")
|
||||||
{
|
{
|
||||||
subUpdateOptions.PendingInvoiceItemInterval =
|
subUpdateOptions.PendingInvoiceItemInterval =
|
||||||
new SubscriptionPendingInvoiceItemIntervalOptions { Interval = "month" };
|
new SubscriptionPendingInvoiceItemIntervalOptions { Interval = "month" };
|
||||||
@ -838,7 +835,7 @@ public class StripePaymentService : IPaymentService
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!isPm5864DollarThresholdEnabled && !invoiceNow)
|
if (invoiceNow)
|
||||||
{
|
{
|
||||||
if (chargeNow)
|
if (chargeNow)
|
||||||
{
|
{
|
||||||
|
@ -297,6 +297,12 @@ public class UserService : UserManager<User>, IUserService, IDisposable
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (await IsManagedByAnyOrganizationAsync(user.Id))
|
||||||
|
{
|
||||||
|
await _mailService.SendCannotDeleteManagedAccountEmailAsync(user.Email);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var token = await base.GenerateUserTokenAsync(user, TokenOptions.DefaultProvider, "DeleteAccount");
|
var token = await base.GenerateUserTokenAsync(user, TokenOptions.DefaultProvider, "DeleteAccount");
|
||||||
await _mailService.SendVerifyDeleteEmailAsync(user.Email, user.Id, token);
|
await _mailService.SendVerifyDeleteEmailAsync(user.Email, user.Id, token);
|
||||||
}
|
}
|
||||||
|
@ -94,6 +94,11 @@ public class NoopMailService : IMailService
|
|||||||
return Task.FromResult(0);
|
return Task.FromResult(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Task SendCannotDeleteManagedAccountEmailAsync(string email)
|
||||||
|
{
|
||||||
|
return Task.FromResult(0);
|
||||||
|
}
|
||||||
|
|
||||||
public Task SendPasswordlessSignInAsync(string returnUrl, string token, string email)
|
public Task SendPasswordlessSignInAsync(string returnUrl, string token, string email)
|
||||||
{
|
{
|
||||||
return Task.FromResult(0);
|
return Task.FromResult(0);
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using Bit.Core;
|
using System.Diagnostics;
|
||||||
|
using Bit.Core;
|
||||||
using Bit.Core.Auth.Enums;
|
using Bit.Core.Auth.Enums;
|
||||||
using Bit.Core.Auth.Models.Api.Request.Accounts;
|
using Bit.Core.Auth.Models.Api.Request.Accounts;
|
||||||
using Bit.Core.Auth.Models.Api.Response.Accounts;
|
using Bit.Core.Auth.Models.Api.Response.Accounts;
|
||||||
@ -149,40 +150,44 @@ public class AccountsController : Controller
|
|||||||
IdentityResult identityResult = null;
|
IdentityResult identityResult = null;
|
||||||
var delaysEnabled = !_featureService.IsEnabled(FeatureFlagKeys.EmailVerificationDisableTimingDelays);
|
var delaysEnabled = !_featureService.IsEnabled(FeatureFlagKeys.EmailVerificationDisableTimingDelays);
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(model.OrgInviteToken) && model.OrganizationUserId.HasValue)
|
switch (model.GetTokenType())
|
||||||
{
|
{
|
||||||
identityResult = await _registerUserCommand.RegisterUserViaOrganizationInviteToken(user, model.MasterPasswordHash,
|
case RegisterFinishTokenType.EmailVerification:
|
||||||
model.OrgInviteToken, model.OrganizationUserId);
|
|
||||||
|
|
||||||
return await ProcessRegistrationResult(identityResult, user, delaysEnabled);
|
|
||||||
}
|
|
||||||
|
|
||||||
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 =
|
identityResult =
|
||||||
await _registerUserCommand.RegisterUserViaEmailVerificationToken(user, model.MasterPasswordHash,
|
await _registerUserCommand.RegisterUserViaEmailVerificationToken(user, model.MasterPasswordHash,
|
||||||
model.EmailVerificationToken);
|
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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<RegisterResponseModel> ProcessRegistrationResult(IdentityResult result, User user, bool delaysEnabled)
|
private async Task<RegisterResponseModel> ProcessRegistrationResult(IdentityResult result, User user, bool delaysEnabled)
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using Bit.Core.Settings;
|
using Bit.Core.Settings;
|
||||||
|
using Bit.Identity.IdentityServer.RequestValidators;
|
||||||
using Duende.IdentityServer.Models;
|
using Duende.IdentityServer.Models;
|
||||||
|
|
||||||
namespace Bit.Identity.IdentityServer;
|
namespace Bit.Identity.IdentityServer;
|
||||||
|
@ -1,15 +1,10 @@
|
|||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using System.Text.Json;
|
|
||||||
using Bit.Core;
|
using Bit.Core;
|
||||||
using Bit.Core.AdminConsole.Entities;
|
|
||||||
using Bit.Core.AdminConsole.Enums;
|
using Bit.Core.AdminConsole.Enums;
|
||||||
using Bit.Core.AdminConsole.Services;
|
using Bit.Core.AdminConsole.Services;
|
||||||
using Bit.Core.Auth.Entities;
|
using Bit.Core.Auth.Entities;
|
||||||
using Bit.Core.Auth.Enums;
|
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.Api.Response;
|
||||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
|
||||||
using Bit.Core.Auth.Repositories;
|
using Bit.Core.Auth.Repositories;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
@ -17,32 +12,26 @@ using Bit.Core.Enums;
|
|||||||
using Bit.Core.Identity;
|
using Bit.Core.Identity;
|
||||||
using Bit.Core.Models.Api;
|
using Bit.Core.Models.Api;
|
||||||
using Bit.Core.Models.Api.Response;
|
using Bit.Core.Models.Api.Response;
|
||||||
using Bit.Core.Models.Data.Organizations;
|
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Settings;
|
using Bit.Core.Settings;
|
||||||
using Bit.Core.Tokens;
|
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Duende.IdentityServer.Validation;
|
using Duende.IdentityServer.Validation;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
|
||||||
namespace Bit.Identity.IdentityServer;
|
namespace Bit.Identity.IdentityServer.RequestValidators;
|
||||||
|
|
||||||
public abstract class BaseRequestValidator<T> where T : class
|
public abstract class BaseRequestValidator<T> where T : class
|
||||||
{
|
{
|
||||||
private UserManager<User> _userManager;
|
private UserManager<User> _userManager;
|
||||||
private readonly IEventService _eventService;
|
private readonly IEventService _eventService;
|
||||||
private readonly IDeviceValidator _deviceValidator;
|
private readonly IDeviceValidator _deviceValidator;
|
||||||
private readonly IOrganizationDuoWebTokenProvider _organizationDuoWebTokenProvider;
|
private readonly ITwoFactorAuthenticationValidator _twoFactorAuthenticationValidator;
|
||||||
private readonly ITemporaryDuoWebV4SDKService _duoWebV4SDKService;
|
|
||||||
private readonly IOrganizationRepository _organizationRepository;
|
|
||||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||||
private readonly IApplicationCacheService _applicationCacheService;
|
|
||||||
private readonly IMailService _mailService;
|
private readonly IMailService _mailService;
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
private readonly GlobalSettings _globalSettings;
|
private readonly GlobalSettings _globalSettings;
|
||||||
private readonly IUserRepository _userRepository;
|
private readonly IUserRepository _userRepository;
|
||||||
private readonly IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> _tokenDataFactory;
|
|
||||||
|
|
||||||
protected ICurrentContext CurrentContext { get; }
|
protected ICurrentContext CurrentContext { get; }
|
||||||
protected IPolicyService PolicyService { get; }
|
protected IPolicyService PolicyService { get; }
|
||||||
@ -56,18 +45,14 @@ public abstract class BaseRequestValidator<T> where T : class
|
|||||||
IUserService userService,
|
IUserService userService,
|
||||||
IEventService eventService,
|
IEventService eventService,
|
||||||
IDeviceValidator deviceValidator,
|
IDeviceValidator deviceValidator,
|
||||||
IOrganizationDuoWebTokenProvider organizationDuoWebTokenProvider,
|
ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator,
|
||||||
ITemporaryDuoWebV4SDKService duoWebV4SDKService,
|
|
||||||
IOrganizationRepository organizationRepository,
|
|
||||||
IOrganizationUserRepository organizationUserRepository,
|
IOrganizationUserRepository organizationUserRepository,
|
||||||
IApplicationCacheService applicationCacheService,
|
|
||||||
IMailService mailService,
|
IMailService mailService,
|
||||||
ILogger logger,
|
ILogger logger,
|
||||||
ICurrentContext currentContext,
|
ICurrentContext currentContext,
|
||||||
GlobalSettings globalSettings,
|
GlobalSettings globalSettings,
|
||||||
IUserRepository userRepository,
|
IUserRepository userRepository,
|
||||||
IPolicyService policyService,
|
IPolicyService policyService,
|
||||||
IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> tokenDataFactory,
|
|
||||||
IFeatureService featureService,
|
IFeatureService featureService,
|
||||||
ISsoConfigRepository ssoConfigRepository,
|
ISsoConfigRepository ssoConfigRepository,
|
||||||
IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder)
|
IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder)
|
||||||
@ -76,18 +61,14 @@ public abstract class BaseRequestValidator<T> where T : class
|
|||||||
_userService = userService;
|
_userService = userService;
|
||||||
_eventService = eventService;
|
_eventService = eventService;
|
||||||
_deviceValidator = deviceValidator;
|
_deviceValidator = deviceValidator;
|
||||||
_organizationDuoWebTokenProvider = organizationDuoWebTokenProvider;
|
_twoFactorAuthenticationValidator = twoFactorAuthenticationValidator;
|
||||||
_duoWebV4SDKService = duoWebV4SDKService;
|
|
||||||
_organizationRepository = organizationRepository;
|
|
||||||
_organizationUserRepository = organizationUserRepository;
|
_organizationUserRepository = organizationUserRepository;
|
||||||
_applicationCacheService = applicationCacheService;
|
|
||||||
_mailService = mailService;
|
_mailService = mailService;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
CurrentContext = currentContext;
|
CurrentContext = currentContext;
|
||||||
_globalSettings = globalSettings;
|
_globalSettings = globalSettings;
|
||||||
PolicyService = policyService;
|
PolicyService = policyService;
|
||||||
_userRepository = userRepository;
|
_userRepository = userRepository;
|
||||||
_tokenDataFactory = tokenDataFactory;
|
|
||||||
FeatureService = featureService;
|
FeatureService = featureService;
|
||||||
SsoConfigRepository = ssoConfigRepository;
|
SsoConfigRepository = ssoConfigRepository;
|
||||||
UserDecryptionOptionsBuilder = userDecryptionOptionsBuilder;
|
UserDecryptionOptionsBuilder = userDecryptionOptionsBuilder;
|
||||||
@ -104,12 +85,6 @@ public abstract class BaseRequestValidator<T> where T : class
|
|||||||
request.UserName, validatorContext.CaptchaResponse.Score);
|
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 valid = await ValidateContextAsync(context, validatorContext);
|
||||||
var user = validatorContext.User;
|
var user = validatorContext.User;
|
||||||
if (!valid)
|
if (!valid)
|
||||||
@ -123,17 +98,37 @@ public abstract class BaseRequestValidator<T> where T : class
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var (isTwoFactorRequired, twoFactorOrganization) = await RequiresTwoFactorAsync(user, request);
|
var (isTwoFactorRequired, twoFactorOrganization) = await _twoFactorAuthenticationValidator.RequiresTwoFactorAsync(user, request);
|
||||||
|
var twoFactorToken = request.Raw["TwoFactorToken"]?.ToString();
|
||||||
|
var twoFactorProvider = request.Raw["TwoFactorProvider"]?.ToString();
|
||||||
|
var twoFactorRemember = request.Raw["TwoFactorRemember"]?.ToString() == "1";
|
||||||
|
var validTwoFactorRequest = !string.IsNullOrWhiteSpace(twoFactorToken) &&
|
||||||
|
!string.IsNullOrWhiteSpace(twoFactorProvider);
|
||||||
|
|
||||||
if (isTwoFactorRequired)
|
if (isTwoFactorRequired)
|
||||||
{
|
{
|
||||||
if (!twoFactorRequest || !Enum.TryParse(twoFactorProvider, out TwoFactorProviderType twoFactorProviderType))
|
// 2FA required and not provided response
|
||||||
|
if (!validTwoFactorRequest ||
|
||||||
|
!Enum.TryParse(twoFactorProvider, out TwoFactorProviderType twoFactorProviderType))
|
||||||
{
|
{
|
||||||
await BuildTwoFactorResultAsync(user, twoFactorOrganization, context);
|
var resultDict = await _twoFactorAuthenticationValidator
|
||||||
|
.BuildTwoFactorResultAsync(user, twoFactorOrganization);
|
||||||
|
if (resultDict == null)
|
||||||
|
{
|
||||||
|
await BuildErrorResultAsync("No two-step providers enabled.", false, context, user);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var verified = await VerifyTwoFactor(user, twoFactorOrganization,
|
// Include Master Password Policy in 2FA response
|
||||||
twoFactorProviderType, twoFactorToken);
|
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 (!verified || isBot)
|
||||||
{
|
{
|
||||||
if (twoFactorProviderType != TwoFactorProviderType.Remember)
|
if (twoFactorProviderType != TwoFactorProviderType.Remember)
|
||||||
@ -143,16 +138,20 @@ public abstract class BaseRequestValidator<T> where T : class
|
|||||||
}
|
}
|
||||||
else if (twoFactorProviderType == TwoFactorProviderType.Remember)
|
else if (twoFactorProviderType == TwoFactorProviderType.Remember)
|
||||||
{
|
{
|
||||||
await BuildTwoFactorResultAsync(user, twoFactorOrganization, context);
|
var resultDict = await _twoFactorAuthenticationValidator
|
||||||
|
.BuildTwoFactorResultAsync(user, twoFactorOrganization);
|
||||||
|
|
||||||
|
// Include Master Password Policy in 2FA response
|
||||||
|
resultDict.Add("MasterPasswordPolicy", await GetMasterPasswordPolicy(user));
|
||||||
|
SetTwoFactorResult(context, resultDict);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
twoFactorRequest = false;
|
validTwoFactorRequest = false;
|
||||||
twoFactorRemember = false;
|
twoFactorRemember = false;
|
||||||
twoFactorToken = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Force legacy users to the web for migration
|
// Force legacy users to the web for migration
|
||||||
@ -165,7 +164,6 @@ public abstract class BaseRequestValidator<T> where T : class
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns true if can finish validation process
|
|
||||||
if (await IsValidAuthTypeAsync(user, request.GrantType))
|
if (await IsValidAuthTypeAsync(user, request.GrantType))
|
||||||
{
|
{
|
||||||
var device = await _deviceValidator.SaveDeviceAsync(user, request);
|
var device = await _deviceValidator.SaveDeviceAsync(user, request);
|
||||||
@ -174,8 +172,7 @@ public abstract class BaseRequestValidator<T> where T : class
|
|||||||
await BuildErrorResultAsync("No device information provided.", false, context, user);
|
await BuildErrorResultAsync("No device information provided.", false, context, user);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
await BuildSuccessResultAsync(user, context, device, validTwoFactorRequest && twoFactorRemember);
|
||||||
await BuildSuccessResultAsync(user, context, device, twoFactorRequest && twoFactorRemember);
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -238,67 +235,6 @@ public abstract class BaseRequestValidator<T> where T : class
|
|||||||
await SetSuccessResult(context, user, claims, customResponse);
|
await SetSuccessResult(context, user, claims, customResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async Task BuildTwoFactorResultAsync(User user, Organization organization, T context)
|
|
||||||
{
|
|
||||||
var providerKeys = new List<byte>();
|
|
||||||
var providers = new Dictionary<string, Dictionary<string, object>>();
|
|
||||||
|
|
||||||
var enabledProviders = new List<KeyValuePair<TwoFactorProviderType, TwoFactorProvider>>();
|
|
||||||
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<string, object>
|
|
||||||
{
|
|
||||||
{ "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)
|
protected async Task BuildErrorResultAsync(string message, bool twoFactorRequest, T context, User user)
|
||||||
{
|
{
|
||||||
if (user != null)
|
if (user != null)
|
||||||
@ -329,35 +265,13 @@ public abstract class BaseRequestValidator<T> where T : class
|
|||||||
protected abstract void SetErrorResult(T context, Dictionary<string, object> customResponse);
|
protected abstract void SetErrorResult(T context, Dictionary<string, object> customResponse);
|
||||||
protected abstract ClaimsPrincipal GetSubject(T context);
|
protected abstract ClaimsPrincipal GetSubject(T context);
|
||||||
|
|
||||||
protected virtual async Task<Tuple<bool, Organization>> RequiresTwoFactorAsync(User user, ValidatedTokenRequest request)
|
/// <summary>
|
||||||
{
|
/// Check if the user is required to authenticate via SSO. If the user requires SSO, but they are
|
||||||
if (request.GrantType == "client_credentials")
|
/// logging in using an API Key (client_credentials) then they are allowed to bypass the SSO requirement.
|
||||||
{
|
/// </summary>
|
||||||
// Do not require MFA for api key logins
|
/// <param name="user">user trying to login</param>
|
||||||
return new Tuple<bool, Organization>(false, null);
|
/// <param name="grantType">magic string identifying the grant type requested</param>
|
||||||
}
|
/// <returns></returns>
|
||||||
|
|
||||||
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<bool, Organization>(individualRequired || firstEnabledOrg != null, firstEnabledOrg);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<bool> IsValidAuthTypeAsync(User user, string grantType)
|
private async Task<bool> IsValidAuthTypeAsync(User user, string grantType)
|
||||||
{
|
{
|
||||||
if (grantType == "authorization_code" || grantType == "client_credentials")
|
if (grantType == "authorization_code" || grantType == "client_credentials")
|
||||||
@ -367,7 +281,6 @@ public abstract class BaseRequestValidator<T> where T : class
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Check if user belongs to any organization with an active SSO policy
|
// Check if user belongs to any organization with an active SSO policy
|
||||||
var anySsoPoliciesApplicableToUser = await PolicyService.AnyPoliciesApplicableToUserAsync(user.Id, PolicyType.RequireSso, OrganizationUserStatusType.Confirmed);
|
var anySsoPoliciesApplicableToUser = await PolicyService.AnyPoliciesApplicableToUserAsync(user.Id, PolicyType.RequireSso, OrganizationUserStatusType.Confirmed);
|
||||||
if (anySsoPoliciesApplicableToUser)
|
if (anySsoPoliciesApplicableToUser)
|
||||||
@ -379,134 +292,6 @@ public abstract class BaseRequestValidator<T> where T : class
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool OrgUsing2fa(IDictionary<Guid, OrganizationAbility> orgAbilities, Guid orgId)
|
|
||||||
{
|
|
||||||
return orgAbilities != null && orgAbilities.ContainsKey(orgId) &&
|
|
||||||
orgAbilities[orgId].Enabled && orgAbilities[orgId].Using2fa;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<bool> 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<Dictionary<string, object>> 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<string, object>
|
|
||||||
{
|
|
||||||
["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<Dictionary<string, object>>(token);
|
|
||||||
}
|
|
||||||
else if (type == TwoFactorProviderType.Email)
|
|
||||||
{
|
|
||||||
var twoFactorEmail = (string)provider.MetaData["Email"];
|
|
||||||
var redactedEmail = CoreHelpers.RedactEmailAddress(twoFactorEmail);
|
|
||||||
return new Dictionary<string, object> { ["Email"] = redactedEmail };
|
|
||||||
}
|
|
||||||
else if (type == TwoFactorProviderType.YubiKey)
|
|
||||||
{
|
|
||||||
return new Dictionary<string, object> { ["Nfc"] = (bool)provider.MetaData["Nfc"] };
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
case TwoFactorProviderType.OrganizationDuo:
|
|
||||||
if (await _organizationDuoWebTokenProvider.CanGenerateTwoFactorTokenAsync(organization))
|
|
||||||
{
|
|
||||||
var duoResponse = new Dictionary<string, object>
|
|
||||||
{
|
|
||||||
["Host"] = provider.MetaData["Host"],
|
|
||||||
["AuthUrl"] = await _duoWebV4SDKService.GenerateAsync(provider, user),
|
|
||||||
};
|
|
||||||
|
|
||||||
return duoResponse;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task ResetFailedAuthDetailsAsync(User user)
|
private async Task ResetFailedAuthDetailsAsync(User user)
|
||||||
{
|
{
|
||||||
// Early escape if db hit not necessary
|
// Early escape if db hit not necessary
|
@ -1,9 +1,7 @@
|
|||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using Bit.Core.AdminConsole.Services;
|
using Bit.Core.AdminConsole.Services;
|
||||||
using Bit.Core.Auth.Identity;
|
|
||||||
using Bit.Core.Auth.Models.Api.Response;
|
using Bit.Core.Auth.Models.Api.Response;
|
||||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
|
||||||
using Bit.Core.Auth.Repositories;
|
using Bit.Core.Auth.Repositories;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
@ -11,7 +9,6 @@ using Bit.Core.IdentityServer;
|
|||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Settings;
|
using Bit.Core.Settings;
|
||||||
using Bit.Core.Tokens;
|
|
||||||
using Duende.IdentityServer.Extensions;
|
using Duende.IdentityServer.Extensions;
|
||||||
using Duende.IdentityServer.Validation;
|
using Duende.IdentityServer.Validation;
|
||||||
using HandlebarsDotNet;
|
using HandlebarsDotNet;
|
||||||
@ -20,7 +17,7 @@ using Microsoft.AspNetCore.Identity;
|
|||||||
|
|
||||||
#nullable enable
|
#nullable enable
|
||||||
|
|
||||||
namespace Bit.Identity.IdentityServer;
|
namespace Bit.Identity.IdentityServer.RequestValidators;
|
||||||
|
|
||||||
public class CustomTokenRequestValidator : BaseRequestValidator<CustomTokenRequestValidationContext>,
|
public class CustomTokenRequestValidator : BaseRequestValidator<CustomTokenRequestValidationContext>,
|
||||||
ICustomTokenRequestValidator
|
ICustomTokenRequestValidator
|
||||||
@ -29,28 +26,36 @@ public class CustomTokenRequestValidator : BaseRequestValidator<CustomTokenReque
|
|||||||
|
|
||||||
public CustomTokenRequestValidator(
|
public CustomTokenRequestValidator(
|
||||||
UserManager<User> userManager,
|
UserManager<User> userManager,
|
||||||
IDeviceValidator deviceValidator,
|
|
||||||
IUserService userService,
|
IUserService userService,
|
||||||
IEventService eventService,
|
IEventService eventService,
|
||||||
IOrganizationDuoWebTokenProvider organizationDuoWebTokenProvider,
|
IDeviceValidator deviceValidator,
|
||||||
ITemporaryDuoWebV4SDKService duoWebV4SDKService,
|
ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator,
|
||||||
IOrganizationRepository organizationRepository,
|
|
||||||
IOrganizationUserRepository organizationUserRepository,
|
IOrganizationUserRepository organizationUserRepository,
|
||||||
IApplicationCacheService applicationCacheService,
|
|
||||||
IMailService mailService,
|
IMailService mailService,
|
||||||
ILogger<CustomTokenRequestValidator> logger,
|
ILogger<CustomTokenRequestValidator> logger,
|
||||||
ICurrentContext currentContext,
|
ICurrentContext currentContext,
|
||||||
GlobalSettings globalSettings,
|
GlobalSettings globalSettings,
|
||||||
ISsoConfigRepository ssoConfigRepository,
|
|
||||||
IUserRepository userRepository,
|
IUserRepository userRepository,
|
||||||
IPolicyService policyService,
|
IPolicyService policyService,
|
||||||
IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> tokenDataFactory,
|
|
||||||
IFeatureService featureService,
|
IFeatureService featureService,
|
||||||
IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder)
|
ISsoConfigRepository ssoConfigRepository,
|
||||||
: base(userManager, userService, eventService, deviceValidator,
|
IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder
|
||||||
organizationDuoWebTokenProvider, duoWebV4SDKService, organizationRepository, organizationUserRepository,
|
)
|
||||||
applicationCacheService, mailService, logger, currentContext, globalSettings,
|
: base(
|
||||||
userRepository, policyService, tokenDataFactory, featureService, ssoConfigRepository,
|
userManager,
|
||||||
|
userService,
|
||||||
|
eventService,
|
||||||
|
deviceValidator,
|
||||||
|
twoFactorAuthenticationValidator,
|
||||||
|
organizationUserRepository,
|
||||||
|
mailService,
|
||||||
|
logger,
|
||||||
|
currentContext,
|
||||||
|
globalSettings,
|
||||||
|
userRepository,
|
||||||
|
policyService,
|
||||||
|
featureService,
|
||||||
|
ssoConfigRepository,
|
||||||
userDecryptionOptionsBuilder)
|
userDecryptionOptionsBuilder)
|
||||||
{
|
{
|
||||||
_userManager = userManager;
|
_userManager = userManager;
|
||||||
@ -70,7 +75,7 @@ public class CustomTokenRequestValidator : BaseRequestValidator<CustomTokenReque
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
string[] allowedGrantTypes = { "authorization_code", "client_credentials" };
|
string[] allowedGrantTypes = ["authorization_code", "client_credentials"];
|
||||||
if (!allowedGrantTypes.Contains(context.Result.ValidatedRequest.GrantType)
|
if (!allowedGrantTypes.Contains(context.Result.ValidatedRequest.GrantType)
|
||||||
|| context.Result.ValidatedRequest.ClientId.StartsWith("organization")
|
|| context.Result.ValidatedRequest.ClientId.StartsWith("organization")
|
||||||
|| context.Result.ValidatedRequest.ClientId.StartsWith("installation")
|
|| context.Result.ValidatedRequest.ClientId.StartsWith("installation")
|
@ -8,7 +8,7 @@ using Bit.Core.Services;
|
|||||||
using Bit.Core.Settings;
|
using Bit.Core.Settings;
|
||||||
using Duende.IdentityServer.Validation;
|
using Duende.IdentityServer.Validation;
|
||||||
|
|
||||||
namespace Bit.Identity.IdentityServer;
|
namespace Bit.Identity.IdentityServer.RequestValidators;
|
||||||
|
|
||||||
public interface IDeviceValidator
|
public interface IDeviceValidator
|
||||||
{
|
{
|
||||||
@ -41,6 +41,12 @@ public class DeviceValidator(
|
|||||||
private readonly IMailService _mailService = mailService;
|
private readonly IMailService _mailService = mailService;
|
||||||
private readonly ICurrentContext _currentContext = currentContext;
|
private readonly ICurrentContext _currentContext = currentContext;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Save a device to the database. If the device is already known, it will be returned.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="user">The user is assumed NOT null, still going to check though</param>
|
||||||
|
/// <param name="request">Duende Validated Request that contains the data to create the device object</param>
|
||||||
|
/// <returns>Returns null if user or device is malformed; The existing device if already in DB; a new device login</returns>
|
||||||
public async Task<Device> SaveDeviceAsync(User user, ValidatedTokenRequest request)
|
public async Task<Device> SaveDeviceAsync(User user, ValidatedTokenRequest request)
|
||||||
{
|
{
|
||||||
var device = GetDeviceFromRequest(request);
|
var device = GetDeviceFromRequest(request);
|
@ -1,8 +1,6 @@
|
|||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using Bit.Core;
|
using Bit.Core;
|
||||||
using Bit.Core.AdminConsole.Services;
|
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.Repositories;
|
||||||
using Bit.Core.Auth.Services;
|
using Bit.Core.Auth.Services;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
@ -10,13 +8,12 @@ using Bit.Core.Entities;
|
|||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Settings;
|
using Bit.Core.Settings;
|
||||||
using Bit.Core.Tokens;
|
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Duende.IdentityServer.Models;
|
using Duende.IdentityServer.Models;
|
||||||
using Duende.IdentityServer.Validation;
|
using Duende.IdentityServer.Validation;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
|
||||||
namespace Bit.Identity.IdentityServer;
|
namespace Bit.Identity.IdentityServer.RequestValidators;
|
||||||
|
|
||||||
public class ResourceOwnerPasswordValidator : BaseRequestValidator<ResourceOwnerPasswordValidationContext>,
|
public class ResourceOwnerPasswordValidator : BaseRequestValidator<ResourceOwnerPasswordValidationContext>,
|
||||||
IResourceOwnerPasswordValidator
|
IResourceOwnerPasswordValidator
|
||||||
@ -31,11 +28,8 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator<ResourceOwner
|
|||||||
IUserService userService,
|
IUserService userService,
|
||||||
IEventService eventService,
|
IEventService eventService,
|
||||||
IDeviceValidator deviceValidator,
|
IDeviceValidator deviceValidator,
|
||||||
IOrganizationDuoWebTokenProvider organizationDuoWebTokenProvider,
|
ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator,
|
||||||
ITemporaryDuoWebV4SDKService duoWebV4SDKService,
|
|
||||||
IOrganizationRepository organizationRepository,
|
|
||||||
IOrganizationUserRepository organizationUserRepository,
|
IOrganizationUserRepository organizationUserRepository,
|
||||||
IApplicationCacheService applicationCacheService,
|
|
||||||
IMailService mailService,
|
IMailService mailService,
|
||||||
ILogger<ResourceOwnerPasswordValidator> logger,
|
ILogger<ResourceOwnerPasswordValidator> logger,
|
||||||
ICurrentContext currentContext,
|
ICurrentContext currentContext,
|
||||||
@ -44,14 +38,25 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator<ResourceOwner
|
|||||||
IAuthRequestRepository authRequestRepository,
|
IAuthRequestRepository authRequestRepository,
|
||||||
IUserRepository userRepository,
|
IUserRepository userRepository,
|
||||||
IPolicyService policyService,
|
IPolicyService policyService,
|
||||||
IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> tokenDataFactory,
|
|
||||||
IFeatureService featureService,
|
IFeatureService featureService,
|
||||||
ISsoConfigRepository ssoConfigRepository,
|
ISsoConfigRepository ssoConfigRepository,
|
||||||
IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder)
|
IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder)
|
||||||
: base(userManager, userService, eventService, deviceValidator,
|
: base(
|
||||||
organizationDuoWebTokenProvider, duoWebV4SDKService, organizationRepository, organizationUserRepository,
|
userManager,
|
||||||
applicationCacheService, mailService, logger, currentContext, globalSettings, userRepository, policyService,
|
userService,
|
||||||
tokenDataFactory, featureService, ssoConfigRepository, userDecryptionOptionsBuilder)
|
eventService,
|
||||||
|
deviceValidator,
|
||||||
|
twoFactorAuthenticationValidator,
|
||||||
|
organizationUserRepository,
|
||||||
|
mailService,
|
||||||
|
logger,
|
||||||
|
currentContext,
|
||||||
|
globalSettings,
|
||||||
|
userRepository,
|
||||||
|
policyService,
|
||||||
|
featureService,
|
||||||
|
ssoConfigRepository,
|
||||||
|
userDecryptionOptionsBuilder)
|
||||||
{
|
{
|
||||||
_userManager = userManager;
|
_userManager = userManager;
|
||||||
_currentContext = currentContext;
|
_currentContext = currentContext;
|
@ -0,0 +1,297 @@
|
|||||||
|
|
||||||
|
using System.Text.Json;
|
||||||
|
using Bit.Core;
|
||||||
|
using Bit.Core.AdminConsole.Entities;
|
||||||
|
using Bit.Core.Auth.Enums;
|
||||||
|
using Bit.Core.Auth.Identity;
|
||||||
|
using Bit.Core.Auth.Models;
|
||||||
|
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||||
|
using Bit.Core.Context;
|
||||||
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.Models.Data.Organizations;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
|
using Bit.Core.Services;
|
||||||
|
using Bit.Core.Tokens;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
using Duende.IdentityServer.Validation;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
|
||||||
|
namespace Bit.Identity.IdentityServer.RequestValidators;
|
||||||
|
|
||||||
|
public interface ITwoFactorAuthenticationValidator
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="user">the active user for the request</param>
|
||||||
|
/// <param name="request">the request that contains the grant types</param>
|
||||||
|
/// <returns>boolean</returns>
|
||||||
|
Task<Tuple<bool, Organization>> RequiresTwoFactorAsync(User user, ValidatedTokenRequest request);
|
||||||
|
/// <summary>
|
||||||
|
/// Builds the two-factor authentication result for the user based on the available two-factor providers
|
||||||
|
/// from either their user account or Organization.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="user">user trying to login</param>
|
||||||
|
/// <param name="organization">organization associated with the user; Can be null</param>
|
||||||
|
/// <returns>Dictionary with the TwoFactorProviderType as the Key and the Provider Metadata as the Value</returns>
|
||||||
|
Task<Dictionary<string, object>> BuildTwoFactorResultAsync(User user, Organization organization);
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="user">the active User</param>
|
||||||
|
/// <param name="organization">organization of user; can be null</param>
|
||||||
|
/// <param name="twoFactorProviderType">Two Factor Provider to use to verify the token</param>
|
||||||
|
/// <param name="token">secret passed from the user and consumed by the two-factor provider's verify method</param>
|
||||||
|
/// <returns>boolean</returns>
|
||||||
|
Task<bool> VerifyTwoFactor(User user, Organization organization, TwoFactorProviderType twoFactorProviderType, string token);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TwoFactorAuthenticationValidator(
|
||||||
|
IUserService userService,
|
||||||
|
UserManager<User> userManager,
|
||||||
|
IOrganizationDuoWebTokenProvider organizationDuoWebTokenProvider,
|
||||||
|
ITemporaryDuoWebV4SDKService duoWebV4SDKService,
|
||||||
|
IFeatureService featureService,
|
||||||
|
IApplicationCacheService applicationCacheService,
|
||||||
|
IOrganizationUserRepository organizationUserRepository,
|
||||||
|
IOrganizationRepository organizationRepository,
|
||||||
|
IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> ssoEmail2faSessionTokeFactory,
|
||||||
|
ICurrentContext currentContext) : ITwoFactorAuthenticationValidator
|
||||||
|
{
|
||||||
|
private readonly IUserService _userService = userService;
|
||||||
|
private readonly UserManager<User> _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<SsoEmail2faSessionTokenable> _ssoEmail2faSessionTokeFactory = ssoEmail2faSessionTokeFactory;
|
||||||
|
private readonly ICurrentContext _currentContext = currentContext;
|
||||||
|
|
||||||
|
public async Task<Tuple<bool, Organization>> 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<bool, Organization>(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<bool, Organization>(individualRequired || firstEnabledOrg != null, firstEnabledOrg);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Dictionary<string, object>> BuildTwoFactorResultAsync(User user, Organization organization)
|
||||||
|
{
|
||||||
|
var enabledProviders = await GetEnabledTwoFactorProvidersAsync(user, organization);
|
||||||
|
if (enabledProviders.Count == 0)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var providers = new Dictionary<string, Dictionary<string, object>>();
|
||||||
|
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<string, object>
|
||||||
|
{
|
||||||
|
{ "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<bool> 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<List<KeyValuePair<TwoFactorProviderType, TwoFactorProvider>>> GetEnabledTwoFactorProvidersAsync(
|
||||||
|
User user, Organization organization)
|
||||||
|
{
|
||||||
|
var enabledProviders = new List<KeyValuePair<TwoFactorProviderType, TwoFactorProvider>>();
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Builds the parameters for the two-factor authentication
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="organization">We need the organization for Organization Duo Provider type</param>
|
||||||
|
/// <param name="user">The user for which the token is being generated</param>
|
||||||
|
/// <param name="type">Provider Type</param>
|
||||||
|
/// <param name="provider">Raw data that is used to create the response</param>
|
||||||
|
/// <returns>a dictionary with the correct provider configuration or null if the provider is not configured properly</returns>
|
||||||
|
private async Task<Dictionary<string, object>> 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<string, object>();
|
||||||
|
|
||||||
|
// 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<Dictionary<string, object>>(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<Guid, OrganizationAbility> orgAbilities, Guid orgId)
|
||||||
|
{
|
||||||
|
return orgAbilities != null && orgAbilities.ContainsKey(orgId) &&
|
||||||
|
orgAbilities[orgId].Enabled && orgAbilities[orgId].Using2fa;
|
||||||
|
}
|
||||||
|
}
|
@ -1,10 +1,8 @@
|
|||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using Bit.Core;
|
using Bit.Core;
|
||||||
using Bit.Core.AdminConsole.Entities;
|
|
||||||
using Bit.Core.AdminConsole.Services;
|
using Bit.Core.AdminConsole.Services;
|
||||||
using Bit.Core.Auth.Enums;
|
using Bit.Core.Auth.Enums;
|
||||||
using Bit.Core.Auth.Identity;
|
|
||||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||||
using Bit.Core.Auth.Repositories;
|
using Bit.Core.Auth.Repositories;
|
||||||
using Bit.Core.Auth.UserFeatures.WebAuthnLogin;
|
using Bit.Core.Auth.UserFeatures.WebAuthnLogin;
|
||||||
@ -19,7 +17,7 @@ using Duende.IdentityServer.Validation;
|
|||||||
using Fido2NetLib;
|
using Fido2NetLib;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
|
||||||
namespace Bit.Identity.IdentityServer;
|
namespace Bit.Identity.IdentityServer.RequestValidators;
|
||||||
|
|
||||||
public class WebAuthnGrantValidator : BaseRequestValidator<ExtensionGrantValidationContext>, IExtensionGrantValidator
|
public class WebAuthnGrantValidator : BaseRequestValidator<ExtensionGrantValidationContext>, IExtensionGrantValidator
|
||||||
{
|
{
|
||||||
@ -34,11 +32,8 @@ public class WebAuthnGrantValidator : BaseRequestValidator<ExtensionGrantValidat
|
|||||||
IUserService userService,
|
IUserService userService,
|
||||||
IEventService eventService,
|
IEventService eventService,
|
||||||
IDeviceValidator deviceValidator,
|
IDeviceValidator deviceValidator,
|
||||||
IOrganizationDuoWebTokenProvider organizationDuoWebTokenProvider,
|
ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator,
|
||||||
ITemporaryDuoWebV4SDKService duoWebV4SDKService,
|
|
||||||
IOrganizationRepository organizationRepository,
|
|
||||||
IOrganizationUserRepository organizationUserRepository,
|
IOrganizationUserRepository organizationUserRepository,
|
||||||
IApplicationCacheService applicationCacheService,
|
|
||||||
IMailService mailService,
|
IMailService mailService,
|
||||||
ILogger<CustomTokenRequestValidator> logger,
|
ILogger<CustomTokenRequestValidator> logger,
|
||||||
ICurrentContext currentContext,
|
ICurrentContext currentContext,
|
||||||
@ -46,16 +41,27 @@ public class WebAuthnGrantValidator : BaseRequestValidator<ExtensionGrantValidat
|
|||||||
ISsoConfigRepository ssoConfigRepository,
|
ISsoConfigRepository ssoConfigRepository,
|
||||||
IUserRepository userRepository,
|
IUserRepository userRepository,
|
||||||
IPolicyService policyService,
|
IPolicyService policyService,
|
||||||
IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> tokenDataFactory,
|
|
||||||
IDataProtectorTokenFactory<WebAuthnLoginAssertionOptionsTokenable> assertionOptionsDataProtector,
|
IDataProtectorTokenFactory<WebAuthnLoginAssertionOptionsTokenable> assertionOptionsDataProtector,
|
||||||
IFeatureService featureService,
|
IFeatureService featureService,
|
||||||
IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder,
|
IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder,
|
||||||
IAssertWebAuthnLoginCredentialCommand assertWebAuthnLoginCredentialCommand
|
IAssertWebAuthnLoginCredentialCommand assertWebAuthnLoginCredentialCommand
|
||||||
)
|
)
|
||||||
: base(userManager, userService, eventService, deviceValidator,
|
: base(
|
||||||
organizationDuoWebTokenProvider, duoWebV4SDKService, organizationRepository, organizationUserRepository,
|
userManager,
|
||||||
applicationCacheService, mailService, logger, currentContext, globalSettings,
|
userService,
|
||||||
userRepository, policyService, tokenDataFactory, featureService, ssoConfigRepository, userDecryptionOptionsBuilder)
|
eventService,
|
||||||
|
deviceValidator,
|
||||||
|
twoFactorAuthenticationValidator,
|
||||||
|
organizationUserRepository,
|
||||||
|
mailService,
|
||||||
|
logger,
|
||||||
|
currentContext,
|
||||||
|
globalSettings,
|
||||||
|
userRepository,
|
||||||
|
policyService,
|
||||||
|
featureService,
|
||||||
|
ssoConfigRepository,
|
||||||
|
userDecryptionOptionsBuilder)
|
||||||
{
|
{
|
||||||
_assertionOptionsDataProtector = assertionOptionsDataProtector;
|
_assertionOptionsDataProtector = assertionOptionsDataProtector;
|
||||||
_assertWebAuthnLoginCredentialCommand = assertWebAuthnLoginCredentialCommand;
|
_assertWebAuthnLoginCredentialCommand = assertWebAuthnLoginCredentialCommand;
|
||||||
@ -122,12 +128,6 @@ public class WebAuthnGrantValidator : BaseRequestValidator<ExtensionGrantValidat
|
|||||||
return context.Result.Subject;
|
return context.Result.Subject;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override Task<Tuple<bool, Organization>> 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<bool, Organization>(false, null));
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void SetTwoFactorResult(ExtensionGrantValidationContext context,
|
protected override void SetTwoFactorResult(ExtensionGrantValidationContext context,
|
||||||
Dictionary<string, object> customResponse)
|
Dictionary<string, object> customResponse)
|
||||||
{
|
{
|
@ -3,6 +3,7 @@ using Bit.Core.IdentityServer;
|
|||||||
using Bit.Core.Settings;
|
using Bit.Core.Settings;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Bit.Identity.IdentityServer;
|
using Bit.Identity.IdentityServer;
|
||||||
|
using Bit.Identity.IdentityServer.RequestValidators;
|
||||||
using Bit.SharedWeb.Utilities;
|
using Bit.SharedWeb.Utilities;
|
||||||
using Duende.IdentityServer.ResponseHandling;
|
using Duende.IdentityServer.ResponseHandling;
|
||||||
using Duende.IdentityServer.Services;
|
using Duende.IdentityServer.Services;
|
||||||
@ -21,6 +22,7 @@ public static class ServiceCollectionExtensions
|
|||||||
services.AddTransient<IAuthorizationCodeStore, AuthorizationCodeStore>();
|
services.AddTransient<IAuthorizationCodeStore, AuthorizationCodeStore>();
|
||||||
services.AddTransient<IUserDecryptionOptionsBuilder, UserDecryptionOptionsBuilder>();
|
services.AddTransient<IUserDecryptionOptionsBuilder, UserDecryptionOptionsBuilder>();
|
||||||
services.AddTransient<IDeviceValidator, DeviceValidator>();
|
services.AddTransient<IDeviceValidator, DeviceValidator>();
|
||||||
|
services.AddTransient<ITwoFactorAuthenticationValidator, TwoFactorAuthenticationValidator>();
|
||||||
|
|
||||||
var issuerUri = new Uri(globalSettings.BaseServiceUri.InternalIdentity);
|
var issuerUri = new Uri(globalSettings.BaseServiceUri.InternalIdentity);
|
||||||
var identityServerBuilder = services
|
var identityServerBuilder = services
|
||||||
|
@ -196,8 +196,7 @@ public class OrganizationUserRepository : Repository<OrganizationUser, Guid>, IO
|
|||||||
return results.SingleOrDefault();
|
return results.SingleOrDefault();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
public async Task<Tuple<OrganizationUserUserDetails?, ICollection<CollectionAccessSelection>>>
|
public async Task<(OrganizationUserUserDetails? OrganizationUser, ICollection<CollectionAccessSelection> Collections)> GetDetailsByIdWithCollectionsAsync(Guid id)
|
||||||
GetDetailsByIdWithCollectionsAsync(Guid id)
|
|
||||||
{
|
{
|
||||||
using (var connection = new SqlConnection(ConnectionString))
|
using (var connection = new SqlConnection(ConnectionString))
|
||||||
{
|
{
|
||||||
@ -206,9 +205,9 @@ public class OrganizationUserRepository : Repository<OrganizationUser, Guid>, IO
|
|||||||
new { Id = id },
|
new { Id = id },
|
||||||
commandType: CommandType.StoredProcedure);
|
commandType: CommandType.StoredProcedure);
|
||||||
|
|
||||||
var user = (await results.ReadAsync<OrganizationUserUserDetails>()).SingleOrDefault();
|
var organizationUserUserDetails = (await results.ReadAsync<OrganizationUserUserDetails>()).SingleOrDefault();
|
||||||
var collections = (await results.ReadAsync<CollectionAccessSelection>()).ToList();
|
var collections = (await results.ReadAsync<CollectionAccessSelection>()).ToList();
|
||||||
return new Tuple<OrganizationUserUserDetails?, ICollection<CollectionAccessSelection>>(user, collections);
|
return (organizationUserUserDetails, collections);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -248,7 +248,7 @@ public class OrganizationUserRepository : Repository<Core.Entities.OrganizationU
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Tuple<OrganizationUserUserDetails, ICollection<CollectionAccessSelection>>> GetDetailsByIdWithCollectionsAsync(Guid id)
|
public async Task<(OrganizationUserUserDetails? OrganizationUser, ICollection<CollectionAccessSelection> Collections)> GetDetailsByIdWithCollectionsAsync(Guid id)
|
||||||
{
|
{
|
||||||
var organizationUserUserDetails = await GetDetailsByIdAsync(id);
|
var organizationUserUserDetails = await GetDetailsByIdAsync(id);
|
||||||
using (var scope = ServiceScopeFactory.CreateScope())
|
using (var scope = ServiceScopeFactory.CreateScope())
|
||||||
@ -265,7 +265,7 @@ public class OrganizationUserRepository : Repository<Core.Entities.OrganizationU
|
|||||||
HidePasswords = cu.HidePasswords,
|
HidePasswords = cu.HidePasswords,
|
||||||
Manage = cu.Manage
|
Manage = cu.Manage
|
||||||
}).ToListAsync();
|
}).ToListAsync();
|
||||||
return new Tuple<OrganizationUserUserDetails, ICollection<CollectionAccessSelection>>(organizationUserUserDetails, collections);
|
return (organizationUserUserDetails, collections);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,6 +21,10 @@ public class DeviceEntityTypeConfiguration : IEntityTypeConfiguration<Device>
|
|||||||
.HasIndex(d => d.Identifier)
|
.HasIndex(d => d.Identifier)
|
||||||
.IsClustered(false);
|
.IsClustered(false);
|
||||||
|
|
||||||
|
builder.Property(c => c.Active)
|
||||||
|
.ValueGeneratedNever()
|
||||||
|
.HasDefaultValue(true);
|
||||||
|
|
||||||
builder.ToTable(nameof(Device));
|
builder.ToTable(nameof(Device));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="6.8.1" />
|
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="6.9.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
@ -274,6 +274,11 @@ public static class ServiceCollectionExtensions
|
|||||||
services.AddKeyedSingleton<IPushNotificationService, RelayPushNotificationService>("implementation");
|
services.AddKeyedSingleton<IPushNotificationService, RelayPushNotificationService>("implementation");
|
||||||
services.AddSingleton<IPushRegistrationService, RelayPushRegistrationService>();
|
services.AddSingleton<IPushRegistrationService, RelayPushRegistrationService>();
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
services.AddSingleton<IPushRegistrationService, NoopPushRegistrationService>();
|
||||||
|
}
|
||||||
|
|
||||||
if (CoreHelpers.SettingHasValue(globalSettings.InternalIdentityKey) &&
|
if (CoreHelpers.SettingHasValue(globalSettings.InternalIdentityKey) &&
|
||||||
CoreHelpers.SettingHasValue(globalSettings.BaseServiceUri.InternalNotifications))
|
CoreHelpers.SettingHasValue(globalSettings.BaseServiceUri.InternalNotifications))
|
||||||
{
|
{
|
||||||
@ -290,10 +295,6 @@ public static class ServiceCollectionExtensions
|
|||||||
services.AddKeyedSingleton<IPushNotificationService, AzureQueuePushNotificationService>("implementation");
|
services.AddKeyedSingleton<IPushNotificationService, AzureQueuePushNotificationService>("implementation");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
services.AddSingleton<IPushRegistrationService, NoopPushRegistrationService>();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!globalSettings.SelfHosted && CoreHelpers.SettingHasValue(globalSettings.Mail.ConnectionString))
|
if (!globalSettings.SelfHosted && CoreHelpers.SettingHasValue(globalSettings.Mail.ConnectionString))
|
||||||
{
|
{
|
||||||
|
@ -9,7 +9,8 @@
|
|||||||
@RevisionDate DATETIME2(7),
|
@RevisionDate DATETIME2(7),
|
||||||
@EncryptedUserKey VARCHAR(MAX) = NULL,
|
@EncryptedUserKey VARCHAR(MAX) = NULL,
|
||||||
@EncryptedPublicKey VARCHAR(MAX) = NULL,
|
@EncryptedPublicKey VARCHAR(MAX) = NULL,
|
||||||
@EncryptedPrivateKey VARCHAR(MAX) = NULL
|
@EncryptedPrivateKey VARCHAR(MAX) = NULL,
|
||||||
|
@Active BIT = 1
|
||||||
AS
|
AS
|
||||||
BEGIN
|
BEGIN
|
||||||
SET NOCOUNT ON
|
SET NOCOUNT ON
|
||||||
@ -26,7 +27,8 @@ BEGIN
|
|||||||
[RevisionDate],
|
[RevisionDate],
|
||||||
[EncryptedUserKey],
|
[EncryptedUserKey],
|
||||||
[EncryptedPublicKey],
|
[EncryptedPublicKey],
|
||||||
[EncryptedPrivateKey]
|
[EncryptedPrivateKey],
|
||||||
|
[Active]
|
||||||
)
|
)
|
||||||
VALUES
|
VALUES
|
||||||
(
|
(
|
||||||
@ -40,6 +42,7 @@ BEGIN
|
|||||||
@RevisionDate,
|
@RevisionDate,
|
||||||
@EncryptedUserKey,
|
@EncryptedUserKey,
|
||||||
@EncryptedPublicKey,
|
@EncryptedPublicKey,
|
||||||
@EncryptedPrivateKey
|
@EncryptedPrivateKey,
|
||||||
|
@Active
|
||||||
)
|
)
|
||||||
END
|
END
|
||||||
|
@ -1,12 +0,0 @@
|
|||||||
CREATE PROCEDURE [dbo].[Device_DeleteById]
|
|
||||||
@Id UNIQUEIDENTIFIER
|
|
||||||
AS
|
|
||||||
BEGIN
|
|
||||||
SET NOCOUNT ON
|
|
||||||
|
|
||||||
DELETE
|
|
||||||
FROM
|
|
||||||
[dbo].[Device]
|
|
||||||
WHERE
|
|
||||||
[Id] = @Id
|
|
||||||
END
|
|
@ -9,7 +9,8 @@
|
|||||||
@RevisionDate DATETIME2(7),
|
@RevisionDate DATETIME2(7),
|
||||||
@EncryptedUserKey VARCHAR(MAX) = NULL,
|
@EncryptedUserKey VARCHAR(MAX) = NULL,
|
||||||
@EncryptedPublicKey VARCHAR(MAX) = NULL,
|
@EncryptedPublicKey VARCHAR(MAX) = NULL,
|
||||||
@EncryptedPrivateKey VARCHAR(MAX) = NULL
|
@EncryptedPrivateKey VARCHAR(MAX) = NULL,
|
||||||
|
@Active BIT = 1
|
||||||
AS
|
AS
|
||||||
BEGIN
|
BEGIN
|
||||||
SET NOCOUNT ON
|
SET NOCOUNT ON
|
||||||
@ -26,7 +27,8 @@ BEGIN
|
|||||||
[RevisionDate] = @RevisionDate,
|
[RevisionDate] = @RevisionDate,
|
||||||
[EncryptedUserKey] = @EncryptedUserKey,
|
[EncryptedUserKey] = @EncryptedUserKey,
|
||||||
[EncryptedPublicKey] = @EncryptedPublicKey,
|
[EncryptedPublicKey] = @EncryptedPublicKey,
|
||||||
[EncryptedPrivateKey] = @EncryptedPrivateKey
|
[EncryptedPrivateKey] = @EncryptedPrivateKey,
|
||||||
|
[Active] = @Active
|
||||||
WHERE
|
WHERE
|
||||||
[Id] = @Id
|
[Id] = @Id
|
||||||
END
|
END
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user