1
0
mirror of https://github.com/bitwarden/server.git synced 2025-04-05 21:18:13 -05:00

Merge remote-tracking branch 'origin/PM-16517-Additional-storage-separate-product-personal-use' into PM-16517-Additional-storage-separate-product-personal-use

This commit is contained in:
Jonas Hendrickx 2025-03-05 14:55:02 +01:00
commit 8ceae99b6a
270 changed files with 34605 additions and 2580 deletions

10
.github/renovate.json vendored
View File

@ -12,20 +12,20 @@
{ {
"groupName": "dockerfile minor", "groupName": "dockerfile minor",
"matchManagers": ["dockerfile"], "matchManagers": ["dockerfile"],
"matchUpdateTypes": ["minor", "patch"] "matchUpdateTypes": ["minor"]
}, },
{ {
"groupName": "docker-compose minor", "groupName": "docker-compose minor",
"matchManagers": ["docker-compose"], "matchManagers": ["docker-compose"],
"matchUpdateTypes": ["minor", "patch"] "matchUpdateTypes": ["minor"]
}, },
{ {
"groupName": "gh minor", "groupName": "github-action minor",
"matchManagers": ["github-actions"], "matchManagers": ["github-actions"],
"matchUpdateTypes": ["minor", "patch"] "matchUpdateTypes": ["minor"]
}, },
{ {
"matchManagers": ["github-actions", "dockerfile", "docker-compose"], "matchManagers": ["dockerfile", "docker-compose"],
"commitMessagePrefix": "[deps] BRE:" "commitMessagePrefix": "[deps] BRE:"
}, },
{ {

View File

@ -30,7 +30,7 @@ jobs:
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@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0 uses: actions/setup-dotnet@87b7050bc53ea08284295505d98d2aa94301e852 # v4.2.0
- name: Verify format - name: Verify format
run: dotnet format --verify-no-changes run: dotnet format --verify-no-changes
@ -81,7 +81,7 @@ jobs:
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@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0 uses: actions/setup-dotnet@87b7050bc53ea08284295505d98d2aa94301e852 # v4.2.0
- name: Set up Node - name: Set up Node
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
@ -120,7 +120,7 @@ jobs:
ls -atlh ../../../ ls -atlh ../../../
- name: Upload project artifact - name: Upload project artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
with: with:
name: ${{ matrix.project_name }}.zip name: ${{ matrix.project_name }}.zip
path: ${{ matrix.base_path }}/${{ matrix.project_name }}/${{ matrix.project_name }}.zip path: ${{ matrix.base_path }}/${{ matrix.project_name }}/${{ matrix.project_name }}.zip
@ -278,7 +278,7 @@ jobs:
- name: Build Docker image - name: Build Docker image
id: build-docker id: build-docker
uses: docker/build-push-action@4f58ea79222b3b9dc2c8bbdd6debcef730109a75 # v6.9.0 uses: docker/build-push-action@67a2d409c0a876cbe6b11854e3e25193efe4e62d # v6.12.0
with: with:
context: ${{ matrix.base_path }}/${{ matrix.project_name }} context: ${{ matrix.base_path }}/${{ matrix.project_name }}
file: ${{ matrix.base_path }}/${{ matrix.project_name }}/Dockerfile file: ${{ matrix.base_path }}/${{ matrix.project_name }}/Dockerfile
@ -314,7 +314,7 @@ jobs:
output-format: sarif output-format: sarif
- name: Upload Grype results to GitHub - name: Upload Grype results to GitHub
uses: github/codeql-action/upload-sarif@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0 uses: github/codeql-action/upload-sarif@48ab28a6f5dbc2a99bf1e0131198dd8f1df78169 # v3.28.0
with: with:
sarif_file: ${{ steps.container-scan.outputs.sarif }} sarif_file: ${{ steps.container-scan.outputs.sarif }}
@ -329,7 +329,7 @@ jobs:
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@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0 uses: actions/setup-dotnet@87b7050bc53ea08284295505d98d2aa94301e852 # v4.2.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
@ -393,7 +393,7 @@ jobs:
if: | if: |
github.event_name != 'pull_request_target' github.event_name != 'pull_request_target'
&& (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc') && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc')
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
with: with:
name: docker-stub-US.zip name: docker-stub-US.zip
path: docker-stub-US.zip path: docker-stub-US.zip
@ -403,7 +403,7 @@ jobs:
if: | if: |
github.event_name != 'pull_request_target' github.event_name != 'pull_request_target'
&& (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc') && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc')
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
with: with:
name: docker-stub-EU.zip name: docker-stub-EU.zip
path: docker-stub-EU.zip path: docker-stub-EU.zip
@ -413,7 +413,7 @@ jobs:
if: | if: |
github.event_name != 'pull_request_target' github.event_name != 'pull_request_target'
&& (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc') && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc')
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
with: with:
name: docker-stub-US-sha256.txt name: docker-stub-US-sha256.txt
path: docker-stub-US-sha256.txt path: docker-stub-US-sha256.txt
@ -423,7 +423,7 @@ jobs:
if: | if: |
github.event_name != 'pull_request_target' github.event_name != 'pull_request_target'
&& (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc') && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc')
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
with: with:
name: docker-stub-EU-sha256.txt name: docker-stub-EU-sha256.txt
path: docker-stub-EU-sha256.txt path: docker-stub-EU-sha256.txt
@ -447,7 +447,7 @@ jobs:
GLOBALSETTINGS__SQLSERVER__CONNECTIONSTRING: "placeholder" GLOBALSETTINGS__SQLSERVER__CONNECTIONSTRING: "placeholder"
- name: Upload Public API Swagger artifact - name: Upload Public API Swagger artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
with: with:
name: swagger.json name: swagger.json
path: swagger.json path: swagger.json
@ -481,14 +481,14 @@ jobs:
GLOBALSETTINGS__SQLSERVER__CONNECTIONSTRING: "placeholder" GLOBALSETTINGS__SQLSERVER__CONNECTIONSTRING: "placeholder"
- name: Upload Internal API Swagger artifact - name: Upload Internal API Swagger artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
with: with:
name: internal.json name: internal.json
path: internal.json path: internal.json
if-no-files-found: error if-no-files-found: error
- name: Upload Identity Swagger artifact - name: Upload Identity Swagger artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
with: with:
name: identity.json name: identity.json
path: identity.json path: identity.json
@ -517,7 +517,7 @@ jobs:
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@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0 uses: actions/setup-dotnet@87b7050bc53ea08284295505d98d2aa94301e852 # v4.2.0
- name: Print environment - name: Print environment
run: | run: |
@ -533,7 +533,7 @@ jobs:
- name: Upload project artifact for Windows - name: Upload project artifact for Windows
if: ${{ contains(matrix.target, 'win') == true }} if: ${{ contains(matrix.target, 'win') == true }}
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
with: with:
name: MsSqlMigratorUtility-${{ matrix.target }} name: MsSqlMigratorUtility-${{ matrix.target }}
path: util/MsSqlMigratorUtility/obj/build-output/publish/MsSqlMigratorUtility.exe path: util/MsSqlMigratorUtility/obj/build-output/publish/MsSqlMigratorUtility.exe
@ -541,7 +541,7 @@ jobs:
- name: Upload project artifact - name: Upload project artifact
if: ${{ contains(matrix.target, 'win') == false }} if: ${{ contains(matrix.target, 'win') == false }}
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
with: with:
name: MsSqlMigratorUtility-${{ matrix.target }} name: MsSqlMigratorUtility-${{ matrix.target }}
path: util/MsSqlMigratorUtility/obj/build-output/publish/MsSqlMigratorUtility path: util/MsSqlMigratorUtility/obj/build-output/publish/MsSqlMigratorUtility

View File

@ -37,7 +37,7 @@ jobs:
- name: Collect - name: Collect
id: collect id: collect
uses: launchdarkly/find-code-references-in-pull-request@d008aa4f321d8cd35314d9cb095388dcfde84439 # v2.0.0 uses: launchdarkly/find-code-references-in-pull-request@30f4c4ab2949bbf258b797ced2fbf6dea34df9ce # v2.1.0
with: with:
project-key: default project-key: default
environment-key: dev environment-key: dev

View File

@ -1,33 +1,14 @@
name: Ephemeral environment cleanup name: Ephemeral Environment
on: on:
pull_request: pull_request:
types: [unlabeled] types: [labeled]
jobs: jobs:
validate-pr: trigger-ee-updates:
name: Validate PR name: Trigger Ephemeral Environment updates
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
outputs: if: github.event.label.name == 'ephemeral-environment'
config-exists: ${{ steps.validate-config.outputs.config-exists }}
steps:
- name: Checkout PR
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Validate config exists in path
id: validate-config
run: |
if [[ -f "ephemeral-environments/$GITHUB_HEAD_REF.yaml" ]]; then
echo "Ephemeral environment config found in path, continuing."
echo "config-exists=true" >> $GITHUB_OUTPUT
fi
cleanup-config:
name: Cleanup ephemeral environment
runs-on: ubuntu-24.04
needs: validate-pr
if: ${{ needs.validate-pr.outputs.config-exists }}
steps: steps:
- name: Log in to Azure - CI subscription - name: Log in to Azure - CI subscription
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
@ -41,7 +22,7 @@ jobs:
keyvault: "bitwarden-ci" keyvault: "bitwarden-ci"
secrets: "github-pat-bitwarden-devops-bot-repo-scope" secrets: "github-pat-bitwarden-devops-bot-repo-scope"
- name: Trigger Ephemeral Environment cleanup - name: Trigger Ephemeral Environment update
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
with: with:
github-token: ${{ steps.retrieve-secret-pat.outputs.github-pat-bitwarden-devops-bot-repo-scope }} github-token: ${{ steps.retrieve-secret-pat.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
@ -49,11 +30,9 @@ jobs:
await github.rest.actions.createWorkflowDispatch({ await github.rest.actions.createWorkflowDispatch({
owner: 'bitwarden', owner: 'bitwarden',
repo: 'devops', repo: 'devops',
workflow_id: '_ephemeral_environment_pr_manager.yml', workflow_id: '_update_ephemeral_tags.yml',
ref: 'main', ref: 'main',
inputs: { inputs: {
ephemeral_env_branch: process.env.GITHUB_HEAD_REF, ephemeral_env_branch: process.env.GITHUB_HEAD_REF
cleanup_config: true,
project: 'server'
} }
}) })

View File

@ -85,7 +85,7 @@ jobs:
- name: Create release - name: Create release
if: ${{ inputs.release_type != 'Dry Run' }} if: ${{ inputs.release_type != 'Dry Run' }}
uses: ncipollo/release-action@2c591bcc8ecdcd2db72b97d6147f871fcd833ba5 # v1.14.0 uses: ncipollo/release-action@cdcc88a9acf3ca41c16c37bb7d21b9ad48560d87 # v1.15.0
with: with:
artifacts: "docker-stub-US.zip, artifacts: "docker-stub-US.zip,
docker-stub-US-sha256.txt, docker-stub-US-sha256.txt,

View File

@ -52,7 +52,7 @@ jobs:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- name: Generate GH App token - name: Generate GH App token
uses: actions/create-github-app-token@5d869da34e18e7287c1daad50e0b8ea0f506ce69 # v1.11.0 uses: actions/create-github-app-token@c1a285145b9d317df6ced56c09f525b5c2b6f755 # v1.11.1
id: app-token id: app-token
with: with:
app-id: ${{ secrets.BW_GHAPP_ID }} app-id: ${{ secrets.BW_GHAPP_ID }}
@ -98,7 +98,7 @@ jobs:
version: ${{ inputs.version_number_override }} version: ${{ inputs.version_number_override }}
- name: Generate GH App token - name: Generate GH App token
uses: actions/create-github-app-token@5d869da34e18e7287c1daad50e0b8ea0f506ce69 # v1.11.0 uses: actions/create-github-app-token@c1a285145b9d317df6ced56c09f525b5c2b6f755 # v1.11.1
id: app-token id: app-token
with: with:
app-id: ${{ secrets.BW_GHAPP_ID }} app-id: ${{ secrets.BW_GHAPP_ID }}
@ -197,7 +197,7 @@ jobs:
- setup - setup
steps: steps:
- name: Generate GH App token - name: Generate GH App token
uses: actions/create-github-app-token@5d869da34e18e7287c1daad50e0b8ea0f506ce69 # v1.11.0 uses: actions/create-github-app-token@c1a285145b9d317df6ced56c09f525b5c2b6f755 # v1.11.1
id: app-token id: app-token
with: with:
app-id: ${{ secrets.BW_GHAPP_ID }} app-id: ${{ secrets.BW_GHAPP_ID }}
@ -206,6 +206,7 @@ jobs:
- name: Check out main branch - name: Check out main branch
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with: with:
fetch-depth: 0
ref: main ref: main
token: ${{ steps.app-token.outputs.token }} token: ${{ steps.app-token.outputs.token }}

View File

@ -31,7 +31,7 @@ jobs:
ref: ${{ github.event.pull_request.head.sha }} ref: ${{ github.event.pull_request.head.sha }}
- name: Scan with Checkmarx - name: Scan with Checkmarx
uses: checkmarx/ast-github-action@f0869bd1a37fddc06499a096101e6c900e815d81 # 2.0.36 uses: checkmarx/ast-github-action@184bf2f64f55d1c93fd6636d539edf274703e434 # 2.0.41
env: env:
INCREMENTAL: "${{ contains(github.event_name, 'pull_request') && '--sast-incremental' || '' }}" INCREMENTAL: "${{ contains(github.event_name, 'pull_request') && '--sast-incremental' || '' }}"
with: with:
@ -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@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0 uses: github/codeql-action/upload-sarif@48ab28a6f5dbc2a99bf1e0131198dd8f1df78169 # v3.28.0
with: with:
sarif_file: cx_result.sarif sarif_file: cx_result.sarif
@ -60,7 +60,7 @@ jobs:
steps: steps:
- name: Set up JDK 17 - name: Set up JDK 17
uses: actions/setup-java@8df1039502a15bceb9433410b1a100fbe190c53b # v4.5.0 uses: actions/setup-java@7a6d8a8234af8eb26422e24e3006232cccaa061b # v4.6.0
with: with:
java-version: 17 java-version: 17
distribution: "zulu" distribution: "zulu"
@ -72,7 +72,7 @@ jobs:
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@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0 uses: actions/setup-dotnet@87b7050bc53ea08284295505d98d2aa94301e852 # v4.2.0
- name: Install SonarCloud scanner - name: Install SonarCloud scanner
run: dotnet tool install dotnet-sonarscanner -g run: dotnet tool install dotnet-sonarscanner -g
@ -80,7 +80,6 @@ jobs:
- name: Scan with SonarCloud - name: Scan with SonarCloud
env: env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: | run: |
dotnet-sonarscanner begin /k:"${{ github.repository_owner }}_${{ github.event.repository.name }}" \ dotnet-sonarscanner begin /k:"${{ github.repository_owner }}_${{ github.event.repository.name }}" \
/d:sonar.test.inclusions=test/,bitwarden_license/test/ \ /d:sonar.test.inclusions=test/,bitwarden_license/test/ \

View File

@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
- name: Check - name: Check
uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e # v9.0.0 uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9.1.0
with: with:
stale-issue-label: "needs-reply" stale-issue-label: "needs-reply"
stale-pr-label: "needs-changes" stale-pr-label: "needs-changes"

View File

@ -57,7 +57,7 @@ jobs:
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Set up .NET - name: Set up .NET
uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0 uses: actions/setup-dotnet@87b7050bc53ea08284295505d98d2aa94301e852 # v4.2.0
- name: Restore tools - name: Restore tools
run: dotnet tool restore run: dotnet tool restore
@ -107,7 +107,7 @@ jobs:
run: 'dotnet ef database update --connection "$CONN_STR" -- --GlobalSettings:MySql:ConnectionString="$CONN_STR"' run: 'dotnet ef database update --connection "$CONN_STR" -- --GlobalSettings:MySql:ConnectionString="$CONN_STR"'
env: env:
CONN_STR: "server=localhost;uid=root;pwd=SET_A_PASSWORD_HERE_123;database=vault_dev;Allow User Variables=true" CONN_STR: "server=localhost;uid=root;pwd=SET_A_PASSWORD_HERE_123;database=vault_dev;Allow User Variables=true"
- name: Migrate MariaDB - name: Migrate MariaDB
working-directory: "util/MySqlMigrations" working-directory: "util/MySqlMigrations"
run: 'dotnet ef database update --connection "$CONN_STR" -- --GlobalSettings:MySql:ConnectionString="$CONN_STR"' run: 'dotnet ef database update --connection "$CONN_STR" -- --GlobalSettings:MySql:ConnectionString="$CONN_STR"'
@ -186,7 +186,7 @@ jobs:
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Set up .NET - name: Set up .NET
uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0 uses: actions/setup-dotnet@87b7050bc53ea08284295505d98d2aa94301e852 # v4.2.0
- name: Print environment - name: Print environment
run: | run: |
@ -200,7 +200,7 @@ jobs:
shell: pwsh shell: pwsh
- name: Upload DACPAC - name: Upload DACPAC
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
with: with:
name: sql.dacpac name: sql.dacpac
path: Sql.dacpac path: Sql.dacpac
@ -226,7 +226,7 @@ jobs:
shell: pwsh shell: pwsh
- name: Report validation results - name: Report validation results
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
with: with:
name: report.xml name: report.xml
path: | path: |
@ -237,7 +237,7 @@ jobs:
run: | run: |
if grep -q "<Operations>" "report.xml"; then if grep -q "<Operations>" "report.xml"; then
echo echo
echo "Migrations are out of sync with sqlproj!" echo "Migration files are not in sync with the files in the Sql project. Review to make sure that any stored procedures / other db changes match with the stored procedures in the Sql project."
exit 1 exit 1
else else
echo "Report looks good" echo "Report looks good"

View File

@ -49,7 +49,7 @@ jobs:
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Set up .NET - name: Set up .NET
uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0 uses: actions/setup-dotnet@87b7050bc53ea08284295505d98d2aa94301e852 # v4.2.0
- name: Print environment - name: Print environment
run: | run: |
@ -78,6 +78,3 @@ jobs:
- name: Upload to codecov.io - name: Upload to codecov.io
uses: codecov/codecov-action@1e68e06f1dbfde0e4cefc87efeba9e4643565303 # v5.1.2 uses: codecov/codecov-action@1e68e06f1dbfde0e4cefc87efeba9e4643565303 # v5.1.2
if: ${{ needs.check-test-secrets.outputs.available == 'true' }}
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

18
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,18 @@
{
"recommendations": [
"nick-rudenko.back-n-forth",
"streetsidesoftware.code-spell-checker",
"MS-vsliveshare.vsliveshare",
"mhutchie.git-graph",
"donjayamanne.githistory",
"eamodio.gitlens",
"jakebathman.mysql-syntax",
"ckolkman.vscode-postgres",
"ms-dotnettools.csharp",
"formulahendry.dotnet-test-explorer",
"adrianwilczynski.user-secrets"
]
}

View File

@ -3,7 +3,7 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<Version>2025.1.0</Version> <Version>2025.1.5</Version>
<RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace> <RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
@ -64,4 +64,4 @@
</ItemGroup> </ItemGroup>
</Target> </Target>
</Project> </Project>

View File

@ -352,12 +352,10 @@ public class ProviderBillingService(
throw new BadRequestException("billingTaxIdTypeInferenceError"); throw new BadRequestException("billingTaxIdTypeInferenceError");
} }
customerCreateOptions.TaxIdData = taxInfo.HasTaxId customerCreateOptions.TaxIdData =
? [
[ new CustomerTaxIdDataOptions { Type = taxIdType, Value = taxInfo.TaxIdNumber }
new CustomerTaxIdDataOptions { Type = taxIdType, Value = taxInfo.TaxIdNumber } ];
]
: null;
} }
try try

View File

@ -5,7 +5,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="CsvHelper" Version="32.0.3" /> <PackageReference Include="CsvHelper" Version="33.0.1" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -779,9 +779,9 @@
} }
}, },
"node_modules/browserslist": { "node_modules/browserslist": {
"version": "4.24.2", "version": "4.24.3",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.2.tgz", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.3.tgz",
"integrity": "sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==", "integrity": "sha512-1CPmv8iobE2fyRMV97dAcMVegvvWKxmq94hkLiAkUGwKVTyDLw33K+ZxiFrREKmmps4rIw6grcCFCnTMSZ/YiA==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@ -799,9 +799,9 @@
], ],
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"caniuse-lite": "^1.0.30001669", "caniuse-lite": "^1.0.30001688",
"electron-to-chromium": "^1.5.41", "electron-to-chromium": "^1.5.73",
"node-releases": "^2.0.18", "node-releases": "^2.0.19",
"update-browserslist-db": "^1.1.1" "update-browserslist-db": "^1.1.1"
}, },
"bin": { "bin": {
@ -819,9 +819,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/caniuse-lite": { "node_modules/caniuse-lite": {
"version": "1.0.30001688", "version": "1.0.30001690",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001688.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001690.tgz",
"integrity": "sha512-Nmqpru91cuABu/DTCXbM2NSRHzM2uVHfPnhJ/1zEAJx/ILBRVmz3pzH4N7DZqbdG0gWClsCC05Oj0mJ/1AWMbA==", "integrity": "sha512-5ExiE3qQN6oF8Clf8ifIDcMRCRE/dMGcETG/XGMD8/XiXm6HXQgQTh1yZYLXXpSOsEUlJm1Xr7kGULZTuGtP/w==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@ -840,9 +840,9 @@
"license": "CC-BY-4.0" "license": "CC-BY-4.0"
}, },
"node_modules/chokidar": { "node_modules/chokidar": {
"version": "4.0.1", "version": "4.0.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
"integrity": "sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==", "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -972,16 +972,16 @@
} }
}, },
"node_modules/electron-to-chromium": { "node_modules/electron-to-chromium": {
"version": "1.5.73", "version": "1.5.75",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.73.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.75.tgz",
"integrity": "sha512-8wGNxG9tAG5KhGd3eeA0o6ixhiNdgr0DcHWm85XPCphwZgD1lIEoi6t3VERayWao7SF7AAZTw6oARGJeVjH8Kg==", "integrity": "sha512-Lf3++DumRE/QmweGjU+ZcKqQ+3bKkU/qjaKYhIJKEOhgIO9Xs6IiAQFkfFoj+RhgDk4LUeNsLo6plExHqSyu6Q==",
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/enhanced-resolve": { "node_modules/enhanced-resolve": {
"version": "5.17.1", "version": "5.18.0",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.0.tgz",
"integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==", "integrity": "sha512-0/r0MySGYG8YqlayBZ6MuCfECmHFdJ5qyPh8s8wa5Hnm6SaFLSK1VYCbj+NKp090Nm1caZhD+QTnmxO7esYGyQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -1271,9 +1271,9 @@
} }
}, },
"node_modules/is-core-module": { "node_modules/is-core-module": {
"version": "2.15.1", "version": "2.16.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
"integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -1792,19 +1792,22 @@
} }
}, },
"node_modules/resolve": { "node_modules/resolve": {
"version": "1.22.8", "version": "1.22.10",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
"integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"is-core-module": "^2.13.0", "is-core-module": "^2.16.0",
"path-parse": "^1.0.7", "path-parse": "^1.0.7",
"supports-preserve-symlinks-flag": "^1.0.0" "supports-preserve-symlinks-flag": "^1.0.0"
}, },
"bin": { "bin": {
"resolve": "bin/resolve" "resolve": "bin/resolve"
}, },
"engines": {
"node": ">= 0.4"
},
"funding": { "funding": {
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
@ -2082,17 +2085,17 @@
} }
}, },
"node_modules/terser-webpack-plugin": { "node_modules/terser-webpack-plugin": {
"version": "5.3.10", "version": "5.3.11",
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz", "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.11.tgz",
"integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==", "integrity": "sha512-RVCsMfuD0+cTt3EwX8hSl2Ks56EbFHWmhluwcqoPKtBnfjiT6olaq7PRIRfhyU8nnC2MrnDrBLfrD/RGE+cVXQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@jridgewell/trace-mapping": "^0.3.20", "@jridgewell/trace-mapping": "^0.3.25",
"jest-worker": "^27.4.5", "jest-worker": "^27.4.5",
"schema-utils": "^3.1.1", "schema-utils": "^4.3.0",
"serialize-javascript": "^6.0.1", "serialize-javascript": "^6.0.2",
"terser": "^5.26.0" "terser": "^5.31.1"
}, },
"engines": { "engines": {
"node": ">= 10.13.0" "node": ">= 10.13.0"
@ -2116,59 +2119,6 @@
} }
} }
}, },
"node_modules/terser-webpack-plugin/node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true,
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
"json-schema-traverse": "^0.4.1",
"uri-js": "^4.2.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/terser-webpack-plugin/node_modules/ajv-keywords": {
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
"integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"ajv": "^6.9.1"
}
},
"node_modules/terser-webpack-plugin/node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
"dev": true,
"license": "MIT"
},
"node_modules/terser-webpack-plugin/node_modules/schema-utils": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz",
"integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/json-schema": "^7.0.8",
"ajv": "^6.12.5",
"ajv-keywords": "^3.5.2"
},
"engines": {
"node": ">= 10.13.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/webpack"
}
},
"node_modules/to-regex-range": { "node_modules/to-regex-range": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",

View File

@ -7,11 +7,13 @@ param(
[switch]$mysql, [switch]$mysql,
[switch]$mssql, [switch]$mssql,
[switch]$sqlite, [switch]$sqlite,
[switch]$selfhost [switch]$selfhost,
[switch]$test
) )
# Abort on any error # Abort on any error
$ErrorActionPreference = "Stop" $ErrorActionPreference = "Stop"
$currentDir = Get-Location
if (!$all -and !$postgres -and !$mysql -and !$sqlite) { if (!$all -and !$postgres -and !$mysql -and !$sqlite) {
$mssql = $true; $mssql = $true;
@ -25,36 +27,62 @@ if ($all -or $postgres -or $mysql -or $sqlite) {
} }
} }
if ($all -or $mssql) { function Get-UserSecrets {
function Get-UserSecrets { # The dotnet cli command sometimes adds //BEGIN and //END comments to the output, Where-Object removes comments
# The dotnet cli command sometimes adds //BEGIN and //END comments to the output, Where-Object removes comments # to ensure a valid json
# to ensure a valid json return dotnet user-secrets list --json --project "$currentDir/../src/Api" | Where-Object { $_ -notmatch "^//" } | ConvertFrom-Json
return dotnet user-secrets list --json --project ../src/Api | Where-Object { $_ -notmatch "^//" } | ConvertFrom-Json
}
if ($selfhost) {
$msSqlConnectionString = $(Get-UserSecrets).'dev:selfHostOverride:globalSettings:sqlServer:connectionString'
$envName = "self-host"
} else {
$msSqlConnectionString = $(Get-UserSecrets).'globalSettings:sqlServer:connectionString'
$envName = "cloud"
}
Write-Host "Starting Microsoft SQL Server Migrations for $envName"
dotnet run --project ../util/MsSqlMigratorUtility/ "$msSqlConnectionString"
} }
$currentDir = Get-Location if ($all -or $mssql) {
if ($all -or !$test) {
if ($selfhost) {
$msSqlConnectionString = $(Get-UserSecrets).'dev:selfHostOverride:globalSettings:sqlServer:connectionString'
$envName = "self-host"
} else {
$msSqlConnectionString = $(Get-UserSecrets).'globalSettings:sqlServer:connectionString'
$envName = "cloud"
}
Foreach ($item in @(@($mysql, "MySQL", "MySqlMigrations"), @($postgres, "PostgreSQL", "PostgresMigrations"), @($sqlite, "SQLite", "SqliteMigrations"))) { Write-Host "Starting Microsoft SQL Server Migrations for $envName"
dotnet run --project ../util/MsSqlMigratorUtility/ "$msSqlConnectionString"
}
if ($all -or $test) {
$testMsSqlConnectionString = $(Get-UserSecrets).'databases:3:connectionString'
if ($testMsSqlConnectionString) {
$testEnvName = "test databases"
Write-Host "Starting Microsoft SQL Server Migrations for $testEnvName"
dotnet run --project ../util/MsSqlMigratorUtility/ "$testMsSqlConnectionString"
} else {
Write-Host "Connection string for a test MSSQL database not found in secrets.json!"
}
}
}
Foreach ($item in @(
@($mysql, "MySQL", "MySqlMigrations", "mySql", 2),
@($postgres, "PostgreSQL", "PostgresMigrations", "postgreSql", 0),
@($sqlite, "SQLite", "SqliteMigrations", "sqlite", 1)
)) {
if (!$item[0] -and !$all) { if (!$item[0] -and !$all) {
continue continue
} }
Write-Host "Starting $($item[1]) Migrations"
Set-Location "$currentDir/../util/$($item[2])/" Set-Location "$currentDir/../util/$($item[2])/"
dotnet ef database update if(!$test -or $all) {
Write-Host "Starting $($item[1]) Migrations"
$connectionString = $(Get-UserSecrets)."globalSettings:$($item[3]):connectionString"
dotnet ef database update --connection "$connectionString"
}
if ($test -or $all) {
$testConnectionString = $(Get-UserSecrets)."databases:$($item[4]):connectionString"
if ($testConnectionString) {
Write-Host "Starting $($item[1]) Migrations for test databases"
dotnet ef database update --connection "$testConnectionString"
} else {
Write-Host "Connection string for a test $($item[1]) database not found in secrets.json!"
}
}
} }
Set-Location "$currentDir" Set-Location "$currentDir"

View File

@ -21,7 +21,7 @@
"connectionString": "server=localhost;uid=root;pwd=SET_A_PASSWORD_HERE_123;database=vault_dev" "connectionString": "server=localhost;uid=root;pwd=SET_A_PASSWORD_HERE_123;database=vault_dev"
}, },
"sqlite": { "sqlite": {
"connectionString": "Data Source=/path/to/bitwardenServer/repository/server/dev/db/bitwarden.sqlite" "connectionString": "Data Source=/path/to/bitwardenServer/repository/server/dev/db/bitwarden.db"
}, },
"identityServer": { "identityServer": {
"certificateThumbprint": "<your Identity certificate thumbprint with no spaces>" "certificateThumbprint": "<your Identity certificate thumbprint with no spaces>"

View File

@ -3,9 +3,9 @@ using Bit.Admin.AdminConsole.Models;
using Bit.Admin.Enums; using Bit.Admin.Enums;
using Bit.Admin.Services; using Bit.Admin.Services;
using Bit.Admin.Utilities; using Bit.Admin.Utilities;
using Bit.Core;
using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;
using Bit.Core.AdminConsole.Providers.Interfaces; using Bit.Core.AdminConsole.Providers.Interfaces;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Enums; using Bit.Core.Billing.Enums;
@ -57,6 +57,7 @@ public class OrganizationsController : Controller
private readonly IRemoveOrganizationFromProviderCommand _removeOrganizationFromProviderCommand; private readonly IRemoveOrganizationFromProviderCommand _removeOrganizationFromProviderCommand;
private readonly IProviderBillingService _providerBillingService; private readonly IProviderBillingService _providerBillingService;
private readonly IFeatureService _featureService; private readonly IFeatureService _featureService;
private readonly IOrganizationInitiateDeleteCommand _organizationInitiateDeleteCommand;
public OrganizationsController( public OrganizationsController(
IOrganizationService organizationService, IOrganizationService organizationService,
@ -83,7 +84,8 @@ public class OrganizationsController : Controller
IProviderOrganizationRepository providerOrganizationRepository, IProviderOrganizationRepository providerOrganizationRepository,
IRemoveOrganizationFromProviderCommand removeOrganizationFromProviderCommand, IRemoveOrganizationFromProviderCommand removeOrganizationFromProviderCommand,
IProviderBillingService providerBillingService, IProviderBillingService providerBillingService,
IFeatureService featureService) IFeatureService featureService,
IOrganizationInitiateDeleteCommand organizationInitiateDeleteCommand)
{ {
_organizationService = organizationService; _organizationService = organizationService;
_organizationRepository = organizationRepository; _organizationRepository = organizationRepository;
@ -110,6 +112,7 @@ public class OrganizationsController : Controller
_removeOrganizationFromProviderCommand = removeOrganizationFromProviderCommand; _removeOrganizationFromProviderCommand = removeOrganizationFromProviderCommand;
_providerBillingService = providerBillingService; _providerBillingService = providerBillingService;
_featureService = featureService; _featureService = featureService;
_organizationInitiateDeleteCommand = organizationInitiateDeleteCommand;
} }
[RequirePermission(Permission.Org_List_View)] [RequirePermission(Permission.Org_List_View)]
@ -320,7 +323,7 @@ public class OrganizationsController : Controller
var organization = await _organizationRepository.GetByIdAsync(id); var organization = await _organizationRepository.GetByIdAsync(id);
if (organization != null) if (organization != null)
{ {
await _organizationService.InitiateDeleteAsync(organization, model.AdminEmail); await _organizationInitiateDeleteCommand.InitiateDeleteAsync(organization, model.AdminEmail);
TempData["Success"] = "The request to initiate deletion of the organization has been sent."; TempData["Success"] = "The request to initiate deletion of the organization has been sent.";
} }
} }
@ -476,14 +479,6 @@ public class OrganizationsController : Controller
Organization organization, Organization organization,
OrganizationEditModel update) OrganizationEditModel update)
{ {
var scaleMSPOnClientOrganizationUpdate =
_featureService.IsEnabled(FeatureFlagKeys.PM14401_ScaleMSPOnClientOrganizationUpdate);
if (!scaleMSPOnClientOrganizationUpdate)
{
return;
}
var provider = await _providerRepository.GetByOrganizationIdAsync(organization.Id); var provider = await _providerRepository.GetByOrganizationIdAsync(organization.Id);
// No scaling required // No scaling required

View File

@ -10,4 +10,6 @@ public class OrganizationsModel : PagedModel<Organization>
public bool? Paid { get; set; } public bool? Paid { get; set; }
public string Action { get; set; } public string Action { get; set; }
public bool SelfHosted { get; set; } public bool SelfHosted { get; set; }
public double StorageGB(Organization org) => org.Storage.HasValue ? Math.Round(org.Storage.Value / 1073741824D, 2) : 0;
} }

View File

@ -10,6 +10,7 @@
var canViewOrganizationInformation = AccessControlService.UserHasPermission(Permission.Org_OrgInformation_View); var canViewOrganizationInformation = AccessControlService.UserHasPermission(Permission.Org_OrgInformation_View);
var canViewBillingInformation = AccessControlService.UserHasPermission(Permission.Org_BillingInformation_View); var canViewBillingInformation = AccessControlService.UserHasPermission(Permission.Org_BillingInformation_View);
var canInitiateTrial = AccessControlService.UserHasPermission(Permission.Org_InitiateTrial); var canInitiateTrial = AccessControlService.UserHasPermission(Permission.Org_InitiateTrial);
var canRequestDelete = AccessControlService.UserHasPermission(Permission.Org_RequestDelete);
var canDelete = AccessControlService.UserHasPermission(Permission.Org_Delete); var canDelete = AccessControlService.UserHasPermission(Permission.Org_Delete);
var canUnlinkFromProvider = AccessControlService.UserHasPermission(Permission.Provider_Edit); var canUnlinkFromProvider = AccessControlService.UserHasPermission(Permission.Provider_Edit);
} }
@ -120,12 +121,15 @@
Unlink provider Unlink provider
</button> </button>
} }
@if (canDelete) @if (canRequestDelete)
{ {
<form asp-action="DeleteInitiation" asp-route-id="@Model.Organization.Id" id="initiate-delete-form"> <form asp-action="DeleteInitiation" asp-route-id="@Model.Organization.Id" id="initiate-delete-form">
<input type="hidden" name="AdminEmail" id="AdminEmail" /> <input type="hidden" name="AdminEmail" id="AdminEmail" />
<button class="btn btn-danger me-2" type="submit">Request Delete</button> <button class="btn btn-danger me-2" type="submit">Request Delete</button>
</form> </form>
}
@if (canDelete)
{
<form asp-action="Delete" asp-route-id="@Model.Organization.Id" <form asp-action="Delete" asp-route-id="@Model.Organization.Id"
onsubmit="return confirm('Are you sure you want to hard delete this organization?')"> onsubmit="return confirm('Are you sure you want to hard delete this organization?')">
<button class="btn btn-outline-danger" type="submit">Delete</button> <button class="btn btn-outline-danger" type="submit">Delete</button>

View File

@ -81,16 +81,7 @@
<i class="fa fa-smile-o fa-lg fa-fw text-body-secondary" title="Freeloader"></i> <i class="fa fa-smile-o fa-lg fa-fw text-body-secondary" title="Freeloader"></i>
} }
} }
@if(org.MaxStorageGb.HasValue && org.MaxStorageGb > 1) <i class="fa fa-hdd-o fa-lg fa-fw" title="Used Storage, @Model.StorageGB(org) GB"></i>
{
<i class="fa fa-plus-square fa-lg fa-fw"
title="Additional Storage, @(org.MaxStorageGb - 1) GB"></i>
}
else
{
<i class="fa fa-plus-square-o fa-lg fa-fw text-body-secondary"
title="No Additional Storage"></i>
}
@if(org.Enabled) @if(org.Enabled)
{ {
<i class="fa fa-check-circle fa-lg fa-fw" <i class="fa fa-check-circle fa-lg fa-fw"

View File

@ -3,7 +3,6 @@ using System.Text.Json;
using Bit.Admin.Enums; using Bit.Admin.Enums;
using Bit.Admin.Models; using Bit.Admin.Models;
using Bit.Admin.Utilities; using Bit.Admin.Utilities;
using Bit.Core;
using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Entities; using Bit.Core.Entities;
@ -16,7 +15,6 @@ using Bit.Core.Settings;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using TaxRate = Bit.Core.Entities.TaxRate;
namespace Bit.Admin.Controllers; namespace Bit.Admin.Controllers;
@ -33,7 +31,6 @@ public class ToolsController : Controller
private readonly IOrganizationUserRepository _organizationUserRepository; private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly IProviderUserRepository _providerUserRepository; private readonly IProviderUserRepository _providerUserRepository;
private readonly IPaymentService _paymentService; private readonly IPaymentService _paymentService;
private readonly ITaxRateRepository _taxRateRepository;
private readonly IStripeAdapter _stripeAdapter; private readonly IStripeAdapter _stripeAdapter;
private readonly IWebHostEnvironment _environment; private readonly IWebHostEnvironment _environment;
@ -46,7 +43,6 @@ public class ToolsController : Controller
IInstallationRepository installationRepository, IInstallationRepository installationRepository,
IOrganizationUserRepository organizationUserRepository, IOrganizationUserRepository organizationUserRepository,
IProviderUserRepository providerUserRepository, IProviderUserRepository providerUserRepository,
ITaxRateRepository taxRateRepository,
IPaymentService paymentService, IPaymentService paymentService,
IStripeAdapter stripeAdapter, IStripeAdapter stripeAdapter,
IWebHostEnvironment environment) IWebHostEnvironment environment)
@ -59,7 +55,6 @@ public class ToolsController : Controller
_installationRepository = installationRepository; _installationRepository = installationRepository;
_organizationUserRepository = organizationUserRepository; _organizationUserRepository = organizationUserRepository;
_providerUserRepository = providerUserRepository; _providerUserRepository = providerUserRepository;
_taxRateRepository = taxRateRepository;
_paymentService = paymentService; _paymentService = paymentService;
_stripeAdapter = stripeAdapter; _stripeAdapter = stripeAdapter;
_environment = environment; _environment = environment;
@ -226,7 +221,6 @@ public class ToolsController : Controller
return RedirectToAction("Edit", "Organizations", new { id = model.OrganizationId.Value }); return RedirectToAction("Edit", "Organizations", new { id = model.OrganizationId.Value });
} }
[RequireFeature(FeatureFlagKeys.PromoteProviderServiceUserTool)]
[RequirePermission(Permission.Tools_PromoteProviderServiceUser)] [RequirePermission(Permission.Tools_PromoteProviderServiceUser)]
public IActionResult PromoteProviderServiceUser() public IActionResult PromoteProviderServiceUser()
{ {
@ -235,7 +229,6 @@ public class ToolsController : Controller
[HttpPost] [HttpPost]
[ValidateAntiForgeryToken] [ValidateAntiForgeryToken]
[RequireFeature(FeatureFlagKeys.PromoteProviderServiceUserTool)]
[RequirePermission(Permission.Tools_PromoteProviderServiceUser)] [RequirePermission(Permission.Tools_PromoteProviderServiceUser)]
public async Task<IActionResult> PromoteProviderServiceUser(PromoteProviderServiceUserModel model) public async Task<IActionResult> PromoteProviderServiceUser(PromoteProviderServiceUserModel model)
{ {
@ -346,165 +339,6 @@ public class ToolsController : Controller
} }
} }
[RequirePermission(Permission.Tools_ManageTaxRates)]
public async Task<IActionResult> TaxRate(int page = 1, int count = 25)
{
if (page < 1)
{
page = 1;
}
if (count < 1)
{
count = 1;
}
var skip = (page - 1) * count;
var rates = await _taxRateRepository.SearchAsync(skip, count);
return View(new TaxRatesModel
{
Items = rates.ToList(),
Page = page,
Count = count
});
}
[RequirePermission(Permission.Tools_ManageTaxRates)]
public async Task<IActionResult> TaxRateAddEdit(string stripeTaxRateId = null)
{
if (string.IsNullOrWhiteSpace(stripeTaxRateId))
{
return View(new TaxRateAddEditModel());
}
var rate = await _taxRateRepository.GetByIdAsync(stripeTaxRateId);
var model = new TaxRateAddEditModel()
{
StripeTaxRateId = stripeTaxRateId,
Country = rate.Country,
State = rate.State,
PostalCode = rate.PostalCode,
Rate = rate.Rate
};
return View(model);
}
[ValidateAntiForgeryToken]
[RequirePermission(Permission.Tools_ManageTaxRates)]
public async Task<IActionResult> TaxRateUpload(IFormFile file)
{
if (file == null || file.Length == 0)
{
throw new ArgumentNullException(nameof(file));
}
// Build rates and validate them first before updating DB & Stripe
var taxRateUpdates = new List<TaxRate>();
var currentTaxRates = await _taxRateRepository.GetAllActiveAsync();
using var reader = new StreamReader(file.OpenReadStream());
while (!reader.EndOfStream)
{
var line = await reader.ReadLineAsync();
if (string.IsNullOrWhiteSpace(line))
{
continue;
}
var taxParts = line.Split(',');
if (taxParts.Length < 2)
{
throw new Exception($"This line is not in the format of <postal code>,<rate>,<state code>,<country code>: {line}");
}
var postalCode = taxParts[0].Trim();
if (string.IsNullOrWhiteSpace(postalCode))
{
throw new Exception($"'{line}' is not valid, the first element must contain a postal code.");
}
if (!decimal.TryParse(taxParts[1], out var rate) || rate <= 0M || rate > 100)
{
throw new Exception($"{taxParts[1]} is not a valid rate/decimal for {postalCode}");
}
var state = taxParts.Length > 2 ? taxParts[2] : null;
var country = (taxParts.Length > 3 ? taxParts[3] : null);
if (string.IsNullOrWhiteSpace(country))
{
country = "US";
}
var taxRate = currentTaxRates.FirstOrDefault(r => r.Country == country && r.PostalCode == postalCode) ??
new TaxRate
{
Country = country,
PostalCode = postalCode,
Active = true,
};
taxRate.Rate = rate;
taxRate.State = state ?? taxRate.State;
taxRateUpdates.Add(taxRate);
}
foreach (var taxRate in taxRateUpdates)
{
if (!string.IsNullOrWhiteSpace(taxRate.Id))
{
await _paymentService.UpdateTaxRateAsync(taxRate);
}
else
{
await _paymentService.CreateTaxRateAsync(taxRate);
}
}
return RedirectToAction("TaxRate");
}
[HttpPost]
[ValidateAntiForgeryToken]
[RequirePermission(Permission.Tools_ManageTaxRates)]
public async Task<IActionResult> TaxRateAddEdit(TaxRateAddEditModel model)
{
var existingRateCheck = await _taxRateRepository.GetByLocationAsync(new TaxRate() { Country = model.Country, PostalCode = model.PostalCode });
if (existingRateCheck.Any())
{
ModelState.AddModelError(nameof(model.PostalCode), "A tax rate already exists for this Country/Postal Code combination.");
}
if (!ModelState.IsValid)
{
return View(model);
}
var taxRate = new TaxRate()
{
Id = model.StripeTaxRateId,
Country = model.Country,
State = model.State,
PostalCode = model.PostalCode,
Rate = model.Rate
};
if (!string.IsNullOrWhiteSpace(model.StripeTaxRateId))
{
await _paymentService.UpdateTaxRateAsync(taxRate);
}
else
{
await _paymentService.CreateTaxRateAsync(taxRate);
}
return RedirectToAction("TaxRate");
}
[RequirePermission(Permission.Tools_ManageTaxRates)]
public async Task<IActionResult> TaxRateArchive(string stripeTaxRateId)
{
if (!string.IsNullOrWhiteSpace(stripeTaxRateId))
{
await _paymentService.ArchiveTaxRateAsync(new TaxRate() { Id = stripeTaxRateId });
}
return RedirectToAction("TaxRate");
}
[RequirePermission(Permission.Tools_ManageStripeSubscriptions)] [RequirePermission(Permission.Tools_ManageStripeSubscriptions)]
public async Task<IActionResult> StripeSubscriptions(StripeSubscriptionListOptions options) public async Task<IActionResult> StripeSubscriptions(StripeSubscriptionListOptions options)
{ {

View File

@ -107,7 +107,8 @@ public class UsersController : Controller
var billingHistoryInfo = await _paymentService.GetBillingHistoryAsync(user); var billingHistoryInfo = await _paymentService.GetBillingHistoryAsync(user);
var isTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user); var isTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user);
var verifiedDomain = await AccountDeprovisioningEnabled(user.Id); var verifiedDomain = await AccountDeprovisioningEnabled(user.Id);
return View(new UserEditModel(user, isTwoFactorEnabled, ciphers, billingInfo, billingHistoryInfo, _globalSettings, verifiedDomain)); var deviceVerificationRequired = await _userService.ActiveNewDeviceVerificationException(user.Id);
return View(new UserEditModel(user, isTwoFactorEnabled, ciphers, billingInfo, billingHistoryInfo, _globalSettings, verifiedDomain, deviceVerificationRequired));
} }
[HttpPost] [HttpPost]
@ -162,6 +163,22 @@ public class UsersController : Controller
return RedirectToAction("Index"); return RedirectToAction("Index");
} }
[HttpPost]
[ValidateAntiForgeryToken]
[RequirePermission(Permission.User_NewDeviceException_Edit)]
[RequireFeature(FeatureFlagKeys.NewDeviceVerification)]
public async Task<IActionResult> ToggleNewDeviceVerification(Guid id)
{
var user = await _userRepository.GetByIdAsync(id);
if (user == null)
{
return RedirectToAction("Index");
}
await _userService.ToggleNewDeviceVerificationException(user.Id);
return RedirectToAction("Edit", new { id });
}
// TODO: Feature flag to be removed in PM-14207 // TODO: Feature flag to be removed in PM-14207
private async Task<bool?> AccountDeprovisioningEnabled(Guid userId) private async Task<bool?> AccountDeprovisioningEnabled(Guid userId)
{ {

View File

@ -17,6 +17,7 @@ public enum Permission
User_Billing_View, User_Billing_View,
User_Billing_Edit, User_Billing_Edit,
User_Billing_LaunchGateway, User_Billing_LaunchGateway,
User_NewDeviceException_Edit,
Org_List_View, Org_List_View,
Org_OrgInformation_View, Org_OrgInformation_View,
@ -24,6 +25,7 @@ public enum Permission
Org_CheckEnabledBox, Org_CheckEnabledBox,
Org_BusinessInformation_View, Org_BusinessInformation_View,
Org_InitiateTrial, Org_InitiateTrial,
Org_RequestDelete,
Org_Delete, Org_Delete,
Org_BillingInformation_View, Org_BillingInformation_View,
Org_BillingInformation_DownloadInvoice, Org_BillingInformation_DownloadInvoice,

View File

@ -1,10 +0,0 @@
namespace Bit.Admin.Models;
public class TaxRateAddEditModel
{
public string StripeTaxRateId { get; set; }
public string Country { get; set; }
public string State { get; set; }
public string PostalCode { get; set; }
public decimal Rate { get; set; }
}

View File

@ -1,8 +0,0 @@
using Bit.Core.Entities;
namespace Bit.Admin.Models;
public class TaxRatesModel : PagedModel<TaxRate>
{
public string Message { get; set; }
}

View File

@ -18,10 +18,13 @@ public class UserEditModel
BillingInfo billingInfo, BillingInfo billingInfo,
BillingHistoryInfo billingHistoryInfo, BillingHistoryInfo billingHistoryInfo,
GlobalSettings globalSettings, GlobalSettings globalSettings,
bool? claimedAccount) bool? claimedAccount,
bool? activeNewDeviceVerificationException)
{ {
User = UserViewModel.MapViewModel(user, isTwoFactorEnabled, ciphers, claimedAccount); User = UserViewModel.MapViewModel(user, isTwoFactorEnabled, ciphers, claimedAccount);
ActiveNewDeviceVerificationException = activeNewDeviceVerificationException ?? false;
BillingInfo = billingInfo; BillingInfo = billingInfo;
BillingHistoryInfo = billingHistoryInfo; BillingHistoryInfo = billingHistoryInfo;
BraintreeMerchantId = globalSettings.Braintree.MerchantId; BraintreeMerchantId = globalSettings.Braintree.MerchantId;
@ -44,6 +47,8 @@ public class UserEditModel
public string RandomLicenseKey => CoreHelpers.SecureRandomString(20); public string RandomLicenseKey => CoreHelpers.SecureRandomString(20);
public string OneYearExpirationDate => DateTime.Now.AddYears(1).ToString("yyyy-MM-ddTHH:mm"); public string OneYearExpirationDate => DateTime.Now.AddYears(1).ToString("yyyy-MM-ddTHH:mm");
public string BraintreeMerchantId { get; init; } public string BraintreeMerchantId { get; init; }
public bool ActiveNewDeviceVerificationException { get; init; }
[Display(Name = "Name")] [Display(Name = "Name")]
public string Name { get; init; } public string Name { get; init; }

View File

@ -12,7 +12,6 @@ public static class RolePermissionMapping
Permission.User_List_View, Permission.User_List_View,
Permission.User_UserInformation_View, Permission.User_UserInformation_View,
Permission.User_GeneralDetails_View, Permission.User_GeneralDetails_View,
Permission.Org_CheckEnabledBox,
Permission.User_Delete, Permission.User_Delete,
Permission.User_UpgradePremium, Permission.User_UpgradePremium,
Permission.User_BillingInformation_View, Permission.User_BillingInformation_View,
@ -24,12 +23,15 @@ public static class RolePermissionMapping
Permission.User_Billing_View, Permission.User_Billing_View,
Permission.User_Billing_Edit, Permission.User_Billing_Edit,
Permission.User_Billing_LaunchGateway, Permission.User_Billing_LaunchGateway,
Permission.User_NewDeviceException_Edit,
Permission.Org_CheckEnabledBox,
Permission.Org_List_View, Permission.Org_List_View,
Permission.Org_OrgInformation_View, Permission.Org_OrgInformation_View,
Permission.Org_GeneralDetails_View, Permission.Org_GeneralDetails_View,
Permission.Org_BusinessInformation_View, Permission.Org_BusinessInformation_View,
Permission.Org_InitiateTrial, Permission.Org_InitiateTrial,
Permission.Org_Delete, Permission.Org_Delete,
Permission.Org_RequestDelete,
Permission.Org_BillingInformation_View, Permission.Org_BillingInformation_View,
Permission.Org_BillingInformation_DownloadInvoice, Permission.Org_BillingInformation_DownloadInvoice,
Permission.Org_Plan_View, Permission.Org_Plan_View,
@ -56,7 +58,6 @@ public static class RolePermissionMapping
Permission.User_List_View, Permission.User_List_View,
Permission.User_UserInformation_View, Permission.User_UserInformation_View,
Permission.User_GeneralDetails_View, Permission.User_GeneralDetails_View,
Permission.Org_CheckEnabledBox,
Permission.User_Delete, Permission.User_Delete,
Permission.User_UpgradePremium, Permission.User_UpgradePremium,
Permission.User_BillingInformation_View, Permission.User_BillingInformation_View,
@ -69,11 +70,14 @@ public static class RolePermissionMapping
Permission.User_Billing_View, Permission.User_Billing_View,
Permission.User_Billing_Edit, Permission.User_Billing_Edit,
Permission.User_Billing_LaunchGateway, Permission.User_Billing_LaunchGateway,
Permission.User_NewDeviceException_Edit,
Permission.Org_CheckEnabledBox,
Permission.Org_List_View, Permission.Org_List_View,
Permission.Org_OrgInformation_View, Permission.Org_OrgInformation_View,
Permission.Org_GeneralDetails_View, Permission.Org_GeneralDetails_View,
Permission.Org_BusinessInformation_View, Permission.Org_BusinessInformation_View,
Permission.Org_Delete, Permission.Org_Delete,
Permission.Org_RequestDelete,
Permission.Org_BillingInformation_View, Permission.Org_BillingInformation_View,
Permission.Org_BillingInformation_DownloadInvoice, Permission.Org_BillingInformation_DownloadInvoice,
Permission.Org_BillingInformation_CreateEditTransaction, Permission.Org_BillingInformation_CreateEditTransaction,
@ -104,7 +108,6 @@ public static class RolePermissionMapping
Permission.User_List_View, Permission.User_List_View,
Permission.User_UserInformation_View, Permission.User_UserInformation_View,
Permission.User_GeneralDetails_View, Permission.User_GeneralDetails_View,
Permission.Org_CheckEnabledBox,
Permission.User_UpgradePremium, Permission.User_UpgradePremium,
Permission.User_BillingInformation_View, Permission.User_BillingInformation_View,
Permission.User_BillingInformation_DownloadInvoice, Permission.User_BillingInformation_DownloadInvoice,
@ -112,9 +115,10 @@ 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_NewDeviceException_Edit,
Permission.Org_CheckEnabledBox,
Permission.Org_List_View, Permission.Org_List_View,
Permission.Org_OrgInformation_View, Permission.Org_OrgInformation_View,
Permission.Org_Delete,
Permission.Org_GeneralDetails_View, Permission.Org_GeneralDetails_View,
Permission.Org_BusinessInformation_View, Permission.Org_BusinessInformation_View,
Permission.Org_BillingInformation_View, Permission.Org_BillingInformation_View,
@ -124,6 +128,7 @@ public static class RolePermissionMapping
Permission.Org_Licensing_View, Permission.Org_Licensing_View,
Permission.Org_Billing_View, Permission.Org_Billing_View,
Permission.Org_Billing_LaunchGateway, Permission.Org_Billing_LaunchGateway,
Permission.Org_RequestDelete,
Permission.Provider_List_View, Permission.Provider_List_View,
Permission.Provider_View Permission.Provider_View
} }
@ -133,7 +138,6 @@ public static class RolePermissionMapping
Permission.User_List_View, Permission.User_List_View,
Permission.User_UserInformation_View, Permission.User_UserInformation_View,
Permission.User_GeneralDetails_View, Permission.User_GeneralDetails_View,
Permission.Org_CheckEnabledBox,
Permission.User_UpgradePremium, Permission.User_UpgradePremium,
Permission.User_BillingInformation_View, Permission.User_BillingInformation_View,
Permission.User_BillingInformation_DownloadInvoice, Permission.User_BillingInformation_DownloadInvoice,
@ -144,6 +148,7 @@ public static class RolePermissionMapping
Permission.User_Billing_View, Permission.User_Billing_View,
Permission.User_Billing_Edit, Permission.User_Billing_Edit,
Permission.User_Billing_LaunchGateway, Permission.User_Billing_LaunchGateway,
Permission.Org_CheckEnabledBox,
Permission.Org_List_View, Permission.Org_List_View,
Permission.Org_OrgInformation_View, Permission.Org_OrgInformation_View,
Permission.Org_GeneralDetails_View, Permission.Org_GeneralDetails_View,
@ -157,7 +162,7 @@ public static class RolePermissionMapping
Permission.Org_Billing_View, Permission.Org_Billing_View,
Permission.Org_Billing_Edit, Permission.Org_Billing_Edit,
Permission.Org_Billing_LaunchGateway, Permission.Org_Billing_LaunchGateway,
Permission.Org_Delete, Permission.Org_RequestDelete,
Permission.Provider_Edit, Permission.Provider_Edit,
Permission.Provider_View, Permission.Provider_View,
Permission.Provider_List_View, Permission.Provider_List_View,
@ -175,12 +180,12 @@ public static class RolePermissionMapping
Permission.User_List_View, Permission.User_List_View,
Permission.User_UserInformation_View, Permission.User_UserInformation_View,
Permission.User_GeneralDetails_View, Permission.User_GeneralDetails_View,
Permission.Org_CheckEnabledBox,
Permission.User_BillingInformation_View, Permission.User_BillingInformation_View,
Permission.User_BillingInformation_DownloadInvoice, Permission.User_BillingInformation_DownloadInvoice,
Permission.User_Premium_View, Permission.User_Premium_View,
Permission.User_Licensing_View, Permission.User_Licensing_View,
Permission.User_Licensing_Edit, Permission.User_Licensing_Edit,
Permission.Org_CheckEnabledBox,
Permission.Org_List_View, Permission.Org_List_View,
Permission.Org_OrgInformation_View, Permission.Org_OrgInformation_View,
Permission.Org_GeneralDetails_View, Permission.Org_GeneralDetails_View,

View File

@ -1,10 +1,8 @@
@using Bit.Admin.Enums; @using Bit.Admin.Enums;
@using Bit.Core
@inject SignInManager<IdentityUser> SignInManager @inject SignInManager<IdentityUser> SignInManager
@inject Bit.Core.Settings.GlobalSettings GlobalSettings @inject Bit.Core.Settings.GlobalSettings GlobalSettings
@inject Bit.Admin.Services.IAccessControlService AccessControlService @inject Bit.Admin.Services.IAccessControlService AccessControlService
@inject Bit.Core.Services.IFeatureService FeatureService
@{ @{
var canViewUsers = AccessControlService.UserHasPermission(Permission.User_List_View); var canViewUsers = AccessControlService.UserHasPermission(Permission.User_List_View);
@ -13,16 +11,14 @@
var canChargeBraintree = AccessControlService.UserHasPermission(Permission.Tools_ChargeBrainTreeCustomer); var canChargeBraintree = AccessControlService.UserHasPermission(Permission.Tools_ChargeBrainTreeCustomer);
var canCreateTransaction = AccessControlService.UserHasPermission(Permission.Tools_CreateEditTransaction); var canCreateTransaction = AccessControlService.UserHasPermission(Permission.Tools_CreateEditTransaction);
var canPromoteAdmin = AccessControlService.UserHasPermission(Permission.Tools_PromoteAdmin); var canPromoteAdmin = AccessControlService.UserHasPermission(Permission.Tools_PromoteAdmin);
var canPromoteProviderServiceUser = FeatureService.IsEnabled(FeatureFlagKeys.PromoteProviderServiceUserTool) && var canPromoteProviderServiceUser = AccessControlService.UserHasPermission(Permission.Tools_PromoteProviderServiceUser);
AccessControlService.UserHasPermission(Permission.Tools_PromoteProviderServiceUser);
var canGenerateLicense = AccessControlService.UserHasPermission(Permission.Tools_GenerateLicenseFile); var canGenerateLicense = AccessControlService.UserHasPermission(Permission.Tools_GenerateLicenseFile);
var canManageTaxRates = AccessControlService.UserHasPermission(Permission.Tools_ManageTaxRates);
var canManageStripeSubscriptions = AccessControlService.UserHasPermission(Permission.Tools_ManageStripeSubscriptions); var canManageStripeSubscriptions = AccessControlService.UserHasPermission(Permission.Tools_ManageStripeSubscriptions);
var canProcessStripeEvents = AccessControlService.UserHasPermission(Permission.Tools_ProcessStripeEvents); var canProcessStripeEvents = AccessControlService.UserHasPermission(Permission.Tools_ProcessStripeEvents);
var canMigrateProviders = AccessControlService.UserHasPermission(Permission.Tools_MigrateProviders); var canMigrateProviders = AccessControlService.UserHasPermission(Permission.Tools_MigrateProviders);
var canViewTools = canChargeBraintree || canCreateTransaction || canPromoteAdmin || canPromoteProviderServiceUser || var canViewTools = canChargeBraintree || canCreateTransaction || canPromoteAdmin || canPromoteProviderServiceUser ||
canGenerateLicense || canManageTaxRates || canManageStripeSubscriptions; canGenerateLicense || canManageStripeSubscriptions;
} }
<!DOCTYPE html> <!DOCTYPE html>
@ -107,12 +103,6 @@
Generate License Generate License
</a> </a>
} }
@if (canManageTaxRates)
{
<a class="dropdown-item" asp-controller="Tools" asp-action="TaxRate">
Manage Tax Rates
</a>
}
@if (canManageStripeSubscriptions) @if (canManageStripeSubscriptions)
{ {
<a class="dropdown-item" asp-controller="Tools" asp-action="StripeSubscriptions"> <a class="dropdown-item" asp-controller="Tools" asp-action="StripeSubscriptions">

View File

@ -1,127 +0,0 @@
@model TaxRatesModel
@{
ViewData["Title"] = "Tax Rates";
}
<h1>Manage Tax Rates</h1>
<h2>Bulk Upload Tax Rates</h2>
<section>
<p>
Upload a CSV file containing multiple tax rates in bulk in order to update existing rates by country
and postal code OR to create new rates where a currently active rate is not found already.
</p>
<p>CSV Upload Format</p>
<ul>
<li><b>Postal Code</b> (required) - The postal code for the tax rate.</li>
<li><b>Rate</b> (required) - The effective tax rate for this postal code.</li>
<li><b>State</b> (<i>optional</i>) - The ISO-2 character code for the state. Optional but recommended.</li>
<li><b>Country</b> (<i>optional</i>) - The ISO-2 character country code, defaults to "US" if not provided.</li>
</ul>
<p>Example (white-space is ignored):</p>
<div class="card mb-2">
<div class="card-body">
<pre class="mb-0">87654,8.25,FL,US
22334,8.5,CA
11223,7</pre>
</div>
</div>
<form method="post" enctype="multipart/form-data" asp-action="TaxRateUpload">
<div class="mb-3">
<input type="file" class="form-control" name="file" />
</div>
<div class="mb-3">
<input type="submit" value="Upload" class="btn btn-primary" />
</div>
</form>
</section>
<hr class="my-4">
<h2>View &amp; Manage Tax Rates</h2>
<a class="btn btn-primary mb-3" asp-controller="Tools" asp-action="TaxRateAddEdit">Add a Rate</a>
<div class="table-responsive">
<table class="table table-striped table-hover align-middle">
<thead>
<tr>
<th style="width: 190px;">Id</th>
<th style="width: 80px;">Country</th>
<th style="width: 80px;">State</th>
<th style="width: 150px;">Postal Code</th>
<th style="width: 160px;">Tax Rate</th>
<th style="width: 80px;"></th>
</tr>
</thead>
<tbody>
@if(!Model.Items.Any())
{
<tr>
<td colspan="6">No results to list.</td>
</tr>
}
else
{
@foreach(var rate in Model.Items)
{
<tr>
<td>
@{
var taxRateToEdit = new Dictionary<string, string>
{
{ "id", rate.Id },
{ "stripeTaxRateId", rate.Id }
};
}
<a asp-controller="Tools" asp-action="TaxRateAddEdit" asp-all-route-data="taxRateToEdit">@rate.Id</a>
</td>
<td>
@rate.Country
</td>
<td>
@rate.State
</td>
<td>
@rate.PostalCode
</td>
<td>
@rate.Rate%
</td>
<td>
<a class="delete-button" data-id="@rate.Id" asp-controller="Tools" asp-action="TaxRateArchive" asp-route-stripeTaxRateId="@rate.Id">
<i class="fa fa-trash fa-lg fa-fw"></i>
</a>
</td>
</tr>
}
}
</tbody>
</table>
</div>
<nav aria-label="Tax rates pagination">
<ul class="pagination">
@if(Model.PreviousPage.HasValue)
{
<li class="page-item">
<a class="page-link" asp-controller="Tools" asp-action="TaxRate" asp-route-page="@Model.PreviousPage.Value" asp-route-count="@Model.Count">Previous</a>
</li>
}
else
{
<li class="page-item disabled">
<a class="page-link" href="#" tabindex="-1">Previous</a>
</li>
}
@if(Model.NextPage.HasValue)
{
<li class="page-item">
<a class="page-link" asp-controller="Tools" asp-action="TaxRate" asp-route-page="@Model.NextPage.Value" asp-route-count="@Model.Count">Next</a>
</li>
}
else
{
<li class="page-item disabled">
<a class="page-link" href="#" tabindex="-1">Next</a>
</li>
}
</ul>
</nav>

View File

@ -1,356 +0,0 @@
@model TaxRateAddEditModel
@{
ViewData["Title"] = "Add/Edit Tax Rate";
}
<h1>@(string.IsNullOrWhiteSpace(Model.StripeTaxRateId) ? "Create" : "Edit") Tax Rate</h1>
@if (!string.IsNullOrWhiteSpace(Model.StripeTaxRateId))
{
<p>Note: Updating a Tax Rate archives the currently selected rate and creates a new rate with a new ID. The previous data still exists in a disabled state.</p>
}
<form method="post">
<div asp-validation-summary="All" class="alert alert-danger"></div>
<input type="hidden" asp-for="StripeTaxRateId">
<div class="row">
<div class="col-md">
<div class="form-group">
<label asp-for="Country"></label>
<select asp-for="Country" class="form-control" required>
<option value="">-- Select --</option>
<option value="US">United States</option>
<option value="CN">China</option>
<option value="FR">France</option>
<option value="DE">Germany</option>
<option value="CA">Canada</option>
<option value="GB">United Kingdom</option>
<option value="AU">Australia</option>
<option value="IN">India</option>
<option value="-" disabled></option>
<option value="AF">Afghanistan</option>
<option value="AX">Åland Islands</option>
<option value="AL">Albania</option>
<option value="DZ">Algeria</option>
<option value="AS">American Samoa</option>
<option value="AD">Andorra</option>
<option value="AO">Angola</option>
<option value="AI">Anguilla</option>
<option value="AQ">Antarctica</option>
<option value="AG">Antigua and Barbuda</option>
<option value="AR">Argentina</option>
<option value="AM">Armenia</option>
<option value="AW">Aruba</option>
<option value="AT">Austria</option>
<option value="AZ">Azerbaijan</option>
<option value="BS">Bahamas</option>
<option value="BH">Bahrain</option>
<option value="BD">Bangladesh</option>
<option value="BB">Barbados</option>
<option value="BY">Belarus</option>
<option value="BE">Belgium</option>
<option value="BZ">Belize</option>
<option value="BJ">Benin</option>
<option value="BM">Bermuda</option>
<option value="BT">Bhutan</option>
<option value="BO">Bolivia, Plurinational State of</option>
<option value="BQ">Bonaire, Sint Eustatius and Saba</option>
<option value="BA">Bosnia and Herzegovina</option>
<option value="BW">Botswana</option>
<option value="BV">Bouvet Island</option>
<option value="BR">Brazil</option>
<option value="IO">British Indian Ocean Territory</option>
<option value="BN">Brunei Darussalam</option>
<option value="BG">Bulgaria</option>
<option value="BF">Burkina Faso</option>
<option value="BI">Burundi</option>
<option value="KH">Cambodia</option>
<option value="CM">Cameroon</option>
<option value="CV">Cape Verde</option>
<option value="KY">Cayman Islands</option>
<option value="CF">Central African Republic</option>
<option value="TD">Chad</option>
<option value="CL">Chile</option>
<option value="CX">Christmas Island</option>
<option value="CC">Cocos (Keeling) Islands</option>
<option value="CO">Colombia</option>
<option value="KM">Comoros</option>
<option value="CG">Congo</option>
<option value="CD">Congo, the Democratic Republic of the</option>
<option value="CK">Cook Islands</option>
<option value="CR">Costa Rica</option>
<option value="CI">Côte d'Ivoire</option>
<option value="HR">Croatia</option>
<option value="CU">Cuba</option>
<option value="CW">Curaçao</option>
<option value="CY">Cyprus</option>
<option value="CZ">Czech Republic</option>
<option value="DK">Denmark</option>
<option value="DJ">Djibouti</option>
<option value="DM">Dominica</option>
<option value="DO">Dominican Republic</option>
<option value="EC">Ecuador</option>
<option value="EG">Egypt</option>
<option value="SV">El Salvador</option>
<option value="GQ">Equatorial Guinea</option>
<option value="ER">Eritrea</option>
<option value="EE">Estonia</option>
<option value="ET">Ethiopia</option>
<option value="FK">Falkland Islands (Malvinas)</option>
<option value="FO">Faroe Islands</option>
<option value="FJ">Fiji</option>
<option value="FI">Finland</option>
<option value="GF">French Guiana</option>
<option value="PF">French Polynesia</option>
<option value="TF">French Southern Territories</option>
<option value="GA">Gabon</option>
<option value="GM">Gambia</option>
<option value="GE">Georgia</option>
<option value="GH">Ghana</option>
<option value="GI">Gibraltar</option>
<option value="GR">Greece</option>
<option value="GL">Greenland</option>
<option value="GD">Grenada</option>
<option value="GP">Guadeloupe</option>
<option value="GU">Guam</option>
<option value="GT">Guatemala</option>
<option value="GG">Guernsey</option>
<option value="GN">Guinea</option>
<option value="GW">Guinea-Bissau</option>
<option value="GY">Guyana</option>
<option value="HT">Haiti</option>
<option value="HM">Heard Island and McDonald Islands</option>
<option value="VA">Holy See (Vatican City State)</option>
<option value="HN">Honduras</option>
<option value="HK">Hong Kong</option>
<option value="HU">Hungary</option>
<option value="IS">Iceland</option>
<option value="ID">Indonesia</option>
<option value="IR">Iran, Islamic Republic of</option>
<option value="IQ">Iraq</option>
<option value="IE">Ireland</option>
<option value="IM">Isle of Man</option>
<option value="IL">Israel</option>
<option value="IT">Italy</option>
<option value="JM">Jamaica</option>
<option value="JP">Japan</option>
<option value="JE">Jersey</option>
<option value="JO">Jordan</option>
<option value="KZ">Kazakhstan</option>
<option value="KE">Kenya</option>
<option value="KI">Kiribati</option>
<option value="KP">Korea, Democratic People's Republic of</option>
<option value="KR">Korea, Republic of</option>
<option value="KW">Kuwait</option>
<option value="KG">Kyrgyzstan</option>
<option value="LA">Lao People's Democratic Republic</option>
<option value="LV">Latvia</option>
<option value="LB">Lebanon</option>
<option value="LS">Lesotho</option>
<option value="LR">Liberia</option>
<option value="LY">Libya</option>
<option value="LI">Liechtenstein</option>
<option value="LT">Lithuania</option>
<option value="LU">Luxembourg</option>
<option value="MO">Macao</option>
<option value="MK">Macedonia, the former Yugoslav Republic of</option>
<option value="MG">Madagascar</option>
<option value="MW">Malawi</option>
<option value="MY">Malaysia</option>
<option value="MV">Maldives</option>
<option value="ML">Mali</option>
<option value="MT">Malta</option>
<option value="MH">Marshall Islands</option>
<option value="MQ">Martinique</option>
<option value="MR">Mauritania</option>
<option value="MU">Mauritius</option>
<option value="YT">Mayotte</option>
<option value="MX">Mexico</option>
<option value="FM">Micronesia, Federated States of</option>
<option value="MD">Moldova, Republic of</option>
<option value="MC">Monaco</option>
<option value="MN">Mongolia</option>
<option value="ME">Montenegro</option>
<option value="MS">Montserrat</option>
<option value="MA">Morocco</option>
<option value="MZ">Mozambique</option>
<option value="MM">Myanmar</option>
<option value="NA">Namibia</option>
<option value="NR">Nauru</option>
<option value="NP">Nepal</option>
<option value="NL">Netherlands</option>
<option value="NC">New Caledonia</option>
<option value="NZ">New Zealand</option>
<option value="NI">Nicaragua</option>
<option value="NE">Niger</option>
<option value="NG">Nigeria</option>
<option value="NU">Niue</option>
<option value="NF">Norfolk Island</option>
<option value="MP">Northern Mariana Islands</option>
<option value="NO">Norway</option>
<option value="OM">Oman</option>
<option value="PK">Pakistan</option>
<option value="PW">Palau</option>
<option value="PS">Palestinian Territory, Occupied</option>
<option value="PA">Panama</option>
<option value="PG">Papua New Guinea</option>
<option value="PY">Paraguay</option>
<option value="PE">Peru</option>
<option value="PH">Philippines</option>
<option value="PN">Pitcairn</option>
<option value="PL">Poland</option>
<option value="PT">Portugal</option>
<option value="PR">Puerto Rico</option>
<option value="QA">Qatar</option>
<option value="RE">Réunion</option>
<option value="RO">Romania</option>
<option value="RU">Russian Federation</option>
<option value="RW">Rwanda</option>
<option value="BL">Saint Barthélemy</option>
<option value="SH">Saint Helena, Ascension and Tristan da Cunha</option>
<option value="KN">Saint Kitts and Nevis</option>
<option value="LC">Saint Lucia</option>
<option value="MF">Saint Martin (French part)</option>
<option value="PM">Saint Pierre and Miquelon</option>
<option value="VC">Saint Vincent and the Grenadines</option>
<option value="WS">Samoa</option>
<option value="SM">San Marino</option>
<option value="ST">Sao Tome and Principe</option>
<option value="SA">Saudi Arabia</option>
<option value="SN">Senegal</option>
<option value="RS">Serbia</option>
<option value="SC">Seychelles</option>
<option value="SL">Sierra Leone</option>
<option value="SG">Singapore</option>
<option value="SX">Sint Maarten (Dutch part)</option>
<option value="SK">Slovakia</option>
<option value="SI">Slovenia</option>
<option value="SB">Solomon Islands</option>
<option value="SO">Somalia</option>
<option value="ZA">South Africa</option>
<option value="GS">South Georgia and the South Sandwich Islands</option>
<option value="SS">South Sudan</option>
<option value="ES">Spain</option>
<option value="LK">Sri Lanka</option>
<option value="SD">Sudan</option>
<option value="SR">Suriname</option>
<option value="SJ">Svalbard and Jan Mayen</option>
<option value="SZ">Swaziland</option>
<option value="SE">Sweden</option>
<option value="CH">Switzerland</option>
<option value="SY">Syrian Arab Republic</option>
<option value="TW">Taiwan</option>
<option value="TJ">Tajikistan</option>
<option value="TZ">Tanzania, United Republic of</option>
<option value="TH">Thailand</option>
<option value="TL">Timor-Leste</option>
<option value="TG">Togo</option>
<option value="TK">Tokelau</option>
<option value="TO">Tonga</option>
<option value="TT">Trinidad and Tobago</option>
<option value="TN">Tunisia</option>
<option value="TR">Turkey</option>
<option value="TM">Turkmenistan</option>
<option value="TC">Turks and Caicos Islands</option>
<option value="TV">Tuvalu</option>
<option value="UG">Uganda</option>
<option value="UA">Ukraine</option>
<option value="AE">United Arab Emirates</option>
<option value="UM">United States Minor Outlying Islands</option>
<option value="UY">Uruguay</option>
<option value="UZ">Uzbekistan</option>
<option value="VU">Vanuatu</option>
<option value="VE">Venezuela, Bolivarian Republic of</option>
<option value="VN">Viet Nam</option>
<option value="VG">Virgin Islands, British</option>
<option value="VI">Virgin Islands, U.S.</option>
<option value="WF">Wallis and Futuna</option>
<option value="EH">Western Sahara</option>
<option value="YE">Yemen</option>
<option value="ZM">Zambia</option>
<option value="ZW">Zimbabwe</option>
</select>
</div>
</div>
<div class="col-md">
<div class="form-group">
<label asp-for="State"></label>
<select asp-for="State" class="form-control">
<option value="">-- Select --</option>
<option value="AL">Alabama</option>
<option value="AK">Alaska</option>
<option value="AZ">Arizona</option>
<option value="AR">Arkansas</option>
<option value="CA">California</option>
<option value="CO">Colorado</option>
<option value="CT">Connecticut</option>
<option value="DE">Delaware</option>
<option value="DC">District Of Columbia</option>
<option value="FL">Florida</option>
<option value="GA">Georgia</option>
<option value="HI">Hawaii</option>
<option value="ID">Idaho</option>
<option value="IL">Illinois</option>
<option value="IN">Indiana</option>
<option value="IA">Iowa</option>
<option value="KS">Kansas</option>
<option value="KY">Kentucky</option>
<option value="LA">Louisiana</option>
<option value="ME">Maine</option>
<option value="MD">Maryland</option>
<option value="MA">Massachusetts</option>
<option value="MI">Michigan</option>
<option value="MN">Minnesota</option>
<option value="MS">Mississippi</option>
<option value="MO">Missouri</option>
<option value="MT">Montana</option>
<option value="NE">Nebraska</option>
<option value="NV">Nevada</option>
<option value="NH">New Hampshire</option>
<option value="NJ">New Jersey</option>
<option value="NM">New Mexico</option>
<option value="NY">New York</option>
<option value="NC">North Carolina</option>
<option value="ND">North Dakota</option>
<option value="OH">Ohio</option>
<option value="OK">Oklahoma</option>
<option value="OR">Oregon</option>
<option value="PA">Pennsylvania</option>
<option value="RI">Rhode Island</option>
<option value="SC">South Carolina</option>
<option value="SD">South Dakota</option>
<option value="TN">Tennessee</option>
<option value="TX">Texas</option>
<option value="UT">Utah</option>
<option value="VT">Vermont</option>
<option value="VA">Virginia</option>
<option value="WA">Washington</option>
<option value="WV">West Virginia</option>
<option value="WI">Wisconsin</option>
<option value="WY">Wyoming</option>
</select>
</div>
</div>
</div>
<div class="row">
<div class="col-md">
<div class="form-group">
<label asp-for="PostalCode">Postal Code</label>
<input type="text" class="form-control" asp-for="PostalCode" required maxlength="10">
</div>
</div>
<div class="col-md">
<div class="form-group">
<label asp-for="Rate">Tax Rate</label>
<div class="input-group">
<input type="text" class="form-control" asp-for="Rate" pattern="^\d{0,3}.\d{0,3}$" required>
<div class="input-group-append">
<span class="input-group-text">%</span>
</div>
</div>
</div>
</div>
</div>
<button type="submit" class="btn btn-primary mb-2">@(string.IsNullOrWhiteSpace(Model.StripeTaxRateId) ? "Create" : "Save")</button>
</form>

View File

@ -1,11 +1,16 @@
@using Bit.Admin.Enums; @using Bit.Admin.Enums;
@inject Bit.Admin.Services.IAccessControlService AccessControlService @inject Bit.Admin.Services.IAccessControlService AccessControlService
@inject Bit.Core.Services.IFeatureService FeatureService
@inject Bit.Core.Settings.GlobalSettings GlobalSettings
@inject IWebHostEnvironment HostingEnvironment @inject IWebHostEnvironment HostingEnvironment
@model UserEditModel @model UserEditModel
@{ @{
ViewData["Title"] = "User: " + Model.User.Email; ViewData["Title"] = "User: " + Model.User.Email;
var canViewUserInformation = AccessControlService.UserHasPermission(Permission.User_UserInformation_View); var canViewUserInformation = AccessControlService.UserHasPermission(Permission.User_UserInformation_View);
var canViewNewDeviceException = AccessControlService.UserHasPermission(Permission.User_NewDeviceException_Edit) &&
GlobalSettings.EnableNewDeviceVerification &&
FeatureService.IsEnabled(Bit.Core.FeatureFlagKeys.NewDeviceVerification);
var canViewBillingInformation = AccessControlService.UserHasPermission(Permission.User_BillingInformation_View); var canViewBillingInformation = AccessControlService.UserHasPermission(Permission.User_BillingInformation_View);
var canViewGeneral = AccessControlService.UserHasPermission(Permission.User_GeneralDetails_View); var canViewGeneral = AccessControlService.UserHasPermission(Permission.User_GeneralDetails_View);
var canViewPremium = AccessControlService.UserHasPermission(Permission.User_Premium_View); var canViewPremium = AccessControlService.UserHasPermission(Permission.User_Premium_View);
@ -47,13 +52,13 @@
if (gateway.value === '@((byte)Bit.Core.Enums.GatewayType.Stripe)') { if (gateway.value === '@((byte)Bit.Core.Enums.GatewayType.Stripe)') {
const url = '@(HostingEnvironment.IsDevelopment() const url = '@(HostingEnvironment.IsDevelopment()
? "https://dashboard.stripe.com/test" ? "https://dashboard.stripe.com/test"
: "https://dashboard.stripe.com")'; : "https://dashboard.stripe.com")';
window.open(`${url}/customers/${customerId.value}/`, '_blank'); window.open(`${url}/customers/${customerId.value}/`, '_blank');
} else if (gateway.value === '@((byte)Bit.Core.Enums.GatewayType.Braintree)') { } else if (gateway.value === '@((byte)Bit.Core.Enums.GatewayType.Braintree)') {
const url = '@(HostingEnvironment.IsDevelopment() const url = '@(HostingEnvironment.IsDevelopment()
? $"https://www.sandbox.braintreegateway.com/merchants/{Model.BraintreeMerchantId}" ? $"https://www.sandbox.braintreegateway.com/merchants/{Model.BraintreeMerchantId}"
: $"https://www.braintreegateway.com/merchants/{Model.BraintreeMerchantId}")'; : $"https://www.braintreegateway.com/merchants/{Model.BraintreeMerchantId}")';
window.open(`${url}/${customerId.value}`, '_blank'); window.open(`${url}/${customerId.value}`, '_blank');
} }
}); });
@ -67,13 +72,13 @@
if (gateway.value === '@((byte)Bit.Core.Enums.GatewayType.Stripe)') { if (gateway.value === '@((byte)Bit.Core.Enums.GatewayType.Stripe)') {
const url = '@(HostingEnvironment.IsDevelopment() || HostingEnvironment.IsEnvironment("QA") const url = '@(HostingEnvironment.IsDevelopment() || HostingEnvironment.IsEnvironment("QA")
? "https://dashboard.stripe.com/test" ? "https://dashboard.stripe.com/test"
: "https://dashboard.stripe.com")' : "https://dashboard.stripe.com")'
window.open(`${url}/subscriptions/${subId.value}`, '_blank'); window.open(`${url}/subscriptions/${subId.value}`, '_blank');
} else if (gateway.value === '@((byte)Bit.Core.Enums.GatewayType.Braintree)') { } else if (gateway.value === '@((byte)Bit.Core.Enums.GatewayType.Braintree)') {
const url = '@(HostingEnvironment.IsDevelopment() || HostingEnvironment.IsEnvironment("QA") const url = '@(HostingEnvironment.IsDevelopment() || HostingEnvironment.IsEnvironment("QA")
? $"https://www.sandbox.braintreegateway.com/merchants/{Model.BraintreeMerchantId}" ? $"https://www.sandbox.braintreegateway.com/merchants/{Model.BraintreeMerchantId}"
: $"https://www.braintreegateway.com/merchants/{Model.BraintreeMerchantId}")'; : $"https://www.braintreegateway.com/merchants/{Model.BraintreeMerchantId}")';
window.open(`${url}/subscriptions/${subId.value}`, '_blank'); window.open(`${url}/subscriptions/${subId.value}`, '_blank');
} }
}); });
@ -88,11 +93,40 @@
<h2>User Information</h2> <h2>User Information</h2>
@await Html.PartialAsync("_ViewInformation", Model.User) @await Html.PartialAsync("_ViewInformation", Model.User)
} }
@if (canViewNewDeviceException)
{
<h2>New Device Verification </h2>
<dl class="row">
<dt class="col d-flex">
<form asp-action="ToggleNewDeviceVerification" asp-route-id="@Model.User.Id" method="post">
@if (Model.ActiveNewDeviceVerificationException)
{
<p>Status: Bypassed</p>
<button type="submit" class="btn btn-success" id="new-device-verification-exception">Require New
Device Verification</button>
}
else
{
<p>Status: Required</p>
<button type="submit" class="btn btn-outline-danger" id="new-device-verification-exception">Bypass New
Device Verification</button>
}
</form>
</dt>
</dl>
}
@if (canViewBillingInformation) @if (canViewBillingInformation)
{ {
<h2>Billing Information</h2> <h2>Billing Information</h2>
@await Html.PartialAsync("_BillingInformation", @await Html.PartialAsync("_BillingInformation",
new BillingInformationModel { BillingInfo = Model.BillingInfo, BillingHistoryInfo = Model.BillingHistoryInfo, UserId = Model.User.Id, Entity = "User" }) new BillingInformationModel
{
BillingInfo = Model.BillingInfo,
BillingHistoryInfo = Model.BillingHistoryInfo,
UserId = Model.User.Id,
Entity = "User"
})
} }
@if (canViewGeneral) @if (canViewGeneral)
{ {
@ -109,7 +143,7 @@
<label class="form-check-label" asp-for="EmailVerified"></label> <label class="form-check-label" asp-for="EmailVerified"></label>
</div> </div>
} }
<form method="post" id="edit-form"> <form method="post" id="edit-form">
@if (canViewPremium) @if (canViewPremium)
{ {
<h2>Premium</h2> <h2>Premium</h2>
@ -139,54 +173,56 @@
<div class="col-sm"> <div class="col-sm">
<div class="mb-3"> <div class="mb-3">
<label asp-for="PremiumExpirationDate" class="form-label"></label> <label asp-for="PremiumExpirationDate" class="form-label"></label>
<input type="datetime-local" class="form-control" asp-for="PremiumExpirationDate" readonly='@(!canEditLicensing)'> <input type="datetime-local" class="form-control" asp-for="PremiumExpirationDate"
readonly='@(!canEditLicensing)'>
</div> </div>
</div> </div>
</div> </div>
} }
@if (canViewBilling) @if (canViewBilling)
{ {
<h2>Billing</h2> <h2>Billing</h2>
<div class="row"> <div class="row">
<div class="col-md"> <div class="col-md">
<div class="mb-3"> <div class="mb-3">
<label asp-for="Gateway" class="form-label"></label> <label asp-for="Gateway" class="form-label"></label>
<select class="form-select" asp-for="Gateway" disabled='@(canEditBilling ? null : "disabled")' <select class="form-select" asp-for="Gateway" disabled='@(canEditBilling ? null : "disabled")'
asp-items="Html.GetEnumSelectList<Bit.Core.Enums.GatewayType>()"> asp-items="Html.GetEnumSelectList<Bit.Core.Enums.GatewayType>()">
<option value="">--</option> <option value="">--</option>
</select> </select>
</div>
</div> </div>
</div> <div class="col-md">
<div class="col-md"> <div class="mb-3">
<div class="mb-3"> <label asp-for="GatewayCustomerId" class="form-label"></label>
<label asp-for="GatewayCustomerId" class="form-label"></label> <div class="input-group">
<div class="input-group"> <input type="text" class="form-control" asp-for="GatewayCustomerId" readonly='@(!canEditBilling)'>
<input type="text" class="form-control" asp-for="GatewayCustomerId" readonly='@(!canEditBilling)'> @if (canLaunchGateway)
@if (canLaunchGateway) {
{ <button class="btn btn-secondary" type="button" id="gateway-customer-link">
<button class="btn btn-secondary" type="button" id="gateway-customer-link"> <i class="fa fa-external-link"></i>
<i class="fa fa-external-link"></i> </button>
</button> }
} </div>
</div>
</div>
<div class="col-md">
<div class="mb-3">
<label asp-for="GatewaySubscriptionId" class="form-label"></label>
<div class="input-group">
<input type="text" class="form-control" asp-for="GatewaySubscriptionId"
readonly='@(!canEditBilling)'>
@if (canLaunchGateway)
{
<button class="btn btn-secondary" type="button" id="gateway-subscription-link">
<i class="fa fa-external-link"></i>
</button>
}
</div>
</div> </div>
</div> </div>
</div> </div>
<div class="col-md"> }
<div class="mb-3">
<label asp-for="GatewaySubscriptionId" class="form-label"></label>
<div class="input-group">
<input type="text" class="form-control" asp-for="GatewaySubscriptionId" readonly='@(!canEditBilling)'>
@if (canLaunchGateway)
{
<button class="btn btn-secondary" type="button" id="gateway-subscription-link">
<i class="fa fa-external-link"></i>
</button>
}
</div>
</div>
</div>
</div>
}
</form> </form>
<div class="d-flex mt-4"> <div class="d-flex mt-4">
<button type="submit" class="btn btn-primary" form="edit-form">Save</button> <button type="submit" class="btn btn-primary" form="edit-form">Save</button>

View File

@ -780,9 +780,9 @@
} }
}, },
"node_modules/browserslist": { "node_modules/browserslist": {
"version": "4.24.2", "version": "4.24.3",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.2.tgz", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.3.tgz",
"integrity": "sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==", "integrity": "sha512-1CPmv8iobE2fyRMV97dAcMVegvvWKxmq94hkLiAkUGwKVTyDLw33K+ZxiFrREKmmps4rIw6grcCFCnTMSZ/YiA==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@ -800,9 +800,9 @@
], ],
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"caniuse-lite": "^1.0.30001669", "caniuse-lite": "^1.0.30001688",
"electron-to-chromium": "^1.5.41", "electron-to-chromium": "^1.5.73",
"node-releases": "^2.0.18", "node-releases": "^2.0.19",
"update-browserslist-db": "^1.1.1" "update-browserslist-db": "^1.1.1"
}, },
"bin": { "bin": {
@ -820,9 +820,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/caniuse-lite": { "node_modules/caniuse-lite": {
"version": "1.0.30001688", "version": "1.0.30001690",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001688.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001690.tgz",
"integrity": "sha512-Nmqpru91cuABu/DTCXbM2NSRHzM2uVHfPnhJ/1zEAJx/ILBRVmz3pzH4N7DZqbdG0gWClsCC05Oj0mJ/1AWMbA==", "integrity": "sha512-5ExiE3qQN6oF8Clf8ifIDcMRCRE/dMGcETG/XGMD8/XiXm6HXQgQTh1yZYLXXpSOsEUlJm1Xr7kGULZTuGtP/w==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@ -841,9 +841,9 @@
"license": "CC-BY-4.0" "license": "CC-BY-4.0"
}, },
"node_modules/chokidar": { "node_modules/chokidar": {
"version": "4.0.1", "version": "4.0.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
"integrity": "sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==", "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -973,16 +973,16 @@
} }
}, },
"node_modules/electron-to-chromium": { "node_modules/electron-to-chromium": {
"version": "1.5.73", "version": "1.5.75",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.73.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.75.tgz",
"integrity": "sha512-8wGNxG9tAG5KhGd3eeA0o6ixhiNdgr0DcHWm85XPCphwZgD1lIEoi6t3VERayWao7SF7AAZTw6oARGJeVjH8Kg==", "integrity": "sha512-Lf3++DumRE/QmweGjU+ZcKqQ+3bKkU/qjaKYhIJKEOhgIO9Xs6IiAQFkfFoj+RhgDk4LUeNsLo6plExHqSyu6Q==",
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/enhanced-resolve": { "node_modules/enhanced-resolve": {
"version": "5.17.1", "version": "5.18.0",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.0.tgz",
"integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==", "integrity": "sha512-0/r0MySGYG8YqlayBZ6MuCfECmHFdJ5qyPh8s8wa5Hnm6SaFLSK1VYCbj+NKp090Nm1caZhD+QTnmxO7esYGyQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -1272,9 +1272,9 @@
} }
}, },
"node_modules/is-core-module": { "node_modules/is-core-module": {
"version": "2.15.1", "version": "2.16.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
"integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -1793,19 +1793,22 @@
} }
}, },
"node_modules/resolve": { "node_modules/resolve": {
"version": "1.22.8", "version": "1.22.10",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
"integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"is-core-module": "^2.13.0", "is-core-module": "^2.16.0",
"path-parse": "^1.0.7", "path-parse": "^1.0.7",
"supports-preserve-symlinks-flag": "^1.0.0" "supports-preserve-symlinks-flag": "^1.0.0"
}, },
"bin": { "bin": {
"resolve": "bin/resolve" "resolve": "bin/resolve"
}, },
"engines": {
"node": ">= 0.4"
},
"funding": { "funding": {
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
@ -2083,17 +2086,17 @@
} }
}, },
"node_modules/terser-webpack-plugin": { "node_modules/terser-webpack-plugin": {
"version": "5.3.10", "version": "5.3.11",
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz", "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.11.tgz",
"integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==", "integrity": "sha512-RVCsMfuD0+cTt3EwX8hSl2Ks56EbFHWmhluwcqoPKtBnfjiT6olaq7PRIRfhyU8nnC2MrnDrBLfrD/RGE+cVXQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@jridgewell/trace-mapping": "^0.3.20", "@jridgewell/trace-mapping": "^0.3.25",
"jest-worker": "^27.4.5", "jest-worker": "^27.4.5",
"schema-utils": "^3.1.1", "schema-utils": "^4.3.0",
"serialize-javascript": "^6.0.1", "serialize-javascript": "^6.0.2",
"terser": "^5.26.0" "terser": "^5.31.1"
}, },
"engines": { "engines": {
"node": ">= 10.13.0" "node": ">= 10.13.0"
@ -2117,59 +2120,6 @@
} }
} }
}, },
"node_modules/terser-webpack-plugin/node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true,
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
"json-schema-traverse": "^0.4.1",
"uri-js": "^4.2.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/terser-webpack-plugin/node_modules/ajv-keywords": {
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
"integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"ajv": "^6.9.1"
}
},
"node_modules/terser-webpack-plugin/node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
"dev": true,
"license": "MIT"
},
"node_modules/terser-webpack-plugin/node_modules/schema-utils": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz",
"integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/json-schema": "^7.0.8",
"ajv": "^6.12.5",
"ajv-keywords": "^3.5.2"
},
"engines": {
"node": ">= 10.13.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/webpack"
}
},
"node_modules/to-regex-range": { "node_modules/to-regex-range": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",

View File

@ -2,7 +2,6 @@
using Bit.Api.AdminConsole.Models.Response; using Bit.Api.AdminConsole.Models.Response;
using Bit.Api.Models.Response; using Bit.Api.Models.Response;
using Bit.Api.Vault.AuthorizationHandlers.Collections; using Bit.Api.Vault.AuthorizationHandlers.Collections;
using Bit.Core;
using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Authorization; using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Authorization;
using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.Shared.Authorization; using Bit.Core.AdminConsole.OrganizationFeatures.Shared.Authorization;
@ -90,7 +89,7 @@ public class GroupsController : Controller
} }
[HttpGet("")] [HttpGet("")]
public async Task<ListResponseModel<GroupDetailsResponseModel>> GetOrganizationGroups(Guid orgId) public async Task<ListResponseModel<GroupResponseModel>> GetOrganizationGroups(Guid orgId)
{ {
var authResult = await _authorizationService.AuthorizeAsync(User, new OrganizationScope(orgId), GroupOperations.ReadAll); var authResult = await _authorizationService.AuthorizeAsync(User, new OrganizationScope(orgId), GroupOperations.ReadAll);
if (!authResult.Succeeded) if (!authResult.Succeeded)
@ -98,24 +97,15 @@ public class GroupsController : Controller
throw new NotFoundException(); throw new NotFoundException();
} }
if (_featureService.IsEnabled(FeatureFlagKeys.SecureOrgGroupDetails)) var groups = await _groupRepository.GetManyByOrganizationIdAsync(orgId);
{ var responses = groups.Select(g => new GroupResponseModel(g));
var groups = await _groupRepository.GetManyByOrganizationIdAsync(orgId); return new ListResponseModel<GroupResponseModel>(responses);
var responses = groups.Select(g => new GroupDetailsResponseModel(g, []));
return new ListResponseModel<GroupDetailsResponseModel>(responses);
}
var groupDetails = await _groupRepository.GetManyWithCollectionsByOrganizationIdAsync(orgId);
var detailResponses = groupDetails.Select(g => new GroupDetailsResponseModel(g.Item1, g.Item2));
return new ListResponseModel<GroupDetailsResponseModel>(detailResponses);
} }
[HttpGet("details")] [HttpGet("details")]
public async Task<ListResponseModel<GroupDetailsResponseModel>> GetOrganizationGroupDetails(Guid orgId) public async Task<ListResponseModel<GroupDetailsResponseModel>> GetOrganizationGroupDetails(Guid orgId)
{ {
var authResult = _featureService.IsEnabled(FeatureFlagKeys.SecureOrgGroupDetails) var authResult = await _authorizationService.AuthorizeAsync(User, new OrganizationScope(orgId), GroupOperations.ReadAllDetails);
? await _authorizationService.AuthorizeAsync(User, new OrganizationScope(orgId), GroupOperations.ReadAllDetails)
: await _authorizationService.AuthorizeAsync(User, new OrganizationScope(orgId), GroupOperations.ReadAll);
if (!authResult.Succeeded) if (!authResult.Succeeded)
{ {

View File

@ -311,10 +311,8 @@ public class OrganizationUsersController : Controller
throw new UnauthorizedAccessException(); throw new UnauthorizedAccessException();
} }
var masterPasswordPolicy = await _policyRepository.GetByOrganizationIdTypeAsync(orgId, PolicyType.ResetPassword); var useMasterPasswordPolicy = await ShouldHandleResetPasswordAsync(orgId);
var useMasterPasswordPolicy = masterPasswordPolicy != null &&
masterPasswordPolicy.Enabled &&
masterPasswordPolicy.GetDataModel<ResetPasswordDataModel>().AutoEnrollEnabled;
if (useMasterPasswordPolicy && string.IsNullOrWhiteSpace(model.ResetPasswordKey)) if (useMasterPasswordPolicy && string.IsNullOrWhiteSpace(model.ResetPasswordKey))
{ {
throw new BadRequestException(string.Empty, "Master Password reset is required, but not provided."); throw new BadRequestException(string.Empty, "Master Password reset is required, but not provided.");
@ -328,6 +326,23 @@ public class OrganizationUsersController : Controller
} }
} }
private async Task<bool> ShouldHandleResetPasswordAsync(Guid orgId)
{
var organizationAbility = await _applicationCacheService.GetOrganizationAbilityAsync(orgId);
if (organizationAbility is not { UsePolicies: true })
{
return false;
}
var masterPasswordPolicy = await _policyRepository.GetByOrganizationIdTypeAsync(orgId, PolicyType.ResetPassword);
var useMasterPasswordPolicy = masterPasswordPolicy != null &&
masterPasswordPolicy.Enabled &&
masterPasswordPolicy.GetDataModel<ResetPasswordDataModel>().AutoEnrollEnabled;
return useMasterPasswordPolicy;
}
[HttpPost("{id}/confirm")] [HttpPost("{id}/confirm")]
public async Task Confirm(string orgId, string id, [FromBody] OrganizationUserConfirmRequestModel model) public async Task Confirm(string orgId, string id, [FromBody] OrganizationUserConfirmRequestModel model)
{ {

View File

@ -14,6 +14,7 @@ using Bit.Core.AdminConsole.Models.Business.Tokenables;
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationApiKeys.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationApiKeys.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations; using Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Auth.Enums; using Bit.Core.Auth.Enums;
@ -58,6 +59,7 @@ public class OrganizationsController : Controller
private readonly IDataProtectorTokenFactory<OrgDeleteTokenable> _orgDeleteTokenDataFactory; private readonly IDataProtectorTokenFactory<OrgDeleteTokenable> _orgDeleteTokenDataFactory;
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand; private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
private readonly ICloudOrganizationSignUpCommand _cloudOrganizationSignUpCommand; private readonly ICloudOrganizationSignUpCommand _cloudOrganizationSignUpCommand;
private readonly IOrganizationDeleteCommand _organizationDeleteCommand;
public OrganizationsController( public OrganizationsController(
IOrganizationRepository organizationRepository, IOrganizationRepository organizationRepository,
@ -78,7 +80,8 @@ public class OrganizationsController : Controller
IProviderBillingService providerBillingService, IProviderBillingService providerBillingService,
IDataProtectorTokenFactory<OrgDeleteTokenable> orgDeleteTokenDataFactory, IDataProtectorTokenFactory<OrgDeleteTokenable> orgDeleteTokenDataFactory,
IRemoveOrganizationUserCommand removeOrganizationUserCommand, IRemoveOrganizationUserCommand removeOrganizationUserCommand,
ICloudOrganizationSignUpCommand cloudOrganizationSignUpCommand) ICloudOrganizationSignUpCommand cloudOrganizationSignUpCommand,
IOrganizationDeleteCommand organizationDeleteCommand)
{ {
_organizationRepository = organizationRepository; _organizationRepository = organizationRepository;
_organizationUserRepository = organizationUserRepository; _organizationUserRepository = organizationUserRepository;
@ -99,6 +102,7 @@ public class OrganizationsController : Controller
_orgDeleteTokenDataFactory = orgDeleteTokenDataFactory; _orgDeleteTokenDataFactory = orgDeleteTokenDataFactory;
_removeOrganizationUserCommand = removeOrganizationUserCommand; _removeOrganizationUserCommand = removeOrganizationUserCommand;
_cloudOrganizationSignUpCommand = cloudOrganizationSignUpCommand; _cloudOrganizationSignUpCommand = cloudOrganizationSignUpCommand;
_organizationDeleteCommand = organizationDeleteCommand;
} }
[HttpGet("{id}")] [HttpGet("{id}")]
@ -303,7 +307,7 @@ public class OrganizationsController : Controller
} }
} }
await _organizationService.DeleteAsync(organization); await _organizationDeleteCommand.DeleteAsync(organization);
} }
[HttpPost("{id}/delete-recover-token")] [HttpPost("{id}/delete-recover-token")]
@ -333,7 +337,7 @@ public class OrganizationsController : Controller
} }
} }
await _organizationService.DeleteAsync(organization); await _organizationDeleteCommand.DeleteAsync(organization);
} }
[HttpPost("{id}/api-key")] [HttpPost("{id}/api-key")]

View File

@ -57,6 +57,7 @@ public class OrganizationResponseModel : ResponseModel
MaxAutoscaleSmServiceAccounts = organization.MaxAutoscaleSmServiceAccounts; MaxAutoscaleSmServiceAccounts = organization.MaxAutoscaleSmServiceAccounts;
LimitCollectionCreation = organization.LimitCollectionCreation; LimitCollectionCreation = organization.LimitCollectionCreation;
LimitCollectionDeletion = organization.LimitCollectionDeletion; LimitCollectionDeletion = organization.LimitCollectionDeletion;
LimitItemDeletion = organization.LimitItemDeletion;
AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems; AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems;
UseRiskInsights = organization.UseRiskInsights; UseRiskInsights = organization.UseRiskInsights;
} }
@ -102,6 +103,7 @@ public class OrganizationResponseModel : ResponseModel
public int? MaxAutoscaleSmServiceAccounts { get; set; } public int? MaxAutoscaleSmServiceAccounts { get; set; }
public bool LimitCollectionCreation { get; set; } public bool LimitCollectionCreation { get; set; }
public bool LimitCollectionDeletion { get; set; } public bool LimitCollectionDeletion { get; set; }
public bool LimitItemDeletion { get; set; }
public bool AllowAdminAccessToAllCollectionItems { get; set; } public bool AllowAdminAccessToAllCollectionItems { get; set; }
public bool UseRiskInsights { get; set; } public bool UseRiskInsights { get; set; }
} }

View File

@ -67,6 +67,7 @@ public class ProfileOrganizationResponseModel : ResponseModel
AccessSecretsManager = organization.AccessSecretsManager; AccessSecretsManager = organization.AccessSecretsManager;
LimitCollectionCreation = organization.LimitCollectionCreation; LimitCollectionCreation = organization.LimitCollectionCreation;
LimitCollectionDeletion = organization.LimitCollectionDeletion; LimitCollectionDeletion = organization.LimitCollectionDeletion;
LimitItemDeletion = organization.LimitItemDeletion;
AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems; AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems;
UserIsManagedByOrganization = organizationIdsManagingUser.Contains(organization.OrganizationId); UserIsManagedByOrganization = organizationIdsManagingUser.Contains(organization.OrganizationId);
UseRiskInsights = organization.UseRiskInsights; UseRiskInsights = organization.UseRiskInsights;
@ -128,6 +129,7 @@ public class ProfileOrganizationResponseModel : ResponseModel
public bool AccessSecretsManager { get; set; } public bool AccessSecretsManager { get; set; }
public bool LimitCollectionCreation { get; set; } public bool LimitCollectionCreation { get; set; }
public bool LimitCollectionDeletion { get; set; } public bool LimitCollectionDeletion { get; set; }
public bool LimitItemDeletion { get; set; }
public bool AllowAdminAccessToAllCollectionItems { get; set; } public bool AllowAdminAccessToAllCollectionItems { get; set; }
/// <summary> /// <summary>
/// Indicates if the organization manages the user. /// Indicates if the organization manages the user.

View File

@ -47,6 +47,7 @@ public class ProfileProviderOrganizationResponseModel : ProfileOrganizationRespo
ProductTierType = StaticStore.GetPlan(organization.PlanType).ProductTier; ProductTierType = StaticStore.GetPlan(organization.PlanType).ProductTier;
LimitCollectionCreation = organization.LimitCollectionCreation; LimitCollectionCreation = organization.LimitCollectionCreation;
LimitCollectionDeletion = organization.LimitCollectionDeletion; LimitCollectionDeletion = organization.LimitCollectionDeletion;
LimitItemDeletion = organization.LimitItemDeletion;
AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems; AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems;
UseRiskInsights = organization.UseRiskInsights; UseRiskInsights = organization.UseRiskInsights;
} }

View File

@ -41,7 +41,7 @@ public class PoliciesController : Controller
/// </remarks> /// </remarks>
/// <param name="type">The type of policy to be retrieved.</param> /// <param name="type">The type of policy to be retrieved.</param>
[HttpGet("{type}")] [HttpGet("{type}")]
[ProducesResponseType(typeof(GroupResponseModel), (int)HttpStatusCode.OK)] [ProducesResponseType(typeof(PolicyResponseModel), (int)HttpStatusCode.OK)]
[ProducesResponseType((int)HttpStatusCode.NotFound)] [ProducesResponseType((int)HttpStatusCode.NotFound)]
public async Task<IActionResult> Get(PolicyType type) public async Task<IActionResult> Get(PolicyType type)
{ {

View File

@ -50,6 +50,7 @@ public class MemberResponseModel : MemberBaseModel, IResponseModel
Status = user.Status; Status = user.Status;
Collections = collections?.Select(c => new AssociationWithPermissionsResponseModel(c)); Collections = collections?.Select(c => new AssociationWithPermissionsResponseModel(c));
ResetPasswordEnrolled = user.ResetPasswordKey != null; ResetPasswordEnrolled = user.ResetPasswordKey != null;
SsoExternalId = user.SsoExternalId;
} }
/// <summary> /// <summary>
@ -104,4 +105,10 @@ public class MemberResponseModel : MemberBaseModel, IResponseModel
/// </summary> /// </summary>
[Required] [Required]
public bool ResetPasswordEnrolled { get; } public bool ResetPasswordEnrolled { get; }
/// <summary>
/// SSO external identifier for linking this member to an identity provider.
/// </summary>
/// <example>sso_external_id_123456</example>
public string SsoExternalId { get; set; }
} }

View File

@ -969,11 +969,28 @@ public class AccountsController : Controller
[RequireFeature(FeatureFlagKeys.NewDeviceVerification)] [RequireFeature(FeatureFlagKeys.NewDeviceVerification)]
[AllowAnonymous] [AllowAnonymous]
[HttpPost("resend-new-device-otp")] [HttpPost("resend-new-device-otp")]
public async Task ResendNewDeviceOtpAsync([FromBody] UnauthenticatedSecretVerificatioRequestModel request) public async Task ResendNewDeviceOtpAsync([FromBody] UnauthenticatedSecretVerificationRequestModel request)
{ {
await _userService.ResendNewDeviceVerificationEmail(request.Email, request.Secret); await _userService.ResendNewDeviceVerificationEmail(request.Email, request.Secret);
} }
[RequireFeature(FeatureFlagKeys.NewDeviceVerification)]
[HttpPost("verify-devices")]
[HttpPut("verify-devices")]
public async Task SetUserVerifyDevicesAsync([FromBody] SetVerifyDevicesRequestModel request)
{
var user = await _userService.GetUserByPrincipalAsync(User) ?? throw new UnauthorizedAccessException();
if (!await _userService.VerifySecretAsync(user, request.Secret))
{
await Task.Delay(2000);
throw new BadRequestException(string.Empty, "User verification failed.");
}
user.VerifyDevices = request.VerifyDevices;
await _userService.SaveUserAsync(user);
}
private async Task<IEnumerable<Guid>> GetOrganizationIdsManagingUserAsync(Guid userId) private async Task<IEnumerable<Guid>> GetOrganizationIdsManagingUserAsync(Guid userId)
{ {
var organizationManagingUser = await _userService.GetOrganizationsManagingUserAsync(userId); var organizationManagingUser = await _userService.GetOrganizationsManagingUserAsync(userId);

View File

@ -0,0 +1,9 @@
using System.ComponentModel.DataAnnotations;
namespace Bit.Api.Auth.Models.Request.Accounts;
public class SetVerifyDevicesRequestModel : SecretVerificationRequestModel
{
[Required]
public bool VerifyDevices { get; set; }
}

View File

@ -3,7 +3,7 @@ using Bit.Core.Utilities;
namespace Bit.Api.Auth.Models.Request.Accounts; namespace Bit.Api.Auth.Models.Request.Accounts;
public class UnauthenticatedSecretVerificatioRequestModel : SecretVerificationRequestModel public class UnauthenticatedSecretVerificationRequestModel : SecretVerificationRequestModel
{ {
[Required] [Required]
[StrictEmailAddress] [StrictEmailAddress]

View File

@ -1,7 +1,8 @@
#nullable enable #nullable enable
using Bit.Api.AdminConsole.Models.Request.Organizations;
using Bit.Api.Billing.Models.Requests; using Bit.Api.Billing.Models.Requests;
using Bit.Api.Billing.Models.Responses; using Bit.Api.Billing.Models.Responses;
using Bit.Core; using Bit.Core.Billing.Models.Sales;
using Bit.Core.Billing.Services; using Bit.Core.Billing.Services;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Repositories; using Bit.Core.Repositories;
@ -21,7 +22,8 @@ public class OrganizationBillingController(
IOrganizationRepository organizationRepository, IOrganizationRepository organizationRepository,
IPaymentService paymentService, IPaymentService paymentService,
ISubscriberService subscriberService, ISubscriberService subscriberService,
IPaymentHistoryService paymentHistoryService) : BaseBillingController IPaymentHistoryService paymentHistoryService,
IUserService userService) : BaseBillingController
{ {
[HttpGet("metadata")] [HttpGet("metadata")]
public async Task<IResult> GetMetadataAsync([FromRoute] Guid organizationId) public async Task<IResult> GetMetadataAsync([FromRoute] Guid organizationId)
@ -136,11 +138,6 @@ public class OrganizationBillingController(
[HttpGet("payment-method")] [HttpGet("payment-method")]
public async Task<IResult> GetPaymentMethodAsync([FromRoute] Guid organizationId) public async Task<IResult> GetPaymentMethodAsync([FromRoute] Guid organizationId)
{ {
if (!featureService.IsEnabled(FeatureFlagKeys.AC2476_DeprecateStripeSourcesAPI))
{
return Error.NotFound();
}
if (!await currentContext.EditPaymentMethods(organizationId)) if (!await currentContext.EditPaymentMethods(organizationId))
{ {
return Error.Unauthorized(); return Error.Unauthorized();
@ -165,11 +162,6 @@ public class OrganizationBillingController(
[FromRoute] Guid organizationId, [FromRoute] Guid organizationId,
[FromBody] UpdatePaymentMethodRequestBody requestBody) [FromBody] UpdatePaymentMethodRequestBody requestBody)
{ {
if (!featureService.IsEnabled(FeatureFlagKeys.AC2476_DeprecateStripeSourcesAPI))
{
return Error.NotFound();
}
if (!await currentContext.EditPaymentMethods(organizationId)) if (!await currentContext.EditPaymentMethods(organizationId))
{ {
return Error.Unauthorized(); return Error.Unauthorized();
@ -196,11 +188,6 @@ public class OrganizationBillingController(
[FromRoute] Guid organizationId, [FromRoute] Guid organizationId,
[FromBody] VerifyBankAccountRequestBody requestBody) [FromBody] VerifyBankAccountRequestBody requestBody)
{ {
if (!featureService.IsEnabled(FeatureFlagKeys.AC2476_DeprecateStripeSourcesAPI))
{
return Error.NotFound();
}
if (!await currentContext.EditPaymentMethods(organizationId)) if (!await currentContext.EditPaymentMethods(organizationId))
{ {
return Error.Unauthorized(); return Error.Unauthorized();
@ -226,11 +213,6 @@ public class OrganizationBillingController(
[HttpGet("tax-information")] [HttpGet("tax-information")]
public async Task<IResult> GetTaxInformationAsync([FromRoute] Guid organizationId) public async Task<IResult> GetTaxInformationAsync([FromRoute] Guid organizationId)
{ {
if (!featureService.IsEnabled(FeatureFlagKeys.AC2476_DeprecateStripeSourcesAPI))
{
return Error.NotFound();
}
if (!await currentContext.EditPaymentMethods(organizationId)) if (!await currentContext.EditPaymentMethods(organizationId))
{ {
return Error.Unauthorized(); return Error.Unauthorized();
@ -255,11 +237,6 @@ public class OrganizationBillingController(
[FromRoute] Guid organizationId, [FromRoute] Guid organizationId,
[FromBody] TaxInformationRequestBody requestBody) [FromBody] TaxInformationRequestBody requestBody)
{ {
if (!featureService.IsEnabled(FeatureFlagKeys.AC2476_DeprecateStripeSourcesAPI))
{
return Error.NotFound();
}
if (!await currentContext.EditPaymentMethods(organizationId)) if (!await currentContext.EditPaymentMethods(organizationId))
{ {
return Error.Unauthorized(); return Error.Unauthorized();
@ -278,4 +255,35 @@ public class OrganizationBillingController(
return TypedResults.Ok(); return TypedResults.Ok();
} }
[HttpPost("restart-subscription")]
public async Task<IResult> RestartSubscriptionAsync([FromRoute] Guid organizationId,
[FromBody] OrganizationCreateRequestModel model)
{
var user = await userService.GetUserByPrincipalAsync(User);
if (user == null)
{
throw new UnauthorizedAccessException();
}
if (!await currentContext.EditPaymentMethods(organizationId))
{
return Error.Unauthorized();
}
var organization = await organizationRepository.GetByIdAsync(organizationId);
if (organization == null)
{
return Error.NotFound();
}
var organizationSignup = model.ToOrganizationSignup(user);
var sale = OrganizationSale.From(organization, organizationSignup);
var plan = StaticStore.GetPlan(model.PlanType);
sale.Organization.PlanType = plan.Type;
sale.Organization.Plan = plan.Name;
await organizationBillingService.Finalize(sale);
return TypedResults.Ok();
}
} }

View File

@ -9,6 +9,7 @@ public record OrganizationMetadataResponse(
bool IsSubscriptionUnpaid, bool IsSubscriptionUnpaid,
bool HasSubscription, bool HasSubscription,
bool HasOpenInvoice, bool HasOpenInvoice,
bool IsSubscriptionCanceled,
DateTime? InvoiceDueDate, DateTime? InvoiceDueDate,
DateTime? InvoiceCreatedDate, DateTime? InvoiceCreatedDate,
DateTime? SubPeriodEndDate) DateTime? SubPeriodEndDate)
@ -21,6 +22,7 @@ public record OrganizationMetadataResponse(
metadata.IsSubscriptionUnpaid, metadata.IsSubscriptionUnpaid,
metadata.HasSubscription, metadata.HasSubscription,
metadata.HasOpenInvoice, metadata.HasOpenInvoice,
metadata.IsSubscriptionCanceled,
metadata.InvoiceDueDate, metadata.InvoiceDueDate,
metadata.InvoiceCreatedDate, metadata.InvoiceCreatedDate,
metadata.SubPeriodEndDate); metadata.SubPeriodEndDate);

View File

@ -6,7 +6,6 @@ using Bit.Api.Models.Response;
using Bit.Core.Auth.Models.Api.Request; using Bit.Core.Auth.Models.Api.Request;
using Bit.Core.Auth.Models.Api.Response; using Bit.Core.Auth.Models.Api.Response;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
@ -70,11 +69,17 @@ public class DevicesController : Controller
} }
[HttpGet("")] [HttpGet("")]
public async Task<ListResponseModel<DeviceResponseModel>> Get() public async Task<ListResponseModel<DeviceAuthRequestResponseModel>> Get()
{ {
ICollection<Device> devices = await _deviceRepository.GetManyByUserIdAsync(_userService.GetProperUserId(User).Value); var devicesWithPendingAuthData = await _deviceRepository.GetManyByUserIdWithDeviceAuth(_userService.GetProperUserId(User).Value);
var responses = devices.Select(d => new DeviceResponseModel(d));
return new ListResponseModel<DeviceResponseModel>(responses); // Convert from DeviceAuthDetails to DeviceAuthRequestResponseModel
var deviceAuthRequestResponseList = devicesWithPendingAuthData
.Select(DeviceAuthRequestResponseModel.From)
.ToList();
var response = new ListResponseModel<DeviceAuthRequestResponseModel>(deviceAuthRequestResponseList);
return response;
} }
[HttpPost("")] [HttpPost("")]

View File

@ -1,5 +1,4 @@
using Bit.Api.Models.Response; using Bit.Api.Models.Response;
using Bit.Core.Repositories;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@ -10,10 +9,6 @@ namespace Bit.Api.Controllers;
[Authorize("Web")] [Authorize("Web")]
public class PlansController : Controller public class PlansController : Controller
{ {
private readonly ITaxRateRepository _taxRateRepository;
public PlansController(ITaxRateRepository taxRateRepository) => _taxRateRepository = taxRateRepository;
[HttpGet("")] [HttpGet("")]
[AllowAnonymous] [AllowAnonymous]
public ListResponseModel<PlanResponseModel> Get() public ListResponseModel<PlanResponseModel> Get()
@ -21,12 +16,4 @@ public class PlansController : Controller
var responses = StaticStore.Plans.Select(plan => new PlanResponseModel(plan)); var responses = StaticStore.Plans.Select(plan => new PlanResponseModel(plan));
return new ListResponseModel<PlanResponseModel>(responses); return new ListResponseModel<PlanResponseModel>(responses);
} }
[HttpGet("sales-tax-rates")]
public async Task<ListResponseModel<TaxRateResponseModel>> GetTaxRates()
{
var data = await _taxRateRepository.GetAllActiveAsync();
var responses = data.Select(x => new TaxRateResponseModel(x));
return new ListResponseModel<TaxRateResponseModel>(responses);
}
} }

View File

@ -7,12 +7,14 @@ public class OrganizationCollectionManagementUpdateRequestModel
{ {
public bool LimitCollectionCreation { get; set; } public bool LimitCollectionCreation { get; set; }
public bool LimitCollectionDeletion { get; set; } public bool LimitCollectionDeletion { get; set; }
public bool LimitItemDeletion { get; set; }
public bool AllowAdminAccessToAllCollectionItems { get; set; } public bool AllowAdminAccessToAllCollectionItems { get; set; }
public virtual Organization ToOrganization(Organization existingOrganization, IFeatureService featureService) public virtual Organization ToOrganization(Organization existingOrganization, IFeatureService featureService)
{ {
existingOrganization.LimitCollectionCreation = LimitCollectionCreation; existingOrganization.LimitCollectionCreation = LimitCollectionCreation;
existingOrganization.LimitCollectionDeletion = LimitCollectionDeletion; existingOrganization.LimitCollectionDeletion = LimitCollectionDeletion;
existingOrganization.LimitItemDeletion = LimitItemDeletion;
existingOrganization.AllowAdminAccessToAllCollectionItems = AllowAdminAccessToAllCollectionItems; existingOrganization.AllowAdminAccessToAllCollectionItems = AllowAdminAccessToAllCollectionItems;
return existingOrganization; return existingOrganization;
} }

View File

@ -37,6 +37,7 @@ public class ProfileResponseModel : ResponseModel
UsesKeyConnector = user.UsesKeyConnector; UsesKeyConnector = user.UsesKeyConnector;
AvatarColor = user.AvatarColor; AvatarColor = user.AvatarColor;
CreationDate = user.CreationDate; CreationDate = user.CreationDate;
VerifyDevices = user.VerifyDevices;
Organizations = organizationsUserDetails?.Select(o => new ProfileOrganizationResponseModel(o, organizationIdsManagingUser)); Organizations = organizationsUserDetails?.Select(o => new ProfileOrganizationResponseModel(o, organizationIdsManagingUser));
Providers = providerUserDetails?.Select(p => new ProfileProviderResponseModel(p)); Providers = providerUserDetails?.Select(p => new ProfileProviderResponseModel(p));
ProviderOrganizations = ProviderOrganizations =
@ -62,6 +63,7 @@ public class ProfileResponseModel : ResponseModel
public bool UsesKeyConnector { get; set; } public bool UsesKeyConnector { get; set; }
public string AvatarColor { get; set; } public string AvatarColor { get; set; }
public DateTime CreationDate { get; set; } public DateTime CreationDate { get; set; }
public bool VerifyDevices { get; set; }
public IEnumerable<ProfileOrganizationResponseModel> Organizations { get; set; } public IEnumerable<ProfileOrganizationResponseModel> Organizations { get; set; }
public IEnumerable<ProfileProviderResponseModel> Providers { get; set; } public IEnumerable<ProfileProviderResponseModel> Providers { get; set; }
public IEnumerable<ProfileProviderOrganizationResponseModel> ProviderOrganizations { get; set; } public IEnumerable<ProfileProviderOrganizationResponseModel> ProviderOrganizations { get; set; }

View File

@ -1,28 +0,0 @@
using Bit.Core.Entities;
using Bit.Core.Models.Api;
namespace Bit.Api.Models.Response;
public class TaxRateResponseModel : ResponseModel
{
public TaxRateResponseModel(TaxRate taxRate)
: base("profile")
{
if (taxRate == null)
{
throw new ArgumentNullException(nameof(taxRate));
}
Id = taxRate.Id;
Country = taxRate.Country;
State = taxRate.State;
PostalCode = taxRate.PostalCode;
Rate = taxRate.Rate;
}
public string Id { get; set; }
public string Country { get; set; }
public string State { get; set; }
public string PostalCode { get; set; }
public decimal Rate { get; set; }
}

View File

@ -29,6 +29,7 @@ using Bit.Core.Vault.Entities;
using Bit.Api.Auth.Models.Request.WebAuthn; using Bit.Api.Auth.Models.Request.WebAuthn;
using Bit.Core.Auth.Models.Data; using Bit.Core.Auth.Models.Data;
using Bit.Core.Auth.Identity.TokenProviders; using Bit.Core.Auth.Identity.TokenProviders;
using Bit.Core.Tools.ImportFeatures;
using Bit.Core.Tools.ReportFeatures; using Bit.Core.Tools.ReportFeatures;
@ -175,6 +176,7 @@ public class Startup
services.AddCoreLocalizationServices(); services.AddCoreLocalizationServices();
services.AddBillingOperations(); services.AddBillingOperations();
services.AddReportingServices(); services.AddReportingServices();
services.AddImportServices();
// Authorization Handlers // Authorization Handlers
services.AddAuthorizationHandlers(); services.AddAuthorizationHandlers();

View File

@ -7,7 +7,7 @@ using Bit.Core.Exceptions;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Settings; using Bit.Core.Settings;
using Bit.Core.Vault.Services; using Bit.Core.Tools.ImportFeatures.Interfaces;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@ -17,31 +17,30 @@ namespace Bit.Api.Tools.Controllers;
[Authorize("Application")] [Authorize("Application")]
public class ImportCiphersController : Controller public class ImportCiphersController : Controller
{ {
private readonly ICipherService _cipherService;
private readonly IUserService _userService; private readonly IUserService _userService;
private readonly ICurrentContext _currentContext; private readonly ICurrentContext _currentContext;
private readonly ILogger<ImportCiphersController> _logger; private readonly ILogger<ImportCiphersController> _logger;
private readonly GlobalSettings _globalSettings; private readonly GlobalSettings _globalSettings;
private readonly ICollectionRepository _collectionRepository; private readonly ICollectionRepository _collectionRepository;
private readonly IAuthorizationService _authorizationService; private readonly IAuthorizationService _authorizationService;
private readonly IImportCiphersCommand _importCiphersCommand;
public ImportCiphersController( public ImportCiphersController(
ICipherService cipherService,
IUserService userService, IUserService userService,
ICurrentContext currentContext, ICurrentContext currentContext,
ILogger<ImportCiphersController> logger, ILogger<ImportCiphersController> logger,
GlobalSettings globalSettings, GlobalSettings globalSettings,
ICollectionRepository collectionRepository, ICollectionRepository collectionRepository,
IAuthorizationService authorizationService, IAuthorizationService authorizationService,
IOrganizationRepository organizationRepository) IImportCiphersCommand importCiphersCommand)
{ {
_cipherService = cipherService;
_userService = userService; _userService = userService;
_currentContext = currentContext; _currentContext = currentContext;
_logger = logger; _logger = logger;
_globalSettings = globalSettings; _globalSettings = globalSettings;
_collectionRepository = collectionRepository; _collectionRepository = collectionRepository;
_authorizationService = authorizationService; _authorizationService = authorizationService;
_importCiphersCommand = importCiphersCommand;
} }
[HttpPost("import")] [HttpPost("import")]
@ -57,7 +56,7 @@ public class ImportCiphersController : Controller
var userId = _userService.GetProperUserId(User).Value; var userId = _userService.GetProperUserId(User).Value;
var folders = model.Folders.Select(f => f.ToFolder(userId)).ToList(); var folders = model.Folders.Select(f => f.ToFolder(userId)).ToList();
var ciphers = model.Ciphers.Select(c => c.ToCipherDetails(userId, false)).ToList(); var ciphers = model.Ciphers.Select(c => c.ToCipherDetails(userId, false)).ToList();
await _cipherService.ImportCiphersAsync(folders, ciphers, model.FolderRelationships); await _importCiphersCommand.ImportIntoIndividualVaultAsync(folders, ciphers, model.FolderRelationships);
} }
[HttpPost("import-organization")] [HttpPost("import-organization")]
@ -85,7 +84,7 @@ public class ImportCiphersController : Controller
var userId = _userService.GetProperUserId(User).Value; var userId = _userService.GetProperUserId(User).Value;
var ciphers = model.Ciphers.Select(l => l.ToOrganizationCipherDetails(orgId)).ToList(); var ciphers = model.Ciphers.Select(l => l.ToOrganizationCipherDetails(orgId)).ToList();
await _cipherService.ImportCiphersAsync(collections, ciphers, model.CollectionRelationships, userId); await _importCiphersCommand.ImportIntoOrganizationalVaultAsync(collections, ciphers, model.CollectionRelationships, userId);
} }
private async Task<bool> CheckOrgImportPermission(List<Collection> collections, Guid orgId) private async Task<bool> CheckOrgImportPermission(List<Collection> collections, Guid orgId)

View File

@ -1,16 +1,11 @@
using Bit.Api.Models.Response; using Bit.Api.Tools.Authorization;
using Bit.Api.Tools.Authorization;
using Bit.Api.Tools.Models.Response; using Bit.Api.Tools.Models.Response;
using Bit.Api.Vault.Models.Response;
using Bit.Core;
using Bit.Core.AdminConsole.OrganizationFeatures.Shared.Authorization; using Bit.Core.AdminConsole.OrganizationFeatures.Shared.Authorization;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Settings; using Bit.Core.Settings;
using Bit.Core.Vault.Models.Data;
using Bit.Core.Vault.Queries; using Bit.Core.Vault.Queries;
using Bit.Core.Vault.Services; using Bit.Core.Vault.Services;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
@ -56,39 +51,6 @@ public class OrganizationExportController : Controller
[HttpGet("export")] [HttpGet("export")]
public async Task<IActionResult> Export(Guid organizationId) public async Task<IActionResult> Export(Guid organizationId)
{
if (_featureService.IsEnabled(FeatureFlagKeys.PM11360RemoveProviderExportPermission))
{
return await Export_vNext(organizationId);
}
var userId = _userService.GetProperUserId(User).Value;
IEnumerable<Collection> orgCollections = await _collectionService.GetOrganizationCollectionsAsync(organizationId);
(IEnumerable<CipherOrganizationDetails> orgCiphers, Dictionary<Guid, IGrouping<Guid, CollectionCipher>> collectionCiphersGroupDict) = await _cipherService.GetOrganizationCiphers(userId, organizationId);
if (_currentContext.ClientVersion == null || _currentContext.ClientVersion >= new Version("2023.1.0"))
{
var organizationExportResponseModel = new OrganizationExportResponseModel
{
Collections = orgCollections.Select(c => new CollectionResponseModel(c)),
Ciphers = orgCiphers.Select(c => new CipherMiniDetailsResponseModel(c, _globalSettings, collectionCiphersGroupDict, c.OrganizationUseTotp))
};
return Ok(organizationExportResponseModel);
}
// Backward compatibility with versions before 2023.1.0 that use ListResponseModel
var organizationExportListResponseModel = new OrganizationExportListResponseModel
{
Collections = GetOrganizationCollectionsResponse(orgCollections),
Ciphers = GetOrganizationCiphersResponse(orgCiphers, collectionCiphersGroupDict)
};
return Ok(organizationExportListResponseModel);
}
private async Task<IActionResult> Export_vNext(Guid organizationId)
{ {
var canExportAll = await _authorizationService.AuthorizeAsync(User, new OrganizationScope(organizationId), var canExportAll = await _authorizationService.AuthorizeAsync(User, new OrganizationScope(organizationId),
VaultExportOperations.ExportWholeVault); VaultExportOperations.ExportWholeVault);
@ -116,19 +78,4 @@ public class OrganizationExportController : Controller
// Unauthorized // Unauthorized
throw new NotFoundException(); throw new NotFoundException();
} }
private ListResponseModel<CollectionResponseModel> GetOrganizationCollectionsResponse(IEnumerable<Collection> orgCollections)
{
var collections = orgCollections.Select(c => new CollectionResponseModel(c));
return new ListResponseModel<CollectionResponseModel>(collections);
}
private ListResponseModel<CipherMiniDetailsResponseModel> GetOrganizationCiphersResponse(IEnumerable<CipherOrganizationDetails> orgCiphers,
Dictionary<Guid, IGrouping<Guid, CollectionCipher>> collectionCiphersGroupDict)
{
var responses = orgCiphers.Select(c => new CipherMiniDetailsResponseModel(c, _globalSettings,
collectionCiphersGroupDict, c.OrganizationUseTotp));
return new ListResponseModel<CipherMiniDetailsResponseModel>(responses);
}
} }

View File

@ -9,7 +9,6 @@ using Bit.Core.Context;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Settings; using Bit.Core.Settings;
using Bit.Core.Tools.Entities;
using Bit.Core.Tools.Enums; using Bit.Core.Tools.Enums;
using Bit.Core.Tools.Models.Data; using Bit.Core.Tools.Models.Data;
using Bit.Core.Tools.Repositories; using Bit.Core.Tools.Repositories;
@ -163,32 +162,6 @@ public class SendsController : Controller
return new SendResponseModel(send, _globalSettings); return new SendResponseModel(send, _globalSettings);
} }
[HttpPost("file")]
[Obsolete("Deprecated File Send API", false)]
[RequestSizeLimit(Constants.FileSize101mb)]
[DisableFormValueModelBinding]
public async Task<SendResponseModel> PostFile()
{
if (!Request?.ContentType.Contains("multipart/") ?? true)
{
throw new BadRequestException("Invalid content.");
}
Send send = null;
await Request.GetSendFileAsync(async (stream, fileName, model) =>
{
model.ValidateCreation();
var userId = _userService.GetProperUserId(User).Value;
var (madeSend, madeData) = model.ToSend(userId, fileName, _sendService);
send = madeSend;
await _sendService.SaveFileSendAsync(send, madeData, model.FileLength.GetValueOrDefault(0));
await _sendService.UploadFileToExistingSendAsync(stream, send);
});
return new SendResponseModel(send, _globalSettings);
}
[HttpPost("file/v2")] [HttpPost("file/v2")]
public async Task<SendFileUploadDataResponseModel> PostFile([FromBody] SendRequestModel model) public async Task<SendFileUploadDataResponseModel> PostFile([FromBody] SendRequestModel model)
{ {

View File

@ -4,6 +4,7 @@ using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Authorization;
using Bit.Core.IdentityServer; using Bit.Core.IdentityServer;
using Bit.Core.Settings; using Bit.Core.Settings;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using Bit.Core.Vault.Authorization.SecurityTasks;
using Bit.SharedWeb.Health; using Bit.SharedWeb.Health;
using Bit.SharedWeb.Swagger; using Bit.SharedWeb.Swagger;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
@ -104,5 +105,7 @@ public static class ServiceCollectionExtensions
services.AddScoped<IAuthorizationHandler, CollectionAuthorizationHandler>(); services.AddScoped<IAuthorizationHandler, CollectionAuthorizationHandler>();
services.AddScoped<IAuthorizationHandler, GroupAuthorizationHandler>(); services.AddScoped<IAuthorizationHandler, GroupAuthorizationHandler>();
services.AddScoped<IAuthorizationHandler, VaultExportAuthorizationHandler>(); services.AddScoped<IAuthorizationHandler, VaultExportAuthorizationHandler>();
services.AddScoped<IAuthorizationHandler, SecurityTaskAuthorizationHandler>();
services.AddScoped<IAuthorizationHandler, SecurityTaskOrganizationAuthorizationHandler>();
} }
} }

View File

@ -1097,7 +1097,7 @@ public class CiphersController : Controller
[HttpDelete("{id}/attachment/{attachmentId}")] [HttpDelete("{id}/attachment/{attachmentId}")]
[HttpPost("{id}/attachment/{attachmentId}/delete")] [HttpPost("{id}/attachment/{attachmentId}/delete")]
public async Task DeleteAttachment(Guid id, string attachmentId) public async Task<DeleteAttachmentResponseData> DeleteAttachment(Guid id, string attachmentId)
{ {
var userId = _userService.GetProperUserId(User).Value; var userId = _userService.GetProperUserId(User).Value;
var cipher = await GetByIdAsync(id, userId); var cipher = await GetByIdAsync(id, userId);
@ -1106,7 +1106,7 @@ public class CiphersController : Controller
throw new NotFoundException(); throw new NotFoundException();
} }
await _cipherService.DeleteAttachmentAsync(cipher, attachmentId, userId, false); return await _cipherService.DeleteAttachmentAsync(cipher, attachmentId, userId, false);
} }
[HttpDelete("{id}/attachment/{attachmentId}/admin")] [HttpDelete("{id}/attachment/{attachmentId}/admin")]

View File

@ -19,15 +19,18 @@ public class SecurityTaskController : Controller
private readonly IUserService _userService; private readonly IUserService _userService;
private readonly IGetTaskDetailsForUserQuery _getTaskDetailsForUserQuery; private readonly IGetTaskDetailsForUserQuery _getTaskDetailsForUserQuery;
private readonly IMarkTaskAsCompleteCommand _markTaskAsCompleteCommand; private readonly IMarkTaskAsCompleteCommand _markTaskAsCompleteCommand;
private readonly IGetTasksForOrganizationQuery _getTasksForOrganizationQuery;
public SecurityTaskController( public SecurityTaskController(
IUserService userService, IUserService userService,
IGetTaskDetailsForUserQuery getTaskDetailsForUserQuery, IGetTaskDetailsForUserQuery getTaskDetailsForUserQuery,
IMarkTaskAsCompleteCommand markTaskAsCompleteCommand) IMarkTaskAsCompleteCommand markTaskAsCompleteCommand,
IGetTasksForOrganizationQuery getTasksForOrganizationQuery)
{ {
_userService = userService; _userService = userService;
_getTaskDetailsForUserQuery = getTaskDetailsForUserQuery; _getTaskDetailsForUserQuery = getTaskDetailsForUserQuery;
_markTaskAsCompleteCommand = markTaskAsCompleteCommand; _markTaskAsCompleteCommand = markTaskAsCompleteCommand;
_getTasksForOrganizationQuery = getTasksForOrganizationQuery;
} }
/// <summary> /// <summary>
@ -54,4 +57,18 @@ public class SecurityTaskController : Controller
await _markTaskAsCompleteCommand.CompleteAsync(taskId); await _markTaskAsCompleteCommand.CompleteAsync(taskId);
return NoContent(); return NoContent();
} }
/// <summary>
/// Retrieves security tasks for an organization. Restricted to organization administrators.
/// </summary>
/// <param name="organizationId">The organization Id</param>
/// <param name="status">Optional filter for task status. If not provided, returns tasks of all statuses.</param>
[HttpGet("organization")]
public async Task<ListResponseModel<SecurityTasksResponseModel>> ListForOrganization(
[FromQuery] Guid organizationId, [FromQuery] SecurityTaskStatus? status)
{
var securityTasks = await _getTasksForOrganizationQuery.GetTasksAsync(organizationId, status);
var response = securityTasks.Select(x => new SecurityTasksResponseModel(x)).ToList();
return new ListResponseModel<SecurityTasksResponseModel>(response);
}
} }

View File

@ -17,7 +17,6 @@ public class FreshdeskController : Controller
private readonly BillingSettings _billingSettings; private readonly BillingSettings _billingSettings;
private readonly IUserRepository _userRepository; private readonly IUserRepository _userRepository;
private readonly IOrganizationRepository _organizationRepository; private readonly IOrganizationRepository _organizationRepository;
private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly ILogger<FreshdeskController> _logger; private readonly ILogger<FreshdeskController> _logger;
private readonly GlobalSettings _globalSettings; private readonly GlobalSettings _globalSettings;
private readonly IHttpClientFactory _httpClientFactory; private readonly IHttpClientFactory _httpClientFactory;
@ -25,7 +24,6 @@ public class FreshdeskController : Controller
public FreshdeskController( public FreshdeskController(
IUserRepository userRepository, IUserRepository userRepository,
IOrganizationRepository organizationRepository, IOrganizationRepository organizationRepository,
IOrganizationUserRepository organizationUserRepository,
IOptions<BillingSettings> billingSettings, IOptions<BillingSettings> billingSettings,
ILogger<FreshdeskController> logger, ILogger<FreshdeskController> logger,
GlobalSettings globalSettings, GlobalSettings globalSettings,
@ -34,7 +32,6 @@ public class FreshdeskController : Controller
_billingSettings = billingSettings?.Value; _billingSettings = billingSettings?.Value;
_userRepository = userRepository; _userRepository = userRepository;
_organizationRepository = organizationRepository; _organizationRepository = organizationRepository;
_organizationUserRepository = organizationUserRepository;
_logger = logger; _logger = logger;
_globalSettings = globalSettings; _globalSettings = globalSettings;
_httpClientFactory = httpClientFactory; _httpClientFactory = httpClientFactory;

View File

@ -1,53 +0,0 @@
using Microsoft.AspNetCore.Mvc;
namespace Billing.Controllers;
public class LoginController : Controller
{
/*
private readonly PasswordlessSignInManager<IdentityUser> _signInManager;
public LoginController(
PasswordlessSignInManager<IdentityUser> signInManager)
{
_signInManager = signInManager;
}
public IActionResult Index()
{
return View();
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Index(LoginModel model)
{
if (ModelState.IsValid)
{
var result = await _signInManager.PasswordlessSignInAsync(model.Email,
Url.Action("Confirm", "Login", null, Request.Scheme));
if (result.Succeeded)
{
return RedirectToAction("Index", "Home");
}
else
{
ModelState.AddModelError(string.Empty, "Account not found.");
}
}
return View(model);
}
public async Task<IActionResult> Confirm(string email, string token)
{
var result = await _signInManager.PasswordlessSignInAsync(email, token, false);
if (!result.Succeeded)
{
return View("Error");
}
return RedirectToAction("Index", "Home");
}
*/
}

View File

@ -32,5 +32,6 @@ public class JobsHostedService : BaseJobsHostedService
public static void AddJobsServices(IServiceCollection services) public static void AddJobsServices(IServiceCollection services)
{ {
services.AddTransient<AliveJob>(); services.AddTransient<AliveJob>();
services.AddTransient<SubscriptionCancellationJob>();
} }
} }

View File

@ -0,0 +1,58 @@
using Bit.Billing.Services;
using Bit.Core.Repositories;
using Quartz;
using Stripe;
namespace Bit.Billing.Jobs;
public class SubscriptionCancellationJob(
IStripeFacade stripeFacade,
IOrganizationRepository organizationRepository)
: IJob
{
public async Task Execute(IJobExecutionContext context)
{
var subscriptionId = context.MergedJobDataMap.GetString("subscriptionId");
var organizationId = new Guid(context.MergedJobDataMap.GetString("organizationId") ?? string.Empty);
var organization = await organizationRepository.GetByIdAsync(organizationId);
if (organization == null || organization.Enabled)
{
// Organization was deleted or re-enabled by CS, skip cancellation
return;
}
var subscription = await stripeFacade.GetSubscription(subscriptionId);
if (subscription?.Status != "unpaid")
{
// Subscription is no longer unpaid, skip cancellation
return;
}
// Cancel the subscription
await stripeFacade.CancelSubscription(subscriptionId, new SubscriptionCancelOptions());
// Void any open invoices
var options = new InvoiceListOptions
{
Status = "open",
Subscription = subscriptionId,
Limit = 100
};
var invoices = await stripeFacade.ListInvoices(options);
foreach (var invoice in invoices)
{
await stripeFacade.VoidInvoice(invoice.Id);
}
while (invoices.HasMore)
{
options.StartingAfter = invoices.Data.Last().Id;
invoices = await stripeFacade.ListInvoices(options);
foreach (var invoice in invoices)
{
await stripeFacade.VoidInvoice(invoice.Id);
}
}
}
}

View File

@ -80,12 +80,6 @@ public interface IStripeFacade
RequestOptions requestOptions = null, RequestOptions requestOptions = null,
CancellationToken cancellationToken = default); CancellationToken cancellationToken = default);
Task<TaxRate> GetTaxRate(
string taxRateId,
TaxRateGetOptions options = null,
RequestOptions requestOptions = null,
CancellationToken cancellationToken = default);
Task<Discount> DeleteCustomerDiscount( Task<Discount> DeleteCustomerDiscount(
string customerId, string customerId,
RequestOptions requestOptions = null, RequestOptions requestOptions = null,

View File

@ -296,7 +296,7 @@ public class StripeEventUtilityService : IStripeEventUtilityService
btObjIdField = "provider_id"; btObjIdField = "provider_id";
btObjId = providerId.Value; btObjId = providerId.Value;
} }
var btInvoiceAmount = invoice.AmountDue / 100M; var btInvoiceAmount = Math.Round(invoice.AmountDue / 100M, 2);
var existingTransactions = organizationId.HasValue var existingTransactions = organizationId.HasValue
? await _transactionRepository.GetManyByOrganizationIdAsync(organizationId.Value) ? await _transactionRepository.GetManyByOrganizationIdAsync(organizationId.Value)
@ -318,26 +318,34 @@ public class StripeEventUtilityService : IStripeEventUtilityService
Result<Braintree.Transaction> transactionResult; Result<Braintree.Transaction> transactionResult;
try try
{ {
transactionResult = await _btGateway.Transaction.SaleAsync( var transactionRequest = new Braintree.TransactionRequest
new Braintree.TransactionRequest {
Amount = btInvoiceAmount,
CustomerId = customer.Metadata["btCustomerId"],
Options = new Braintree.TransactionOptionsRequest
{ {
Amount = btInvoiceAmount, SubmitForSettlement = true,
CustomerId = customer.Metadata["btCustomerId"], PayPal = new Braintree.TransactionOptionsPayPalRequest
Options = new Braintree.TransactionOptionsRequest
{ {
SubmitForSettlement = true, CustomField =
PayPal = new Braintree.TransactionOptionsPayPalRequest $"{btObjIdField}:{btObjId},region:{_globalSettings.BaseServiceUri.CloudRegion}"
{
CustomField =
$"{btObjIdField}:{btObjId},region:{_globalSettings.BaseServiceUri.CloudRegion}"
}
},
CustomFields = new Dictionary<string, string>
{
[btObjIdField] = btObjId.ToString(),
["region"] = _globalSettings.BaseServiceUri.CloudRegion
} }
}); },
CustomFields = new Dictionary<string, string>
{
[btObjIdField] = btObjId.ToString(),
["region"] = _globalSettings.BaseServiceUri.CloudRegion
}
};
_logger.LogInformation("Creating Braintree transaction with Amount: {Amount}, CustomerId: {CustomerId}, " +
"CustomField: {CustomField}, CustomFields: {@CustomFields}",
transactionRequest.Amount,
transactionRequest.CustomerId,
transactionRequest.Options.PayPal.CustomField,
transactionRequest.CustomFields);
transactionResult = await _btGateway.Transaction.SaleAsync(transactionRequest);
} }
catch (NotFoundException e) catch (NotFoundException e)
{ {
@ -345,9 +353,19 @@ public class StripeEventUtilityService : IStripeEventUtilityService
"Attempted to make a payment with Braintree, but customer did not exist for the given btCustomerId present on the Stripe metadata"); "Attempted to make a payment with Braintree, but customer did not exist for the given btCustomerId present on the Stripe metadata");
throw; throw;
} }
catch (Exception e)
{
_logger.LogError(e, "Exception occurred while trying to pay invoice with Braintree");
throw;
}
if (!transactionResult.IsSuccess()) if (!transactionResult.IsSuccess())
{ {
_logger.LogWarning("Braintree transaction failed. Error: {ErrorMessage}, Transaction Status: {Status}, Validation Errors: {ValidationErrors}",
transactionResult.Message,
transactionResult.Target?.Status,
string.Join(", ", transactionResult.Errors.DeepAll().Select(e => $"Code: {e.Code}, Message: {e.Message}, Attribute: {e.Attribute}")));
if (invoice.AttemptCount < 4) if (invoice.AttemptCount < 4)
{ {
await _mailService.SendPaymentFailedAsync(customer.Email, btInvoiceAmount, true); await _mailService.SendPaymentFailedAsync(customer.Email, btInvoiceAmount, true);

View File

@ -10,7 +10,6 @@ public class StripeFacade : IStripeFacade
private readonly InvoiceService _invoiceService = new(); private readonly InvoiceService _invoiceService = new();
private readonly PaymentMethodService _paymentMethodService = new(); private readonly PaymentMethodService _paymentMethodService = new();
private readonly SubscriptionService _subscriptionService = new(); private readonly SubscriptionService _subscriptionService = new();
private readonly TaxRateService _taxRateService = new();
private readonly DiscountService _discountService = new(); private readonly DiscountService _discountService = new();
public async Task<Charge> GetCharge( public async Task<Charge> GetCharge(
@ -99,13 +98,6 @@ public class StripeFacade : IStripeFacade
CancellationToken cancellationToken = default) => CancellationToken cancellationToken = default) =>
await _subscriptionService.CancelAsync(subscriptionId, options, requestOptions, cancellationToken); await _subscriptionService.CancelAsync(subscriptionId, options, requestOptions, cancellationToken);
public async Task<TaxRate> GetTaxRate(
string taxRateId,
TaxRateGetOptions options = null,
RequestOptions requestOptions = null,
CancellationToken cancellationToken = default) =>
await _taxRateService.GetAsync(taxRateId, options, requestOptions, cancellationToken);
public async Task<Discount> DeleteCustomerDiscount( public async Task<Discount> DeleteCustomerDiscount(
string customerId, string customerId,
RequestOptions requestOptions = null, RequestOptions requestOptions = null,

View File

@ -1,9 +1,12 @@
using Bit.Billing.Constants; using Bit.Billing.Constants;
using Bit.Billing.Jobs;
using Bit.Core;
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces; using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
using Bit.Core.Platform.Push; using Bit.Core.Platform.Push;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using Quartz;
using Stripe; using Stripe;
using Event = Stripe.Event; using Event = Stripe.Event;
@ -19,6 +22,8 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler
private readonly IUserService _userService; private readonly IUserService _userService;
private readonly IPushNotificationService _pushNotificationService; private readonly IPushNotificationService _pushNotificationService;
private readonly IOrganizationRepository _organizationRepository; private readonly IOrganizationRepository _organizationRepository;
private readonly ISchedulerFactory _schedulerFactory;
private readonly IFeatureService _featureService;
public SubscriptionUpdatedHandler( public SubscriptionUpdatedHandler(
IStripeEventService stripeEventService, IStripeEventService stripeEventService,
@ -28,7 +33,9 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler
IOrganizationSponsorshipRenewCommand organizationSponsorshipRenewCommand, IOrganizationSponsorshipRenewCommand organizationSponsorshipRenewCommand,
IUserService userService, IUserService userService,
IPushNotificationService pushNotificationService, IPushNotificationService pushNotificationService,
IOrganizationRepository organizationRepository) IOrganizationRepository organizationRepository,
ISchedulerFactory schedulerFactory,
IFeatureService featureService)
{ {
_stripeEventService = stripeEventService; _stripeEventService = stripeEventService;
_stripeEventUtilityService = stripeEventUtilityService; _stripeEventUtilityService = stripeEventUtilityService;
@ -38,6 +45,8 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler
_userService = userService; _userService = userService;
_pushNotificationService = pushNotificationService; _pushNotificationService = pushNotificationService;
_organizationRepository = organizationRepository; _organizationRepository = organizationRepository;
_schedulerFactory = schedulerFactory;
_featureService = featureService;
} }
/// <summary> /// <summary>
@ -55,6 +64,10 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler
when organizationId.HasValue: when organizationId.HasValue:
{ {
await _organizationService.DisableAsync(organizationId.Value, subscription.CurrentPeriodEnd); await _organizationService.DisableAsync(organizationId.Value, subscription.CurrentPeriodEnd);
if (subscription.Status == StripeSubscriptionStatus.Unpaid)
{
await ScheduleCancellationJobAsync(subscription.Id, organizationId.Value);
}
break; break;
} }
case StripeSubscriptionStatus.Unpaid or StripeSubscriptionStatus.IncompleteExpired: case StripeSubscriptionStatus.Unpaid or StripeSubscriptionStatus.IncompleteExpired:
@ -183,4 +196,27 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler
await _stripeFacade.DeleteSubscriptionDiscount(subscription.Id); await _stripeFacade.DeleteSubscriptionDiscount(subscription.Id);
} }
} }
private async Task ScheduleCancellationJobAsync(string subscriptionId, Guid organizationId)
{
var isResellerManagedOrgAlertEnabled = _featureService.IsEnabled(FeatureFlagKeys.ResellerManagedOrgAlert);
if (isResellerManagedOrgAlertEnabled)
{
var scheduler = await _schedulerFactory.GetScheduler();
var job = JobBuilder.Create<SubscriptionCancellationJob>()
.WithIdentity($"cancel-sub-{subscriptionId}", "subscription-cancellations")
.UsingJobData("subscriptionId", subscriptionId)
.UsingJobData("organizationId", organizationId.ToString())
.Build();
var trigger = TriggerBuilder.Create()
.WithIdentity($"cancel-trigger-{subscriptionId}", "subscription-cancellations")
.StartAt(DateTimeOffset.UtcNow.AddDays(7))
.Build();
await scheduler.ScheduleJob(job, trigger);
}
}
} }

View File

@ -1,7 +1,6 @@
using Bit.Billing.Constants; using Bit.Billing.Constants;
using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Constants;
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces; using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
@ -161,18 +160,13 @@ public class UpcomingInvoiceHandler : IUpcomingInvoiceHandler
private async Task<Subscription> TryEnableAutomaticTaxAsync(Subscription subscription) private async Task<Subscription> TryEnableAutomaticTaxAsync(Subscription subscription)
{ {
var customerGetOptions = new CustomerGetOptions { Expand = ["tax"] }; if (subscription.AutomaticTax.Enabled)
var customer = await _stripeFacade.GetCustomer(subscription.CustomerId, customerGetOptions);
if (subscription.AutomaticTax.Enabled ||
customer.Tax?.AutomaticTax != StripeConstants.AutomaticTaxStatus.Supported)
{ {
return subscription; return subscription;
} }
var subscriptionUpdateOptions = new SubscriptionUpdateOptions var subscriptionUpdateOptions = new SubscriptionUpdateOptions
{ {
DefaultTaxRates = [],
AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true } AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }
}; };

View File

@ -9,6 +9,7 @@ using Bit.Core.Settings;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using Bit.SharedWeb.Utilities; using Bit.SharedWeb.Utilities;
using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.DependencyInjection.Extensions;
using Quartz;
using Stripe; using Stripe;
namespace Bit.Billing; namespace Bit.Billing;
@ -101,6 +102,13 @@ public class Startup
services.AddScoped<IStripeEventService, StripeEventService>(); services.AddScoped<IStripeEventService, StripeEventService>();
services.AddScoped<IProviderEventService, ProviderEventService>(); services.AddScoped<IProviderEventService, ProviderEventService>();
// Add Quartz services first
services.AddQuartz(q =>
{
q.UseMicrosoftDependencyInjectionJobFactory();
});
services.AddQuartzHostedService();
// Jobs service // Jobs service
Jobs.JobsHostedService.AddJobsServices(services); Jobs.JobsHostedService.AddJobsServices(services);
services.AddHostedService<Jobs.JobsHostedService>(); services.AddHostedService<Jobs.JobsHostedService>();

View File

@ -103,6 +103,12 @@ public class Organization : ITableObject<Guid>, IStorableSubscriber, IRevisable,
/// </summary> /// </summary>
public bool AllowAdminAccessToAllCollectionItems { get; set; } public bool AllowAdminAccessToAllCollectionItems { get; set; }
/// <summary>
/// If set to true, members can only delete items when they have a Can Manage permission over the collection.
/// If set to false, members can delete items when they have a Can Manage OR Can Edit permission over the collection.
/// </summary>
public bool LimitItemDeletion { get; set; }
/// <summary> /// <summary>
/// Risk Insights is a reporting feature that provides insights into the security of an organization's vault. /// Risk Insights is a reporting feature that provides insights into the security of an organization's vault.
/// </summary> /// </summary>

View File

@ -4,10 +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", Order = 0)] [Display(ShortName = "MSP", Name = "Managed Service Provider", Description = "Creates provider portal for client organization management", Order = 0)]
Msp = 0, Msp = 0,
[Display(ShortName = "Reseller", Name = "Reseller", Description = "Access to clients billing", Order = 1000)] [Display(ShortName = "Reseller", Name = "Reseller", Description = "Creates Bitwarden Portal page for client organization billing management", Order = 1000)]
Reseller = 1, Reseller = 1,
[Display(ShortName = "MOE", Name = "Multi-organization Enterprise", Description = "Access to multiple organizations", Order = 1)] [Display(ShortName = "MOE", Name = "Multi-organization Enterprises", Description = "Creates provider portal for multi-organization management", Order = 1)]
MultiOrganizationEnterprise = 2, MultiOrganizationEnterprise = 2,
} }

View File

@ -23,6 +23,7 @@ public class OrganizationAbility
UsePolicies = organization.UsePolicies; UsePolicies = organization.UsePolicies;
LimitCollectionCreation = organization.LimitCollectionCreation; LimitCollectionCreation = organization.LimitCollectionCreation;
LimitCollectionDeletion = organization.LimitCollectionDeletion; LimitCollectionDeletion = organization.LimitCollectionDeletion;
LimitItemDeletion = organization.LimitItemDeletion;
AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems; AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems;
UseRiskInsights = organization.UseRiskInsights; UseRiskInsights = organization.UseRiskInsights;
} }
@ -41,6 +42,7 @@ public class OrganizationAbility
public bool UsePolicies { get; set; } public bool UsePolicies { get; set; }
public bool LimitCollectionCreation { get; set; } public bool LimitCollectionCreation { get; set; }
public bool LimitCollectionDeletion { get; set; } public bool LimitCollectionDeletion { get; set; }
public bool LimitItemDeletion { get; set; }
public bool AllowAdminAccessToAllCollectionItems { get; set; } public bool AllowAdminAccessToAllCollectionItems { get; set; }
public bool UseRiskInsights { get; set; } public bool UseRiskInsights { get; set; }
} }

View File

@ -56,6 +56,7 @@ public class OrganizationUserOrganizationDetails
public int? SmServiceAccounts { get; set; } public int? SmServiceAccounts { get; set; }
public bool LimitCollectionCreation { get; set; } public bool LimitCollectionCreation { get; set; }
public bool LimitCollectionDeletion { get; set; } public bool LimitCollectionDeletion { get; set; }
public bool LimitItemDeletion { get; set; }
public bool AllowAdminAccessToAllCollectionItems { get; set; } public bool AllowAdminAccessToAllCollectionItems { get; set; }
public bool UseRiskInsights { get; set; } public bool UseRiskInsights { get; set; }
} }

View File

@ -146,6 +146,7 @@ public class SelfHostedOrganizationDetails : Organization
OwnersNotifiedOfAutoscaling = OwnersNotifiedOfAutoscaling, OwnersNotifiedOfAutoscaling = OwnersNotifiedOfAutoscaling,
LimitCollectionCreation = LimitCollectionCreation, LimitCollectionCreation = LimitCollectionCreation,
LimitCollectionDeletion = LimitCollectionDeletion, LimitCollectionDeletion = LimitCollectionDeletion,
LimitItemDeletion = LimitItemDeletion,
AllowAdminAccessToAllCollectionItems = AllowAdminAccessToAllCollectionItems, AllowAdminAccessToAllCollectionItems = AllowAdminAccessToAllCollectionItems,
Status = Status Status = Status
}; };

View File

@ -42,6 +42,7 @@ public class ProviderUserOrganizationDetails
public PlanType PlanType { get; set; } public PlanType PlanType { get; set; }
public bool LimitCollectionCreation { get; set; } public bool LimitCollectionCreation { get; set; }
public bool LimitCollectionDeletion { get; set; } public bool LimitCollectionDeletion { get; set; }
public bool LimitItemDeletion { get; set; }
public bool AllowAdminAccessToAllCollectionItems { get; set; } public bool AllowAdminAccessToAllCollectionItems { get; set; }
public bool UseRiskInsights { get; set; } public bool UseRiskInsights { get; set; }
public ProviderType ProviderType { get; set; } public ProviderType ProviderType { get; set; }

View File

@ -0,0 +1,14 @@
using Bit.Core.Models.Mail;
namespace Bit.Core.AdminConsole.Models.Mail;
public class DeviceApprovalRequestedViewModel : BaseMailModel
{
public Guid OrganizationId { get; set; }
public string UserNameRequestingAccess { get; set; }
public string Url => string.Format("{0}/organizations/{1}/settings/device-approvals",
WebVaultUrl,
OrganizationId);
}

View File

@ -6,6 +6,6 @@ namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interface
public interface IUpdateOrganizationUserCommand public interface IUpdateOrganizationUserCommand
{ {
Task UpdateUserAsync(OrganizationUser user, Guid? savingUserId, Task UpdateUserAsync(OrganizationUser organizationUser, Guid? savingUserId,
List<CollectionAccessSelection>? collectionAccess, IEnumerable<Guid>? groupAccess); List<CollectionAccessSelection>? collectionAccess, IEnumerable<Guid>? groupAccess);
} }

View File

@ -1,6 +1,7 @@
#nullable enable #nullable enable
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Enums;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
@ -49,48 +50,64 @@ public class UpdateOrganizationUserCommand : IUpdateOrganizationUserCommand
/// <summary> /// <summary>
/// Update an organization user. /// Update an organization user.
/// </summary> /// </summary>
/// <param name="user">The modified user to save.</param> /// <param name="organizationUser">The modified organization user to save.</param>
/// <param name="savingUserId">The userId of the currently logged in user who is making the change.</param> /// <param name="savingUserId">The userId of the currently logged in user who is making the change.</param>
/// <param name="collectionAccess">The user's updated collection access. If set to null, this removes all collection access.</param> /// <param name="collectionAccess">The user's updated collection access. If set to null, this removes all collection access.</param>
/// <param name="groupAccess">The user's updated group access. If set to null, groups are not updated.</param> /// <param name="groupAccess">The user's updated group access. If set to null, groups are not updated.</param>
/// <exception cref="BadRequestException"></exception> /// <exception cref="BadRequestException"></exception>
public async Task UpdateUserAsync(OrganizationUser user, Guid? savingUserId, public async Task UpdateUserAsync(OrganizationUser organizationUser, Guid? savingUserId,
List<CollectionAccessSelection>? collectionAccess, IEnumerable<Guid>? groupAccess) List<CollectionAccessSelection>? collectionAccess, IEnumerable<Guid>? groupAccess)
{ {
// Avoid multiple enumeration // Avoid multiple enumeration
collectionAccess = collectionAccess?.ToList(); collectionAccess = collectionAccess?.ToList();
groupAccess = groupAccess?.ToList(); groupAccess = groupAccess?.ToList();
if (user.Id.Equals(default(Guid))) if (organizationUser.Id.Equals(default(Guid)))
{ {
throw new BadRequestException("Invite the user first."); throw new BadRequestException("Invite the user first.");
} }
var originalUser = await _organizationUserRepository.GetByIdAsync(user.Id); var originalOrganizationUser = await _organizationUserRepository.GetByIdAsync(organizationUser.Id);
if (originalUser == null || user.OrganizationId != originalUser.OrganizationId) if (originalOrganizationUser == null || organizationUser.OrganizationId != originalOrganizationUser.OrganizationId)
{ {
throw new NotFoundException(); throw new NotFoundException();
} }
var organization = await _organizationRepository.GetByIdAsync(organizationUser.OrganizationId);
if (organization == null)
{
throw new NotFoundException();
}
if (organizationUser.UserId.HasValue && organization.PlanType == PlanType.Free && organizationUser.Type is OrganizationUserType.Admin or OrganizationUserType.Owner)
{
// Since free organizations only supports a few users there is not much point in avoiding N+1 queries for this.
var adminCount = await _organizationUserRepository.GetCountByFreeOrganizationAdminUserAsync(organizationUser.UserId.Value);
if (adminCount > 0)
{
throw new BadRequestException("User can only be an admin of one free organization.");
}
}
if (collectionAccess?.Any() == true) if (collectionAccess?.Any() == true)
{ {
await ValidateCollectionAccessAsync(originalUser, collectionAccess.ToList()); await ValidateCollectionAccessAsync(originalOrganizationUser, collectionAccess.ToList());
} }
if (groupAccess?.Any() == true) if (groupAccess?.Any() == true)
{ {
await ValidateGroupAccessAsync(originalUser, groupAccess.ToList()); await ValidateGroupAccessAsync(originalOrganizationUser, groupAccess.ToList());
} }
if (savingUserId.HasValue) if (savingUserId.HasValue)
{ {
await _organizationService.ValidateOrganizationUserUpdatePermissions(user.OrganizationId, user.Type, originalUser.Type, user.GetPermissions()); await _organizationService.ValidateOrganizationUserUpdatePermissions(organizationUser.OrganizationId, organizationUser.Type, originalOrganizationUser.Type, organizationUser.GetPermissions());
} }
await _organizationService.ValidateOrganizationCustomPermissionsEnabledAsync(user.OrganizationId, user.Type); await _organizationService.ValidateOrganizationCustomPermissionsEnabledAsync(organizationUser.OrganizationId, organizationUser.Type);
if (user.Type != OrganizationUserType.Owner && if (organizationUser.Type != OrganizationUserType.Owner &&
!await _hasConfirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync(user.OrganizationId, new[] { user.Id })) !await _hasConfirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync(organizationUser.OrganizationId, new[] { organizationUser.Id }))
{ {
throw new BadRequestException("Organization must have at least one confirmed owner."); throw new BadRequestException("Organization must have at least one confirmed owner.");
} }
@ -106,26 +123,25 @@ public class UpdateOrganizationUserCommand : IUpdateOrganizationUserCommand
// Only autoscale (if required) after all validation has passed so that we know it's a valid request before // Only autoscale (if required) after all validation has passed so that we know it's a valid request before
// updating Stripe // updating Stripe
if (!originalUser.AccessSecretsManager && user.AccessSecretsManager) if (!originalOrganizationUser.AccessSecretsManager && organizationUser.AccessSecretsManager)
{ {
var additionalSmSeatsRequired = await _countNewSmSeatsRequiredQuery.CountNewSmSeatsRequiredAsync(user.OrganizationId, 1); var additionalSmSeatsRequired = await _countNewSmSeatsRequiredQuery.CountNewSmSeatsRequiredAsync(organizationUser.OrganizationId, 1);
if (additionalSmSeatsRequired > 0) if (additionalSmSeatsRequired > 0)
{ {
var organization = await _organizationRepository.GetByIdAsync(user.OrganizationId);
var update = new SecretsManagerSubscriptionUpdate(organization, true) var update = new SecretsManagerSubscriptionUpdate(organization, true)
.AdjustSeats(additionalSmSeatsRequired); .AdjustSeats(additionalSmSeatsRequired);
await _updateSecretsManagerSubscriptionCommand.UpdateSubscriptionAsync(update); await _updateSecretsManagerSubscriptionCommand.UpdateSubscriptionAsync(update);
} }
} }
await _organizationUserRepository.ReplaceAsync(user, collectionAccess); await _organizationUserRepository.ReplaceAsync(organizationUser, collectionAccess);
if (groupAccess != null) if (groupAccess != null)
{ {
await _organizationUserRepository.UpdateGroupsAsync(user.Id, groupAccess); await _organizationUserRepository.UpdateGroupsAsync(organizationUser.Id, groupAccess);
} }
await _eventService.LogOrganizationUserEventAsync(user, EventType.OrganizationUser_Updated); await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Updated);
} }
private async Task ValidateCollectionAccessAsync(OrganizationUser originalUser, private async Task ValidateCollectionAccessAsync(OrganizationUser originalUser,

View File

@ -125,28 +125,8 @@ public class CloudOrganizationSignUpCommand(
} }
else if (plan.Type != PlanType.Free) else if (plan.Type != PlanType.Free)
{ {
if (featureService.IsEnabled(FeatureFlagKeys.AC2476_DeprecateStripeSourcesAPI)) var sale = OrganizationSale.From(organization, signup);
{ await organizationBillingService.Finalize(sale);
var sale = OrganizationSale.From(organization, signup);
await organizationBillingService.Finalize(sale);
}
else
{
if (signup.PaymentMethodType != null)
{
await paymentService.PurchaseOrganizationAsync(organization, signup.PaymentMethodType.Value,
signup.PaymentToken, plan, signup.AdditionalStorageGb, signup.AdditionalSeats,
signup.PremiumAccessAddon, signup.TaxInfo, signup.IsFromProvider, signup.AdditionalSmSeats.GetValueOrDefault(),
signup.AdditionalServiceAccounts.GetValueOrDefault(), signup.IsFromSecretsManagerTrial);
}
else
{
await paymentService.PurchaseOrganizationNoPaymentMethod(organization, plan, signup.AdditionalSeats,
signup.PremiumAccessAddon, signup.AdditionalSmSeats.GetValueOrDefault(),
signup.AdditionalServiceAccounts.GetValueOrDefault(), signup.IsFromSecretsManagerTrial);
}
}
} }
var ownerId = signup.IsFromProvider ? default : signup.Owner.Id; var ownerId = signup.IsFromProvider ? default : signup.Owner.Id;

View File

@ -0,0 +1,13 @@
using Bit.Core.AdminConsole.Entities;
namespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;
public interface IOrganizationDeleteCommand
{
/// <summary>
/// Permanently deletes an organization and performs necessary cleanup.
/// </summary>
/// <param name="organization">The organization to delete.</param>
/// <exception cref="BadRequestException">Thrown when the organization cannot be deleted due to configuration constraints.</exception>
Task DeleteAsync(Organization organization);
}

View File

@ -0,0 +1,14 @@
using Bit.Core.AdminConsole.Entities;
namespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;
public interface IOrganizationInitiateDeleteCommand
{
/// <summary>
/// Initiates a secure deletion process for an organization by requesting confirmation from an organization admin.
/// </summary>
/// <param name="organization">The organization to be deleted.</param>
/// <param name="orgAdminEmail">The email address of the organization admin who will confirm the deletion.</param>
/// <exception cref="BadRequestException">Thrown when the specified admin email is invalid or lacks sufficient permissions.</exception>
Task InitiateDeleteAsync(Organization organization, string orgAdminEmail);
}

View File

@ -0,0 +1,69 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Repositories;
using Bit.Core.Context;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Tools.Enums;
using Bit.Core.Tools.Models.Business;
using Bit.Core.Tools.Services;
namespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
public class OrganizationDeleteCommand : IOrganizationDeleteCommand
{
private readonly IApplicationCacheService _applicationCacheService;
private readonly ICurrentContext _currentContext;
private readonly IOrganizationRepository _organizationRepository;
private readonly IPaymentService _paymentService;
private readonly IReferenceEventService _referenceEventService;
private readonly ISsoConfigRepository _ssoConfigRepository;
public OrganizationDeleteCommand(
IApplicationCacheService applicationCacheService,
ICurrentContext currentContext,
IOrganizationRepository organizationRepository,
IPaymentService paymentService,
IReferenceEventService referenceEventService,
ISsoConfigRepository ssoConfigRepository)
{
_applicationCacheService = applicationCacheService;
_currentContext = currentContext;
_organizationRepository = organizationRepository;
_paymentService = paymentService;
_referenceEventService = referenceEventService;
_ssoConfigRepository = ssoConfigRepository;
}
public async Task DeleteAsync(Organization organization)
{
await ValidateDeleteOrganizationAsync(organization);
if (!string.IsNullOrWhiteSpace(organization.GatewaySubscriptionId))
{
try
{
var eop = !organization.ExpirationDate.HasValue ||
organization.ExpirationDate.Value >= DateTime.UtcNow;
await _paymentService.CancelSubscriptionAsync(organization, eop);
await _referenceEventService.RaiseEventAsync(
new ReferenceEvent(ReferenceEventType.DeleteAccount, organization, _currentContext));
}
catch (GatewayException) { }
}
await _organizationRepository.DeleteAsync(organization);
await _applicationCacheService.DeleteOrganizationAbilityAsync(organization.Id);
}
private async Task ValidateDeleteOrganizationAsync(Organization organization)
{
var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(organization.Id);
if (ssoConfig?.GetData()?.MemberDecryptionType == MemberDecryptionType.KeyConnector)
{
throw new BadRequestException("You cannot delete an Organization that is using Key Connector.");
}
}
}

View File

@ -0,0 +1,49 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Models.Business.Tokenables;
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Tokens;
namespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
public class OrganizationInitiateDeleteCommand : IOrganizationInitiateDeleteCommand
{
private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly IUserRepository _userRepository;
private readonly IDataProtectorTokenFactory<OrgDeleteTokenable> _orgDeleteTokenDataFactory;
private readonly IMailService _mailService;
public const string OrganizationAdminNotFoundErrorMessage = "Org admin not found.";
public OrganizationInitiateDeleteCommand(
IOrganizationUserRepository organizationUserRepository,
IUserRepository userRepository,
IDataProtectorTokenFactory<OrgDeleteTokenable> orgDeleteTokenDataFactory,
IMailService mailService)
{
_organizationUserRepository = organizationUserRepository;
_userRepository = userRepository;
_orgDeleteTokenDataFactory = orgDeleteTokenDataFactory;
_mailService = mailService;
}
public async Task InitiateDeleteAsync(Organization organization, string orgAdminEmail)
{
var orgAdmin = await _userRepository.GetByEmailAsync(orgAdminEmail);
if (orgAdmin == null)
{
throw new BadRequestException(OrganizationAdminNotFoundErrorMessage);
}
var orgAdminOrgUser = await _organizationUserRepository.GetDetailsByUserAsync(orgAdmin.Id, organization.Id);
if (orgAdminOrgUser == null || orgAdminOrgUser.Status is not OrganizationUserStatusType.Confirmed ||
(orgAdminOrgUser.Type is not OrganizationUserType.Admin and not OrganizationUserType.Owner))
{
throw new BadRequestException(OrganizationAdminNotFoundErrorMessage);
}
var token = _orgDeleteTokenDataFactory.Protect(new OrgDeleteTokenable(organization, 1));
await _mailService.SendInitiateDeleteOrganzationEmailAsync(orgAdminEmail, organization, token);
}
}

View File

@ -87,16 +87,23 @@ public class TwoFactorAuthenticationPolicyValidator : IPolicyValidator
return; return;
} }
var organizationUsersTwoFactorEnabled = var revocableUsersWithTwoFactorStatus =
await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(currentActiveRevocableOrganizationUsers); await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(currentActiveRevocableOrganizationUsers);
if (NonCompliantMembersWillLoseAccess(currentActiveRevocableOrganizationUsers, organizationUsersTwoFactorEnabled)) var nonCompliantUsers = revocableUsersWithTwoFactorStatus.Where(x => !x.twoFactorIsEnabled);
if (!nonCompliantUsers.Any())
{
return;
}
if (MembersWithNoMasterPasswordWillLoseAccess(currentActiveRevocableOrganizationUsers, nonCompliantUsers))
{ {
throw new BadRequestException(NonCompliantMembersWillLoseAccessMessage); throw new BadRequestException(NonCompliantMembersWillLoseAccessMessage);
} }
var commandResult = await _revokeNonCompliantOrganizationUserCommand.RevokeNonCompliantOrganizationUsersAsync( var commandResult = await _revokeNonCompliantOrganizationUserCommand.RevokeNonCompliantOrganizationUsersAsync(
new RevokeOrganizationUsersRequest(organizationId, currentActiveRevocableOrganizationUsers, performedBy)); new RevokeOrganizationUsersRequest(organizationId, nonCompliantUsers.Select(x => x.user), performedBy));
if (commandResult.HasErrors) if (commandResult.HasErrors)
{ {
@ -104,7 +111,7 @@ public class TwoFactorAuthenticationPolicyValidator : IPolicyValidator
} }
await Task.WhenAll(currentActiveRevocableOrganizationUsers.Select(x => await Task.WhenAll(currentActiveRevocableOrganizationUsers.Select(x =>
_mailService.SendOrganizationUserRevokedForPolicySingleOrgEmailAsync(organization.DisplayName(), x.Email))); _mailService.SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(organization.DisplayName(), x.Email)));
} }
private async Task RemoveNonCompliantUsersAsync(Guid organizationId) private async Task RemoveNonCompliantUsersAsync(Guid organizationId)
@ -141,7 +148,7 @@ public class TwoFactorAuthenticationPolicyValidator : IPolicyValidator
} }
} }
private static bool NonCompliantMembersWillLoseAccess( private static bool MembersWithNoMasterPasswordWillLoseAccess(
IEnumerable<OrganizationUserUserDetails> orgUserDetails, IEnumerable<OrganizationUserUserDetails> orgUserDetails,
IEnumerable<(OrganizationUserUserDetails user, bool isTwoFactorEnabled)> organizationUsersTwoFactorEnabled) => IEnumerable<(OrganizationUserUserDetails user, bool isTwoFactorEnabled)> organizationUsersTwoFactorEnabled) =>
orgUserDetails.Any(x => orgUserDetails.Any(x =>

View File

@ -28,8 +28,6 @@ public interface IOrganizationService
/// </summary> /// </summary>
Task<(Organization organization, OrganizationUser organizationUser)> SignUpAsync(OrganizationLicense license, User owner, Task<(Organization organization, OrganizationUser organizationUser)> SignUpAsync(OrganizationLicense license, User owner,
string ownerKey, string collectionName, string publicKey, string privateKey); string ownerKey, string collectionName, string publicKey, string privateKey);
Task InitiateDeleteAsync(Organization organization, string orgAdminEmail);
Task DeleteAsync(Organization organization);
Task EnableAsync(Guid organizationId, DateTime? expirationDate); Task EnableAsync(Guid organizationId, DateTime? expirationDate);
Task DisableAsync(Guid organizationId, DateTime? expirationDate); Task DisableAsync(Guid organizationId, DateTime? expirationDate);
Task UpdateExpirationDateAsync(Guid organizationId, DateTime? expirationDate); Task UpdateExpirationDateAsync(Guid organizationId, DateTime? expirationDate);

View File

@ -4,7 +4,6 @@ using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.Models.Business; using Bit.Core.AdminConsole.Models.Business;
using Bit.Core.AdminConsole.Models.Business.Tokenables;
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
@ -68,7 +67,6 @@ public class OrganizationService : IOrganizationService
private readonly IProviderUserRepository _providerUserRepository; private readonly IProviderUserRepository _providerUserRepository;
private readonly ICountNewSmSeatsRequiredQuery _countNewSmSeatsRequiredQuery; private readonly ICountNewSmSeatsRequiredQuery _countNewSmSeatsRequiredQuery;
private readonly IUpdateSecretsManagerSubscriptionCommand _updateSecretsManagerSubscriptionCommand; private readonly IUpdateSecretsManagerSubscriptionCommand _updateSecretsManagerSubscriptionCommand;
private readonly IDataProtectorTokenFactory<OrgDeleteTokenable> _orgDeleteTokenDataFactory;
private readonly IProviderRepository _providerRepository; private readonly IProviderRepository _providerRepository;
private readonly IOrgUserInviteTokenableFactory _orgUserInviteTokenableFactory; private readonly IOrgUserInviteTokenableFactory _orgUserInviteTokenableFactory;
private readonly IDataProtectorTokenFactory<OrgUserInviteTokenable> _orgUserInviteTokenDataFactory; private readonly IDataProtectorTokenFactory<OrgUserInviteTokenable> _orgUserInviteTokenDataFactory;
@ -106,7 +104,6 @@ public class OrganizationService : IOrganizationService
IOrgUserInviteTokenableFactory orgUserInviteTokenableFactory, IOrgUserInviteTokenableFactory orgUserInviteTokenableFactory,
IDataProtectorTokenFactory<OrgUserInviteTokenable> orgUserInviteTokenDataFactory, IDataProtectorTokenFactory<OrgUserInviteTokenable> orgUserInviteTokenDataFactory,
IUpdateSecretsManagerSubscriptionCommand updateSecretsManagerSubscriptionCommand, IUpdateSecretsManagerSubscriptionCommand updateSecretsManagerSubscriptionCommand,
IDataProtectorTokenFactory<OrgDeleteTokenable> orgDeleteTokenDataFactory,
IProviderRepository providerRepository, IProviderRepository providerRepository,
IFeatureService featureService, IFeatureService featureService,
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery, ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
@ -139,7 +136,6 @@ public class OrganizationService : IOrganizationService
_providerUserRepository = providerUserRepository; _providerUserRepository = providerUserRepository;
_countNewSmSeatsRequiredQuery = countNewSmSeatsRequiredQuery; _countNewSmSeatsRequiredQuery = countNewSmSeatsRequiredQuery;
_updateSecretsManagerSubscriptionCommand = updateSecretsManagerSubscriptionCommand; _updateSecretsManagerSubscriptionCommand = updateSecretsManagerSubscriptionCommand;
_orgDeleteTokenDataFactory = orgDeleteTokenDataFactory;
_providerRepository = providerRepository; _providerRepository = providerRepository;
_orgUserInviteTokenableFactory = orgUserInviteTokenableFactory; _orgUserInviteTokenableFactory = orgUserInviteTokenableFactory;
_orgUserInviteTokenDataFactory = orgUserInviteTokenDataFactory; _orgUserInviteTokenDataFactory = orgUserInviteTokenDataFactory;
@ -690,44 +686,6 @@ public class OrganizationService : IOrganizationService
} }
} }
public async Task InitiateDeleteAsync(Organization organization, string orgAdminEmail)
{
var orgAdmin = await _userRepository.GetByEmailAsync(orgAdminEmail);
if (orgAdmin == null)
{
throw new BadRequestException("Org admin not found.");
}
var orgAdminOrgUser = await _organizationUserRepository.GetDetailsByUserAsync(orgAdmin.Id, organization.Id);
if (orgAdminOrgUser == null || orgAdminOrgUser.Status != OrganizationUserStatusType.Confirmed ||
(orgAdminOrgUser.Type != OrganizationUserType.Admin && orgAdminOrgUser.Type != OrganizationUserType.Owner))
{
throw new BadRequestException("Org admin not found.");
}
var token = _orgDeleteTokenDataFactory.Protect(new OrgDeleteTokenable(organization, 1));
await _mailService.SendInitiateDeleteOrganzationEmailAsync(orgAdminEmail, organization, token);
}
public async Task DeleteAsync(Organization organization)
{
await ValidateDeleteOrganizationAsync(organization);
if (!string.IsNullOrWhiteSpace(organization.GatewaySubscriptionId))
{
try
{
var eop = !organization.ExpirationDate.HasValue ||
organization.ExpirationDate.Value >= DateTime.UtcNow;
await _paymentService.CancelSubscriptionAsync(organization, eop);
await _referenceEventService.RaiseEventAsync(
new ReferenceEvent(ReferenceEventType.DeleteAccount, organization, _currentContext));
}
catch (GatewayException) { }
}
await _organizationRepository.DeleteAsync(organization);
await _applicationCacheService.DeleteOrganizationAbilityAsync(organization.Id);
}
public async Task EnableAsync(Guid organizationId, DateTime? expirationDate) public async Task EnableAsync(Guid organizationId, DateTime? expirationDate)
{ {
var org = await GetOrgById(organizationId); var org = await GetOrgById(organizationId);
@ -802,6 +760,11 @@ public class OrganizationService : IOrganizationService
Description = organization.DisplayBusinessName() Description = organization.DisplayBusinessName()
}); });
} }
if (eventType == EventType.Organization_CollectionManagement_Updated)
{
await _pushNotificationService.PushSyncOrganizationCollectionManagementSettingsAsync(organization);
}
} }
public async Task UpdateTwoFactorProviderAsync(Organization organization, TwoFactorProviderType type) public async Task UpdateTwoFactorProviderAsync(Organization organization, TwoFactorProviderType type)
@ -1973,15 +1936,6 @@ public class OrganizationService : IOrganizationService
return true; return true;
} }
private async Task ValidateDeleteOrganizationAsync(Organization organization)
{
var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(organization.Id);
if (ssoConfig?.GetData()?.MemberDecryptionType == MemberDecryptionType.KeyConnector)
{
throw new BadRequestException("You cannot delete an Organization that is using Key Connector.");
}
}
public async Task RevokeUserAsync(OrganizationUser organizationUser, Guid? revokingUserId) public async Task RevokeUserAsync(OrganizationUser organizationUser, Guid? revokingUserId)
{ {
if (revokingUserId.HasValue && organizationUser.UserId == revokingUserId.Value) if (revokingUserId.HasValue && organizationUser.UserId == revokingUserId.Value)
@ -2160,7 +2114,8 @@ public class OrganizationService : IOrganizationService
// Query Two Factor Authentication status for all users in the organization // Query Two Factor Authentication status for all users in the organization
// This is an optimization to avoid querying the Two Factor Authentication status for each user individually // This is an optimization to avoid querying the Two Factor Authentication status for each user individually
var organizationUsersTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(filteredUsers.Select(ou => ou.UserId.Value)); var organizationUsersTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(
filteredUsers.Where(ou => ou.UserId.HasValue).Select(ou => ou.UserId.Value));
var result = new List<Tuple<OrganizationUser, string>>(); var result = new List<Tuple<OrganizationUser, string>>();
@ -2183,7 +2138,8 @@ public class OrganizationService : IOrganizationService
throw new BadRequestException("Only owners can restore other owners."); throw new BadRequestException("Only owners can restore other owners.");
} }
var twoFactorIsEnabled = organizationUsersTwoFactorEnabled.FirstOrDefault(ou => ou.userId == organizationUser.UserId.Value).twoFactorIsEnabled; var twoFactorIsEnabled = organizationUser.UserId.HasValue
&& organizationUsersTwoFactorEnabled.FirstOrDefault(ou => ou.userId == organizationUser.UserId.Value).twoFactorIsEnabled;
await CheckPoliciesBeforeRestoreAsync(organizationUser, twoFactorIsEnabled); await CheckPoliciesBeforeRestoreAsync(organizationUser, twoFactorIsEnabled);
var status = GetPriorActiveOrganizationUserStatusType(organizationUser); var status = GetPriorActiveOrganizationUserStatusType(organizationUser);
@ -2242,7 +2198,7 @@ public class OrganizationService : IOrganizationService
if (!userHasTwoFactorEnabled) if (!userHasTwoFactorEnabled)
{ {
var invitedTwoFactorPolicies = await _policyService.GetPoliciesApplicableToUserAsync(userId, var invitedTwoFactorPolicies = await _policyService.GetPoliciesApplicableToUserAsync(userId,
PolicyType.TwoFactorAuthentication, OrganizationUserStatusType.Invited); PolicyType.TwoFactorAuthentication, OrganizationUserStatusType.Revoked);
if (invitedTwoFactorPolicies.Any(p => p.OrganizationId == orgUser.OrganizationId)) if (invitedTwoFactorPolicies.Any(p => p.OrganizationId == orgUser.OrganizationId))
{ {
twoFactorCompliant = false; twoFactorCompliant = false;

View File

@ -1,5 +1,12 @@
namespace Bit.Core.Auth.Enums; namespace Bit.Core.Auth.Enums;
/**
* The type of auth request.
*
* Note:
* Used by the Device_ReadActiveWithPendingAuthRequestsByUserId.sql stored procedure.
* If the enum changes be aware of this reference.
*/
public enum AuthRequestType : byte public enum AuthRequestType : byte
{ {
AuthenticateAndUnlock = 0, AuthenticateAndUnlock = 0,

View File

@ -0,0 +1,51 @@
using Bit.Core.Auth.Models.Data;
using Bit.Core.Auth.Utilities;
using Bit.Core.Enums;
using Bit.Core.Models.Api;
namespace Bit.Core.Auth.Models.Api.Response;
public class DeviceAuthRequestResponseModel : ResponseModel
{
public DeviceAuthRequestResponseModel()
: base("device") { }
public static DeviceAuthRequestResponseModel From(DeviceAuthDetails deviceAuthDetails)
{
var converted = new DeviceAuthRequestResponseModel
{
Id = deviceAuthDetails.Id,
Name = deviceAuthDetails.Name,
Type = deviceAuthDetails.Type,
Identifier = deviceAuthDetails.Identifier,
CreationDate = deviceAuthDetails.CreationDate,
IsTrusted = deviceAuthDetails.IsTrusted()
};
if (deviceAuthDetails.AuthRequestId != null && deviceAuthDetails.AuthRequestCreatedAt != null)
{
converted.DevicePendingAuthRequest = new PendingAuthRequest
{
Id = (Guid)deviceAuthDetails.AuthRequestId,
CreationDate = (DateTime)deviceAuthDetails.AuthRequestCreatedAt
};
}
return converted;
}
public Guid Id { get; set; }
public string Name { get; set; }
public DeviceType Type { get; set; }
public string Identifier { get; set; }
public DateTime CreationDate { get; set; }
public bool IsTrusted { get; set; }
public PendingAuthRequest DevicePendingAuthRequest { get; set; }
public class PendingAuthRequest
{
public Guid Id { get; set; }
public DateTime CreationDate { get; set; }
}
}

View File

@ -0,0 +1,81 @@
using Bit.Core.Auth.Utilities;
using Bit.Core.Entities;
using Bit.Core.Enums;
namespace Bit.Core.Auth.Models.Data;
public class DeviceAuthDetails : Device
{
public bool IsTrusted { get; set; }
public Guid? AuthRequestId { get; set; }
public DateTime? AuthRequestCreatedAt { get; set; }
/**
* Constructor for EF response.
*/
public DeviceAuthDetails(
Device device,
Guid? authRequestId,
DateTime? authRequestCreationDate)
{
if (device == null)
{
throw new ArgumentNullException(nameof(device));
}
Id = device.Id;
Name = device.Name;
Type = device.Type;
Identifier = device.Identifier;
CreationDate = device.CreationDate;
IsTrusted = device.IsTrusted();
AuthRequestId = authRequestId;
AuthRequestCreatedAt = authRequestCreationDate;
}
/**
* Constructor for dapper response.
* Note: if the authRequestId or authRequestCreationDate is null it comes back as
* an empty guid and a min value for datetime. That could change if the stored
* procedure runs on a different kind of db.
*/
public DeviceAuthDetails(
Guid id,
Guid userId,
string name,
short type,
string identifier,
string pushToken,
DateTime creationDate,
DateTime revisionDate,
string encryptedUserKey,
string encryptedPublicKey,
string encryptedPrivateKey,
bool active,
Guid authRequestId,
DateTime authRequestCreationDate)
{
Id = id;
Name = name;
Type = (DeviceType)type;
Identifier = identifier;
CreationDate = creationDate;
IsTrusted = new Device
{
Id = id,
UserId = userId,
Name = name,
Type = (DeviceType)type,
Identifier = identifier,
PushToken = pushToken,
RevisionDate = revisionDate,
EncryptedUserKey = encryptedUserKey,
EncryptedPublicKey = encryptedPublicKey,
EncryptedPrivateKey = encryptedPrivateKey,
Active = active
}.IsTrusted();
AuthRequestId = authRequestId != Guid.Empty ? authRequestId : null;
AuthRequestCreatedAt =
authRequestCreationDate != DateTime.MinValue ? authRequestCreationDate : null;
}
}

View File

@ -1,5 +1,4 @@
 using Bit.Core.Auth.Entities;
using Bit.Core.Auth.Entities;
namespace Bit.Core.Auth.Models.Data; namespace Bit.Core.Auth.Models.Data;

View File

@ -12,6 +12,7 @@ using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Settings; using Bit.Core.Settings;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using Microsoft.Extensions.Logging;
#nullable enable #nullable enable
@ -27,6 +28,9 @@ public class AuthRequestService : IAuthRequestService
private readonly IPushNotificationService _pushNotificationService; private readonly IPushNotificationService _pushNotificationService;
private readonly IEventService _eventService; private readonly IEventService _eventService;
private readonly IOrganizationUserRepository _organizationUserRepository; private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly IMailService _mailService;
private readonly IFeatureService _featureService;
private readonly ILogger<AuthRequestService> _logger;
public AuthRequestService( public AuthRequestService(
IAuthRequestRepository authRequestRepository, IAuthRequestRepository authRequestRepository,
@ -36,7 +40,10 @@ public class AuthRequestService : IAuthRequestService
ICurrentContext currentContext, ICurrentContext currentContext,
IPushNotificationService pushNotificationService, IPushNotificationService pushNotificationService,
IEventService eventService, IEventService eventService,
IOrganizationUserRepository organizationRepository) IOrganizationUserRepository organizationRepository,
IMailService mailService,
IFeatureService featureService,
ILogger<AuthRequestService> logger)
{ {
_authRequestRepository = authRequestRepository; _authRequestRepository = authRequestRepository;
_userRepository = userRepository; _userRepository = userRepository;
@ -46,6 +53,9 @@ public class AuthRequestService : IAuthRequestService
_pushNotificationService = pushNotificationService; _pushNotificationService = pushNotificationService;
_eventService = eventService; _eventService = eventService;
_organizationUserRepository = organizationRepository; _organizationUserRepository = organizationRepository;
_mailService = mailService;
_featureService = featureService;
_logger = logger;
} }
public async Task<AuthRequest?> GetAuthRequestAsync(Guid id, Guid userId) public async Task<AuthRequest?> GetAuthRequestAsync(Guid id, Guid userId)
@ -132,6 +142,8 @@ public class AuthRequestService : IAuthRequestService
{ {
var createdAuthRequest = await CreateAuthRequestAsync(model, user, organizationUser.OrganizationId); var createdAuthRequest = await CreateAuthRequestAsync(model, user, organizationUser.OrganizationId);
firstAuthRequest ??= createdAuthRequest; firstAuthRequest ??= createdAuthRequest;
await NotifyAdminsOfDeviceApprovalRequestAsync(organizationUser, user);
} }
// I know this won't be null because I have already validated that at least one organization exists // I know this won't be null because I have already validated that at least one organization exists
@ -276,4 +288,19 @@ public class AuthRequestService : IAuthRequestService
{ {
return DateTime.UtcNow > savedDate.Add(allowedLifetime); return DateTime.UtcNow > savedDate.Add(allowedLifetime);
} }
private async Task NotifyAdminsOfDeviceApprovalRequestAsync(OrganizationUser organizationUser, User user)
{
if (!_featureService.IsEnabled(FeatureFlagKeys.DeviceApprovalRequestAdminNotifications))
{
_logger.LogWarning("Skipped sending device approval notification to admins - feature flag disabled");
return;
}
var admins = await _organizationUserRepository.GetManyByMinimumRoleAsync(
organizationUser.OrganizationId,
OrganizationUserType.Admin);
var adminEmails = admins.Select(a => a.Email).Distinct().ToList();
await _mailService.SendDeviceApprovalRequestedNotificationEmailAsync(adminEmails, organizationUser.OrganizationId, user.Email, user.Name);
}
} }

View File

@ -23,7 +23,6 @@ namespace Bit.Core.Auth.UserFeatures.Registration.Implementations;
public class RegisterUserCommand : IRegisterUserCommand public class RegisterUserCommand : IRegisterUserCommand
{ {
private readonly IGlobalSettings _globalSettings; private readonly IGlobalSettings _globalSettings;
private readonly IOrganizationUserRepository _organizationUserRepository; private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly IPolicyRepository _policyRepository; private readonly IPolicyRepository _policyRepository;

View File

@ -22,16 +22,9 @@ public class OrganizationLicenseClaimsFactory : ILicenseClaimsFactory<Organizati
var claims = new List<Claim> var claims = new List<Claim>
{ {
new(nameof(OrganizationLicenseConstants.LicenseType), LicenseType.Organization.ToString()), new(nameof(OrganizationLicenseConstants.LicenseType), LicenseType.Organization.ToString()),
new Claim(nameof(OrganizationLicenseConstants.LicenseKey), entity.LicenseKey),
new(nameof(OrganizationLicenseConstants.InstallationId), licenseContext.InstallationId.ToString()),
new(nameof(OrganizationLicenseConstants.Id), entity.Id.ToString()), new(nameof(OrganizationLicenseConstants.Id), entity.Id.ToString()),
new(nameof(OrganizationLicenseConstants.Name), entity.Name),
new(nameof(OrganizationLicenseConstants.BillingEmail), entity.BillingEmail),
new(nameof(OrganizationLicenseConstants.Enabled), entity.Enabled.ToString()), new(nameof(OrganizationLicenseConstants.Enabled), entity.Enabled.ToString()),
new(nameof(OrganizationLicenseConstants.Plan), entity.Plan),
new(nameof(OrganizationLicenseConstants.PlanType), entity.PlanType.ToString()), new(nameof(OrganizationLicenseConstants.PlanType), entity.PlanType.ToString()),
new(nameof(OrganizationLicenseConstants.Seats), entity.Seats.ToString()),
new(nameof(OrganizationLicenseConstants.MaxCollections), entity.MaxCollections.ToString()),
new(nameof(OrganizationLicenseConstants.UsePolicies), entity.UsePolicies.ToString()), new(nameof(OrganizationLicenseConstants.UsePolicies), entity.UsePolicies.ToString()),
new(nameof(OrganizationLicenseConstants.UseSso), entity.UseSso.ToString()), new(nameof(OrganizationLicenseConstants.UseSso), entity.UseSso.ToString()),
new(nameof(OrganizationLicenseConstants.UseKeyConnector), entity.UseKeyConnector.ToString()), new(nameof(OrganizationLicenseConstants.UseKeyConnector), entity.UseKeyConnector.ToString()),
@ -43,32 +36,79 @@ public class OrganizationLicenseClaimsFactory : ILicenseClaimsFactory<Organizati
new(nameof(OrganizationLicenseConstants.Use2fa), entity.Use2fa.ToString()), new(nameof(OrganizationLicenseConstants.Use2fa), entity.Use2fa.ToString()),
new(nameof(OrganizationLicenseConstants.UseApi), entity.UseApi.ToString()), new(nameof(OrganizationLicenseConstants.UseApi), entity.UseApi.ToString()),
new(nameof(OrganizationLicenseConstants.UseResetPassword), entity.UseResetPassword.ToString()), new(nameof(OrganizationLicenseConstants.UseResetPassword), entity.UseResetPassword.ToString()),
new(nameof(OrganizationLicenseConstants.MaxStorageGb), entity.MaxStorageGb.ToString()),
new(nameof(OrganizationLicenseConstants.SelfHost), entity.SelfHost.ToString()), new(nameof(OrganizationLicenseConstants.SelfHost), entity.SelfHost.ToString()),
new(nameof(OrganizationLicenseConstants.UsersGetPremium), entity.UsersGetPremium.ToString()), new(nameof(OrganizationLicenseConstants.UsersGetPremium), entity.UsersGetPremium.ToString()),
new(nameof(OrganizationLicenseConstants.UseCustomPermissions), entity.UseCustomPermissions.ToString()), new(nameof(OrganizationLicenseConstants.UseCustomPermissions), entity.UseCustomPermissions.ToString()),
new(nameof(OrganizationLicenseConstants.Issued), DateTime.UtcNow.ToString(CultureInfo.InvariantCulture)),
new(nameof(OrganizationLicenseConstants.UsePasswordManager), entity.UsePasswordManager.ToString()), new(nameof(OrganizationLicenseConstants.UsePasswordManager), entity.UsePasswordManager.ToString()),
new(nameof(OrganizationLicenseConstants.UseSecretsManager), entity.UseSecretsManager.ToString()), new(nameof(OrganizationLicenseConstants.UseSecretsManager), entity.UseSecretsManager.ToString()),
new(nameof(OrganizationLicenseConstants.SmSeats), entity.SmSeats.ToString()),
new(nameof(OrganizationLicenseConstants.SmServiceAccounts), entity.SmServiceAccounts.ToString()),
// LimitCollectionCreationDeletion was split and removed from the // LimitCollectionCreationDeletion was split and removed from the
// license. Left here with an assignment from the new values for // license. Left here with an assignment from the new values for
// backwards compatibility. // backwards compatibility.
new(nameof(OrganizationLicenseConstants.LimitCollectionCreationDeletion), new(nameof(OrganizationLicenseConstants.LimitCollectionCreationDeletion),
(entity.LimitCollectionCreation || entity.LimitCollectionDeletion).ToString()), (entity.LimitCollectionCreation || entity.LimitCollectionDeletion).ToString()),
new(nameof(OrganizationLicenseConstants.AllowAdminAccessToAllCollectionItems), entity.AllowAdminAccessToAllCollectionItems.ToString()), new(nameof(OrganizationLicenseConstants.AllowAdminAccessToAllCollectionItems), entity.AllowAdminAccessToAllCollectionItems.ToString()),
new(nameof(OrganizationLicenseConstants.Issued), DateTime.UtcNow.ToString(CultureInfo.InvariantCulture)),
new(nameof(OrganizationLicenseConstants.Expires), expires.ToString(CultureInfo.InvariantCulture)), new(nameof(OrganizationLicenseConstants.Expires), expires.ToString(CultureInfo.InvariantCulture)),
new(nameof(OrganizationLicenseConstants.Refresh), refresh.ToString(CultureInfo.InvariantCulture)), new(nameof(OrganizationLicenseConstants.Refresh), refresh.ToString(CultureInfo.InvariantCulture)),
new(nameof(OrganizationLicenseConstants.ExpirationWithoutGracePeriod), expirationWithoutGracePeriod.ToString(CultureInfo.InvariantCulture)), new(nameof(OrganizationLicenseConstants.ExpirationWithoutGracePeriod), expirationWithoutGracePeriod.ToString(CultureInfo.InvariantCulture)),
new(nameof(OrganizationLicenseConstants.Trial), trial.ToString()), new(nameof(OrganizationLicenseConstants.Trial), trial.ToString()),
}; };
if (entity.Name is not null)
{
claims.Add(new(nameof(OrganizationLicenseConstants.Name), entity.Name));
}
if (entity.BillingEmail is not null)
{
claims.Add(new(nameof(OrganizationLicenseConstants.BillingEmail), entity.BillingEmail));
}
if (entity.Plan is not null)
{
claims.Add(new(nameof(OrganizationLicenseConstants.Plan), entity.Plan));
}
if (entity.BusinessName is not null) if (entity.BusinessName is not null)
{ {
claims.Add(new Claim(nameof(OrganizationLicenseConstants.BusinessName), entity.BusinessName)); claims.Add(new Claim(nameof(OrganizationLicenseConstants.BusinessName), entity.BusinessName));
} }
if (entity.LicenseKey is not null)
{
claims.Add(new Claim(nameof(OrganizationLicenseConstants.LicenseKey), entity.LicenseKey));
}
if (licenseContext.InstallationId.HasValue)
{
claims.Add(new Claim(nameof(OrganizationLicenseConstants.InstallationId), licenseContext.InstallationId.ToString()));
}
if (entity.Seats.HasValue)
{
claims.Add(new Claim(nameof(OrganizationLicenseConstants.Seats), entity.Seats.ToString()));
}
if (entity.MaxCollections.HasValue)
{
claims.Add(new Claim(nameof(OrganizationLicenseConstants.MaxCollections), entity.MaxCollections.ToString()));
}
if (entity.MaxStorageGb.HasValue)
{
claims.Add(new Claim(nameof(OrganizationLicenseConstants.MaxStorageGb), entity.MaxStorageGb.ToString()));
}
if (entity.SmSeats.HasValue)
{
claims.Add(new Claim(nameof(OrganizationLicenseConstants.SmSeats), entity.SmSeats.ToString()));
}
if (entity.SmServiceAccounts.HasValue)
{
claims.Add(new Claim(nameof(OrganizationLicenseConstants.SmServiceAccounts), entity.SmServiceAccounts.ToString()));
}
return Task.FromResult(claims); return Task.FromResult(claims);
} }

View File

@ -21,31 +21,39 @@ public class UserLicenseClaimsFactory : ILicenseClaimsFactory<User>
{ {
new(nameof(UserLicenseConstants.LicenseType), LicenseType.User.ToString()), new(nameof(UserLicenseConstants.LicenseType), LicenseType.User.ToString()),
new(nameof(UserLicenseConstants.Id), entity.Id.ToString()), new(nameof(UserLicenseConstants.Id), entity.Id.ToString()),
new(nameof(UserLicenseConstants.Name), entity.Name),
new(nameof(UserLicenseConstants.Email), entity.Email),
new(nameof(UserLicenseConstants.Premium), entity.Premium.ToString()), new(nameof(UserLicenseConstants.Premium), entity.Premium.ToString()),
new(nameof(UserLicenseConstants.Issued), DateTime.UtcNow.ToString(CultureInfo.InvariantCulture)), new(nameof(UserLicenseConstants.Issued), DateTime.UtcNow.ToString(CultureInfo.InvariantCulture)),
new(nameof(UserLicenseConstants.Trial), trial.ToString()), new(nameof(UserLicenseConstants.Trial), trial.ToString()),
}; };
if (entity.Email is not null)
{
claims.Add(new(nameof(UserLicenseConstants.Email), entity.Email));
}
if (entity.Name is not null)
{
claims.Add(new(nameof(UserLicenseConstants.Name), entity.Name));
}
if (entity.LicenseKey is not null) if (entity.LicenseKey is not null)
{ {
claims.Add(new(nameof(UserLicenseConstants.LicenseKey), entity.LicenseKey)); claims.Add(new(nameof(UserLicenseConstants.LicenseKey), entity.LicenseKey));
} }
if (entity.MaxStorageGb is not null) if (entity.MaxStorageGb.HasValue)
{ {
claims.Add(new(nameof(UserLicenseConstants.MaxStorageGb), entity.MaxStorageGb.ToString())); claims.Add(new(nameof(UserLicenseConstants.MaxStorageGb), entity.MaxStorageGb.ToString()));
} }
if (expires is not null) if (expires.HasValue)
{ {
claims.Add(new(nameof(UserLicenseConstants.Expires), expires.ToString())); claims.Add(new(nameof(UserLicenseConstants.Expires), expires.Value.ToString(CultureInfo.InvariantCulture)));
} }
if (refresh is not null) if (refresh.HasValue)
{ {
claims.Add(new(nameof(UserLicenseConstants.Refresh), refresh.ToString())); claims.Add(new(nameof(UserLicenseConstants.Refresh), refresh.Value.ToString(CultureInfo.InvariantCulture)));
} }
return Task.FromResult(claims); return Task.FromResult(claims);

View File

@ -7,6 +7,7 @@ public record OrganizationMetadata(
bool IsSubscriptionUnpaid, bool IsSubscriptionUnpaid,
bool HasSubscription, bool HasSubscription,
bool HasOpenInvoice, bool HasOpenInvoice,
bool IsSubscriptionCanceled,
DateTime? InvoiceDueDate, DateTime? InvoiceDueDate,
DateTime? InvoiceCreatedDate, DateTime? InvoiceCreatedDate,
DateTime? SubPeriodEndDate); DateTime? SubPeriodEndDate);

View File

@ -69,7 +69,7 @@ public class OrganizationBillingService(
if (string.IsNullOrWhiteSpace(organization.GatewaySubscriptionId)) if (string.IsNullOrWhiteSpace(organization.GatewaySubscriptionId))
{ {
return new OrganizationMetadata(isEligibleForSelfHost, isManaged, false, return new OrganizationMetadata(isEligibleForSelfHost, isManaged, false,
false, false, false, null, null, null); false, false, false, false, null, null, null);
} }
var customer = await subscriberService.GetCustomer(organization, var customer = await subscriberService.GetCustomer(organization,
@ -79,6 +79,7 @@ public class OrganizationBillingService(
var isOnSecretsManagerStandalone = IsOnSecretsManagerStandalone(organization, customer, subscription); var isOnSecretsManagerStandalone = IsOnSecretsManagerStandalone(organization, customer, subscription);
var isSubscriptionUnpaid = IsSubscriptionUnpaid(subscription); var isSubscriptionUnpaid = IsSubscriptionUnpaid(subscription);
var isSubscriptionCanceled = IsSubscriptionCanceled(subscription);
var hasSubscription = true; var hasSubscription = true;
var openInvoice = await HasOpenInvoiceAsync(subscription); var openInvoice = await HasOpenInvoiceAsync(subscription);
var hasOpenInvoice = openInvoice.HasOpenInvoice; var hasOpenInvoice = openInvoice.HasOpenInvoice;
@ -87,7 +88,7 @@ public class OrganizationBillingService(
var subPeriodEndDate = subscription?.CurrentPeriodEnd; var subPeriodEndDate = subscription?.CurrentPeriodEnd;
return new OrganizationMetadata(isEligibleForSelfHost, isManaged, isOnSecretsManagerStandalone, return new OrganizationMetadata(isEligibleForSelfHost, isManaged, isOnSecretsManagerStandalone,
isSubscriptionUnpaid, hasSubscription, hasOpenInvoice, invoiceDueDate, invoiceCreatedDate, subPeriodEndDate); isSubscriptionUnpaid, hasSubscription, hasOpenInvoice, isSubscriptionCanceled, invoiceDueDate, invoiceCreatedDate, subPeriodEndDate);
} }
public async Task UpdatePaymentMethod( public async Task UpdatePaymentMethod(
@ -359,7 +360,7 @@ public class OrganizationBillingService(
{ {
AutomaticTax = new SubscriptionAutomaticTaxOptions AutomaticTax = new SubscriptionAutomaticTaxOptions
{ {
Enabled = customer.Tax?.AutomaticTax == StripeConstants.AutomaticTaxStatus.Supported Enabled = true
}, },
CollectionMethod = StripeConstants.CollectionMethod.ChargeAutomatically, CollectionMethod = StripeConstants.CollectionMethod.ChargeAutomatically,
Customer = customer.Id, Customer = customer.Id,
@ -437,5 +438,15 @@ public class OrganizationBillingService(
? (true, invoice.Created, invoice.DueDate) ? (true, invoice.Created, invoice.DueDate)
: (false, null, null); : (false, null, null);
} }
private static bool IsSubscriptionCanceled(Subscription subscription)
{
if (subscription == null)
{
return false;
}
return subscription.Status == "canceled";
}
#endregion #endregion
} }

View File

@ -28,7 +28,6 @@ public class PaymentHistoryService(
var invoices = await stripeAdapter.InvoiceListAsync(new StripeInvoiceListOptions var invoices = await stripeAdapter.InvoiceListAsync(new StripeInvoiceListOptions
{ {
Customer = subscriber.GatewayCustomerId, Customer = subscriber.GatewayCustomerId,
Subscription = subscriber.GatewaySubscriptionId,
Limit = pageSize, Limit = pageSize,
Status = status, Status = status,
StartingAfter = startAfter StartingAfter = startAfter

View File

@ -235,7 +235,7 @@ public class PremiumUserBillingService(
{ {
AutomaticTax = new SubscriptionAutomaticTaxOptions AutomaticTax = new SubscriptionAutomaticTaxOptions
{ {
Enabled = customer.Tax?.AutomaticTax == StripeConstants.AutomaticTaxStatus.Supported, Enabled = true
}, },
CollectionMethod = StripeConstants.CollectionMethod.ChargeAutomatically, CollectionMethod = StripeConstants.CollectionMethod.ChargeAutomatically,
Customer = customer.Id, Customer = customer.Id,

View File

@ -661,21 +661,11 @@ public class SubscriberService(
} }
} }
if (SubscriberIsEligibleForAutomaticTax(subscriber, customer)) await stripeAdapter.SubscriptionUpdateAsync(subscriber.GatewaySubscriptionId,
{ new SubscriptionUpdateOptions
await stripeAdapter.SubscriptionUpdateAsync(subscriber.GatewaySubscriptionId, {
new SubscriptionUpdateOptions AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true },
{ });
AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }
});
}
return;
bool SubscriberIsEligibleForAutomaticTax(ISubscriber localSubscriber, Customer localCustomer)
=> !string.IsNullOrEmpty(localSubscriber.GatewaySubscriptionId) &&
(localCustomer.Subscriptions?.Any(sub => sub.Id == localSubscriber.GatewaySubscriptionId && !sub.AutomaticTax.Enabled) ?? false) &&
localCustomer.Tax?.AutomaticTax == StripeConstants.AutomaticTaxStatus.Supported;
} }
public async Task VerifyBankAccount( public async Task VerifyBankAccount(

View File

@ -101,7 +101,13 @@ public static class AuthenticationSchemes
public static class FeatureFlagKeys public static class FeatureFlagKeys
{ {
public const string BrowserFilelessImport = "browser-fileless-import"; /* Admin Console Team */
public const string ProviderClientVaultPrivacyBanner = "ac-2833-provider-client-vault-privacy-banner";
public const string AccountDeprovisioning = "pm-10308-account-deprovisioning";
public const string VerifiedSsoDomainEndpoint = "pm-12337-refactor-sso-details-endpoint";
public const string IntegrationPage = "pm-14505-admin-console-integration-page";
public const string DeviceApprovalRequestAdminNotifications = "pm-15637-device-approval-request-admin-notifications";
public const string ReturnErrorOnExistingKeypair = "return-error-on-existing-keypair"; public const string ReturnErrorOnExistingKeypair = "return-error-on-existing-keypair";
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";
@ -115,11 +121,9 @@ public static class FeatureFlagKeys
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 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 InlineMenuFieldQualification = "inline-menu-field-qualification"; public const string InlineMenuFieldQualification = "inline-menu-field-qualification";
public const string TwoFactorComponentRefactor = "two-factor-component-refactor"; public const string TwoFactorComponentRefactor = "two-factor-component-refactor";
public const string InlineMenuPositioningImprovements = "inline-menu-positioning-improvements"; public const string InlineMenuPositioningImprovements = "inline-menu-positioning-improvements";
public const string ProviderClientVaultPrivacyBanner = "ac-2833-provider-client-vault-privacy-banner";
public const string DeviceTrustLogging = "pm-8285-device-trust-logging"; public const string DeviceTrustLogging = "pm-8285-device-trust-logging";
public const string SSHKeyItemVaultItem = "ssh-key-vault-item"; public const string SSHKeyItemVaultItem = "ssh-key-vault-item";
public const string SSHAgent = "ssh-agent"; public const string SSHAgent = "ssh-agent";
@ -131,41 +135,41 @@ public static class FeatureFlagKeys
public const string DelayFido2PageScriptInitWithinMv2 = "delay-fido2-page-script-init-within-mv2"; public const string DelayFido2PageScriptInitWithinMv2 = "delay-fido2-page-script-init-within-mv2";
public const string NativeCarouselFlow = "native-carousel-flow"; public const string NativeCarouselFlow = "native-carousel-flow";
public const string NativeCreateAccountFlow = "native-create-account-flow"; public const string NativeCreateAccountFlow = "native-create-account-flow";
public const string AccountDeprovisioning = "pm-10308-account-deprovisioning";
public const string NotificationBarAddLoginImprovements = "notification-bar-add-login-improvements"; public const string NotificationBarAddLoginImprovements = "notification-bar-add-login-improvements";
public const string AC2476_DeprecateStripeSourcesAPI = "AC-2476-deprecate-stripe-sources-api"; public const string BlockBrowserInjectionsByDomain = "block-browser-injections-by-domain";
public const string NotificationRefresh = "notification-refresh";
public const string PersistPopupView = "persist-popup-view"; public const string PersistPopupView = "persist-popup-view";
public const string CipherKeyEncryption = "cipher-key-encryption"; public const string CipherKeyEncryption = "cipher-key-encryption";
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 RemoveServerVersionHeader = "remove-server-version-header"; public const string RemoveServerVersionHeader = "remove-server-version-header";
public const string SecureOrgGroupDetails = "pm-3479-secure-org-group-details";
public const string VerifiedSsoDomainEndpoint = "pm-12337-refactor-sso-details-endpoint";
public const string PM12275_MultiOrganizationEnterprises = "pm-12275-multi-organization-enterprises"; public const string PM12275_MultiOrganizationEnterprises = "pm-12275-multi-organization-enterprises";
public const string GeneratorToolsModernization = "generator-tools-modernization"; public const string GeneratorToolsModernization = "generator-tools-modernization";
public const string NewDeviceVerification = "new-device-verification"; public const string NewDeviceVerification = "new-device-verification";
public const string RiskInsightsCriticalApplication = "pm-14466-risk-insights-critical-application"; public const string RiskInsightsCriticalApplication = "pm-14466-risk-insights-critical-application";
public const string IntegrationPage = "pm-14505-admin-console-integration-page";
public const string NewDeviceVerificationTemporaryDismiss = "new-device-temporary-dismiss"; public const string NewDeviceVerificationTemporaryDismiss = "new-device-temporary-dismiss";
public const string NewDeviceVerificationPermanentDismiss = "new-device-permanent-dismiss"; public const string NewDeviceVerificationPermanentDismiss = "new-device-permanent-dismiss";
public const string SecurityTasks = "security-tasks"; public const string SecurityTasks = "security-tasks";
public const string PM14401_ScaleMSPOnClientOrganizationUpdate = "PM-14401-scale-msp-on-client-organization-update";
public const string PM11360RemoveProviderExportPermission = "pm-11360-remove-provider-export-permission";
public const string DisableFreeFamiliesSponsorship = "PM-12274-disable-free-families-sponsorship"; public const string DisableFreeFamiliesSponsorship = "PM-12274-disable-free-families-sponsorship";
public const string MacOsNativeCredentialSync = "macos-native-credential-sync"; public const string MacOsNativeCredentialSync = "macos-native-credential-sync";
public const string PM9111ExtensionPersistAddEditForm = "pm-9111-extension-persist-add-edit-form"; public const string PM9111ExtensionPersistAddEditForm = "pm-9111-extension-persist-add-edit-form";
public const string InlineMenuTotp = "inline-menu-totp"; public const string InlineMenuTotp = "inline-menu-totp";
public const string PM12443RemovePagingLogic = "pm-12443-remove-paging-logic";
public const string SelfHostLicenseRefactor = "pm-11516-self-host-license-refactor"; public const string SelfHostLicenseRefactor = "pm-11516-self-host-license-refactor";
public const string PromoteProviderServiceUserTool = "pm-15128-promote-provider-service-user-tool";
public const string PrivateKeyRegeneration = "pm-12241-private-key-regeneration"; public const string PrivateKeyRegeneration = "pm-12241-private-key-regeneration";
public const string AuthenticatorSynciOS = "enable-authenticator-sync-ios"; public const string AuthenticatorSynciOS = "enable-authenticator-sync-ios";
public const string AuthenticatorSyncAndroid = "enable-authenticator-sync-android"; public const string AuthenticatorSyncAndroid = "enable-authenticator-sync-android";
public const string AppReviewPrompt = "app-review-prompt"; public const string AppReviewPrompt = "app-review-prompt";
public const string ResellerManagedOrgAlert = "PM-15814-alert-owners-of-reseller-managed-orgs"; public const string ResellerManagedOrgAlert = "PM-15814-alert-owners-of-reseller-managed-orgs";
public const string Argon2Default = "argon2-default";
public const string UsePricingService = "use-pricing-service"; public const string UsePricingService = "use-pricing-service";
public const string RecordInstallationLastActivityDate = "installation-last-activity-date"; public const string RecordInstallationLastActivityDate = "installation-last-activity-date";
public const string EnablePasswordManagerSyncAndroid = "enable-password-manager-sync-android";
public const string EnablePasswordManagerSynciOS = "enable-password-manager-sync-ios";
public const string AccountDeprovisioningBanner = "pm-17120-account-deprovisioning-admin-console-banner";
public const string SingleTapPasskeyCreation = "single-tap-passkey-creation";
public const string SingleTapPasskeyAuthentication = "single-tap-passkey-authentication";
public const string EnableRiskInsightsNotifications = "enable-risk-insights-notifications";
public static List<string> GetAllKeys() public static List<string> GetAllKeys()
{ {

View File

@ -43,7 +43,9 @@ public interface ICurrentContext
Task<bool> AccessEventLogs(Guid orgId); Task<bool> AccessEventLogs(Guid orgId);
Task<bool> AccessImportExport(Guid orgId); Task<bool> AccessImportExport(Guid orgId);
Task<bool> AccessReports(Guid orgId); Task<bool> AccessReports(Guid orgId);
[Obsolete("Deprecated. Use an authorization handler checking the specific permissions required instead.")]
Task<bool> EditAnyCollection(Guid orgId); Task<bool> EditAnyCollection(Guid orgId);
[Obsolete("Deprecated. Use an authorization handler checking the specific permissions required instead.")]
Task<bool> ViewAllCollections(Guid orgId); Task<bool> ViewAllCollections(Guid orgId);
Task<bool> ManageGroups(Guid orgId); Task<bool> ManageGroups(Guid orgId);
Task<bool> ManagePolicies(Guid orgId); Task<bool> ManagePolicies(Guid orgId);

Some files were not shown because too many files have changed in this diff Show More