1
0
mirror of https://github.com/bitwarden/server.git synced 2024-11-27 13:05:23 +01:00

Merge branch 'main' into ephemeral-test-01

This commit is contained in:
MtnBurrit0 2024-10-31 08:39:14 -06:00 committed by GitHub
commit 8673cc939f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
379 changed files with 40269 additions and 4001 deletions

View File

@ -3,7 +3,7 @@
"isRoot": true,
"tools": {
"swashbuckle.aspnetcore.cli": {
"version": "6.8.0",
"version": "6.9.0",
"commands": ["swagger"]
},
"dotnet-ef": {

26
.github/CODEOWNERS vendored
View File

@ -4,13 +4,22 @@
#
# https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
# DevOps for Actions and other workflow changes
.github/workflows @bitwarden/dept-devops
## Docker files have shared ownership ##
**/Dockerfile
**/*.Dockerfile
**/.dockerignore
**/entrypoint.sh
# DevOps for Docker changes
**/Dockerfile @bitwarden/dept-devops
**/*.Dockerfile @bitwarden/dept-devops
**/.dockerignore @bitwarden/dept-devops
## BRE team owns these workflows ##
.github/workflows/publish.yml @bitwarden/dept-bre
## These are shared workflows ##
.github/workflows/_move_finalization_db_scripts.yml
.github/workflows/build.yml
.github/workflows/cleanup-after-pr.yml
.github/workflows/cleanup-rc-branch.yml
.github/workflows/release.yml
.github/workflows/repository-management.yml
# Database Operations for database changes
src/Sql/** @bitwarden/dept-dbops
@ -26,6 +35,9 @@ util/SqliteMigrations/** @bitwarden/dept-dbops
bitwarden_license/src/Sso @bitwarden/team-auth-dev
src/Identity @bitwarden/team-auth-dev
# Key Management team
**/KeyManagement @bitwarden/team-key-management-dev
**/SecretsManager @bitwarden/team-secrets-manager-dev
**/Tools @bitwarden/team-tools-dev
@ -57,6 +69,6 @@ src/EventsProcessor @bitwarden/team-admin-console-dev
src/Admin/Controllers/ToolsController.cs @bitwarden/team-billing-dev
src/Admin/Views/Tools @bitwarden/team-billing-dev
# Multiple owners - DO NOT REMOVE (DevOps)
# Multiple owners - DO NOT REMOVE (BRE)
**/packages.lock.json
Directory.Build.props

View File

@ -1,4 +1,3 @@
---
name: _move_finalization_db_scripts
run-name: Move finalization database scripts
@ -30,7 +29,7 @@ jobs:
secrets: "github-pat-bitwarden-devops-bot-repo-scope"
- name: Check out branch
uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
token: ${{ steps.retrieve-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
@ -54,7 +53,7 @@ jobs:
if: ${{ needs.setup.outputs.copy_finalization_scripts == 'true' }}
steps:
- name: Checkout
uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 0
@ -108,7 +107,7 @@ jobs:
devops-alerts-slack-webhook-url"
- name: Import GPG keys
uses: crazy-max/ghaction-import-gpg@01dd5d3ca463c7f10f7f4f7b4f177225ac661ee4 # v6.1.0
uses: crazy-max/ghaction-import-gpg@cb9bde2e2525e640591a934b1fd28eef1dcaf5e5 # v6.2.0
with:
gpg_private_key: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key }}
passphrase: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key-passphrase }}

View File

@ -1,4 +1,3 @@
---
name: Automatic responses
on:
issues:

View File

@ -1,4 +1,3 @@
---
name: Build
on:
@ -19,10 +18,10 @@ jobs:
runs-on: ubuntu-22.04
steps:
- name: Check out repo
uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Set up .NET
uses: actions/setup-dotnet@6bd8b7f7774af54e05809fcc5431931b3eb1ddee # v4.0.1
uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0
- name: Verify format
run: dotnet format --verify-no-changes
@ -68,13 +67,13 @@ jobs:
node: true
steps:
- name: Check out repo
uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Set up .NET
uses: actions/setup-dotnet@6bd8b7f7774af54e05809fcc5431931b3eb1ddee # v4.0.1
uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0
- name: Set up Node
uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with:
cache: "npm"
cache-dependency-path: "**/package-lock.json"
@ -110,7 +109,7 @@ jobs:
ls -atlh ../../../
- name: Upload project artifact
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: ${{ matrix.project_name }}.zip
path: ${{ matrix.base_path }}/${{ matrix.project_name }}/${{ matrix.project_name }}.zip
@ -173,7 +172,7 @@ jobs:
dotnet: true
steps:
- name: Check out repo
uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Check branch to publish
env:
@ -263,7 +262,7 @@ jobs:
-d ${{ matrix.base_path }}/${{ matrix.project_name }}/obj/build-output/publish
- name: Build Docker image
uses: docker/build-push-action@32945a339266b759abcbdc89316275140b0fc960 # v6.8.0
uses: docker/build-push-action@4f58ea79222b3b9dc2c8bbdd6debcef730109a75 # v6.9.0
with:
context: ${{ matrix.base_path }}/${{ matrix.project_name }}
file: ${{ matrix.base_path }}/${{ matrix.project_name }}/Dockerfile
@ -275,14 +274,14 @@ jobs:
- name: Scan Docker image
id: container-scan
uses: anchore/scan-action@64a33b277ea7a1215a3c142735a1091341939ff5 # v4.1.2
uses: anchore/scan-action@5ed195cc06065322983cae4bb31e2a751feb86fd # v5.2.0
with:
image: ${{ steps.image-tags.outputs.primary_tag }}
fail-build: false
output-format: sarif
- name: Upload Grype results to GitHub
uses: github/codeql-action/upload-sarif@461ef6c76dfe95d5c364de2f431ddbd31a417628 # v3.26.9
uses: github/codeql-action/upload-sarif@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0
with:
sarif_file: ${{ steps.container-scan.outputs.sarif }}
@ -292,10 +291,10 @@ jobs:
needs: build-docker
steps:
- name: Check out repo
uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Set up .NET
uses: actions/setup-dotnet@6bd8b7f7774af54e05809fcc5431931b3eb1ddee # v4.0.1
uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0
- name: Log in to Azure - production subscription
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
@ -311,7 +310,7 @@ jobs:
github.ref == 'refs/heads/hotfix-rc'
run: |
# Set proper setup image based on branch
case "${{ github.ref }}" in
case "$GITHUB_REF" in
"refs/heads/main")
SETUP_IMAGE="$_AZ_REGISTRY/setup:dev"
;;
@ -355,7 +354,7 @@ jobs:
- name: Upload Docker stub US artifact
if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc'
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: docker-stub-US.zip
path: docker-stub-US.zip
@ -363,7 +362,7 @@ jobs:
- name: Upload Docker stub EU artifact
if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc'
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: docker-stub-EU.zip
path: docker-stub-EU.zip
@ -371,7 +370,7 @@ jobs:
- name: Upload Docker stub US checksum artifact
if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc'
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: docker-stub-US-sha256.txt
path: docker-stub-US-sha256.txt
@ -379,7 +378,7 @@ jobs:
- name: Upload Docker stub EU checksum artifact
if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc'
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: docker-stub-EU-sha256.txt
path: docker-stub-EU-sha256.txt
@ -403,12 +402,12 @@ jobs:
GLOBALSETTINGS__SQLSERVER__CONNECTIONSTRING: "placeholder"
- name: Upload Public API Swagger artifact
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: swagger.json
path: swagger.json
if-no-files-found: error
- name: Build Internal API Swagger
run: |
cd ./src/Api
@ -416,17 +415,17 @@ jobs:
dotnet tool restore
echo "Publish API"
dotnet publish -c "Release" -o obj/build-output/publish
dotnet swagger tofile --output ../../internal.json --host https://api.bitwarden.com \
./obj/build-output/publish/Api.dll internal
cd ../Identity
echo "Restore Identity tools"
dotnet tool restore
echo "Publish Identity"
dotnet publish -c "Release" -o obj/build-output/publish
dotnet swagger tofile --output ../../identity.json --host https://identity.bitwarden.com \
./obj/build-output/publish/Identity.dll v1
cd ../..
@ -437,18 +436,18 @@ jobs:
GLOBALSETTINGS__SQLSERVER__CONNECTIONSTRING: "placeholder"
- name: Upload Internal API Swagger artifact
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: internal.json
path: internal.json
if-no-files-found: error
- name: Upload Identity Swagger artifact
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: identity.json
path: identity.json
if-no-files-found: error
if-no-files-found: error
build-mssqlmigratorutility:
name: Build MSSQL migrator utility
@ -467,10 +466,10 @@ jobs:
- win-x64
steps:
- name: Check out repo
uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Set up .NET
uses: actions/setup-dotnet@6bd8b7f7774af54e05809fcc5431931b3eb1ddee # v4.0.1
uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0
- name: Print environment
run: |
@ -486,7 +485,7 @@ jobs:
- name: Upload project artifact for Windows
if: ${{ contains(matrix.target, 'win') == true }}
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: MsSqlMigratorUtility-${{ matrix.target }}
path: util/MsSqlMigratorUtility/obj/build-output/publish/MsSqlMigratorUtility.exe
@ -494,7 +493,7 @@ jobs:
- name: Upload project artifact
if: ${{ contains(matrix.target, 'win') == false }}
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: MsSqlMigratorUtility-${{ matrix.target }}
path: util/MsSqlMigratorUtility/obj/build-output/publish/MsSqlMigratorUtility
@ -528,9 +527,9 @@ jobs:
workflow_id: 'build-unified.yml',
ref: 'main',
inputs: {
server_branch: '${{ github.ref }}'
server_branch: process.env.GITHUB_REF
}
})
});
trigger-k8s-deploy:
name: Trigger k8s deploy
@ -566,6 +565,39 @@ jobs:
}
})
trigger-ee-updates:
name: Trigger Ephemeral Environment updates
if: github.ref != 'refs/heads/main' && contains(github.event.pull_request.labels.*.name, 'ephemeral-environment')
runs-on: ubuntu-24.04
needs: build-docker
steps:
- name: Log in to Azure - CI subscription
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
with:
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
- name: Retrieve GitHub PAT secrets
id: retrieve-secret-pat
uses: bitwarden/gh-actions/get-keyvault-secrets@main
with:
keyvault: "bitwarden-ci"
secrets: "github-pat-bitwarden-devops-bot-repo-scope"
- name: Trigger Ephemeral Environment update
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
with:
github-token: ${{ steps.retrieve-secret-pat.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
script: |
await github.rest.actions.createWorkflowDispatch({
owner: 'bitwarden',
repo: 'devops',
workflow_id: '_update_ephemeral_tags.yml',
ref: 'main',
inputs: {
ephemeral_env_branch: process.env.GITHUB_HEAD_REF
}
})
check-failures:
name: Check for failures
if: always()

View File

@ -1,4 +1,3 @@
---
name: Container registry cleanup
on:

View File

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

View File

@ -1,4 +1,3 @@
---
name: Cleanup RC Branch
on:
@ -24,7 +23,7 @@ jobs:
secrets: "github-pat-bitwarden-devops-bot-repo-scope"
- name: Checkout main
uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: main
token: ${{ steps.retrieve-bot-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }}

View File

@ -33,7 +33,7 @@ jobs:
steps:
- name: Check out repository
uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Collect
id: collect

View File

@ -1,102 +0,0 @@
---
name: Container registry purge
on:
schedule:
- cron: "0 0 * * SUN"
workflow_dispatch:
inputs: {}
jobs:
purge:
name: Purge old images
runs-on: ubuntu-22.04
steps:
- name: Log in to Azure
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
with:
creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
- name: Purge images
env:
REGISTRY: bitwardenprod
AGO_DUR_VER: "180d"
AGO_DUR: "30d"
run: |
REPO_LIST=$(az acr repository list -n $REGISTRY -o tsv)
for REPO in $REPO_LIST
do
PURGE_LATEST=""
PURGE_VERSION=""
PURGE_ELSE=""
TAG_LIST=$(az acr repository show-tags -n $REGISTRY --repository $REPO -o tsv)
for TAG in $TAG_LIST
do
if [ $TAG = "latest" ] || [ $TAG = "dev" ]; then
PURGE_LATEST+="--filter '$REPO:$TAG' "
elif [[ $TAG =~ [0-9]+\.[0-9]+\.[0-9]+ ]]; then
PURGE_VERSION+="--filter '$REPO:$TAG' "
else
PURGE_ELSE+="--filter '$REPO:$TAG' "
fi
done
if [ ! -z "$PURGE_LATEST" ]
then
PURGE_LATEST_CMD="acr purge $PURGE_LATEST --ago $AGO_DUR_VER --untagged --keep 1"
az acr run --cmd "$PURGE_LATEST_CMD" --registry $REGISTRY /dev/null &
fi
if [ ! -z "$PURGE_VERSION" ]
then
PURGE_VERSION_CMD="acr purge $PURGE_VERSION --ago $AGO_DUR_VER --untagged"
az acr run --cmd "$PURGE_VERSION_CMD" --registry $REGISTRY /dev/null &
fi
if [ ! -z "$PURGE_ELSE" ]
then
PURGE_ELSE_CMD="acr purge $PURGE_ELSE --ago $AGO_DUR --untagged"
az acr run --cmd "$PURGE_ELSE_CMD" --registry $REGISTRY /dev/null &
fi
wait
done
check-failures:
name: Check for failures
if: always()
runs-on: ubuntu-22.04
needs: [purge]
steps:
- name: Check if any job failed
if: |
(github.ref == 'refs/heads/main'
|| github.ref == 'refs/heads/rc'
|| github.ref == 'refs/heads/hotfix-rc')
&& contains(needs.*.result, 'failure')
run: exit 1
- name: Log in to Azure - CI subscription
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
if: failure()
with:
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
- name: Retrieve secrets
id: retrieve-secrets
uses: bitwarden/gh-actions/get-keyvault-secrets@main
if: failure()
with:
keyvault: "bitwarden-ci"
secrets: "devops-alerts-slack-webhook-url"
- name: Notify Slack on failure
uses: act10ns/slack@44541246747a30eb3102d87f7a4cc5471b0ffb7d # v2.1.0
if: failure()
env:
SLACK_WEBHOOK_URL: ${{ steps.retrieve-secrets.outputs.devops-alerts-slack-webhook-url }}
with:
status: ${{ job.status }}

View File

@ -1,4 +1,3 @@
---
name: Enforce PR labels
on:
@ -7,13 +6,13 @@ on:
types: [labeled, unlabeled, opened, reopened, synchronize]
jobs:
enforce-label:
if: ${{ contains(github.event.*.labels.*.name, 'hold') || contains(github.event.*.labels.*.name, 'needs-qa') || contains(github.event.*.labels.*.name, 'DB-migrations-changed') }}
if: ${{ contains(github.event.*.labels.*.name, 'hold') || contains(github.event.*.labels.*.name, 'needs-qa') || contains(github.event.*.labels.*.name, 'DB-migrations-changed') || contains(github.event.*.labels.*.name, 'ephemeral-environment') }}
name: Enforce label
runs-on: ubuntu-22.04
steps:
- name: Check for label
run: |
echo "PRs with the hold or needs-qa labels cannot be merged"
echo "### :x: PRs with the hold or needs-qa labels cannot be merged" >> $GITHUB_STEP_SUMMARY
echo "PRs with the hold, needs-qa or ephemeral-environment labels cannot be merged"
echo "### :x: PRs with the hold, needs-qa or ephemeral-environment labels cannot be merged" >> $GITHUB_STEP_SUMMARY
exit 1

View File

@ -1,7 +1,6 @@
# Runs if there are changes to the paths: list.
# Starts a matrix job to check for modified files, then sets output based on the results.
# The input decides if the label job is ran, adding a label to the PR.
---
name: Protect files
on:
@ -29,7 +28,7 @@ jobs:
label: "DB-migrations-changed"
steps:
- name: Check out repo
uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 2

View File

@ -1,4 +1,3 @@
---
name: Publish
run-name: Publish ${{ inputs.publish_type }}
@ -99,7 +98,7 @@ jobs:
echo "Github Release Option: $RELEASE_OPTION"
- name: Check out repo
uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Set up project name
id: setup

View File

@ -1,4 +1,3 @@
---
name: Release
run-name: Release ${{ inputs.release_type }}
@ -37,7 +36,7 @@ jobs:
fi
- name: Check out repo
uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Check release version
id: version

View File

@ -1,88 +1,70 @@
---
name: Version Bump
name: Repository management
on:
workflow_dispatch:
inputs:
branch_to_cut:
default: "rc"
description: "Branch to cut"
options:
- "rc"
- "hotfix-rc"
required: true
type: choice
target_ref:
default: "main"
description: "Branch/Tag to target for cut"
required: true
type: string
version_number_override:
description: "New version override (leave blank for automatic calculation, example: '2024.1.0')"
required: false
type: string
cut_rc_branch:
description: "Cut RC branch?"
default: true
type: boolean
enable_slack_notification:
description: "Enable Slack notifications for upcoming release?"
default: false
type: boolean
jobs:
cut_branch:
name: Cut branch
runs-on: ubuntu-22.04
steps:
- name: Check out target ref
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ inputs.target_ref }}
- name: Check if ${{ inputs.branch_to_cut }} branch exists
env:
BRANCH_NAME: ${{ inputs.branch_to_cut }}
run: |
if [[ $(git ls-remote --heads origin $BRANCH_NAME) ]]; then
echo "$BRANCH_NAME already exists! Please delete $BRANCH_NAME before running again." >> $GITHUB_STEP_SUMMARY
exit 1
fi
- name: Cut branch
env:
BRANCH_NAME: ${{ inputs.branch_to_cut }}
run: |
git switch --quiet --create $BRANCH_NAME
git push --quiet --set-upstream origin $BRANCH_NAME
bump_version:
name: Bump Version
runs-on: ubuntu-22.04
needs: cut_branch
outputs:
version: ${{ steps.set-final-version-output.outputs.version }}
steps:
- name: Validate version input
- name: Validate version input format
if: ${{ inputs.version_number_override != '' }}
uses: bitwarden/gh-actions/version-check@main
with:
version: ${{ inputs.version_number_override }}
- name: Slack Notification Check
run: |
if [[ "${{ inputs.enable_slack_notification }}" == true ]]; then
echo "Slack notifications enabled."
else
echo "Slack notifications disabled."
fi
- name: Check out branch
uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0
- name: Check if RC branch exists
if: ${{ inputs.cut_rc_branch == true }}
run: |
remote_rc_branch_check=$(git ls-remote --heads origin rc | wc -l)
if [[ "${remote_rc_branch_check}" -gt 0 ]]; then
echo "Remote RC branch exists."
echo "Please delete current RC branch before running again."
exit 1
fi
- name: Log in to Azure - CI subscription
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
- name: Retrieve secrets
id: retrieve-secrets
uses: bitwarden/gh-actions/get-keyvault-secrets@main
with:
keyvault: "bitwarden-ci"
secrets: "github-gpg-private-key,
github-gpg-private-key-passphrase"
- name: Import GPG key
uses: crazy-max/ghaction-import-gpg@01dd5d3ca463c7f10f7f4f7b4f177225ac661ee4 # v6.1.0
with:
gpg_private_key: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key }}
passphrase: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key-passphrase }}
git_user_signingkey: true
git_commit_gpgsign: true
- name: Set up Git
run: |
git config --local user.email "106330231+bitwarden-devops-bot@users.noreply.github.com"
git config --local user.name "bitwarden-devops-bot"
- name: Create version branch
id: create-branch
run: |
NAME=version_bump_${{ github.ref_name }}_$(date +"%Y-%m-%d")
git switch -c $NAME
echo "name=$NAME" >> $GITHUB_OUTPUT
ref: main
- name: Install xmllint
run: |
@ -103,16 +85,16 @@ jobs:
run: |
# Error if version has not changed.
if [[ "$NEW_VERSION" == "$CURRENT_VERSION" ]]; then
echo "Version has not changed."
echo "Specified override version is the same as the current version." >> $GITHUB_STEP_SUMMARY
exit 1
fi
# Check if version is newer.
printf '%s\n' "${CURRENT_VERSION}" "${NEW_VERSION}" | sort -C -V
if [ $? -eq 0 ]; then
echo "Version check successful."
echo "Version is newer than the current version."
else
echo "Version check failed."
echo "Version is older than the current version." >> $GITHUB_STEP_SUMMARY
exit 1
fi
@ -148,25 +130,23 @@ jobs:
echo "version=${{ steps.calculate-next-version.outputs.version }}" >> $GITHUB_OUTPUT
fi
- name: Check if version changed
id: version-changed
- name: Configure Git
run: |
if [ -n "$(git status --porcelain)" ]; then
echo "changes_to_commit=TRUE" >> $GITHUB_OUTPUT
else
echo "changes_to_commit=FALSE" >> $GITHUB_OUTPUT
echo "No changes to commit!";
fi
git config --local user.email "actions@github.com"
git config --local user.name "Github Actions"
- name: Create version branch
id: create-branch
run: |
NAME=version_bump_${{ github.ref_name }}_$(date +"%Y-%m-%d")
git switch -c $NAME
echo "name=$NAME" >> $GITHUB_OUTPUT
- name: Commit files
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
run: git commit -m "Bumped version to ${{ steps.set-final-version-output.outputs.version }}" -a
- name: Push changes
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
env:
PR_BRANCH: ${{ steps.create-branch.outputs.name }}
run: git push -u origin $PR_BRANCH
run: git push
- name: Generate GH App token
uses: actions/create-github-app-token@5d869da34e18e7287c1daad50e0b8ea0f506ce69 # v1.11.0
@ -177,7 +157,6 @@ jobs:
owner: ${{ github.repository_owner }}
- name: Create version PR
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
id: create-pr
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
@ -196,41 +175,30 @@ jobs:
- [ ] Tech debt (refactoring, code cleanup, dependency upgrades, etc)
- [ ] Build/deploy pipeline (DevOps)
- [X] Other
## Objective
Automated version bump to ${{ steps.set-final-version-output.outputs.version }}")
echo "pr_number=${PR_URL##*/}" >> $GITHUB_OUTPUT
- name: Approve PR
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ steps.create-pr.outputs.pr_number }}
run: gh pr review $PR_NUMBER --approve
- name: Merge PR
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
PR_NUMBER: ${{ steps.create-pr.outputs.pr_number }}
run: gh pr merge $PR_NUMBER --squash --auto --delete-branch
- name: Report upcoming release version to Slack
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' && inputs.enable_slack_notification == true }}
uses: bitwarden/gh-actions/report-upcoming-release-version@main
with:
version: ${{ steps.set-final-version-output.outputs.version }}
project: ${{ github.repository }}
AZURE_KV_CI_SERVICE_PRINCIPAL: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
cut_rc:
name: Cut RC branch
if: ${{ inputs.cut_rc_branch == true }}
needs: bump_version
cherry_pick:
name: Cherry-Pick Commit(s)
runs-on: ubuntu-22.04
needs: bump_version
steps:
- name: Check out branch
uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0
- name: Check out main branch
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: main
@ -254,13 +222,76 @@ jobs:
sleep 10
done
- name: Cut RC branch
- name: Get last version commit(s)
id: get-commits
run: |
git switch --quiet --create rc
git push --quiet --set-upstream origin rc
git switch main
MAIN_COMMIT=$(git log --reverse --pretty=format:"%H" --max-count=1 Directory.Build.props)
echo "main_commit=$MAIN_COMMIT" >> $GITHUB_OUTPUT
move-future-db-scripts:
if [[ $(git ls-remote --heads origin rc) ]]; then
git switch rc
RC_COMMIT=$(git log --reverse --pretty=format:"%H" --max-count=1 Directory.Build.props)
echo "rc_commit=$RC_COMMIT" >> $GITHUB_OUTPUT
RC_VERSION=$(xmllint -xpath "/Project/PropertyGroup/Version/text()" Directory.Build.props)
echo "rc_version=$RC_VERSION" >> $GITHUB_OUTPUT
fi
- name: Configure Git
run: |
git config --local user.email "actions@github.com"
git config --local user.name "Github Actions"
- name: Perform cherry-pick(s)
env:
CUT_BRANCH: ${{ inputs.branch_to_cut }}
MAIN_COMMIT: ${{ steps.get-commits.outputs.main_commit }}
RC_COMMIT: ${{ steps.get-commits.outputs.rc_commit }}
RC_VERSION: ${{ steps.get-commits.outputs.rc_version }}
run: |
# If we are cutting 'hotfix-rc':
if [[ "$CUT_BRANCH" == "hotfix-rc" ]]; then
# If the 'rc' branch exists:
if [[ $(git ls-remote --heads origin rc) ]]; then
# Chery-pick from 'rc' into 'hotfix-rc'
git switch hotfix-rc
HOTFIX_RC_VERSION=$(xmllint -xpath "/Project/PropertyGroup/Version/text()" Directory.Build.props)
if [[ "$HOTFIX_RC_VERSION" != "$RC_VERSION" ]]; then
git cherry-pick --strategy-option=theirs -x $RC_COMMIT
git push -u origin hotfix-rc
fi
# Cherry-pick from 'main' into 'rc'
git switch rc
git cherry-pick --strategy-option=theirs -x $MAIN_COMMIT
git push -u origin rc
# If the 'rc' branch does not exist:
else
# Cherry-pick from 'main' into 'hotfix-rc'
git switch hotfix-rc
git cherry-pick --strategy-option=theirs -x $MAIN_COMMIT
git push -u origin hotfix-rc
fi
# If we are cutting 'rc':
elif [[ "$CUT_BRANCH" == "rc" ]]; then
# Cherry-pick from 'main' into 'rc'
git switch rc
git cherry-pick --strategy-option=theirs -x $MAIN_COMMIT
git push -u origin rc
fi
move_future_db_scripts:
name: Move finalization database scripts
needs: cut_rc
needs: cherry_pick
uses: ./.github/workflows/_move_finalization_db_scripts.yml
secrets: inherit

View File

@ -26,12 +26,12 @@ jobs:
steps:
- name: Check out repo
uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Scan with Checkmarx
uses: checkmarx/ast-github-action@9fda5a4a2c297608117a5a56af424502a9192e57 # 2.0.34
uses: checkmarx/ast-github-action@f0869bd1a37fddc06499a096101e6c900e815d81 # 2.0.36
env:
INCREMENTAL: "${{ contains(github.event_name, 'pull_request') && '--sast-incremental' || '' }}"
with:
@ -46,7 +46,7 @@ jobs:
--output-path . ${{ env.INCREMENTAL }}
- name: Upload Checkmarx results to GitHub
uses: github/codeql-action/upload-sarif@461ef6c76dfe95d5c364de2f431ddbd31a417628 # v3.26.9
uses: github/codeql-action/upload-sarif@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0
with:
sarif_file: cx_result.sarif
@ -60,19 +60,19 @@ jobs:
steps:
- name: Set up JDK 17
uses: actions/setup-java@b36c23c0d998641eff861008f374ee103c25ac73 # v4.4.0
uses: actions/setup-java@8df1039502a15bceb9433410b1a100fbe190c53b # v4.5.0
with:
java-version: 17
distribution: "zulu"
- name: Check out repo
uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 0
ref: ${{ github.event.pull_request.head.sha }}
- name: Set up .NET
uses: actions/setup-dotnet@6bd8b7f7774af54e05809fcc5431931b3eb1ddee # v4.0.1
uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0
- name: Install SonarCloud scanner
run: dotnet tool install dotnet-sonarscanner -g

View File

@ -1,4 +1,3 @@
---
name: Staleness
on:
workflow_dispatch:

View File

@ -1,4 +1,3 @@
---
name: Database testing
on:
@ -36,10 +35,10 @@ jobs:
runs-on: ubuntu-22.04
steps:
- name: Check out repo
uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Set up .NET
uses: actions/setup-dotnet@6bd8b7f7774af54e05809fcc5431931b3eb1ddee # v4.0.1
uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0
- name: Restore tools
run: dotnet tool restore
@ -55,7 +54,7 @@ jobs:
# I've seen the SQL Server container not be ready for commands right after starting up and just needing a bit longer to be ready
- name: Sleep
run: sleep 15s
- name: Checking pending model changes (MySQL)
working-directory: "util/MySqlMigrations"
run: 'dotnet ef migrations has-pending-model-changes -- --GlobalSettings:MySql:ConnectionString="$CONN_STR"'
@ -114,7 +113,7 @@ jobs:
BW_TEST_DATABASES__3__CONNECTIONSTRING: "Data Source=${{ runner.temp }}/test.db"
run: dotnet test --logger "trx;LogFileName=infrastructure-test-results.trx"
shell: pwsh
- name: Print MySQL Logs
if: failure()
run: 'docker logs $(docker ps --quiet --filter "name=mysql")'
@ -147,10 +146,10 @@ jobs:
runs-on: ubuntu-22.04
steps:
- name: Check out repo
uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Set up .NET
uses: actions/setup-dotnet@6bd8b7f7774af54e05809fcc5431931b3eb1ddee # v4.0.1
uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0
- name: Print environment
run: |
@ -164,7 +163,7 @@ jobs:
shell: pwsh
- name: Upload DACPAC
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: sql.dacpac
path: Sql.dacpac
@ -190,7 +189,7 @@ jobs:
shell: pwsh
- name: Report validation results
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: report.xml
path: |

View File

@ -46,10 +46,10 @@ jobs:
steps:
- name: Check out repo
uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Set up .NET
uses: actions/setup-dotnet@6bd8b7f7774af54e05809fcc5431931b3eb1ddee # v4.0.1
uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0
- name: Print environment
run: |
@ -77,7 +77,7 @@ jobs:
fail-on-error: true
- name: Upload to codecov.io
uses: codecov/codecov-action@e28ff129e5465c2c0dcc6f003fc735cb6ae0c673 # v4.5.0
uses: codecov/codecov-action@b9fd7d16f6d7d1b5d2bec1a2887e65ceed900238 # v4.6.0
if: ${{ needs.check-test-secrets.outputs.available == 'true' }}
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

View File

@ -3,7 +3,7 @@
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Version>2024.9.2</Version>
<Version>2024.10.1</Version>
<RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>

View File

@ -40,6 +40,36 @@ public class CreateProviderCommand : ICreateProviderCommand
}
public async Task CreateMspAsync(Provider provider, string ownerEmail, int teamsMinimumSeats, int enterpriseMinimumSeats)
{
var providerId = await CreateProviderAsync(provider, ownerEmail);
var isConsolidatedBillingEnabled = _featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling);
if (isConsolidatedBillingEnabled)
{
await CreateProviderPlanAsync(providerId, PlanType.TeamsMonthly, teamsMinimumSeats);
await CreateProviderPlanAsync(providerId, PlanType.EnterpriseMonthly, enterpriseMinimumSeats);
}
}
public async Task CreateResellerAsync(Provider provider)
{
await ProviderRepositoryCreateAsync(provider, ProviderStatusType.Created);
}
public async Task CreateMultiOrganizationEnterpriseAsync(Provider provider, string ownerEmail, PlanType plan, int minimumSeats)
{
var providerId = await CreateProviderAsync(provider, ownerEmail);
var isConsolidatedBillingEnabled = _featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling);
if (isConsolidatedBillingEnabled)
{
await CreateProviderPlanAsync(providerId, plan, minimumSeats);
}
}
private async Task<Guid> CreateProviderAsync(Provider provider, string ownerEmail)
{
var owner = await _userRepository.GetByEmailAsync(ownerEmail);
if (owner == null)
@ -64,27 +94,10 @@ public class CreateProviderCommand : ICreateProviderCommand
Status = ProviderUserStatusType.Confirmed,
};
if (isConsolidatedBillingEnabled)
{
var providerPlans = new List<ProviderPlan>
{
CreateProviderPlan(provider.Id, PlanType.TeamsMonthly, teamsMinimumSeats),
CreateProviderPlan(provider.Id, PlanType.EnterpriseMonthly, enterpriseMinimumSeats)
};
foreach (var providerPlan in providerPlans)
{
await _providerPlanRepository.CreateAsync(providerPlan);
}
}
await _providerUserRepository.CreateAsync(providerUser);
await _providerService.SendProviderSetupInviteEmailAsync(provider, owner.Email);
}
public async Task CreateResellerAsync(Provider provider)
{
await ProviderRepositoryCreateAsync(provider, ProviderStatusType.Created);
return provider.Id;
}
private async Task ProviderRepositoryCreateAsync(Provider provider, ProviderStatusType status)
@ -95,9 +108,9 @@ public class CreateProviderCommand : ICreateProviderCommand
await _providerRepository.CreateAsync(provider);
}
private ProviderPlan CreateProviderPlan(Guid providerId, PlanType planType, int seatMinimum)
private async Task CreateProviderPlanAsync(Guid providerId, PlanType planType, int seatMinimum)
{
return new ProviderPlan
var plan = new ProviderPlan
{
ProviderId = providerId,
PlanType = planType,
@ -105,5 +118,6 @@ public class CreateProviderCommand : ICreateProviderCommand
PurchasedSeats = 0,
AllocatedSeats = 0
};
await _providerPlanRepository.CreateAsync(plan);
}
}

View File

@ -2,6 +2,7 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.AdminConsole.Providers.Interfaces;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Constants;
@ -27,6 +28,7 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
private readonly IFeatureService _featureService;
private readonly IProviderBillingService _providerBillingService;
private readonly ISubscriberService _subscriberService;
private readonly IHasConfirmedOwnersExceptQuery _hasConfirmedOwnersExceptQuery;
public RemoveOrganizationFromProviderCommand(
IEventService eventService,
@ -37,7 +39,8 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
IStripeAdapter stripeAdapter,
IFeatureService featureService,
IProviderBillingService providerBillingService,
ISubscriberService subscriberService)
ISubscriberService subscriberService,
IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery)
{
_eventService = eventService;
_mailService = mailService;
@ -48,6 +51,7 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
_featureService = featureService;
_providerBillingService = providerBillingService;
_subscriberService = subscriberService;
_hasConfirmedOwnersExceptQuery = hasConfirmedOwnersExceptQuery;
}
public async Task RemoveOrganizationFromProvider(
@ -63,7 +67,7 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
throw new BadRequestException("Failed to remove organization. Please contact support.");
}
if (!await _organizationService.HasConfirmedOwnersExceptAsync(
if (!await _hasConfirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync(
providerOrganization.OrganizationId,
Array.Empty<Guid>(),
includeProvider: false))

View File

@ -379,42 +379,23 @@ public class ProviderBillingService(
var subscriptionItemOptionsList = new List<SubscriptionItemOptions>();
var teamsProviderPlan =
providerPlans.SingleOrDefault(providerPlan => providerPlan.PlanType == PlanType.TeamsMonthly);
if (teamsProviderPlan == null || !teamsProviderPlan.IsConfigured())
foreach (var providerPlan in providerPlans)
{
logger.LogError("Cannot start subscription for provider ({ProviderID}) that has no configured Teams plan", provider.Id);
var plan = StaticStore.GetPlan(providerPlan.PlanType);
throw new BillingException();
if (!providerPlan.IsConfigured())
{
logger.LogError("Cannot start subscription for provider ({ProviderID}) that has no configured {ProviderName} plan", provider.Id, plan.Name);
throw new BillingException();
}
subscriptionItemOptionsList.Add(new SubscriptionItemOptions
{
Price = plan.PasswordManager.StripeProviderPortalSeatPlanId,
Quantity = providerPlan.SeatMinimum
});
}
var teamsPlan = StaticStore.GetPlan(PlanType.TeamsMonthly);
subscriptionItemOptionsList.Add(new SubscriptionItemOptions
{
Price = teamsPlan.PasswordManager.StripeProviderPortalSeatPlanId,
Quantity = teamsProviderPlan.SeatMinimum
});
var enterpriseProviderPlan =
providerPlans.SingleOrDefault(providerPlan => providerPlan.PlanType == PlanType.EnterpriseMonthly);
if (enterpriseProviderPlan == null || !enterpriseProviderPlan.IsConfigured())
{
logger.LogError("Cannot start subscription for provider ({ProviderID}) that has no configured Enterprise plan", provider.Id);
throw new BillingException();
}
var enterprisePlan = StaticStore.GetPlan(PlanType.EnterpriseMonthly);
subscriptionItemOptionsList.Add(new SubscriptionItemOptions
{
Price = enterprisePlan.PasswordManager.StripeProviderPortalSeatPlanId,
Quantity = enterpriseProviderPlan.SeatMinimum
});
var subscriptionCreateOptions = new SubscriptionCreateOptions
{
AutomaticTax = new SubscriptionAutomaticTaxOptions

View File

@ -17,7 +17,6 @@ namespace Bit.Scim.Controllers.v2;
[ExceptionHandlerFilter]
public class UsersController : Controller
{
private readonly IUserService _userService;
private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly IOrganizationService _organizationService;
private readonly IGetUsersListQuery _getUsersListQuery;
@ -27,7 +26,6 @@ public class UsersController : Controller
private readonly ILogger<UsersController> _logger;
public UsersController(
IUserService userService,
IOrganizationUserRepository organizationUserRepository,
IOrganizationService organizationService,
IGetUsersListQuery getUsersListQuery,
@ -36,7 +34,6 @@ public class UsersController : Controller
IPostUserCommand postUserCommand,
ILogger<UsersController> logger)
{
_userService = userService;
_organizationUserRepository = organizationUserRepository;
_organizationService = organizationService;
_getUsersListQuery = getUsersListQuery;
@ -60,17 +57,15 @@ public class UsersController : Controller
[HttpGet("")]
public async Task<IActionResult> Get(
Guid organizationId,
[FromQuery] string filter,
[FromQuery] int? count,
[FromQuery] int? startIndex)
[FromQuery] GetUsersQueryParamModel model)
{
var usersListQueryResult = await _getUsersListQuery.GetUsersListAsync(organizationId, filter, count, startIndex);
var usersListQueryResult = await _getUsersListQuery.GetUsersListAsync(organizationId, model);
var scimListResponseModel = new ScimListResponseModel<ScimUserResponseModel>
{
Resources = usersListQueryResult.userList.Select(u => new ScimUserResponseModel(u)).ToList(),
ItemsPerPage = count.GetValueOrDefault(usersListQueryResult.userList.Count()),
ItemsPerPage = model.Count,
TotalResults = usersListQueryResult.totalResults,
StartIndex = startIndex.GetValueOrDefault(1),
StartIndex = model.StartIndex,
};
return Ok(scimListResponseModel);
}
@ -98,7 +93,7 @@ public class UsersController : Controller
if (model.Active && orgUser.Status == OrganizationUserStatusType.Revoked)
{
await _organizationService.RestoreUserAsync(orgUser, EventSystemUser.SCIM, _userService);
await _organizationService.RestoreUserAsync(orgUser, EventSystemUser.SCIM);
}
else if (!model.Active && orgUser.Status != OrganizationUserStatusType.Revoked)
{

View File

@ -0,0 +1,12 @@
using System.ComponentModel.DataAnnotations;
public class GetUsersQueryParamModel
{
public string Filter { get; init; } = string.Empty;
[Range(1, int.MaxValue)]
public int Count { get; init; } = 50;
[Range(1, int.MaxValue)]
public int StartIndex { get; init; } = 1;
}

View File

@ -69,6 +69,7 @@ public class Startup
// Services
services.AddBaseServices(globalSettings);
services.AddDefaultServices(globalSettings);
services.AddDistributedCache(globalSettings);
services.AddBillingOperations();
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();

View File

@ -13,11 +13,16 @@ public class GetUsersListQuery : IGetUsersListQuery
_organizationUserRepository = organizationUserRepository;
}
public async Task<(IEnumerable<OrganizationUserUserDetails> userList, int totalResults)> GetUsersListAsync(Guid organizationId, string filter, int? count, int? startIndex)
public async Task<(IEnumerable<OrganizationUserUserDetails> userList, int totalResults)> GetUsersListAsync(Guid organizationId, GetUsersQueryParamModel userQueryParams)
{
string emailFilter = null;
string usernameFilter = null;
string externalIdFilter = null;
int count = userQueryParams.Count;
int startIndex = userQueryParams.StartIndex;
string filter = userQueryParams.Filter;
if (!string.IsNullOrWhiteSpace(filter))
{
var filterLower = filter.ToLowerInvariant();
@ -56,11 +61,11 @@ public class GetUsersListQuery : IGetUsersListQuery
}
totalResults = userList.Count;
}
else if (string.IsNullOrWhiteSpace(filter) && startIndex.HasValue && count.HasValue)
else if (string.IsNullOrWhiteSpace(filter))
{
userList = orgUsers.OrderBy(ou => ou.Email)
.Skip(startIndex.Value - 1)
.Take(count.Value)
.Skip(startIndex - 1)
.Take(count)
.ToList();
totalResults = orgUsers.Count;
}

View File

@ -4,5 +4,5 @@ namespace Bit.Scim.Users.Interfaces;
public interface IGetUsersListQuery
{
Task<(IEnumerable<OrganizationUserUserDetails> userList, int totalResults)> GetUsersListAsync(Guid organizationId, string filter, int? count, int? startIndex);
Task<(IEnumerable<OrganizationUserUserDetails> userList, int totalResults)> GetUsersListAsync(Guid organizationId, GetUsersQueryParamModel userQueryParams);
}

View File

@ -9,18 +9,15 @@ namespace Bit.Scim.Users;
public class PatchUserCommand : IPatchUserCommand
{
private readonly IUserService _userService;
private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly IOrganizationService _organizationService;
private readonly ILogger<PatchUserCommand> _logger;
public PatchUserCommand(
IUserService userService,
IOrganizationUserRepository organizationUserRepository,
IOrganizationService organizationService,
ILogger<PatchUserCommand> logger)
{
_userService = userService;
_organizationUserRepository = organizationUserRepository;
_organizationService = organizationService;
_logger = logger;
@ -74,7 +71,7 @@ public class PatchUserCommand : IPatchUserCommand
{
if (active && orgUser.Status == OrganizationUserStatusType.Revoked)
{
await _organizationService.RestoreUserAsync(orgUser, EventSystemUser.SCIM, _userService);
await _organizationService.RestoreUserAsync(orgUser, EventSystemUser.SCIM);
return true;
}
else if (!active && orgUser.Status != OrganizationUserStatusType.Revoked)

View File

@ -9,7 +9,7 @@
"version": "0.0.0",
"license": "-",
"dependencies": {
"bootstrap": "5.3.3",
"bootstrap": "4.6.2",
"font-awesome": "4.7.0",
"jquery": "3.7.1",
"popper.js": "1.16.1"
@ -18,9 +18,9 @@
"css-loader": "7.1.2",
"expose-loader": "5.0.0",
"mini-css-extract-plugin": "2.9.1",
"sass": "1.77.8",
"sass-loader": "16.0.1",
"webpack": "5.94.0",
"sass": "1.79.5",
"sass-loader": "16.0.2",
"webpack": "5.95.0",
"webpack-cli": "5.1.4"
}
},
@ -98,10 +98,296 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@parcel/watcher": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.4.1.tgz",
"integrity": "sha512-HNjmfLQEVRZmHRET336f20H/8kOozUGwk7yajvsonjNxbj2wBTK1WsQuHkD5yYh9RxFGL2EyDHryOihOwUoKDA==",
"dev": true,
"license": "MIT",
"dependencies": {
"detect-libc": "^1.0.3",
"is-glob": "^4.0.3",
"micromatch": "^4.0.5",
"node-addon-api": "^7.0.0"
},
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
},
"optionalDependencies": {
"@parcel/watcher-android-arm64": "2.4.1",
"@parcel/watcher-darwin-arm64": "2.4.1",
"@parcel/watcher-darwin-x64": "2.4.1",
"@parcel/watcher-freebsd-x64": "2.4.1",
"@parcel/watcher-linux-arm-glibc": "2.4.1",
"@parcel/watcher-linux-arm64-glibc": "2.4.1",
"@parcel/watcher-linux-arm64-musl": "2.4.1",
"@parcel/watcher-linux-x64-glibc": "2.4.1",
"@parcel/watcher-linux-x64-musl": "2.4.1",
"@parcel/watcher-win32-arm64": "2.4.1",
"@parcel/watcher-win32-ia32": "2.4.1",
"@parcel/watcher-win32-x64": "2.4.1"
}
},
"node_modules/@parcel/watcher-android-arm64": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.4.1.tgz",
"integrity": "sha512-LOi/WTbbh3aTn2RYddrO8pnapixAziFl6SMxHM69r3tvdSm94JtCenaKgk1GRg5FJ5wpMCpHeW+7yqPlvZv7kg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-darwin-arm64": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.4.1.tgz",
"integrity": "sha512-ln41eihm5YXIY043vBrrHfn94SIBlqOWmoROhsMVTSXGh0QahKGy77tfEywQ7v3NywyxBBkGIfrWRHm0hsKtzA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-darwin-x64": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.4.1.tgz",
"integrity": "sha512-yrw81BRLjjtHyDu7J61oPuSoeYWR3lDElcPGJyOvIXmor6DEo7/G2u1o7I38cwlcoBHQFULqF6nesIX3tsEXMg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-freebsd-x64": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.4.1.tgz",
"integrity": "sha512-TJa3Pex/gX3CWIx/Co8k+ykNdDCLx+TuZj3f3h7eOjgpdKM+Mnix37RYsYU4LHhiYJz3DK5nFCCra81p6g050w==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-arm-glibc": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.4.1.tgz",
"integrity": "sha512-4rVYDlsMEYfa537BRXxJ5UF4ddNwnr2/1O4MHM5PjI9cvV2qymvhwZSFgXqbS8YoTk5i/JR0L0JDs69BUn45YA==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-arm64-glibc": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.4.1.tgz",
"integrity": "sha512-BJ7mH985OADVLpbrzCLgrJ3TOpiZggE9FMblfO65PlOCdG++xJpKUJ0Aol74ZUIYfb8WsRlUdgrZxKkz3zXWYA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-arm64-musl": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.4.1.tgz",
"integrity": "sha512-p4Xb7JGq3MLgAfYhslU2SjoV9G0kI0Xry0kuxeG/41UfpjHGOhv7UoUDAz/jb1u2elbhazy4rRBL8PegPJFBhA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-x64-glibc": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.4.1.tgz",
"integrity": "sha512-s9O3fByZ/2pyYDPoLM6zt92yu6P4E39a03zvO0qCHOTjxmt3GHRMLuRZEWhWLASTMSrrnVNWdVI/+pUElJBBBg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-x64-musl": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.4.1.tgz",
"integrity": "sha512-L2nZTYR1myLNST0O632g0Dx9LyMNHrn6TOt76sYxWLdff3cB22/GZX2UPtJnaqQPdCRoszoY5rcOj4oMTtp5fQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-win32-arm64": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.4.1.tgz",
"integrity": "sha512-Uq2BPp5GWhrq/lcuItCHoqxjULU1QYEcyjSO5jqqOK8RNFDBQnenMMx4gAl3v8GiWa59E9+uDM7yZ6LxwUIfRg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-win32-ia32": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.4.1.tgz",
"integrity": "sha512-maNRit5QQV2kgHFSYwftmPBxiuK5u4DXjbXx7q6eKjq5dsLXZ4FJiVvlcw35QXzk0KrUecJmuVFbj4uV9oYrcw==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-win32-x64": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.4.1.tgz",
"integrity": "sha512-+DvS92F9ezicfswqrvIRM2njcYJbd5mb9CUgtrHCHmvn7pPPa+nMDRu1o1bYYz/l5IB2NVGNJWiH7h1E58IF2A==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@types/estree": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
"integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
"integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
"dev": true,
"license": "MIT"
},
@ -113,13 +399,13 @@
"license": "MIT"
},
"node_modules/@types/node": {
"version": "22.1.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.1.0.tgz",
"integrity": "sha512-AOmuRF0R2/5j1knA3c6G3HOk523Ga+l+ZXltX8SF1+5oqcXijjfTd8fY3XRZqSihEu9XhtQnKYLmkFaoxgsJHw==",
"version": "22.7.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz",
"integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.13.0"
"undici-types": "~6.19.2"
}
},
"node_modules/@webassemblyjs/ast": {
@ -415,35 +701,8 @@
"ajv": "^8.8.2"
}
},
"node_modules/anymatch": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
"dev": true,
"license": "ISC",
"dependencies": {
"normalize-path": "^3.0.0",
"picomatch": "^2.0.4"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/binary-extensions": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
"integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/bootstrap": {
"version": "5.3.3",
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.6.2.tgz",
"integrity": "sha512-51Bbp/Uxr9aTuy6ca/8FbFloBUJZLHwnhTcnjIeRn2suQWsWzcuJhGjKDB5eppVte/8oCdOL3VuwxvZDUggwGQ==",
"funding": [
@ -476,9 +735,9 @@
}
},
"node_modules/browserslist": {
"version": "4.23.3",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz",
"integrity": "sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==",
"version": "4.24.0",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.0.tgz",
"integrity": "sha512-Rmb62sR1Zpjql25eSanFGEhAxcFwfA1K0GuQcLoaJBAcENegrQut3hYdhXFF1obQfiDyqIW/cLM5HSJ/9k884A==",
"dev": true,
"funding": [
{
@ -496,8 +755,8 @@
],
"license": "MIT",
"dependencies": {
"caniuse-lite": "^1.0.30001646",
"electron-to-chromium": "^1.5.4",
"caniuse-lite": "^1.0.30001663",
"electron-to-chromium": "^1.5.28",
"node-releases": "^2.0.18",
"update-browserslist-db": "^1.1.0"
},
@ -516,9 +775,9 @@
"license": "MIT"
},
"node_modules/caniuse-lite": {
"version": "1.0.30001651",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001651.tgz",
"integrity": "sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==",
"version": "1.0.30001668",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001668.tgz",
"integrity": "sha512-nWLrdxqCdblixUO+27JtGJJE/txpJlyUy5YN1u53wLZkP0emYCo5zgS6QYft7VUYR42LGgi/S5hdLZTrnyIddw==",
"dev": true,
"funding": [
{
@ -537,28 +796,19 @@
"license": "CC-BY-4.0"
},
"node_modules/chokidar": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz",
"integrity": "sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==",
"dev": true,
"license": "MIT",
"dependencies": {
"anymatch": "~3.1.2",
"braces": "~3.0.2",
"glob-parent": "~5.1.2",
"is-binary-path": "~2.1.0",
"is-glob": "~4.0.1",
"normalize-path": "~3.0.0",
"readdirp": "~3.6.0"
"readdirp": "^4.0.1"
},
"engines": {
"node": ">= 8.10.0"
"node": ">= 14.16.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
},
"optionalDependencies": {
"fsevents": "~2.3.2"
}
},
"node_modules/chrome-trace-event": {
@ -664,10 +914,23 @@
"node": ">=4"
}
},
"node_modules/detect-libc": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
"integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"detect-libc": "bin/detect-libc.js"
},
"engines": {
"node": ">=0.10"
}
},
"node_modules/electron-to-chromium": {
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.5.tgz",
"integrity": "sha512-QR7/A7ZkMS8tZuoftC/jfqNkZLQO779SSW3YuZHP4eXpj3EffGLFcB/Xu9AAZQzLccTiCV+EmUo3ha4mQ9wnlA==",
"version": "1.5.36",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.36.tgz",
"integrity": "sha512-HYTX8tKge/VNp6FGO+f/uVDmUkq+cEfcxYhKf15Akc4M5yxt5YmorwlAitKWjWhWQnKcDRBAQKXkhqqXMqcrjw==",
"dev": true,
"license": "ISC"
},
@ -686,9 +949,9 @@
}
},
"node_modules/envinfo": {
"version": "7.13.0",
"resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.13.0.tgz",
"integrity": "sha512-cvcaMr7KqXVh4nyzGTVqTum+gAiL265x5jUWQIDLq//zOGbW+gSW/C+OWLleY/rs9Qole6AZLMXPbtIFQbqu+Q==",
"version": "7.14.0",
"resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.14.0.tgz",
"integrity": "sha512-CO40UI41xDQzhLB1hWyqUKgFhs250pNcGbyGKe1l/e4FSaI/+YE4IMG76GDt0In67WLPACIITC+sOi08x4wIvg==",
"dev": true,
"license": "MIT",
"bin": {
@ -706,9 +969,9 @@
"license": "MIT"
},
"node_modules/escalade": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz",
"integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==",
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
"dev": true,
"license": "MIT",
"engines": {
@ -804,9 +1067,9 @@
"license": "MIT"
},
"node_modules/fast-uri": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.1.tgz",
"integrity": "sha512-MWipKbbYiYI0UC7cl8m/i/IWTqfC8YXsqjzybjddLsFjStroQzsHXkc73JutMvBiXmOvapk+axIl79ig5t55Bw==",
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.2.tgz",
"integrity": "sha512-GR6f0hD7XXyNJa25Tb9BuIdN0tdr+0BMi6/CJPH3wJO1JjNG3n/VsSw38AwRdKZABm8lGbPfakLRkYzx2V9row==",
"dev": true,
"license": "MIT"
},
@ -866,21 +1129,6 @@
"node": ">=0.10.3"
}
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
@ -891,19 +1139,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/glob-parent": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"dev": true,
"license": "ISC",
"dependencies": {
"is-glob": "^4.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/glob-to-regexp": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz",
@ -991,23 +1226,10 @@
"node": ">=10.13.0"
}
},
"node_modules/is-binary-path": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
"dev": true,
"license": "MIT",
"dependencies": {
"binary-extensions": "^2.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/is-core-module": {
"version": "2.15.0",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.0.tgz",
"integrity": "sha512-Dd+Lb2/zvk9SKy1TGCt1wFJFo/MWBPMX5x7KcvLajWTGuomczdQX61PvY5yK6SVACwpoexWo81IfFyoKY2QnTA==",
"version": "2.15.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz",
"integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -1158,6 +1380,20 @@
"dev": true,
"license": "MIT"
},
"node_modules/micromatch": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
"dev": true,
"license": "MIT",
"dependencies": {
"braces": "^3.0.3",
"picomatch": "^2.3.1"
},
"engines": {
"node": ">=8.6"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
@ -1228,6 +1464,13 @@
"dev": true,
"license": "MIT"
},
"node_modules/node-addon-api": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
"dev": true,
"license": "MIT"
},
"node_modules/node-releases": {
"version": "2.0.18",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz",
@ -1235,16 +1478,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/normalize-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/p-limit": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
@ -1312,9 +1545,9 @@
"license": "MIT"
},
"node_modules/picocolors": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz",
"integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==",
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz",
"integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==",
"dev": true,
"license": "ISC"
},
@ -1356,9 +1589,9 @@
}
},
"node_modules/postcss": {
"version": "8.4.41",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.41.tgz",
"integrity": "sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==",
"version": "8.4.47",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz",
"integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==",
"dev": true,
"funding": [
{
@ -1377,8 +1610,8 @@
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.7",
"picocolors": "^1.0.1",
"source-map-js": "^1.2.0"
"picocolors": "^1.1.0",
"source-map-js": "^1.2.1"
},
"engines": {
"node": "^10 || ^12 || >=14"
@ -1448,9 +1681,9 @@
}
},
"node_modules/postcss-selector-parser": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.1.tgz",
"integrity": "sha512-b4dlw/9V8A71rLIDsSwVmak9z2DuBUB7CA1/wSdelNEzqsjoSPeADTWNO09lpH49Diy3/JIZ2bSPB1dI3LJCHg==",
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
"integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -1489,16 +1722,17 @@
}
},
"node_modules/readdirp": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz",
"integrity": "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==",
"dev": true,
"license": "MIT",
"dependencies": {
"picomatch": "^2.2.1"
},
"engines": {
"node": ">=8.10.0"
"node": ">= 14.16.0"
},
"funding": {
"type": "individual",
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/rechoir": {
@ -1587,13 +1821,14 @@
"license": "MIT"
},
"node_modules/sass": {
"version": "1.77.8",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.77.8.tgz",
"integrity": "sha512-4UHg6prsrycW20fqLGPShtEvo/WyHRVRHwOP4DzkUrObWoWI05QBSfzU71TVB7PFaL104TwNaHpjlWXAZbQiNQ==",
"version": "1.79.5",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.79.5.tgz",
"integrity": "sha512-W1h5kp6bdhqFh2tk3DsI771MoEJjvrSY/2ihJRJS4pjIyfJCw0nTsxqhnrUzaLMOJjFchj8rOvraI/YUVjtx5g==",
"dev": true,
"license": "MIT",
"dependencies": {
"chokidar": ">=3.0.0 <4.0.0",
"@parcel/watcher": "^2.4.1",
"chokidar": "^4.0.0",
"immutable": "^4.0.0",
"source-map-js": ">=0.6.2 <2.0.0"
},
@ -1605,9 +1840,9 @@
}
},
"node_modules/sass-loader": {
"version": "16.0.1",
"resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.1.tgz",
"integrity": "sha512-xACl1ToTsKnL9Ce5yYpRxrLj9QUDCnwZNhzpC7tKiFyA8zXsd3Ap+HGVnbCgkdQcm43E+i6oKAWBsvGA6ZoiMw==",
"version": "16.0.2",
"resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.2.tgz",
"integrity": "sha512-Ll6iXZ1EYwYT19SqW4mSBb76vSSi8JgzElmzIerhEGgzB5hRjDQIWsPmuk1UrAXkR16KJHqVY0eH+5/uw9Tmfw==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -1735,9 +1970,9 @@
}
},
"node_modules/source-map-js": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz",
"integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==",
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
@ -1795,9 +2030,9 @@
}
},
"node_modules/terser": {
"version": "5.31.5",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.31.5.tgz",
"integrity": "sha512-YPmas0L0rE1UyLL/llTWA0SiDOqIcAQYLeUj7cJYzXHlRTAnMSg9pPe4VJ5PlKvTrPQsdVFuiRiwyeNlYgwh2Q==",
"version": "5.34.1",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.34.1.tgz",
"integrity": "sha512-FsJZ7iZLd/BXkz+4xrRTGJ26o/6VTjQytUk8b8OxkwcD2I+79VPJlz7qss1+zE7h8GNIScFqXcDyJ/KqBYZFVA==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
@ -1915,16 +2150,16 @@
}
},
"node_modules/undici-types": {
"version": "6.13.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.13.0.tgz",
"integrity": "sha512-xtFJHudx8S2DSoujjMd1WeWvn7KKWFRESZTMeL1RptAYERu29D6jphMjjY+vn96jvN3kVPDNxU/E13VTaXj6jg==",
"version": "6.19.8",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
"dev": true,
"license": "MIT"
},
"node_modules/update-browserslist-db": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz",
"integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==",
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz",
"integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==",
"dev": true,
"funding": [
{
@ -1942,8 +2177,8 @@
],
"license": "MIT",
"dependencies": {
"escalade": "^3.1.2",
"picocolors": "^1.0.1"
"escalade": "^3.2.0",
"picocolors": "^1.1.0"
},
"bin": {
"update-browserslist-db": "cli.js"
@ -1970,9 +2205,9 @@
"license": "MIT"
},
"node_modules/watchpack": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.1.tgz",
"integrity": "sha512-8wrBCMtVhqcXP2Sup1ctSkga6uc2Bx0IIvKyT7yTFier5AXHooSI+QyQQAtTb7+E0IUCCKyTFmXqdqgum2XWGg==",
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz",
"integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -1984,9 +2219,9 @@
}
},
"node_modules/webpack": {
"version": "5.94.0",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.94.0.tgz",
"integrity": "sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg==",
"version": "5.95.0",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.95.0.tgz",
"integrity": "sha512-2t3XstrKULz41MNMBF+cJ97TyHdyQ8HCt//pqErqDvNjU9YQBnZxIHa11VXsi7F3mb5/aO2tuDxdeTPdU7xu9Q==",
"dev": true,
"license": "MIT",
"dependencies": {

View File

@ -8,7 +8,7 @@
"build": "webpack"
},
"dependencies": {
"bootstrap": "5.3.3",
"bootstrap": "4.6.2",
"font-awesome": "4.7.0",
"jquery": "3.7.1",
"popper.js": "1.16.1"
@ -17,9 +17,9 @@
"css-loader": "7.1.2",
"expose-loader": "5.0.0",
"mini-css-extract-plugin": "2.9.1",
"sass": "1.77.8",
"sass-loader": "16.0.1",
"webpack": "5.94.0",
"sass": "1.79.5",
"sass-loader": "16.0.2",
"webpack": "5.95.0",
"webpack-cli": "5.1.4"
}
}

View File

@ -3,6 +3,7 @@ using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Billing.Enums;
using Bit.Core.Entities;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
@ -19,23 +20,30 @@ public class CreateProviderCommandTests
[Theory, BitAutoData]
public async Task CreateMspAsync_UserIdIsInvalid_Throws(Provider provider, SutProvider<CreateProviderCommand> sutProvider)
{
// Arrange
provider.Type = ProviderType.Msp;
// Act
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.CreateMspAsync(provider, default, default, default));
// Assert
Assert.Contains("Invalid owner.", exception.Message);
}
[Theory, BitAutoData]
public async Task CreateMspAsync_Success(Provider provider, User user, SutProvider<CreateProviderCommand> sutProvider)
{
// Arrange
provider.Type = ProviderType.Msp;
var userRepository = sutProvider.GetDependency<IUserRepository>();
userRepository.GetByEmailAsync(user.Email).Returns(user);
// Act
await sutProvider.Sut.CreateMspAsync(provider, user.Email, default, default);
// Assert
await sutProvider.GetDependency<IProviderRepository>().ReceivedWithAnyArgs().CreateAsync(default);
await sutProvider.GetDependency<IProviderService>().Received(1).SendProviderSetupInviteEmailAsync(provider, user.Email);
}
@ -43,11 +51,52 @@ public class CreateProviderCommandTests
[Theory, BitAutoData]
public async Task CreateResellerAsync_Success(Provider provider, SutProvider<CreateProviderCommand> sutProvider)
{
// Arrange
provider.Type = ProviderType.Reseller;
// Act
await sutProvider.Sut.CreateResellerAsync(provider);
// Assert
await sutProvider.GetDependency<IProviderRepository>().ReceivedWithAnyArgs().CreateAsync(default);
await sutProvider.GetDependency<IProviderService>().DidNotReceiveWithAnyArgs().SendProviderSetupInviteEmailAsync(default, default);
}
[Theory, BitAutoData]
public async Task CreateMultiOrganizationEnterpriseAsync_Success(
Provider provider,
User user,
PlanType plan,
int minimumSeats,
SutProvider<CreateProviderCommand> sutProvider)
{
// Arrange
provider.Type = ProviderType.MultiOrganizationEnterprise;
var userRepository = sutProvider.GetDependency<IUserRepository>();
userRepository.GetByEmailAsync(user.Email).Returns(user);
// Act
await sutProvider.Sut.CreateMultiOrganizationEnterpriseAsync(provider, user.Email, plan, minimumSeats);
// Assert
await sutProvider.GetDependency<IProviderRepository>().ReceivedWithAnyArgs().CreateAsync(provider);
await sutProvider.GetDependency<IProviderService>().Received(1).SendProviderSetupInviteEmailAsync(provider, user.Email);
}
[Theory, BitAutoData]
public async Task CreateMultiOrganizationEnterpriseAsync_UserIdIsInvalid_Throws(
Provider provider,
SutProvider<CreateProviderCommand> sutProvider)
{
// Arrange
provider.Type = ProviderType.Msp;
// Act
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.CreateMultiOrganizationEnterpriseAsync(provider, default, default, default));
// Assert
Assert.Contains("Invalid owner.", exception.Message);
}
}

View File

@ -3,6 +3,7 @@ using Bit.Core;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Enums;
@ -75,7 +76,7 @@ public class RemoveOrganizationFromProviderCommandTests
{
providerOrganization.ProviderId = provider.Id;
sutProvider.GetDependency<IOrganizationService>().HasConfirmedOwnersExceptAsync(
sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>().HasConfirmedOwnersExceptAsync(
providerOrganization.OrganizationId,
[],
includeProvider: false)
@ -98,7 +99,7 @@ public class RemoveOrganizationFromProviderCommandTests
organization.GatewayCustomerId = null;
organization.GatewaySubscriptionId = null;
sutProvider.GetDependency<IOrganizationService>().HasConfirmedOwnersExceptAsync(
sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>().HasConfirmedOwnersExceptAsync(
providerOrganization.OrganizationId,
[],
includeProvider: false)
@ -141,7 +142,7 @@ public class RemoveOrganizationFromProviderCommandTests
{
providerOrganization.ProviderId = provider.Id;
sutProvider.GetDependency<IOrganizationService>().HasConfirmedOwnersExceptAsync(
sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>().HasConfirmedOwnersExceptAsync(
providerOrganization.OrganizationId,
[],
includeProvider: false)
@ -208,7 +209,7 @@ public class RemoveOrganizationFromProviderCommandTests
var teamsMonthlyPlan = StaticStore.GetPlan(PlanType.TeamsMonthly);
sutProvider.GetDependency<IOrganizationService>().HasConfirmedOwnersExceptAsync(
sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>().HasConfirmedOwnersExceptAsync(
providerOrganization.OrganizationId,
[],
includeProvider: false)

View File

@ -236,6 +236,46 @@ public class UsersControllerTests : IClassFixture<ScimApplicationFactory>, IAsyn
AssertHelper.AssertPropertyEqual(expectedResponse, responseModel);
}
[Fact]
public async Task GetList_SearchUserNameWithoutOptionalParameters_Success()
{
string filter = "userName eq user2@example.com";
int? itemsPerPage = null;
int? startIndex = null;
var expectedResponse = new ScimListResponseModel<ScimUserResponseModel>
{
ItemsPerPage = 50, //default value
TotalResults = 1,
StartIndex = 1, //default value
Resources = new List<ScimUserResponseModel>
{
new ScimUserResponseModel
{
Id = ScimApplicationFactory.TestOrganizationUserId2,
DisplayName = "Test User 2",
ExternalId = "UB",
Active = true,
Emails = new List<BaseScimUserModel.EmailModel>
{
new BaseScimUserModel.EmailModel { Primary = true, Type = "work", Value = "user2@example.com" }
},
Groups = new List<string>(),
Name = new BaseScimUserModel.NameModel("Test User 2"),
UserName = "user2@example.com",
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
}
},
Schemas = new List<string> { ScimConstants.Scim2SchemaListResponse }
};
var context = await _factory.UsersGetListAsync(ScimApplicationFactory.TestOrganizationId1, filter, itemsPerPage, startIndex);
Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode);
var responseModel = JsonSerializer.Deserialize<ScimListResponseModel<ScimUserResponseModel>>(context.Response.Body, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
AssertHelper.AssertPropertyEqual(expectedResponse, responseModel);
}
[Fact]
public async Task Post_Success()
{

View File

@ -9,7 +9,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.8" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.10" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(MicrosoftNetTestSdkVersion)" />
<PackageReference Include="NSubstitute" Version="$(NSubstituteVersion)" />
<PackageReference Include="xunit" Version="$(XUnitVersion)" />

View File

@ -24,7 +24,7 @@ public class GetUsersListQueryTests
.GetManyDetailsByOrganizationAsync(organizationId)
.Returns(organizationUserUserDetails);
var result = await sutProvider.Sut.GetUsersListAsync(organizationId, null, count, startIndex);
var result = await sutProvider.Sut.GetUsersListAsync(organizationId, new GetUsersQueryParamModel { Count = count, StartIndex = startIndex });
await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1).GetManyDetailsByOrganizationAsync(organizationId);
@ -49,7 +49,7 @@ public class GetUsersListQueryTests
.GetManyDetailsByOrganizationAsync(organizationId)
.Returns(organizationUserUserDetails);
var result = await sutProvider.Sut.GetUsersListAsync(organizationId, filter, null, null);
var result = await sutProvider.Sut.GetUsersListAsync(organizationId, new GetUsersQueryParamModel { Filter = filter });
await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1).GetManyDetailsByOrganizationAsync(organizationId);
@ -71,7 +71,7 @@ public class GetUsersListQueryTests
.GetManyDetailsByOrganizationAsync(organizationId)
.Returns(organizationUserUserDetails);
var result = await sutProvider.Sut.GetUsersListAsync(organizationId, filter, null, null);
var result = await sutProvider.Sut.GetUsersListAsync(organizationId, new GetUsersQueryParamModel { Filter = filter });
await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1).GetManyDetailsByOrganizationAsync(organizationId);
@ -96,7 +96,7 @@ public class GetUsersListQueryTests
.GetManyDetailsByOrganizationAsync(organizationId)
.Returns(organizationUserUserDetails);
var result = await sutProvider.Sut.GetUsersListAsync(organizationId, filter, null, null);
var result = await sutProvider.Sut.GetUsersListAsync(organizationId, new GetUsersQueryParamModel { Filter = filter });
await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1).GetManyDetailsByOrganizationAsync(organizationId);
@ -120,7 +120,7 @@ public class GetUsersListQueryTests
.GetManyDetailsByOrganizationAsync(organizationId)
.Returns(organizationUserUserDetails);
var result = await sutProvider.Sut.GetUsersListAsync(organizationId, filter, null, null);
var result = await sutProvider.Sut.GetUsersListAsync(organizationId, new GetUsersQueryParamModel { Filter = filter });
await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1).GetManyDetailsByOrganizationAsync(organizationId);

View File

@ -43,7 +43,7 @@ public class PatchUserCommandTests
await sutProvider.Sut.PatchUserAsync(organizationUser.OrganizationId, organizationUser.Id, scimPatchModel);
await sutProvider.GetDependency<IOrganizationService>().Received(1).RestoreUserAsync(organizationUser, EventSystemUser.SCIM, Arg.Any<IUserService>());
await sutProvider.GetDependency<IOrganizationService>().Received(1).RestoreUserAsync(organizationUser, EventSystemUser.SCIM);
}
[Theory]
@ -71,7 +71,7 @@ public class PatchUserCommandTests
await sutProvider.Sut.PatchUserAsync(organizationUser.OrganizationId, organizationUser.Id, scimPatchModel);
await sutProvider.GetDependency<IOrganizationService>().Received(1).RestoreUserAsync(organizationUser, EventSystemUser.SCIM, Arg.Any<IUserService>());
await sutProvider.GetDependency<IOrganizationService>().Received(1).RestoreUserAsync(organizationUser, EventSystemUser.SCIM);
}
[Theory]
@ -147,7 +147,7 @@ public class PatchUserCommandTests
await sutProvider.Sut.PatchUserAsync(organizationUser.OrganizationId, organizationUser.Id, scimPatchModel);
await sutProvider.GetDependency<IOrganizationService>().DidNotReceiveWithAnyArgs().RestoreUserAsync(default, EventSystemUser.SCIM, default);
await sutProvider.GetDependency<IOrganizationService>().DidNotReceiveWithAnyArgs().RestoreUserAsync(default, EventSystemUser.SCIM);
await sutProvider.GetDependency<IOrganizationService>().DidNotReceiveWithAnyArgs().RevokeUserAsync(default, EventSystemUser.SCIM);
}

1
dev/.gitignore vendored
View File

@ -5,7 +5,6 @@ secrets.json
# Docker container configurations
.env
authsources.php
directory.ldif
# Development certificates
identity_server_dev.crt

View File

@ -59,7 +59,7 @@ services:
container_name: bw-mysql
ports:
- "3306:3306"
command:
command:
- --default-authentication-plugin=mysql_native_password
- --innodb-print-all-deadlocks=ON
environment:
@ -84,20 +84,6 @@ services:
profiles:
- idp
open-ldap:
image: osixia/openldap:1.5.0
command: --copy-service
environment:
LDAP_ORGANISATION: "Bitwarden"
LDAP_DOMAIN: "bitwarden.com"
volumes:
- ./directory.ldif:/container/service/slapd/assets/config/bootstrap/ldif/output.ldif
ports:
- "389:389"
- "636:636"
profiles:
- ldap
reverse-proxy:
image: nginx:alpine
container_name: reverse-proxy

View File

@ -7,12 +7,10 @@ using Bit.Core;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Providers.Interfaces;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Billing.Extensions;
using Bit.Core.Billing.Services;
using Bit.Core.Context;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.OrganizationConnectionConfigs;
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
using Bit.Core.Repositories;
@ -57,7 +55,6 @@ public class OrganizationsController : Controller
private readonly IRemoveOrganizationFromProviderCommand _removeOrganizationFromProviderCommand;
private readonly IFeatureService _featureService;
private readonly IProviderBillingService _providerBillingService;
private readonly IPolicyService _policyService;
public OrganizationsController(
IOrganizationService organizationService,
@ -84,8 +81,7 @@ public class OrganizationsController : Controller
IProviderOrganizationRepository providerOrganizationRepository,
IRemoveOrganizationFromProviderCommand removeOrganizationFromProviderCommand,
IFeatureService featureService,
IProviderBillingService providerBillingService,
IPolicyService policyService)
IProviderBillingService providerBillingService)
{
_organizationService = organizationService;
_organizationRepository = organizationRepository;
@ -112,7 +108,6 @@ public class OrganizationsController : Controller
_removeOrganizationFromProviderCommand = removeOrganizationFromProviderCommand;
_featureService = featureService;
_providerBillingService = providerBillingService;
_policyService = policyService;
}
[RequirePermission(Permission.Org_List_View)]
@ -240,7 +235,8 @@ public class OrganizationsController : Controller
if (organization.UseSecretsManager &&
!StaticStore.GetPlan(organization.PlanType).SupportsSecretsManager)
{
throw new BadRequestException("Plan does not support Secrets Manager");
TempData["Error"] = "Plan does not support Secrets Manager";
return RedirectToAction("Edit", new { id });
}
await _organizationRepository.ReplaceAsync(organization);
@ -440,13 +436,6 @@ public class OrganizationsController : Controller
organization.MaxAutoscaleSmServiceAccounts = model.MaxAutoscaleSmServiceAccounts;
}
var plan = StaticStore.GetPlan(organization.PlanType);
if (!organization.UsePolicies || !plan.HasPolicies)
{
await DisableOrganizationPoliciesAsync(organization.Id);
}
if (_accessControlService.UserHasPermission(Permission.Org_Licensing_Edit))
{
organization.LicenseKey = model.LicenseKey;
@ -463,18 +452,4 @@ public class OrganizationsController : Controller
return organization;
}
private async Task DisableOrganizationPoliciesAsync(Guid organizationId)
{
var policies = await _policyRepository.GetManyByOrganizationIdAsync(organizationId);
if (policies.Count != 0)
{
await Task.WhenAll(policies.Select(async policy =>
{
policy.Enabled = false;
await _policyService.SaveAsync(policy, _userService, _organizationService, null);
}));
}
}
}

View File

@ -107,9 +107,15 @@ public class ProvidersController : Controller
});
}
public IActionResult Create(int teamsMinimumSeats, int enterpriseMinimumSeats, string ownerEmail = null)
public IActionResult Create()
{
return View(new CreateProviderModel
return View(new CreateProviderModel());
}
[HttpGet("providers/create/msp")]
public IActionResult CreateMsp(int teamsMinimumSeats, int enterpriseMinimumSeats, string ownerEmail = null)
{
return View(new CreateMspProviderModel
{
OwnerEmail = ownerEmail,
TeamsMonthlySeatMinimum = teamsMinimumSeats,
@ -117,10 +123,50 @@ public class ProvidersController : Controller
});
}
[HttpGet("providers/create/reseller")]
public IActionResult CreateReseller()
{
return View(new CreateResellerProviderModel());
}
[HttpGet("providers/create/multi-organization-enterprise")]
public IActionResult CreateMultiOrganizationEnterprise(int enterpriseMinimumSeats, string ownerEmail = null)
{
if (!_featureService.IsEnabled(FeatureFlagKeys.PM12275_MultiOrganizationEnterprises))
{
return RedirectToAction("Create");
}
return View(new CreateMultiOrganizationEnterpriseProviderModel
{
OwnerEmail = ownerEmail,
EnterpriseSeatMinimum = enterpriseMinimumSeats
});
}
[HttpPost]
[ValidateAntiForgeryToken]
[RequirePermission(Permission.Provider_Create)]
public async Task<IActionResult> Create(CreateProviderModel model)
public IActionResult Create(CreateProviderModel model)
{
if (!ModelState.IsValid)
{
return View(model);
}
return model.Type switch
{
ProviderType.Msp => RedirectToAction("CreateMsp"),
ProviderType.Reseller => RedirectToAction("CreateReseller"),
ProviderType.MultiOrganizationEnterprise => RedirectToAction("CreateMultiOrganizationEnterprise"),
_ => View(model)
};
}
[HttpPost("providers/create/msp")]
[ValidateAntiForgeryToken]
[RequirePermission(Permission.Provider_Create)]
public async Task<IActionResult> CreateMsp(CreateMspProviderModel model)
{
if (!ModelState.IsValid)
{
@ -128,19 +174,51 @@ public class ProvidersController : Controller
}
var provider = model.ToProvider();
switch (provider.Type)
await _createProviderCommand.CreateMspAsync(
provider,
model.OwnerEmail,
model.TeamsMonthlySeatMinimum,
model.EnterpriseMonthlySeatMinimum);
return RedirectToAction("Edit", new { id = provider.Id });
}
[HttpPost("providers/create/reseller")]
[ValidateAntiForgeryToken]
[RequirePermission(Permission.Provider_Create)]
public async Task<IActionResult> CreateReseller(CreateResellerProviderModel model)
{
if (!ModelState.IsValid)
{
case ProviderType.Msp:
await _createProviderCommand.CreateMspAsync(
provider,
model.OwnerEmail,
model.TeamsMonthlySeatMinimum,
model.EnterpriseMonthlySeatMinimum);
break;
case ProviderType.Reseller:
await _createProviderCommand.CreateResellerAsync(provider);
break;
return View(model);
}
var provider = model.ToProvider();
await _createProviderCommand.CreateResellerAsync(provider);
return RedirectToAction("Edit", new { id = provider.Id });
}
[HttpPost("providers/create/multi-organization-enterprise")]
[ValidateAntiForgeryToken]
[RequirePermission(Permission.Provider_Create)]
public async Task<IActionResult> CreateMultiOrganizationEnterprise(CreateMultiOrganizationEnterpriseProviderModel model)
{
if (!ModelState.IsValid)
{
return View(model);
}
var provider = model.ToProvider();
if (!_featureService.IsEnabled(FeatureFlagKeys.PM12275_MultiOrganizationEnterprises))
{
return RedirectToAction("Create");
}
await _createProviderCommand.CreateMultiOrganizationEnterpriseAsync(
provider,
model.OwnerEmail,
model.Plan.Value,
model.EnterpriseSeatMinimum);
return RedirectToAction("Edit", new { id = provider.Id });
}
@ -367,7 +445,7 @@ public class ProvidersController : Controller
return BadRequest("Provider does not exist");
}
if (!string.Equals(providerName.Trim(), provider.Name, StringComparison.OrdinalIgnoreCase))
if (!string.Equals(providerName.Trim(), provider.DisplayName(), StringComparison.OrdinalIgnoreCase))
{
return BadRequest("Invalid provider name");
}

View File

@ -0,0 +1,45 @@
using System.ComponentModel.DataAnnotations;
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Enums.Provider;
using Bit.SharedWeb.Utilities;
namespace Bit.Admin.AdminConsole.Models;
public class CreateMspProviderModel : IValidatableObject
{
[Display(Name = "Owner Email")]
public string OwnerEmail { get; set; }
[Display(Name = "Teams (Monthly) Seat Minimum")]
public int TeamsMonthlySeatMinimum { get; set; }
[Display(Name = "Enterprise (Monthly) Seat Minimum")]
public int EnterpriseMonthlySeatMinimum { get; set; }
public virtual Provider ToProvider()
{
return new Provider
{
Type = ProviderType.Msp
};
}
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (string.IsNullOrWhiteSpace(OwnerEmail))
{
var ownerEmailDisplayName = nameof(OwnerEmail).GetDisplayAttribute<CreateMspProviderModel>()?.GetName() ?? nameof(OwnerEmail);
yield return new ValidationResult($"The {ownerEmailDisplayName} field is required.");
}
if (TeamsMonthlySeatMinimum < 0)
{
var teamsMinimumSeatsDisplayName = nameof(TeamsMonthlySeatMinimum).GetDisplayAttribute<CreateMspProviderModel>()?.GetName() ?? nameof(TeamsMonthlySeatMinimum);
yield return new ValidationResult($"The {teamsMinimumSeatsDisplayName} field can not be negative.");
}
if (EnterpriseMonthlySeatMinimum < 0)
{
var enterpriseMinimumSeatsDisplayName = nameof(EnterpriseMonthlySeatMinimum).GetDisplayAttribute<CreateMspProviderModel>()?.GetName() ?? nameof(EnterpriseMonthlySeatMinimum);
yield return new ValidationResult($"The {enterpriseMinimumSeatsDisplayName} field can not be negative.");
}
}
}

View File

@ -0,0 +1,47 @@
using System.ComponentModel.DataAnnotations;
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.Billing.Enums;
using Bit.SharedWeb.Utilities;
namespace Bit.Admin.AdminConsole.Models;
public class CreateMultiOrganizationEnterpriseProviderModel : IValidatableObject
{
[Display(Name = "Owner Email")]
public string OwnerEmail { get; set; }
[Display(Name = "Enterprise Seat Minimum")]
public int EnterpriseSeatMinimum { get; set; }
[Display(Name = "Plan")]
[Required]
public PlanType? Plan { get; set; }
public virtual Provider ToProvider()
{
return new Provider
{
Type = ProviderType.MultiOrganizationEnterprise
};
}
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (string.IsNullOrWhiteSpace(OwnerEmail))
{
var ownerEmailDisplayName = nameof(OwnerEmail).GetDisplayAttribute<CreateMultiOrganizationEnterpriseProviderModel>()?.GetName() ?? nameof(OwnerEmail);
yield return new ValidationResult($"The {ownerEmailDisplayName} field is required.");
}
if (EnterpriseSeatMinimum < 0)
{
var enterpriseSeatMinimumDisplayName = nameof(EnterpriseSeatMinimum).GetDisplayAttribute<CreateMultiOrganizationEnterpriseProviderModel>()?.GetName() ?? nameof(EnterpriseSeatMinimum);
yield return new ValidationResult($"The {enterpriseSeatMinimumDisplayName} field can not be negative.");
}
if (Plan != PlanType.EnterpriseAnnually && Plan != PlanType.EnterpriseMonthly)
{
var planDisplayName = nameof(Plan).GetDisplayAttribute<CreateMultiOrganizationEnterpriseProviderModel>()?.GetName() ?? nameof(Plan);
yield return new ValidationResult($"The {planDisplayName} field must be set to Enterprise Annually or Enterprise Monthly.");
}
}
}

View File

@ -1,84 +1,8 @@
using System.ComponentModel.DataAnnotations;
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Enums.Provider;
using Bit.SharedWeb.Utilities;
using Bit.Core.AdminConsole.Enums.Provider;
namespace Bit.Admin.AdminConsole.Models;
public class CreateProviderModel : IValidatableObject
public class CreateProviderModel
{
public CreateProviderModel() { }
[Display(Name = "Provider Type")]
public ProviderType Type { get; set; }
[Display(Name = "Owner Email")]
public string OwnerEmail { get; set; }
[Display(Name = "Name")]
public string Name { get; set; }
[Display(Name = "Business Name")]
public string BusinessName { get; set; }
[Display(Name = "Primary Billing Email")]
public string BillingEmail { get; set; }
[Display(Name = "Teams (Monthly) Seat Minimum")]
public int TeamsMonthlySeatMinimum { get; set; }
[Display(Name = "Enterprise (Monthly) Seat Minimum")]
public int EnterpriseMonthlySeatMinimum { get; set; }
public virtual Provider ToProvider()
{
return new Provider()
{
Type = Type,
Name = Name,
BusinessName = BusinessName,
BillingEmail = BillingEmail?.ToLowerInvariant().Trim()
};
}
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
switch (Type)
{
case ProviderType.Msp:
if (string.IsNullOrWhiteSpace(OwnerEmail))
{
var ownerEmailDisplayName = nameof(OwnerEmail).GetDisplayAttribute<CreateProviderModel>()?.GetName() ?? nameof(OwnerEmail);
yield return new ValidationResult($"The {ownerEmailDisplayName} field is required.");
}
if (TeamsMonthlySeatMinimum < 0)
{
var teamsMinimumSeatsDisplayName = nameof(TeamsMonthlySeatMinimum).GetDisplayAttribute<CreateProviderModel>()?.GetName() ?? nameof(TeamsMonthlySeatMinimum);
yield return new ValidationResult($"The {teamsMinimumSeatsDisplayName} field can not be negative.");
}
if (EnterpriseMonthlySeatMinimum < 0)
{
var enterpriseMinimumSeatsDisplayName = nameof(EnterpriseMonthlySeatMinimum).GetDisplayAttribute<CreateProviderModel>()?.GetName() ?? nameof(EnterpriseMonthlySeatMinimum);
yield return new ValidationResult($"The {enterpriseMinimumSeatsDisplayName} field can not be negative.");
}
break;
case ProviderType.Reseller:
if (string.IsNullOrWhiteSpace(Name))
{
var nameDisplayName = nameof(Name).GetDisplayAttribute<CreateProviderModel>()?.GetName() ?? nameof(Name);
yield return new ValidationResult($"The {nameDisplayName} field is required.");
}
if (string.IsNullOrWhiteSpace(BusinessName))
{
var businessNameDisplayName = nameof(BusinessName).GetDisplayAttribute<CreateProviderModel>()?.GetName() ?? nameof(BusinessName);
yield return new ValidationResult($"The {businessNameDisplayName} field is required.");
}
if (string.IsNullOrWhiteSpace(BillingEmail))
{
var billingEmailDisplayName = nameof(BillingEmail).GetDisplayAttribute<CreateProviderModel>()?.GetName() ?? nameof(BillingEmail);
yield return new ValidationResult($"The {billingEmailDisplayName} field is required.");
}
break;
}
}
}

View File

@ -0,0 +1,48 @@
using System.ComponentModel.DataAnnotations;
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Enums.Provider;
using Bit.SharedWeb.Utilities;
namespace Bit.Admin.AdminConsole.Models;
public class CreateResellerProviderModel : IValidatableObject
{
[Display(Name = "Name")]
public string Name { get; set; }
[Display(Name = "Business Name")]
public string BusinessName { get; set; }
[Display(Name = "Primary Billing Email")]
public string BillingEmail { get; set; }
public virtual Provider ToProvider()
{
return new Provider
{
Name = Name,
BusinessName = BusinessName,
BillingEmail = BillingEmail?.ToLowerInvariant().Trim(),
Type = ProviderType.Reseller
};
}
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (string.IsNullOrWhiteSpace(Name))
{
var nameDisplayName = nameof(Name).GetDisplayAttribute<CreateProviderModel>()?.GetName() ?? nameof(Name);
yield return new ValidationResult($"The {nameDisplayName} field is required.");
}
if (string.IsNullOrWhiteSpace(BusinessName))
{
var businessNameDisplayName = nameof(BusinessName).GetDisplayAttribute<CreateProviderModel>()?.GetName() ?? nameof(BusinessName);
yield return new ValidationResult($"The {businessNameDisplayName} field is required.");
}
if (string.IsNullOrWhiteSpace(BillingEmail))
{
var billingEmailDisplayName = nameof(BillingEmail).GetDisplayAttribute<CreateProviderModel>()?.GetName() ?? nameof(BillingEmail);
yield return new ValidationResult($"The {billingEmailDisplayName} field is required.");
}
}
}

View File

@ -181,7 +181,6 @@ public class OrganizationEditModel : OrganizationViewModel
*/
public object GetPlansHelper() =>
StaticStore.Plans
.Where(p => p.SupportsSecretsManager)
.Select(p =>
{
var plan = new

View File

@ -1,4 +1,6 @@
@model OrganizationViewModel
@inject Bit.Core.Services.IFeatureService FeatureService
@model OrganizationViewModel
<dl class="row">
<dt class="col-sm-4 col-lg-3">Id</dt>
<dd id="org-id" class="col-sm-8 col-lg-9"><code>@Model.Organization.Id</code></dd>
@ -53,8 +55,19 @@
<dt class="col-sm-4 col-lg-3">Administrators manage all collections</dt>
<dd id="pm-manage-collections" class="col-sm-8 col-lg-9">@(Model.Organization.AllowAdminAccessToAllCollectionItems ? "On" : "Off")</dd>
<dt class="col-sm-4 col-lg-3">Limit collection creation to administrators</dt>
<dd id="pm-collection-creation" class="col-sm-8 col-lg-9">@(Model.Organization.LimitCollectionCreationDeletion ? "On" : "Off")</dd>
@if (!FeatureService.IsEnabled(Bit.Core.FeatureFlagKeys.LimitCollectionCreationDeletionSplit))
{
<dt class="col-sm-4 col-lg-3">Limit collection creation to administrators</dt>
<dd id="pm-collection-creation" class="col-sm-8 col-lg-9">@(Model.Organization.LimitCollectionCreationDeletion ? "On" : "Off")</dd>
}
else
{
<dt class="col-sm-4 col-lg-3">Limit collection creation to administrators</dt>
<dd id="pm-collection-creation" class="col-sm-8 col-lg-9">@(Model.Organization.LimitCollectionCreation ? "On" : "Off")</dd>
<dt class="col-sm-4 col-lg-3">Limit collection deletion to administrators</dt>
<dd id="pm-collection-deletion" class="col-sm-8 col-lg-9">@(Model.Organization.LimitCollectionDeletion ? "On" : "Off")</dd>
}
</dl>
<h2>Secrets Manager</h2>

View File

@ -1,80 +1,48 @@
@using Bit.SharedWeb.Utilities
@using Bit.Core.AdminConsole.Enums.Provider
@using Bit.Core
@model CreateProviderModel
@inject Bit.Core.Services.IFeatureService FeatureService
@{
ViewData["Title"] = "Create Provider";
}
@section Scripts {
<script>
function toggleProviderTypeInfo(value) {
document.querySelectorAll('[id^="info-"]').forEach(el => { el.classList.add('d-none'); });
document.getElementById('info-' + value).classList.remove('d-none');
}
</script>
var providerTypes = Enum.GetValues<ProviderType>()
.OrderBy(x => x.GetDisplayAttribute().Order)
.ToList();
if (!FeatureService.IsEnabled(FeatureFlagKeys.PM12275_MultiOrganizationEnterprises))
{
providerTypes.Remove(ProviderType.MultiOrganizationEnterprise);
}
}
<h1>Create Provider</h1>
<form method="post">
<form method="post" asp-action="Create">
<div asp-validation-summary="All" class="alert alert-danger"></div>
<div class="form-group">
<label asp-for="Type" class="h2"></label>
@foreach(ProviderType providerType in Enum.GetValues(typeof(ProviderType)))
@foreach (var providerType in providerTypes)
{
var providerTypeValue = (int)providerType;
<div class="form-check">
@Html.RadioButtonFor(m => m.Type, providerType, new { id = $"providerType-{providerTypeValue}", @class = "form-check-input", onclick=$"toggleProviderTypeInfo({providerTypeValue})" })
@Html.LabelFor(m => m.Type, providerType.GetDisplayAttribute()?.GetName(), new { @class = "form-check-label align-middle", @for = $"providerType-{providerTypeValue}" })
<br/>
@Html.LabelFor(m => m.Type, providerType.GetDisplayAttribute()?.GetDescription(), new { @class = "form-check-label small text-muted ml-3 align-top", @for = $"providerType-{providerTypeValue}" })
</div>
}
</div>
<div id="@($"info-{(int)ProviderType.Msp}")" class="form-group @(Model.Type != ProviderType.Msp ? "d-none" : string.Empty)">
<h2>MSP Info</h2>
<div class="form-group">
<label asp-for="OwnerEmail"></label>
<input type="text" class="form-control" asp-for="OwnerEmail">
</div>
@if (FeatureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling))
{
<div class="row">
<div class="col-sm">
<div class="form-group">
<label asp-for="TeamsMonthlySeatMinimum"></label>
<input type="number" class="form-control" asp-for="TeamsMonthlySeatMinimum">
<div class="form-group">
<div class="row">
<div class="col">
<div class="form-check">
@Html.RadioButtonFor(m => m.Type, providerType, new { id = $"providerType-{providerTypeValue}", @class = "form-check-input" })
@Html.LabelFor(m => m.Type, providerType.GetDisplayAttribute()?.GetName(), new { @class = "form-check-label align-middle", @for = $"providerType-{providerTypeValue}" })
</div>
</div>
</div>
<div class="col-sm">
<div class="form-group">
<label asp-for="EnterpriseMonthlySeatMinimum"></label>
<input type="number" class="form-control" asp-for="EnterpriseMonthlySeatMinimum">
<div class="row">
<div class="col">
@Html.LabelFor(m => m.Type, providerType.GetDisplayAttribute()?.GetDescription(), new { @class = "form-check-label small text-muted align-top", @for = $"providerType-{providerTypeValue}" })
</div>
</div>
</div>
}
</div>
<div id="@($"info-{(int)ProviderType.Reseller}")" class="form-group @(Model.Type != ProviderType.Reseller ? "d-none" : string.Empty)">
<h2>Reseller Info</h2>
<div class="form-group">
<label asp-for="Name"></label>
<input type="text" class="form-control" asp-for="Name">
</div>
<div class="form-group">
<label asp-for="BusinessName"></label>
<input type="text" class="form-control" asp-for="BusinessName">
</div>
<div class="form-group">
<label asp-for="BillingEmail"></label>
<input type="text" class="form-control" asp-for="BillingEmail">
</div>
</div>
<button type="submit" class="btn btn-primary mb-2">Create Provider</button>
<button type="submit" class="btn btn-primary mb-2">Next</button>
</form>

View File

@ -0,0 +1,39 @@
@using Bit.Core.AdminConsole.Enums.Provider
@using Bit.Core
@model CreateMspProviderModel
@inject Bit.Core.Services.IFeatureService FeatureService
@{
ViewData["Title"] = "Create Managed Service Provider";
}
<h1>Create Managed Service Provider</h1>
<div>
<form class="form-group" method="post" asp-action="CreateMsp">
<div asp-validation-summary="All" class="alert alert-danger"></div>
<div class="form-group">
<label asp-for="OwnerEmail"></label>
<input type="text" class="form-control" asp-for="OwnerEmail">
</div>
@if (FeatureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling))
{
<div class="row">
<div class="col-sm">
<div class="form-group">
<label asp-for="TeamsMonthlySeatMinimum"></label>
<input type="number" class="form-control" asp-for="TeamsMonthlySeatMinimum">
</div>
</div>
<div class="col-sm">
<div class="form-group">
<label asp-for="EnterpriseMonthlySeatMinimum"></label>
<input type="number" class="form-control" asp-for="EnterpriseMonthlySeatMinimum">
</div>
</div>
</div>
}
<button type="submit" class="btn btn-primary mb-2">Create Provider</button>
</form>
</div>

View File

@ -0,0 +1,43 @@
@using Bit.Core.Billing.Enums
@using Microsoft.AspNetCore.Mvc.TagHelpers
@model CreateMultiOrganizationEnterpriseProviderModel
@{
ViewData["Title"] = "Create Multi-organization Enterprise Provider";
}
<h1>Create Multi-organization Enterprise Provider</h1>
<div>
<form class="form-group" method="post" asp-action="CreateMultiOrganizationEnterprise">
<div asp-validation-summary="All" class="alert alert-danger"></div>
<div class="form-group">
<label asp-for="OwnerEmail"></label>
<input type="text" class="form-control" asp-for="OwnerEmail">
</div>
<div class="row">
<div class="col-sm">
<div class="form-group">
@{
var multiOrgPlans = new List<PlanType>
{
PlanType.EnterpriseAnnually,
PlanType.EnterpriseMonthly
};
}
<label asp-for="Plan"></label>
<select class="form-control" asp-for="Plan" asp-items="Html.GetEnumSelectList(multiOrgPlans)">
<option value="">--</option>
</select>
</div>
</div>
<div class="col-sm">
<div class="form-group">
<label asp-for="EnterpriseSeatMinimum"></label>
<input type="number" class="form-control" asp-for="EnterpriseSeatMinimum">
</div>
</div>
</div>
<button type="submit" class="btn btn-primary mb-2">Create Provider</button>
</form>
</div>

View File

@ -0,0 +1,25 @@
@model CreateResellerProviderModel
@{
ViewData["Title"] = "Create Reseller Provider";
}
<h1>Create Reseller Provider</h1>
<div>
<form class="form-group" method="post" asp-action="CreateReseller">
<div asp-validation-summary="All" class="alert alert-danger"></div>
<div class="form-group">
<label asp-for="Name"></label>
<input type="text" class="form-control" asp-for="Name">
</div>
<div class="form-group">
<label asp-for="BusinessName"></label>
<input type="text" class="form-control" asp-for="BusinessName">
</div>
<div class="form-group">
<label asp-for="BillingEmail"></label>
<input type="text" class="form-control" asp-for="BillingEmail">
</div>
<button type="submit" class="btn btn-primary mb-2">Create Provider</button>
</form>
</div>

View File

@ -174,18 +174,15 @@
<div class="d-flex mt-4">
<button type="submit" class="btn btn-primary" form="edit-form">Save</button>
@if (FeatureService.IsEnabled(FeatureFlagKeys.EnableDeleteProvider))
{
<div class="ml-auto d-flex">
<button class="btn btn-danger" onclick="openRequestDeleteModal(@Model.ProviderOrganizations.Count())">Request Delete</button>
<button id="requestDeletionBtn" hidden="hidden" data-toggle="modal" data-target="#requestDeletionModal"></button>
<div class="ml-auto d-flex">
<button class="btn btn-danger" onclick="openRequestDeleteModal(@Model.ProviderOrganizations.Count())">Request Delete</button>
<button id="requestDeletionBtn" hidden="hidden" data-toggle="modal" data-target="#requestDeletionModal"></button>
<button class="btn btn-outline-danger ml-2" onclick="openDeleteModal(@Model.ProviderOrganizations.Count())">Delete</button>
<button id="deleteBtn" hidden="hidden" data-toggle="modal" data-target="#DeleteModal"></button>
<button class="btn btn-outline-danger ml-2" onclick="openDeleteModal(@Model.ProviderOrganizations.Count())">Delete</button>
<button id="deleteBtn" hidden="hidden" data-toggle="modal" data-target="#DeleteModal"></button>
<button id="linkAccWarningBtn" hidden="hidden" data-toggle="modal" data-target="#linkedWarningModal"></button>
<button id="linkAccWarningBtn" hidden="hidden" data-toggle="modal" data-target="#linkedWarningModal"></button>
</div>
}
</div>
</div>
}

View File

@ -20,9 +20,10 @@
function deleteProvider(id) {
const providerName = $('#DeleteModal input#provider-name').val();
const encodedProviderName = encodeURIComponent(providerName);
$.ajax({
type: "POST",
url: `@Url.Action("Delete", "Providers")?id=${id}&providerName=${providerName}`,
url: `@Url.Action("Delete", "Providers")?id=${id}&providerName=${encodedProviderName}`,
dataType: 'json',
contentType: false,
processData: false,

View File

@ -0,0 +1,83 @@
using Bit.Admin.Billing.Models;
using Bit.Admin.Enums;
using Bit.Admin.Utilities;
using Bit.Core.Billing.Migration.Models;
using Bit.Core.Billing.Migration.Services;
using Bit.Core.Utilities;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Bit.Admin.Billing.Controllers;
[Authorize]
[Route("migrate-providers")]
[SelfHosted(NotSelfHostedOnly = true)]
public class MigrateProvidersController(
IProviderMigrator providerMigrator) : Controller
{
[HttpGet]
[RequirePermission(Permission.Tools_MigrateProviders)]
public IActionResult Index()
{
return View(new MigrateProvidersRequestModel());
}
[HttpPost]
[RequirePermission(Permission.Tools_MigrateProviders)]
[ValidateAntiForgeryToken]
public async Task<IActionResult> PostAsync(MigrateProvidersRequestModel request)
{
var providerIds = GetProviderIdsFromInput(request.ProviderIds);
if (providerIds.Count == 0)
{
return RedirectToAction("Index");
}
foreach (var providerId in providerIds)
{
await providerMigrator.Migrate(providerId);
}
return RedirectToAction("Results", new { ProviderIds = string.Join("\r\n", providerIds) });
}
[HttpGet("results")]
[RequirePermission(Permission.Tools_MigrateProviders)]
public async Task<IActionResult> ResultsAsync(MigrateProvidersRequestModel request)
{
var providerIds = GetProviderIdsFromInput(request.ProviderIds);
if (providerIds.Count == 0)
{
return View(Array.Empty<ProviderMigrationResult>());
}
var results = await Task.WhenAll(providerIds.Select(providerMigrator.GetResult));
return View(results);
}
[HttpGet("results/{providerId:guid}")]
[RequirePermission(Permission.Tools_MigrateProviders)]
public async Task<IActionResult> DetailsAsync([FromRoute] Guid providerId)
{
var result = await providerMigrator.GetResult(providerId);
if (result == null)
{
return RedirectToAction("Index");
}
return View(result);
}
private static List<Guid> GetProviderIdsFromInput(string text) => !string.IsNullOrEmpty(text)
? text.Split(
["\r\n", "\r", "\n"],
StringSplitOptions.TrimEntries
)
.Select(id => new Guid(id))
.ToList()
: [];
}

View File

@ -0,0 +1,10 @@
using System.ComponentModel.DataAnnotations;
namespace Bit.Admin.Billing.Models;
public class MigrateProvidersRequestModel
{
[Required]
[Display(Name = "Provider IDs")]
public string ProviderIds { get; set; }
}

View File

@ -0,0 +1,39 @@
@using System.Text.Json
@model Bit.Core.Billing.Migration.Models.ProviderMigrationResult
@{
ViewData["Title"] = "Results";
}
<h1>Migrate Providers</h1>
<h2>Migration Details: @Model.ProviderName</h2>
<dl class="row">
<dt class="col-sm-4 col-lg-3">Id</dt>
<dd class="col-sm-8 col-lg-9"><code>@Model.ProviderId</code></dd>
<dt class="col-sm-4 col-lg-3">Result</dt>
<dd class="col-sm-8 col-lg-9">@Model.Result</dd>
</dl>
<h3>Client Organizations</h3>
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Result</th>
<th>Previous State</th>
</tr>
</thead>
<tbody>
@foreach (var clientResult in Model.Clients)
{
<tr>
<td>@clientResult.OrganizationId</td>
<td>@clientResult.OrganizationName</td>
<td>@clientResult.Result</td>
<td><pre>@Html.Raw(JsonSerializer.Serialize(clientResult.PreviousState))</pre></td>
</tr>
}
</tbody>
</table>
</div>

View File

@ -0,0 +1,46 @@
@model Bit.Admin.Billing.Models.MigrateProvidersRequestModel;
@{
ViewData["Title"] = "Migrate Providers";
}
<h1>Migrate Providers</h1>
<h2>Bulk Consolidated Billing Migration Tool</h2>
<section>
<p>
This tool allows you to provide a list of IDs for Providers that you would like to migrate to Consolidated Billing.
Because of the expensive nature of the operation, you can only migrate 10 Providers at a time.
</p>
<p class="alert alert-warning">
Updates made through this tool are irreversible without manual intervention.
</p>
<p>Example Input (Please enter each Provider ID separated by a new line):</p>
<div class="card">
<div class="card-body">
<pre class="mb-0">f513affc-2290-4336-879e-21ec3ecf3e78
f7a5cb0d-4b74-445c-8d8c-232d1d32bbe2
bf82d3cf-0e21-4f39-b81b-ef52b2fc6a3a
174e82fc-70c3-448d-9fe7-00bad2a3ab00
22a4bbbf-58e3-4e4c-a86a-a0d7caf4ff14</pre>
</div>
</div>
<form method="post" asp-controller="MigrateProviders" asp-action="Post" class="mt-2">
<div asp-validation-summary="All" class="text-danger"></div>
<div class="form-group">
<label asp-for="ProviderIds"></label>
<textarea rows="10" class="form-control" type="text" asp-for="ProviderIds"></textarea>
</div>
<div class="form-group">
<input type="submit" value="Run" class="btn btn-primary mb-2"/>
</div>
</form>
<form method="get" asp-controller="MigrateProviders" asp-action="Results" class="mt-2">
<div asp-validation-summary="All" class="text-danger"></div>
<div class="form-group">
<label asp-for="ProviderIds"></label>
<textarea rows="10" class="form-control" type="text" asp-for="ProviderIds"></textarea>
</div>
<div class="form-group">
<input type="submit" value="See Previous Results" class="btn btn-primary mb-2"/>
</div>
</form>
</section>

View File

@ -0,0 +1,28 @@
@model Bit.Core.Billing.Migration.Models.ProviderMigrationResult[]
@{
ViewData["Title"] = "Results";
}
<h1>Migrate Providers</h1>
<h2>Results</h2>
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Result</th>
</tr>
</thead>
<tbody>
@foreach (var result in Model)
{
<tr>
<td><a href="@Url.Action("Details", "MigrateProviders", new { providerId = result.ProviderId })">@result.ProviderId</a></td>
<td>@result.ProviderName</td>
<td>@result.Result</td>
</tr>
}
</tbody>
</table>
</div>

View File

@ -25,8 +25,8 @@ public class UsersController : Controller
private readonly GlobalSettings _globalSettings;
private readonly IAccessControlService _accessControlService;
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
private readonly IFeatureService _featureService;
private readonly IUserService _userService;
private readonly IFeatureService _featureService;
public UsersController(
IUserRepository userRepository,
@ -35,8 +35,8 @@ public class UsersController : Controller
GlobalSettings globalSettings,
IAccessControlService accessControlService,
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
IFeatureService featureService,
IUserService userService)
IUserService userService,
IFeatureService featureService)
{
_userRepository = userRepository;
_cipherRepository = cipherRepository;
@ -44,8 +44,8 @@ public class UsersController : Controller
_globalSettings = globalSettings;
_accessControlService = accessControlService;
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
_featureService = featureService;
_userService = userService;
_featureService = featureService;
}
[RequirePermission(Permission.User_List_View)]
@ -64,22 +64,8 @@ public class UsersController : Controller
var skip = (page - 1) * count;
var users = await _userRepository.SearchAsync(email, skip, count);
var userModels = new List<UserViewModel>();
if (_featureService.IsEnabled(FeatureFlagKeys.MembersTwoFAQueryOptimization))
{
var twoFactorAuthLookup = (await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(users.Select(u => u.Id))).ToList();
userModels = UserViewModel.MapViewModels(users, twoFactorAuthLookup).ToList();
}
else
{
foreach (var user in users)
{
var isTwoFactorEnabled = await _userService.TwoFactorIsEnabledAsync(user);
userModels.Add(UserViewModel.MapViewModel(user, isTwoFactorEnabled));
}
}
var twoFactorAuthLookup = (await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(users.Select(u => u.Id))).ToList();
var userModels = UserViewModel.MapViewModels(users, twoFactorAuthLookup).ToList();
return View(new UsersModel
{
@ -103,8 +89,8 @@ public class UsersController : Controller
var ciphers = await _cipherRepository.GetManyByUserIdAsync(id);
var isTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user);
return View(UserViewModel.MapViewModel(user, isTwoFactorEnabled, ciphers));
var verifiedDomain = await AccountDeprovisioningEnabled(user.Id);
return View(UserViewModel.MapViewModel(user, isTwoFactorEnabled, ciphers, verifiedDomain));
}
[SelfHosted(NotSelfHostedOnly = true)]
@ -120,7 +106,8 @@ public class UsersController : Controller
var billingInfo = await _paymentService.GetBillingAsync(user);
var billingHistoryInfo = await _paymentService.GetBillingHistoryAsync(user);
var isTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user);
return View(new UserEditModel(user, isTwoFactorEnabled, ciphers, billingInfo, billingHistoryInfo, _globalSettings));
var verifiedDomain = await AccountDeprovisioningEnabled(user.Id);
return View(new UserEditModel(user, isTwoFactorEnabled, ciphers, billingInfo, billingHistoryInfo, _globalSettings, verifiedDomain));
}
[HttpPost]
@ -174,4 +161,12 @@ public class UsersController : Controller
return RedirectToAction("Index");
}
// TODO: Feature flag to be removed in PM-14207
private async Task<bool?> AccountDeprovisioningEnabled(Guid userId)
{
return _featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
? await _userService.IsManagedByAnyOrganizationAsync(userId)
: null;
}
}

View File

@ -0,0 +1,19 @@

using Bit.SharedWeb.Utilities;
// ReSharper disable once CheckNamespace
namespace Microsoft.AspNetCore.Mvc.Rendering;
public static class HtmlHelper
{
public static IEnumerable<SelectListItem> GetEnumSelectList<T>(this IHtmlHelper htmlHelper, IEnumerable<T> values)
where T : Enum
{
return values.Select(v => new SelectListItem
{
Text = v.GetDisplayAttribute().Name,
Value = v.ToString()
});
}
}

View File

@ -48,5 +48,6 @@ public enum Permission
Tools_ManageTaxRates,
Tools_ManageStripeSubscriptions,
Tools_CreateEditTransaction,
Tools_ProcessStripeEvents
Tools_ProcessStripeEvents,
Tools_MigrateProviders
}

View File

@ -20,9 +20,11 @@ public class UserEditModel
IEnumerable<Cipher> ciphers,
BillingInfo billingInfo,
BillingHistoryInfo billingHistoryInfo,
GlobalSettings globalSettings)
GlobalSettings globalSettings,
bool? domainVerified
)
{
User = UserViewModel.MapViewModel(user, isTwoFactorEnabled, ciphers);
User = UserViewModel.MapViewModel(user, isTwoFactorEnabled, ciphers, domainVerified);
BillingInfo = billingInfo;
BillingHistoryInfo = billingHistoryInfo;

View File

@ -14,6 +14,7 @@ public class UserViewModel
public bool Premium { get; }
public short? MaxStorageGb { get; }
public bool EmailVerified { get; }
public bool? DomainVerified { get; }
public bool TwoFactorEnabled { get; }
public DateTime AccountRevisionDate { get; }
public DateTime RevisionDate { get; }
@ -35,6 +36,7 @@ public class UserViewModel
bool premium,
short? maxStorageGb,
bool emailVerified,
bool? domainVerified,
bool twoFactorEnabled,
DateTime accountRevisionDate,
DateTime revisionDate,
@ -56,6 +58,7 @@ public class UserViewModel
Premium = premium;
MaxStorageGb = maxStorageGb;
EmailVerified = emailVerified;
DomainVerified = domainVerified;
TwoFactorEnabled = twoFactorEnabled;
AccountRevisionDate = accountRevisionDate;
RevisionDate = revisionDate;
@ -73,10 +76,10 @@ public class UserViewModel
public static IEnumerable<UserViewModel> MapViewModels(
IEnumerable<User> users,
IEnumerable<(Guid userId, bool twoFactorIsEnabled)> lookup) =>
users.Select(user => MapViewModel(user, lookup));
users.Select(user => MapViewModel(user, lookup, false));
public static UserViewModel MapViewModel(User user,
IEnumerable<(Guid userId, bool twoFactorIsEnabled)> lookup) =>
IEnumerable<(Guid userId, bool twoFactorIsEnabled)> lookup, bool? domainVerified) =>
new(
user.Id,
user.Name,
@ -86,6 +89,7 @@ public class UserViewModel
user.Premium,
user.MaxStorageGb,
user.EmailVerified,
domainVerified,
IsTwoFactorEnabled(user, lookup),
user.AccountRevisionDate,
user.RevisionDate,
@ -100,9 +104,9 @@ public class UserViewModel
Array.Empty<Cipher>());
public static UserViewModel MapViewModel(User user, bool isTwoFactorEnabled) =>
MapViewModel(user, isTwoFactorEnabled, Array.Empty<Cipher>());
MapViewModel(user, isTwoFactorEnabled, Array.Empty<Cipher>(), false);
public static UserViewModel MapViewModel(User user, bool isTwoFactorEnabled, IEnumerable<Cipher> ciphers) =>
public static UserViewModel MapViewModel(User user, bool isTwoFactorEnabled, IEnumerable<Cipher> ciphers, bool? domainVerified) =>
new(
user.Id,
user.Name,
@ -112,6 +116,7 @@ public class UserViewModel
user.Premium,
user.MaxStorageGb,
user.EmailVerified,
domainVerified,
isTwoFactorEnabled,
user.AccountRevisionDate,
user.RevisionDate,

View File

@ -10,6 +10,7 @@ using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Bit.Admin.Services;
using Bit.Core.Billing.Extensions;
using Bit.Core.Billing.Migration;
#if !OSS
using Bit.Commercial.Core.Utilities;
@ -88,8 +89,10 @@ public class Startup
services.AddBaseServices(globalSettings);
services.AddDefaultServices(globalSettings);
services.AddScoped<IAccessControlService, AccessControlService>();
services.AddDistributedCache(globalSettings);
services.AddBillingOperations();
services.AddHttpClient();
services.AddProviderMigration();
#if OSS
services.AddOosServices();

View File

@ -110,6 +110,7 @@ public static class RolePermissionMapping
Permission.User_Licensing_View,
Permission.User_Billing_View,
Permission.User_Billing_LaunchGateway,
Permission.User_Delete,
Permission.Org_List_View,
Permission.Org_OrgInformation_View,
Permission.Org_GeneralDetails_View,
@ -163,6 +164,7 @@ public static class RolePermissionMapping
Permission.Tools_ManageStripeSubscriptions,
Permission.Tools_CreateEditTransaction,
Permission.Tools_ProcessStripeEvents,
Permission.Tools_MigrateProviders
}
},
{ "sales", new List<Permission>

View File

@ -15,6 +15,7 @@
var canManageTaxRates = AccessControlService.UserHasPermission(Permission.Tools_ManageTaxRates);
var canManageStripeSubscriptions = AccessControlService.UserHasPermission(Permission.Tools_ManageStripeSubscriptions);
var canProcessStripeEvents = AccessControlService.UserHasPermission(Permission.Tools_ProcessStripeEvents);
var canMigrateProviders = AccessControlService.UserHasPermission(Permission.Tools_MigrateProviders);
var canViewTools = canChargeBraintree || canCreateTransaction || canPromoteAdmin ||
canGenerateLicense || canManageTaxRates || canManageStripeSubscriptions;
@ -108,12 +109,18 @@
Manage Stripe Subscriptions
</a>
}
@if (canProcessStripeEvents)
@if (canProcessStripeEvents)
{
<a class="dropdown-item" asp-controller="ProcessStripeEvents" asp-action="Index">
Process Stripe Events
</a>
}
@if (canMigrateProviders)
{
<a class="dropdown-item" asp-controller="MigrateProviders" asp-action="index">
Migrate Providers
</a>
}
</div>
</li>
}

View File

@ -1,4 +1,4 @@
@model UserViewModel
@model UserViewModel
<dl class="row">
<dt class="col-sm-4 col-lg-3">Id</dt>
<dd class="col-sm-8 col-lg-9"><code>@Model.Id</code></dd>
@ -12,6 +12,11 @@
<dt class="col-sm-4 col-lg-3">Email Verified</dt>
<dd class="col-sm-8 col-lg-9">@(Model.EmailVerified ? "Yes" : "No")</dd>
@if(Model.DomainVerified.HasValue){
<dt class="col-sm-4 col-lg-3">Domain Verified</dt>
<dd class="col-sm-8 col-lg-9">@(Model.DomainVerified.Value == true ? "Yes" : "No")</dd>
}
<dt class="col-sm-4 col-lg-3">Using 2FA</dt>
<dd class="col-sm-8 col-lg-9">@(Model.TwoFactorEnabled ? "Yes" : "No")</dd>

View File

@ -9,7 +9,7 @@
"version": "0.0.0",
"license": "GPL-3.0",
"dependencies": {
"bootstrap": "5.3.3",
"bootstrap": "4.6.2",
"font-awesome": "4.7.0",
"jquery": "3.7.1",
"popper.js": "1.16.1",
@ -19,9 +19,9 @@
"css-loader": "7.1.2",
"expose-loader": "5.0.0",
"mini-css-extract-plugin": "2.9.1",
"sass": "1.77.8",
"sass-loader": "16.0.1",
"webpack": "5.94.0",
"sass": "1.79.5",
"sass-loader": "16.0.2",
"webpack": "5.95.0",
"webpack-cli": "5.1.4"
}
},
@ -99,10 +99,296 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@parcel/watcher": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.4.1.tgz",
"integrity": "sha512-HNjmfLQEVRZmHRET336f20H/8kOozUGwk7yajvsonjNxbj2wBTK1WsQuHkD5yYh9RxFGL2EyDHryOihOwUoKDA==",
"dev": true,
"license": "MIT",
"dependencies": {
"detect-libc": "^1.0.3",
"is-glob": "^4.0.3",
"micromatch": "^4.0.5",
"node-addon-api": "^7.0.0"
},
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
},
"optionalDependencies": {
"@parcel/watcher-android-arm64": "2.4.1",
"@parcel/watcher-darwin-arm64": "2.4.1",
"@parcel/watcher-darwin-x64": "2.4.1",
"@parcel/watcher-freebsd-x64": "2.4.1",
"@parcel/watcher-linux-arm-glibc": "2.4.1",
"@parcel/watcher-linux-arm64-glibc": "2.4.1",
"@parcel/watcher-linux-arm64-musl": "2.4.1",
"@parcel/watcher-linux-x64-glibc": "2.4.1",
"@parcel/watcher-linux-x64-musl": "2.4.1",
"@parcel/watcher-win32-arm64": "2.4.1",
"@parcel/watcher-win32-ia32": "2.4.1",
"@parcel/watcher-win32-x64": "2.4.1"
}
},
"node_modules/@parcel/watcher-android-arm64": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.4.1.tgz",
"integrity": "sha512-LOi/WTbbh3aTn2RYddrO8pnapixAziFl6SMxHM69r3tvdSm94JtCenaKgk1GRg5FJ5wpMCpHeW+7yqPlvZv7kg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-darwin-arm64": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.4.1.tgz",
"integrity": "sha512-ln41eihm5YXIY043vBrrHfn94SIBlqOWmoROhsMVTSXGh0QahKGy77tfEywQ7v3NywyxBBkGIfrWRHm0hsKtzA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-darwin-x64": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.4.1.tgz",
"integrity": "sha512-yrw81BRLjjtHyDu7J61oPuSoeYWR3lDElcPGJyOvIXmor6DEo7/G2u1o7I38cwlcoBHQFULqF6nesIX3tsEXMg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-freebsd-x64": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.4.1.tgz",
"integrity": "sha512-TJa3Pex/gX3CWIx/Co8k+ykNdDCLx+TuZj3f3h7eOjgpdKM+Mnix37RYsYU4LHhiYJz3DK5nFCCra81p6g050w==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-arm-glibc": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.4.1.tgz",
"integrity": "sha512-4rVYDlsMEYfa537BRXxJ5UF4ddNwnr2/1O4MHM5PjI9cvV2qymvhwZSFgXqbS8YoTk5i/JR0L0JDs69BUn45YA==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-arm64-glibc": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.4.1.tgz",
"integrity": "sha512-BJ7mH985OADVLpbrzCLgrJ3TOpiZggE9FMblfO65PlOCdG++xJpKUJ0Aol74ZUIYfb8WsRlUdgrZxKkz3zXWYA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-arm64-musl": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.4.1.tgz",
"integrity": "sha512-p4Xb7JGq3MLgAfYhslU2SjoV9G0kI0Xry0kuxeG/41UfpjHGOhv7UoUDAz/jb1u2elbhazy4rRBL8PegPJFBhA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-x64-glibc": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.4.1.tgz",
"integrity": "sha512-s9O3fByZ/2pyYDPoLM6zt92yu6P4E39a03zvO0qCHOTjxmt3GHRMLuRZEWhWLASTMSrrnVNWdVI/+pUElJBBBg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-x64-musl": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.4.1.tgz",
"integrity": "sha512-L2nZTYR1myLNST0O632g0Dx9LyMNHrn6TOt76sYxWLdff3cB22/GZX2UPtJnaqQPdCRoszoY5rcOj4oMTtp5fQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-win32-arm64": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.4.1.tgz",
"integrity": "sha512-Uq2BPp5GWhrq/lcuItCHoqxjULU1QYEcyjSO5jqqOK8RNFDBQnenMMx4gAl3v8GiWa59E9+uDM7yZ6LxwUIfRg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-win32-ia32": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.4.1.tgz",
"integrity": "sha512-maNRit5QQV2kgHFSYwftmPBxiuK5u4DXjbXx7q6eKjq5dsLXZ4FJiVvlcw35QXzk0KrUecJmuVFbj4uV9oYrcw==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-win32-x64": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.4.1.tgz",
"integrity": "sha512-+DvS92F9ezicfswqrvIRM2njcYJbd5mb9CUgtrHCHmvn7pPPa+nMDRu1o1bYYz/l5IB2NVGNJWiH7h1E58IF2A==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@types/estree": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
"integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
"integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
"dev": true,
"license": "MIT"
},
@ -114,13 +400,13 @@
"license": "MIT"
},
"node_modules/@types/node": {
"version": "22.1.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.1.0.tgz",
"integrity": "sha512-AOmuRF0R2/5j1knA3c6G3HOk523Ga+l+ZXltX8SF1+5oqcXijjfTd8fY3XRZqSihEu9XhtQnKYLmkFaoxgsJHw==",
"version": "22.7.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz",
"integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.13.0"
"undici-types": "~6.19.2"
}
},
"node_modules/@webassemblyjs/ast": {
@ -416,35 +702,8 @@
"ajv": "^8.8.2"
}
},
"node_modules/anymatch": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
"dev": true,
"license": "ISC",
"dependencies": {
"normalize-path": "^3.0.0",
"picomatch": "^2.0.4"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/binary-extensions": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
"integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/bootstrap": {
"version": "5.3.3",
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.6.2.tgz",
"integrity": "sha512-51Bbp/Uxr9aTuy6ca/8FbFloBUJZLHwnhTcnjIeRn2suQWsWzcuJhGjKDB5eppVte/8oCdOL3VuwxvZDUggwGQ==",
"funding": [
@ -477,9 +736,9 @@
}
},
"node_modules/browserslist": {
"version": "4.23.3",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz",
"integrity": "sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==",
"version": "4.24.0",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.0.tgz",
"integrity": "sha512-Rmb62sR1Zpjql25eSanFGEhAxcFwfA1K0GuQcLoaJBAcENegrQut3hYdhXFF1obQfiDyqIW/cLM5HSJ/9k884A==",
"dev": true,
"funding": [
{
@ -497,8 +756,8 @@
],
"license": "MIT",
"dependencies": {
"caniuse-lite": "^1.0.30001646",
"electron-to-chromium": "^1.5.4",
"caniuse-lite": "^1.0.30001663",
"electron-to-chromium": "^1.5.28",
"node-releases": "^2.0.18",
"update-browserslist-db": "^1.1.0"
},
@ -517,9 +776,9 @@
"license": "MIT"
},
"node_modules/caniuse-lite": {
"version": "1.0.30001651",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001651.tgz",
"integrity": "sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==",
"version": "1.0.30001668",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001668.tgz",
"integrity": "sha512-nWLrdxqCdblixUO+27JtGJJE/txpJlyUy5YN1u53wLZkP0emYCo5zgS6QYft7VUYR42LGgi/S5hdLZTrnyIddw==",
"dev": true,
"funding": [
{
@ -538,28 +797,19 @@
"license": "CC-BY-4.0"
},
"node_modules/chokidar": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz",
"integrity": "sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==",
"dev": true,
"license": "MIT",
"dependencies": {
"anymatch": "~3.1.2",
"braces": "~3.0.2",
"glob-parent": "~5.1.2",
"is-binary-path": "~2.1.0",
"is-glob": "~4.0.1",
"normalize-path": "~3.0.0",
"readdirp": "~3.6.0"
"readdirp": "^4.0.1"
},
"engines": {
"node": ">= 8.10.0"
"node": ">= 14.16.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
},
"optionalDependencies": {
"fsevents": "~2.3.2"
}
},
"node_modules/chrome-trace-event": {
@ -665,10 +915,23 @@
"node": ">=4"
}
},
"node_modules/detect-libc": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
"integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"detect-libc": "bin/detect-libc.js"
},
"engines": {
"node": ">=0.10"
}
},
"node_modules/electron-to-chromium": {
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.5.tgz",
"integrity": "sha512-QR7/A7ZkMS8tZuoftC/jfqNkZLQO779SSW3YuZHP4eXpj3EffGLFcB/Xu9AAZQzLccTiCV+EmUo3ha4mQ9wnlA==",
"version": "1.5.36",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.36.tgz",
"integrity": "sha512-HYTX8tKge/VNp6FGO+f/uVDmUkq+cEfcxYhKf15Akc4M5yxt5YmorwlAitKWjWhWQnKcDRBAQKXkhqqXMqcrjw==",
"dev": true,
"license": "ISC"
},
@ -687,9 +950,9 @@
}
},
"node_modules/envinfo": {
"version": "7.13.0",
"resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.13.0.tgz",
"integrity": "sha512-cvcaMr7KqXVh4nyzGTVqTum+gAiL265x5jUWQIDLq//zOGbW+gSW/C+OWLleY/rs9Qole6AZLMXPbtIFQbqu+Q==",
"version": "7.14.0",
"resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.14.0.tgz",
"integrity": "sha512-CO40UI41xDQzhLB1hWyqUKgFhs250pNcGbyGKe1l/e4FSaI/+YE4IMG76GDt0In67WLPACIITC+sOi08x4wIvg==",
"dev": true,
"license": "MIT",
"bin": {
@ -707,9 +970,9 @@
"license": "MIT"
},
"node_modules/escalade": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz",
"integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==",
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
"dev": true,
"license": "MIT",
"engines": {
@ -805,9 +1068,9 @@
"license": "MIT"
},
"node_modules/fast-uri": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.1.tgz",
"integrity": "sha512-MWipKbbYiYI0UC7cl8m/i/IWTqfC8YXsqjzybjddLsFjStroQzsHXkc73JutMvBiXmOvapk+axIl79ig5t55Bw==",
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.2.tgz",
"integrity": "sha512-GR6f0hD7XXyNJa25Tb9BuIdN0tdr+0BMi6/CJPH3wJO1JjNG3n/VsSw38AwRdKZABm8lGbPfakLRkYzx2V9row==",
"dev": true,
"license": "MIT"
},
@ -867,21 +1130,6 @@
"node": ">=0.10.3"
}
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
@ -892,19 +1140,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/glob-parent": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"dev": true,
"license": "ISC",
"dependencies": {
"is-glob": "^4.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/glob-to-regexp": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz",
@ -992,23 +1227,10 @@
"node": ">=10.13.0"
}
},
"node_modules/is-binary-path": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
"dev": true,
"license": "MIT",
"dependencies": {
"binary-extensions": "^2.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/is-core-module": {
"version": "2.15.0",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.0.tgz",
"integrity": "sha512-Dd+Lb2/zvk9SKy1TGCt1wFJFo/MWBPMX5x7KcvLajWTGuomczdQX61PvY5yK6SVACwpoexWo81IfFyoKY2QnTA==",
"version": "2.15.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz",
"integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -1159,6 +1381,20 @@
"dev": true,
"license": "MIT"
},
"node_modules/micromatch": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
"dev": true,
"license": "MIT",
"dependencies": {
"braces": "^3.0.3",
"picomatch": "^2.3.1"
},
"engines": {
"node": ">=8.6"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
@ -1229,6 +1465,13 @@
"dev": true,
"license": "MIT"
},
"node_modules/node-addon-api": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
"dev": true,
"license": "MIT"
},
"node_modules/node-releases": {
"version": "2.0.18",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz",
@ -1236,16 +1479,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/normalize-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/p-limit": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
@ -1313,9 +1546,9 @@
"license": "MIT"
},
"node_modules/picocolors": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz",
"integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==",
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz",
"integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==",
"dev": true,
"license": "ISC"
},
@ -1357,9 +1590,9 @@
}
},
"node_modules/postcss": {
"version": "8.4.41",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.41.tgz",
"integrity": "sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==",
"version": "8.4.47",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz",
"integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==",
"dev": true,
"funding": [
{
@ -1378,8 +1611,8 @@
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.7",
"picocolors": "^1.0.1",
"source-map-js": "^1.2.0"
"picocolors": "^1.1.0",
"source-map-js": "^1.2.1"
},
"engines": {
"node": "^10 || ^12 || >=14"
@ -1449,9 +1682,9 @@
}
},
"node_modules/postcss-selector-parser": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.1.tgz",
"integrity": "sha512-b4dlw/9V8A71rLIDsSwVmak9z2DuBUB7CA1/wSdelNEzqsjoSPeADTWNO09lpH49Diy3/JIZ2bSPB1dI3LJCHg==",
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
"integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -1490,16 +1723,17 @@
}
},
"node_modules/readdirp": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz",
"integrity": "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==",
"dev": true,
"license": "MIT",
"dependencies": {
"picomatch": "^2.2.1"
},
"engines": {
"node": ">=8.10.0"
"node": ">= 14.16.0"
},
"funding": {
"type": "individual",
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/rechoir": {
@ -1588,13 +1822,14 @@
"license": "MIT"
},
"node_modules/sass": {
"version": "1.77.8",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.77.8.tgz",
"integrity": "sha512-4UHg6prsrycW20fqLGPShtEvo/WyHRVRHwOP4DzkUrObWoWI05QBSfzU71TVB7PFaL104TwNaHpjlWXAZbQiNQ==",
"version": "1.79.5",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.79.5.tgz",
"integrity": "sha512-W1h5kp6bdhqFh2tk3DsI771MoEJjvrSY/2ihJRJS4pjIyfJCw0nTsxqhnrUzaLMOJjFchj8rOvraI/YUVjtx5g==",
"dev": true,
"license": "MIT",
"dependencies": {
"chokidar": ">=3.0.0 <4.0.0",
"@parcel/watcher": "^2.4.1",
"chokidar": "^4.0.0",
"immutable": "^4.0.0",
"source-map-js": ">=0.6.2 <2.0.0"
},
@ -1606,9 +1841,9 @@
}
},
"node_modules/sass-loader": {
"version": "16.0.1",
"resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.1.tgz",
"integrity": "sha512-xACl1ToTsKnL9Ce5yYpRxrLj9QUDCnwZNhzpC7tKiFyA8zXsd3Ap+HGVnbCgkdQcm43E+i6oKAWBsvGA6ZoiMw==",
"version": "16.0.2",
"resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.2.tgz",
"integrity": "sha512-Ll6iXZ1EYwYT19SqW4mSBb76vSSi8JgzElmzIerhEGgzB5hRjDQIWsPmuk1UrAXkR16KJHqVY0eH+5/uw9Tmfw==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -1736,9 +1971,9 @@
}
},
"node_modules/source-map-js": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz",
"integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==",
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
@ -1796,9 +2031,9 @@
}
},
"node_modules/terser": {
"version": "5.31.5",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.31.5.tgz",
"integrity": "sha512-YPmas0L0rE1UyLL/llTWA0SiDOqIcAQYLeUj7cJYzXHlRTAnMSg9pPe4VJ5PlKvTrPQsdVFuiRiwyeNlYgwh2Q==",
"version": "5.34.1",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.34.1.tgz",
"integrity": "sha512-FsJZ7iZLd/BXkz+4xrRTGJ26o/6VTjQytUk8b8OxkwcD2I+79VPJlz7qss1+zE7h8GNIScFqXcDyJ/KqBYZFVA==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
@ -1924,16 +2159,16 @@
}
},
"node_modules/undici-types": {
"version": "6.13.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.13.0.tgz",
"integrity": "sha512-xtFJHudx8S2DSoujjMd1WeWvn7KKWFRESZTMeL1RptAYERu29D6jphMjjY+vn96jvN3kVPDNxU/E13VTaXj6jg==",
"version": "6.19.8",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
"dev": true,
"license": "MIT"
},
"node_modules/update-browserslist-db": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz",
"integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==",
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz",
"integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==",
"dev": true,
"funding": [
{
@ -1951,8 +2186,8 @@
],
"license": "MIT",
"dependencies": {
"escalade": "^3.1.2",
"picocolors": "^1.0.1"
"escalade": "^3.2.0",
"picocolors": "^1.1.0"
},
"bin": {
"update-browserslist-db": "cli.js"
@ -1979,9 +2214,9 @@
"license": "MIT"
},
"node_modules/watchpack": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.1.tgz",
"integrity": "sha512-8wrBCMtVhqcXP2Sup1ctSkga6uc2Bx0IIvKyT7yTFier5AXHooSI+QyQQAtTb7+E0IUCCKyTFmXqdqgum2XWGg==",
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz",
"integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -1993,9 +2228,9 @@
}
},
"node_modules/webpack": {
"version": "5.94.0",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.94.0.tgz",
"integrity": "sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg==",
"version": "5.95.0",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.95.0.tgz",
"integrity": "sha512-2t3XstrKULz41MNMBF+cJ97TyHdyQ8HCt//pqErqDvNjU9YQBnZxIHa11VXsi7F3mb5/aO2tuDxdeTPdU7xu9Q==",
"dev": true,
"license": "MIT",
"dependencies": {

View File

@ -8,7 +8,7 @@
"build": "webpack"
},
"dependencies": {
"bootstrap": "5.3.3",
"bootstrap": "4.6.2",
"font-awesome": "4.7.0",
"jquery": "3.7.1",
"popper.js": "1.16.1",
@ -18,9 +18,9 @@
"css-loader": "7.1.2",
"expose-loader": "5.0.0",
"mini-css-extract-plugin": "2.9.1",
"sass": "1.77.8",
"sass-loader": "16.0.1",
"webpack": "5.94.0",
"sass": "1.79.5",
"sass-loader": "16.0.2",
"webpack": "5.95.0",
"webpack-cli": "5.1.4"
}
}

View File

@ -2,11 +2,13 @@
using Bit.Api.AdminConsole.Models.Request.Organizations;
using Bit.Api.AdminConsole.Models.Response.Organizations;
using Bit.Api.Models.Response;
using Bit.Core;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Core.Utilities;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
@ -99,7 +101,7 @@ public class OrganizationDomainController : Controller
throw new NotFoundException();
}
organizationDomain = await _verifyOrganizationDomainCommand.VerifyOrganizationDomainAsync(organizationDomain);
organizationDomain = await _verifyOrganizationDomainCommand.UserVerifyOrganizationDomainAsync(organizationDomain);
return new OrganizationDomainResponseModel(organizationDomain);
}
@ -133,6 +135,20 @@ public class OrganizationDomainController : Controller
return new OrganizationDomainSsoDetailsResponseModel(ssoResult);
}
[AllowAnonymous]
[HttpPost("domain/sso/verified")]
[RequireFeature(FeatureFlagKeys.VerifiedSsoDomainEndpoint)]
public async Task<VerifiedOrganizationDomainSsoDetailsResponseModel> GetVerifiedOrgDomainSsoDetailsAsync(
[FromBody] OrganizationDomainSsoDetailsRequestModel model)
{
var ssoResults = (await _organizationDomainRepository
.GetVerifiedOrganizationDomainSsoDetailsAsync(model.Email))
.ToList();
return new VerifiedOrganizationDomainSsoDetailsResponseModel(
ssoResults.Select(ssoResult => new VerifiedOrganizationDomainSsoDetailResponseModel(ssoResult)));
}
private async Task ValidateOrganizationAccessAsync(Guid orgIdGuid)
{
if (!await _currentContext.ManageSso(orgIdGuid))

View File

@ -48,12 +48,13 @@ public class OrganizationUsersController : Controller
private readonly IAcceptOrgUserCommand _acceptOrgUserCommand;
private readonly IAuthorizationService _authorizationService;
private readonly IApplicationCacheService _applicationCacheService;
private readonly IFeatureService _featureService;
private readonly ISsoConfigRepository _ssoConfigRepository;
private readonly IOrganizationUserUserDetailsQuery _organizationUserUserDetailsQuery;
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
private readonly IDeleteManagedOrganizationUserAccountCommand _deleteManagedOrganizationUserAccountCommand;
private readonly IGetOrganizationUsersManagementStatusQuery _getOrganizationUsersManagementStatusQuery;
private readonly IFeatureService _featureService;
public OrganizationUsersController(
IOrganizationRepository organizationRepository,
@ -70,11 +71,13 @@ public class OrganizationUsersController : Controller
IAcceptOrgUserCommand acceptOrgUserCommand,
IAuthorizationService authorizationService,
IApplicationCacheService applicationCacheService,
IFeatureService featureService,
ISsoConfigRepository ssoConfigRepository,
IOrganizationUserUserDetailsQuery organizationUserUserDetailsQuery,
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
IDeleteManagedOrganizationUserAccountCommand deleteManagedOrganizationUserAccountCommand)
IRemoveOrganizationUserCommand removeOrganizationUserCommand,
IDeleteManagedOrganizationUserAccountCommand deleteManagedOrganizationUserAccountCommand,
IGetOrganizationUsersManagementStatusQuery getOrganizationUsersManagementStatusQuery,
IFeatureService featureService)
{
_organizationRepository = organizationRepository;
_organizationUserRepository = organizationUserRepository;
@ -90,34 +93,39 @@ public class OrganizationUsersController : Controller
_acceptOrgUserCommand = acceptOrgUserCommand;
_authorizationService = authorizationService;
_applicationCacheService = applicationCacheService;
_featureService = featureService;
_ssoConfigRepository = ssoConfigRepository;
_organizationUserUserDetailsQuery = organizationUserUserDetailsQuery;
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
_removeOrganizationUserCommand = removeOrganizationUserCommand;
_deleteManagedOrganizationUserAccountCommand = deleteManagedOrganizationUserAccountCommand;
_getOrganizationUsersManagementStatusQuery = getOrganizationUsersManagementStatusQuery;
_featureService = featureService;
}
[HttpGet("{id}")]
public async Task<OrganizationUserDetailsResponseModel> Get(string id, bool includeGroups = false)
public async Task<OrganizationUserDetailsResponseModel> Get(Guid id, bool includeGroups = false)
{
var organizationUser = await _organizationUserRepository.GetDetailsByIdWithCollectionsAsync(new Guid(id));
if (organizationUser == null || !await _currentContext.ManageUsers(organizationUser.Item1.OrganizationId))
var (organizationUser, collections) = await _organizationUserRepository.GetDetailsByIdWithCollectionsAsync(id);
if (organizationUser == null || !await _currentContext.ManageUsers(organizationUser.OrganizationId))
{
throw new NotFoundException();
}
var response = new OrganizationUserDetailsResponseModel(organizationUser.Item1, organizationUser.Item2);
var managedByOrganization = await GetManagedByOrganizationStatusAsync(
organizationUser.OrganizationId,
[organizationUser.Id]);
var response = new OrganizationUserDetailsResponseModel(organizationUser, managedByOrganization[organizationUser.Id], collections);
if (includeGroups)
{
response.Groups = await _groupRepository.GetManyIdsByUserIdAsync(organizationUser.Item1.Id);
response.Groups = await _groupRepository.GetManyIdsByUserIdAsync(organizationUser.Id);
}
return response;
}
[HttpGet("mini-details")]
[RequireFeature(FeatureFlagKeys.Pm3478RefactorOrganizationUserApi)]
public async Task<ListResponseModel<OrganizationUserUserMiniDetailsResponseModel>> GetMiniDetails(Guid orgId)
{
var authorizationResult = await _authorizationService.AuthorizeAsync(User, new OrganizationScope(orgId),
@ -142,11 +150,6 @@ public class OrganizationUsersController : Controller
throw new NotFoundException();
}
if (_featureService.IsEnabled(FeatureFlagKeys.MembersTwoFAQueryOptimization))
{
return await Get_vNext(orgId, includeGroups, includeCollections);
}
var organizationUsers = await _organizationUserUserDetailsQuery.GetOrganizationUserUserDetails(
new OrganizationUserUserDetailsQueryRequest
{
@ -155,17 +158,17 @@ public class OrganizationUsersController : Controller
IncludeCollections = includeCollections
}
);
var responseTasks = organizationUsers
.Select(async o =>
var organizationUsersTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(organizationUsers);
var organizationUsersManagementStatus = await GetManagedByOrganizationStatusAsync(orgId, organizationUsers.Select(o => o.Id));
var responses = organizationUsers
.Select(o =>
{
var orgUser = new OrganizationUserUserDetailsResponseModel(o,
await _userService.TwoFactorIsEnabledAsync(o));
var userTwoFactorEnabled = organizationUsersTwoFactorEnabled.FirstOrDefault(u => u.user.Id == o.Id).twoFactorIsEnabled;
var managedByOrganization = organizationUsersManagementStatus[o.Id];
var orgUser = new OrganizationUserUserDetailsResponseModel(o, userTwoFactorEnabled, managedByOrganization);
return orgUser;
});
var responses = await Task.WhenAll(responseTasks);
return new ListResponseModel<OrganizationUserUserDetailsResponseModel>(responses);
}
@ -296,7 +299,7 @@ public class OrganizationUsersController : Controller
await _organizationService.InitPendingOrganization(user.Id, orgId, organizationUserId, model.Keys.PublicKey, model.Keys.EncryptedPrivateKey, model.CollectionName);
await _acceptOrgUserCommand.AcceptOrgUserByEmailTokenAsync(organizationUserId, user, model.Token, _userService);
await _organizationService.ConfirmUserAsync(orgId, organizationUserId, model.Key, user.Id, _userService);
await _organizationService.ConfirmUserAsync(orgId, organizationUserId, model.Key, user.Id);
}
[HttpPost("{organizationUserId}/accept")]
@ -335,8 +338,7 @@ public class OrganizationUsersController : Controller
}
var userId = _userService.GetProperUserId(User);
var result = await _organizationService.ConfirmUserAsync(orgGuidId, new Guid(id), model.Key, userId.Value,
_userService);
var result = await _organizationService.ConfirmUserAsync(orgGuidId, new Guid(id), model.Key, userId.Value);
}
[HttpPost("confirm")]
@ -350,10 +352,7 @@ public class OrganizationUsersController : Controller
}
var userId = _userService.GetProperUserId(User);
var results = _featureService.IsEnabled(FeatureFlagKeys.MembersTwoFAQueryOptimization)
? await _organizationService.ConfirmUsersAsync_vNext(orgGuidId, model.ToDictionary(), userId.Value)
: await _organizationService.ConfirmUsersAsync(orgGuidId, model.ToDictionary(), userId.Value,
_userService);
var results = await _organizationService.ConfirmUsersAsync(orgGuidId, model.ToDictionary(), userId.Value);
return new ListResponseModel<OrganizationUserBulkResponseModel>(results.Select(r =>
new OrganizationUserBulkResponseModel(r.Item1.Id, r.Item2)));
@ -517,30 +516,28 @@ public class OrganizationUsersController : Controller
[HttpDelete("{id}")]
[HttpPost("{id}/remove")]
public async Task Remove(string orgId, string id)
public async Task Remove(Guid orgId, Guid id)
{
var orgGuidId = new Guid(orgId);
if (!await _currentContext.ManageUsers(orgGuidId))
if (!await _currentContext.ManageUsers(orgId))
{
throw new NotFoundException();
}
var userId = _userService.GetProperUserId(User);
await _organizationService.RemoveUserAsync(orgGuidId, new Guid(id), userId.Value);
await _removeOrganizationUserCommand.RemoveUserAsync(orgId, id, userId.Value);
}
[HttpDelete("")]
[HttpPost("remove")]
public async Task<ListResponseModel<OrganizationUserBulkResponseModel>> BulkRemove(string orgId, [FromBody] OrganizationUserBulkRequestModel model)
public async Task<ListResponseModel<OrganizationUserBulkResponseModel>> BulkRemove(Guid orgId, [FromBody] OrganizationUserBulkRequestModel model)
{
var orgGuidId = new Guid(orgId);
if (!await _currentContext.ManageUsers(orgGuidId))
if (!await _currentContext.ManageUsers(orgId))
{
throw new NotFoundException();
}
var userId = _userService.GetProperUserId(User);
var result = await _organizationService.RemoveUsersAsync(orgGuidId, model.Ids, userId.Value);
var result = await _removeOrganizationUserCommand.RemoveUsersAsync(orgId, model.Ids, userId.Value);
return new ListResponseModel<OrganizationUserBulkResponseModel>(result.Select(r =>
new OrganizationUserBulkResponseModel(r.Item1.Id, r.Item2)));
}
@ -616,7 +613,7 @@ public class OrganizationUsersController : Controller
[HttpPut("{id}/restore")]
public async Task RestoreAsync(Guid orgId, Guid id)
{
await RestoreOrRevokeUserAsync(orgId, id, (orgUser, userId) => _organizationService.RestoreUserAsync(orgUser, userId, _userService));
await RestoreOrRevokeUserAsync(orgId, id, (orgUser, userId) => _organizationService.RestoreUserAsync(orgUser, userId));
}
[HttpPatch("restore")]
@ -697,26 +694,14 @@ public class OrganizationUsersController : Controller
new OrganizationUserBulkResponseModel(r.Item1.Id, r.Item2)));
}
private async Task<ListResponseModel<OrganizationUserUserDetailsResponseModel>> Get_vNext(Guid orgId,
bool includeGroups = false, bool includeCollections = false)
private async Task<IDictionary<Guid, bool>> GetManagedByOrganizationStatusAsync(Guid orgId, IEnumerable<Guid> userIds)
{
var organizationUsers = await _organizationUserUserDetailsQuery.GetOrganizationUserUserDetails(
new OrganizationUserUserDetailsQueryRequest
{
OrganizationId = orgId,
IncludeGroups = includeGroups,
IncludeCollections = includeCollections
}
);
var organizationUsersTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(organizationUsers);
var responses = organizationUsers
.Select(o =>
{
var userTwoFactorEnabled = organizationUsersTwoFactorEnabled.FirstOrDefault(u => u.user.Id == o.Id).twoFactorIsEnabled;
var orgUser = new OrganizationUserUserDetailsResponseModel(o, userTwoFactorEnabled);
if (!_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning))
{
return userIds.ToDictionary(kvp => kvp, kvp => false);
}
return orgUser;
});
return new ListResponseModel<OrganizationUserUserDetailsResponseModel>(responses);
var usersOrganizationManagementStatus = await _getOrganizationUsersManagementStatusQuery.GetUsersOrganizationManagementStatusAsync(orgId, userIds);
return usersOrganizationManagementStatus;
}
}

View File

@ -13,6 +13,7 @@ using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Models.Business.Tokenables;
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationApiKeys.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Repositories;
@ -55,6 +56,7 @@ public class OrganizationsController : Controller
private readonly IProviderRepository _providerRepository;
private readonly IProviderBillingService _providerBillingService;
private readonly IDataProtectorTokenFactory<OrgDeleteTokenable> _orgDeleteTokenDataFactory;
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
public OrganizationsController(
IOrganizationRepository organizationRepository,
@ -74,7 +76,8 @@ public class OrganizationsController : Controller
IPushNotificationService pushNotificationService,
IProviderRepository providerRepository,
IProviderBillingService providerBillingService,
IDataProtectorTokenFactory<OrgDeleteTokenable> orgDeleteTokenDataFactory)
IDataProtectorTokenFactory<OrgDeleteTokenable> orgDeleteTokenDataFactory,
IRemoveOrganizationUserCommand removeOrganizationUserCommand)
{
_organizationRepository = organizationRepository;
_organizationUserRepository = organizationUserRepository;
@ -94,6 +97,7 @@ public class OrganizationsController : Controller
_providerRepository = providerRepository;
_providerBillingService = providerBillingService;
_orgDeleteTokenDataFactory = orgDeleteTokenDataFactory;
_removeOrganizationUserCommand = removeOrganizationUserCommand;
}
[HttpGet("{id}")]
@ -120,7 +124,11 @@ public class OrganizationsController : Controller
var userId = _userService.GetProperUserId(User).Value;
var organizations = await _organizationUserRepository.GetManyDetailsByUserAsync(userId,
OrganizationUserStatusType.Confirmed);
var responses = organizations.Select(o => new ProfileOrganizationResponseModel(o));
var organizationManagingActiveUser = await _userService.GetOrganizationsManagingUserAsync(userId);
var organizationIdsManagingActiveUser = organizationManagingActiveUser.Select(o => o.Id);
var responses = organizations.Select(o => new ProfileOrganizationResponseModel(o, organizationIdsManagingActiveUser));
return new ListResponseModel<ProfileOrganizationResponseModel>(responses);
}
@ -229,24 +237,22 @@ public class OrganizationsController : Controller
}
[HttpPost("{id}/leave")]
public async Task Leave(string id)
public async Task Leave(Guid id)
{
var orgGuidId = new Guid(id);
if (!await _currentContext.OrganizationUser(orgGuidId))
if (!await _currentContext.OrganizationUser(id))
{
throw new NotFoundException();
}
var user = await _userService.GetUserByPrincipalAsync(User);
var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(orgGuidId);
var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(id);
if (ssoConfig?.GetData()?.MemberDecryptionType == MemberDecryptionType.KeyConnector && user.UsesKeyConnector)
{
throw new BadRequestException("Your organization's Single Sign-On settings prevent you from leaving.");
}
await _organizationService.RemoveUserAsync(orgGuidId, user.Id);
await _removeOrganizationUserCommand.RemoveUserAsync(id, user.Id);
}
[HttpDelete("{id}")]
@ -514,9 +520,16 @@ public class OrganizationsController : Controller
}
[HttpPut("{id}/collection-management")]
[SelfHosted(NotSelfHostedOnly = true)]
public async Task<OrganizationResponseModel> PutCollectionManagement(Guid id, [FromBody] OrganizationCollectionManagementUpdateRequestModel model)
{
if (
_globalSettings.SelfHosted &&
!_featureService.IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit)
)
{
throw new BadRequestException("Only allowed when not self hosted.");
}
var organization = await _organizationRepository.GetByIdAsync(id);
if (organization == null)
{
@ -528,7 +541,7 @@ public class OrganizationsController : Controller
throw new NotFoundException();
}
await _organizationService.UpdateAsync(model.ToOrganization(organization), eventType: EventType.Organization_CollectionManagement_Updated);
await _organizationService.UpdateAsync(model.ToOrganization(organization, _featureService), eventType: EventType.Organization_CollectionManagement_Updated);
return new OrganizationResponseModel(organization);
}
}

View File

@ -16,6 +16,7 @@ using Bit.Core.Utilities;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Mvc;
using AdminConsoleEntities = Bit.Core.AdminConsole.Entities;
namespace Bit.Api.AdminConsole.Controllers;
@ -25,7 +26,6 @@ public class PoliciesController : Controller
{
private readonly IPolicyRepository _policyRepository;
private readonly IPolicyService _policyService;
private readonly IOrganizationService _organizationService;
private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly IUserService _userService;
private readonly ICurrentContext _currentContext;
@ -36,7 +36,6 @@ public class PoliciesController : Controller
public PoliciesController(
IPolicyRepository policyRepository,
IPolicyService policyService,
IOrganizationService organizationService,
IOrganizationUserRepository organizationUserRepository,
IUserService userService,
ICurrentContext currentContext,
@ -46,7 +45,6 @@ public class PoliciesController : Controller
{
_policyRepository = policyRepository;
_policyService = policyService;
_organizationService = organizationService;
_organizationUserRepository = organizationUserRepository;
_userService = userService;
_currentContext = currentContext;
@ -58,17 +56,16 @@ public class PoliciesController : Controller
}
[HttpGet("{type}")]
public async Task<PolicyResponseModel> Get(string orgId, int type)
public async Task<PolicyResponseModel> Get(Guid orgId, int type)
{
var orgIdGuid = new Guid(orgId);
if (!await _currentContext.ManagePolicies(orgIdGuid))
if (!await _currentContext.ManagePolicies(orgId))
{
throw new NotFoundException();
}
var policy = await _policyRepository.GetByOrganizationIdTypeAsync(orgIdGuid, (PolicyType)type);
var policy = await _policyRepository.GetByOrganizationIdTypeAsync(orgId, (PolicyType)type);
if (policy == null)
{
throw new NotFoundException();
return new PolicyResponseModel(new AdminConsoleEntities.Policy() { Type = (PolicyType)type, Enabled = false });
}
return new PolicyResponseModel(policy);
@ -185,7 +182,7 @@ public class PoliciesController : Controller
}
var userId = _userService.GetProperUserId(User);
await _policyService.SaveAsync(policy, _userService, _organizationService, userId);
await _policyService.SaveAsync(policy, userId);
return new PolicyResponseModel(policy);
}
}

View File

@ -55,6 +55,9 @@ public class OrganizationResponseModel : ResponseModel
SmServiceAccounts = organization.SmServiceAccounts;
MaxAutoscaleSmSeats = organization.MaxAutoscaleSmSeats;
MaxAutoscaleSmServiceAccounts = organization.MaxAutoscaleSmServiceAccounts;
LimitCollectionCreation = organization.LimitCollectionCreation;
LimitCollectionDeletion = organization.LimitCollectionDeletion;
// Deprecated: https://bitwarden.atlassian.net/browse/PM-10863
LimitCollectionCreationDeletion = organization.LimitCollectionCreationDeletion;
AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems;
}
@ -98,6 +101,9 @@ public class OrganizationResponseModel : ResponseModel
public int? SmServiceAccounts { get; set; }
public int? MaxAutoscaleSmSeats { get; set; }
public int? MaxAutoscaleSmServiceAccounts { get; set; }
public bool LimitCollectionCreation { get; set; }
public bool LimitCollectionDeletion { get; set; }
// Deperectated: https://bitwarden.atlassian.net/browse/PM-10863
public bool LimitCollectionCreationDeletion { get; set; }
public bool AllowAdminAccessToAllCollectionItems { get; set; }
}

View File

@ -64,20 +64,27 @@ public class OrganizationUserResponseModel : ResponseModel
public class OrganizationUserDetailsResponseModel : OrganizationUserResponseModel
{
public OrganizationUserDetailsResponseModel(OrganizationUser organizationUser,
public OrganizationUserDetailsResponseModel(
OrganizationUser organizationUser,
bool managedByOrganization,
IEnumerable<CollectionAccessSelection> collections)
: base(organizationUser, "organizationUserDetails")
{
ManagedByOrganization = managedByOrganization;
Collections = collections.Select(c => new SelectionReadOnlyResponseModel(c));
}
public OrganizationUserDetailsResponseModel(OrganizationUserUserDetails organizationUser,
bool managedByOrganization,
IEnumerable<CollectionAccessSelection> collections)
: base(organizationUser, "organizationUserDetails")
{
ManagedByOrganization = managedByOrganization;
Collections = collections.Select(c => new SelectionReadOnlyResponseModel(c));
}
public bool ManagedByOrganization { get; set; }
public IEnumerable<SelectionReadOnlyResponseModel> Collections { get; set; }
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
@ -110,7 +117,7 @@ public class OrganizationUserUserMiniDetailsResponseModel : ResponseModel
public class OrganizationUserUserDetailsResponseModel : OrganizationUserResponseModel
{
public OrganizationUserUserDetailsResponseModel(OrganizationUserUserDetails organizationUser,
bool twoFactorEnabled, string obj = "organizationUserUserDetails")
bool twoFactorEnabled, bool managedByOrganization, string obj = "organizationUserUserDetails")
: base(organizationUser, obj)
{
if (organizationUser == null)
@ -127,6 +134,7 @@ public class OrganizationUserUserDetailsResponseModel : OrganizationUserResponse
Groups = organizationUser.Groups;
// Prevent reset password when using key connector.
ResetPasswordEnrolled = ResetPasswordEnrolled && !organizationUser.UsesKeyConnector;
ManagedByOrganization = managedByOrganization;
}
public string Name { get; set; }
@ -134,6 +142,11 @@ public class OrganizationUserUserDetailsResponseModel : OrganizationUserResponse
public string AvatarColor { get; set; }
public bool TwoFactorEnabled { get; set; }
public bool SsoBound { get; set; }
/// <summary>
/// Indicates if the organization manages the user. If a user is "managed" by an organization,
/// the organization has greater control over their account, and some user actions are restricted.
/// </summary>
public bool ManagedByOrganization { get; set; }
public IEnumerable<SelectionReadOnlyResponseModel> Collections { get; set; }
public IEnumerable<Guid> Groups { get; set; }
}

View File

@ -0,0 +1,23 @@
using Bit.Core.Models.Api;
using Bit.Core.Models.Data.Organizations;
namespace Bit.Api.AdminConsole.Models.Response.Organizations;
public class VerifiedOrganizationDomainSsoDetailResponseModel : ResponseModel
{
public VerifiedOrganizationDomainSsoDetailResponseModel(VerifiedOrganizationDomainSsoDetail data)
: base("verifiedOrganizationDomainSsoDetails")
{
if (data is null)
{
throw new ArgumentNullException(nameof(data));
}
DomainName = data.DomainName;
OrganizationIdentifier = data.OrganizationIdentifier;
OrganizationName = data.OrganizationName;
}
public string DomainName { get; }
public string OrganizationIdentifier { get; }
public string OrganizationName { get; }
}

View File

@ -0,0 +1,8 @@
using Bit.Api.Models.Response;
namespace Bit.Api.AdminConsole.Models.Response.Organizations;
public class VerifiedOrganizationDomainSsoDetailsResponseModel(
IEnumerable<VerifiedOrganizationDomainSsoDetailResponseModel> data,
string continuationToken = null)
: ListResponseModel<VerifiedOrganizationDomainSsoDetailResponseModel>(data, continuationToken);

View File

@ -15,7 +15,10 @@ public class ProfileOrganizationResponseModel : ResponseModel
{
public ProfileOrganizationResponseModel(string str) : base(str) { }
public ProfileOrganizationResponseModel(OrganizationUserOrganizationDetails organization) : this("profileOrganization")
public ProfileOrganizationResponseModel(
OrganizationUserOrganizationDetails organization,
IEnumerable<Guid> organizationIdsManagingUser)
: this("profileOrganization")
{
Id = organization.OrganizationId;
Name = organization.Name;
@ -62,8 +65,12 @@ public class ProfileOrganizationResponseModel : ResponseModel
FamilySponsorshipToDelete = organization.FamilySponsorshipToDelete;
FamilySponsorshipValidUntil = organization.FamilySponsorshipValidUntil;
AccessSecretsManager = organization.AccessSecretsManager;
LimitCollectionCreation = organization.LimitCollectionCreation;
LimitCollectionDeletion = organization.LimitCollectionDeletion;
// Deprecated: https://bitwarden.atlassian.net/browse/PM-10863
LimitCollectionCreationDeletion = organization.LimitCollectionCreationDeletion;
AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems;
UserIsManagedByOrganization = organizationIdsManagingUser.Contains(organization.OrganizationId);
if (organization.SsoConfig != null)
{
@ -120,6 +127,20 @@ public class ProfileOrganizationResponseModel : ResponseModel
public DateTime? FamilySponsorshipValidUntil { get; set; }
public bool? FamilySponsorshipToDelete { get; set; }
public bool AccessSecretsManager { get; set; }
public bool LimitCollectionCreation { get; set; }
public bool LimitCollectionDeletion { get; set; }
// Deprecated: https://bitwarden.atlassian.net/browse/PM-10863
public bool LimitCollectionCreationDeletion { get; set; }
public bool AllowAdminAccessToAllCollectionItems { get; set; }
/// <summary>
/// Indicates if the organization manages the user.
/// </summary>
/// <remarks>
/// An organization manages a user if the user's email domain is verified by the organization and the user is a member of it.
/// The organization must be enabled and able to have verified domains.
/// </remarks>
/// <returns>
/// False if the Account Deprovisioning feature flag is disabled.
/// </returns>
public bool UserIsManagedByOrganization { get; set; }
}

View File

@ -44,6 +44,9 @@ public class ProfileProviderOrganizationResponseModel : ProfileOrganizationRespo
ProviderId = organization.ProviderId;
ProviderName = organization.ProviderName;
ProductTierType = StaticStore.GetPlan(organization.PlanType).ProductTier;
LimitCollectionCreation = organization.LimitCollectionCreation;
LimitCollectionDeletion = organization.LimitCollectionDeletion;
// https://bitwarden.atlassian.net/browse/PM-10863
LimitCollectionCreationDeletion = organization.LimitCollectionCreationDeletion;
AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems;
}

View File

@ -2,12 +2,10 @@
using Bit.Api.AdminConsole.Public.Models.Request;
using Bit.Api.AdminConsole.Public.Models.Response;
using Bit.Api.Models.Public.Response;
using Bit.Core;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
using Bit.Core.Context;
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Microsoft.AspNetCore.Authorization;
@ -29,8 +27,8 @@ public class MembersController : Controller
private readonly IApplicationCacheService _applicationCacheService;
private readonly IPaymentService _paymentService;
private readonly IOrganizationRepository _organizationRepository;
private readonly IFeatureService _featureService;
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
public MembersController(
IOrganizationUserRepository organizationUserRepository,
@ -43,8 +41,8 @@ public class MembersController : Controller
IApplicationCacheService applicationCacheService,
IPaymentService paymentService,
IOrganizationRepository organizationRepository,
IFeatureService featureService,
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery)
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
IRemoveOrganizationUserCommand removeOrganizationUserCommand)
{
_organizationUserRepository = organizationUserRepository;
_groupRepository = groupRepository;
@ -56,8 +54,8 @@ public class MembersController : Controller
_applicationCacheService = applicationCacheService;
_paymentService = paymentService;
_organizationRepository = organizationRepository;
_featureService = featureService;
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
_removeOrganizationUserCommand = removeOrganizationUserCommand;
}
/// <summary>
@ -73,14 +71,13 @@ public class MembersController : Controller
[ProducesResponseType((int)HttpStatusCode.NotFound)]
public async Task<IActionResult> Get(Guid id)
{
var userDetails = await _organizationUserRepository.GetDetailsByIdWithCollectionsAsync(id);
var orgUser = userDetails?.Item1;
var (orgUser, collections) = await _organizationUserRepository.GetDetailsByIdWithCollectionsAsync(id);
if (orgUser == null || orgUser.OrganizationId != _currentContext.OrganizationId)
{
return new NotFoundResult();
}
var response = new MemberResponseModel(orgUser, await _userService.TwoFactorIsEnabledAsync(orgUser),
userDetails.Item2);
collections);
return new JsonResult(response);
}
@ -120,16 +117,11 @@ public class MembersController : Controller
var organizationUserUserDetails = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(_currentContext.OrganizationId.Value);
// TODO: Get all CollectionUser associations for the organization and marry them up here for the response.
if (_featureService.IsEnabled(FeatureFlagKeys.MembersTwoFAQueryOptimization))
var orgUsersTwoFactorIsEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(organizationUserUserDetails);
var memberResponses = organizationUserUserDetails.Select(u =>
{
return await List_vNext(organizationUserUserDetails);
}
var memberResponsesTasks = organizationUserUserDetails.Select(async u =>
{
return new MemberResponseModel(u, await _userService.TwoFactorIsEnabledAsync(u), null);
return new MemberResponseModel(u, orgUsersTwoFactorIsEnabled.FirstOrDefault(tuple => tuple.user == u).twoFactorIsEnabled, null);
});
var memberResponses = await Task.WhenAll(memberResponsesTasks);
var response = new ListResponseModel<MemberResponseModel>(memberResponses);
return new JsonResult(response);
}
@ -243,7 +235,7 @@ public class MembersController : Controller
{
return new NotFoundResult();
}
await _organizationService.RemoveUserAsync(_currentContext.OrganizationId.Value, id, null);
await _removeOrganizationUserCommand.RemoveUserAsync(_currentContext.OrganizationId.Value, id, null);
return new OkResult();
}
@ -268,15 +260,4 @@ public class MembersController : Controller
await _organizationService.ResendInviteAsync(_currentContext.OrganizationId.Value, null, id);
return new OkResult();
}
private async Task<JsonResult> List_vNext(ICollection<OrganizationUserUserDetails> organizationUserUserDetails)
{
var orgUsersTwoFactorIsEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(organizationUserUserDetails);
var memberResponses = organizationUserUserDetails.Select(u =>
{
return new MemberResponseModel(u, orgUsersTwoFactorIsEnabled.FirstOrDefault(tuple => tuple.user == u).twoFactorIsEnabled, null);
});
var response = new ListResponseModel<MemberResponseModel>(memberResponses);
return new JsonResult(response);
}
}

View File

@ -6,7 +6,6 @@ using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Context;
using Bit.Core.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
@ -18,21 +17,15 @@ public class PoliciesController : Controller
{
private readonly IPolicyRepository _policyRepository;
private readonly IPolicyService _policyService;
private readonly IUserService _userService;
private readonly IOrganizationService _organizationService;
private readonly ICurrentContext _currentContext;
public PoliciesController(
IPolicyRepository policyRepository,
IPolicyService policyService,
IUserService userService,
IOrganizationService organizationService,
ICurrentContext currentContext)
{
_policyRepository = policyRepository;
_policyService = policyService;
_userService = userService;
_organizationService = organizationService;
_currentContext = currentContext;
}
@ -99,7 +92,7 @@ public class PoliciesController : Controller
{
policy = model.ToPolicy(policy);
}
await _policyService.SaveAsync(policy, _userService, _organizationService, null);
await _policyService.SaveAsync(policy, null);
var response = new PolicyResponseModel(policy);
return new JsonResult(response);
}

View File

@ -32,10 +32,10 @@
</Choose>
<ItemGroup>
<PackageReference Include="AspNetCore.HealthChecks.SqlServer" Version="8.0.0" />
<PackageReference Include="AspNetCore.HealthChecks.Uris" Version="8.0.0" />
<PackageReference Include="AspNetCore.HealthChecks.SqlServer" Version="8.0.2" />
<PackageReference Include="AspNetCore.HealthChecks.Uris" Version="8.0.1" />
<PackageReference Include="Azure.Messaging.EventGrid" Version="4.25.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.8.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.9.0" />
</ItemGroup>
</Project>

View File

@ -148,6 +148,13 @@ public class AccountsController : Controller
throw new BadRequestException("MasterPasswordHash", "Invalid password.");
}
// If Account Deprovisioning is enabled, we need to check if the user is managed by any organization.
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
&& await _userService.IsManagedByAnyOrganizationAsync(user.Id))
{
throw new BadRequestException("Cannot change emails for accounts owned by an organization. Contact your organization administrator for additional details.");
}
await _userService.InitiateEmailChangeAsync(user, model.NewEmail);
}
@ -165,6 +172,13 @@ public class AccountsController : Controller
throw new BadRequestException("You cannot change your email when using Key Connector.");
}
// If Account Deprovisioning is enabled, we need to check if the user is managed by any organization.
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
&& await _userService.IsManagedByAnyOrganizationAsync(user.Id))
{
throw new BadRequestException("Cannot change emails for accounts owned by an organization. Contact your organization administrator for additional details.");
}
var result = await _userService.ChangeEmailAsync(user, model.MasterPasswordHash, model.NewEmail,
model.NewMasterPasswordHash, model.Token, model.Key);
if (result.Succeeded)
@ -443,11 +457,11 @@ public class AccountsController : Controller
var twoFactorEnabled = await _userService.TwoFactorIsEnabledAsync(user);
var hasPremiumFromOrg = await _userService.HasPremiumFromOrganization(user);
var managedByOrganizationId = await GetManagedByOrganizationIdAsync(user);
var organizationIdsManagingActiveUser = await GetOrganizationIdsManagingUserAsync(user.Id);
var response = new ProfileResponseModel(user, organizationUserDetails, providerUserDetails,
providerUserOrganizationDetails, twoFactorEnabled,
hasPremiumFromOrg, managedByOrganizationId);
hasPremiumFromOrg, organizationIdsManagingActiveUser);
return response;
}
@ -457,7 +471,9 @@ public class AccountsController : Controller
var userId = _userService.GetProperUserId(User);
var organizationUserDetails = await _organizationUserRepository.GetManyDetailsByUserAsync(userId.Value,
OrganizationUserStatusType.Confirmed);
var responseData = organizationUserDetails.Select(o => new ProfileOrganizationResponseModel(o));
var organizationIdsManagingActiveUser = await GetOrganizationIdsManagingUserAsync(userId.Value);
var responseData = organizationUserDetails.Select(o => new ProfileOrganizationResponseModel(o, organizationIdsManagingActiveUser));
return new ListResponseModel<ProfileOrganizationResponseModel>(responseData);
}
@ -475,9 +491,9 @@ public class AccountsController : Controller
var twoFactorEnabled = await _userService.TwoFactorIsEnabledAsync(user);
var hasPremiumFromOrg = await _userService.HasPremiumFromOrganization(user);
var managedByOrganizationId = await GetManagedByOrganizationIdAsync(user);
var organizationIdsManagingActiveUser = await GetOrganizationIdsManagingUserAsync(user.Id);
var response = new ProfileResponseModel(user, null, null, null, twoFactorEnabled, hasPremiumFromOrg, managedByOrganizationId);
var response = new ProfileResponseModel(user, null, null, null, twoFactorEnabled, hasPremiumFromOrg, organizationIdsManagingActiveUser);
return response;
}
@ -494,9 +510,9 @@ public class AccountsController : Controller
var userTwoFactorEnabled = await _userService.TwoFactorIsEnabledAsync(user);
var userHasPremiumFromOrganization = await _userService.HasPremiumFromOrganization(user);
var managedByOrganizationId = await GetManagedByOrganizationIdAsync(user);
var organizationIdsManagingActiveUser = await GetOrganizationIdsManagingUserAsync(user.Id);
var response = new ProfileResponseModel(user, null, null, null, userTwoFactorEnabled, userHasPremiumFromOrganization, managedByOrganizationId);
var response = new ProfileResponseModel(user, null, null, null, userTwoFactorEnabled, userHasPremiumFromOrganization, organizationIdsManagingActiveUser);
return response;
}
@ -647,9 +663,9 @@ public class AccountsController : Controller
var userTwoFactorEnabled = await _userService.TwoFactorIsEnabledAsync(user);
var userHasPremiumFromOrganization = await _userService.HasPremiumFromOrganization(user);
var managedByOrganizationId = await GetManagedByOrganizationIdAsync(user);
var organizationIdsManagingActiveUser = await GetOrganizationIdsManagingUserAsync(user.Id);
var profile = new ProfileResponseModel(user, null, null, null, userTwoFactorEnabled, userHasPremiumFromOrganization, managedByOrganizationId);
var profile = new ProfileResponseModel(user, null, null, null, userTwoFactorEnabled, userHasPremiumFromOrganization, organizationIdsManagingActiveUser);
return new PaymentResponseModel
{
UserProfile = profile,
@ -937,14 +953,9 @@ public class AccountsController : Controller
}
}
private async Task<Guid?> GetManagedByOrganizationIdAsync(User user)
private async Task<IEnumerable<Guid>> GetOrganizationIdsManagingUserAsync(Guid userId)
{
if (!_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning))
{
return null;
}
var organizationManagingUser = await _userService.GetOrganizationManagingUserAsync(user.Id);
return organizationManagingUser?.Id;
var organizationManagingUser = await _userService.GetOrganizationsManagingUserAsync(userId);
return organizationManagingUser.Select(o => o.Id);
}
}

View File

@ -3,7 +3,6 @@ using Bit.Api.Auth.Models.Request.Accounts;
using Bit.Api.Auth.Models.Response.TwoFactor;
using Bit.Api.Models.Request;
using Bit.Api.Models.Response;
using Bit.Core;
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.LoginFeatures.PasswordlessLogin.Interfaces;
using Bit.Core.Auth.Models.Business.Tokenables;
@ -37,7 +36,6 @@ public class TwoFactorController : Controller
private readonly IFeatureService _featureService;
private readonly IDataProtectorTokenFactory<TwoFactorAuthenticatorUserVerificationTokenable> _twoFactorAuthenticatorDataProtector;
private readonly IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> _ssoEmailTwoFactorSessionDataProtector;
private readonly bool _TwoFactorAuthenticatorTokenFeatureFlagEnabled;
public TwoFactorController(
IUserService userService,
@ -61,7 +59,6 @@ public class TwoFactorController : Controller
_featureService = featureService;
_twoFactorAuthenticatorDataProtector = twoFactorAuthenticatorDataProtector;
_ssoEmailTwoFactorSessionDataProtector = ssoEmailTwoFactorSessionDataProtector;
_TwoFactorAuthenticatorTokenFeatureFlagEnabled = _featureService.IsEnabled(FeatureFlagKeys.AuthenticatorTwoFactorToken);
}
[HttpGet("")]
@ -102,13 +99,10 @@ public class TwoFactorController : Controller
public async Task<TwoFactorAuthenticatorResponseModel> GetAuthenticator(
[FromBody] SecretVerificationRequestModel model)
{
var user = _TwoFactorAuthenticatorTokenFeatureFlagEnabled ? await CheckAsync(model, false) : await CheckAsync(model, false, true);
var user = await CheckAsync(model, false);
var response = new TwoFactorAuthenticatorResponseModel(user);
if (_TwoFactorAuthenticatorTokenFeatureFlagEnabled)
{
var tokenable = new TwoFactorAuthenticatorUserVerificationTokenable(user, response.Key);
response.UserVerificationToken = _twoFactorAuthenticatorDataProtector.Protect(tokenable);
}
var tokenable = new TwoFactorAuthenticatorUserVerificationTokenable(user, response.Key);
response.UserVerificationToken = _twoFactorAuthenticatorDataProtector.Protect(tokenable);
return response;
}
@ -117,20 +111,11 @@ public class TwoFactorController : Controller
public async Task<TwoFactorAuthenticatorResponseModel> PutAuthenticator(
[FromBody] UpdateTwoFactorAuthenticatorRequestModel model)
{
User user;
if (_TwoFactorAuthenticatorTokenFeatureFlagEnabled)
var user = model.ToUser(await _userService.GetUserByPrincipalAsync(User));
_twoFactorAuthenticatorDataProtector.TryUnprotect(model.UserVerificationToken, out var decryptedToken);
if (!decryptedToken.TokenIsValid(user, model.Key))
{
user = model.ToUser(await _userService.GetUserByPrincipalAsync(User));
_twoFactorAuthenticatorDataProtector.TryUnprotect(model.UserVerificationToken, out var decryptedToken);
if (!decryptedToken.TokenIsValid(user, model.Key))
{
throw new BadRequestException("UserVerificationToken", "User verification failed.");
}
}
else
{
user = await CheckAsync(model, false);
model.ToUser(user); // populates user obj with proper metadata for VerifyTwoFactorTokenAsync
throw new BadRequestException("UserVerificationToken", "User verification failed.");
}
if (!await _userManager.VerifyTwoFactorTokenAsync(user,
@ -145,7 +130,6 @@ public class TwoFactorController : Controller
return response;
}
[RequireFeature(FeatureFlagKeys.AuthenticatorTwoFactorToken)]
[HttpDelete("authenticator")]
public async Task<TwoFactorProviderResponseModel> DisableAuthenticator(
[FromBody] TwoFactorAuthenticatorDisableRequestModel model)
@ -157,7 +141,7 @@ public class TwoFactorController : Controller
throw new BadRequestException("UserVerificationToken", "User verification failed.");
}
await _userService.DisableTwoFactorProviderAsync(user, model.Type.Value, _organizationService);
await _userService.DisableTwoFactorProviderAsync(user, model.Type.Value);
return new TwoFactorProviderResponseModel(model.Type.Value, user);
}
@ -412,7 +396,7 @@ public class TwoFactorController : Controller
public async Task<TwoFactorProviderResponseModel> PutDisable([FromBody] TwoFactorProviderRequestModel model)
{
var user = await CheckAsync(model, false);
await _userService.DisableTwoFactorProviderAsync(user, model.Type.Value, _organizationService);
await _userService.DisableTwoFactorProviderAsync(user, model.Type.Value);
var response = new TwoFactorProviderResponseModel(model.Type.Value, user);
return response;
}
@ -453,8 +437,7 @@ public class TwoFactorController : Controller
[AllowAnonymous]
public async Task PostRecover([FromBody] TwoFactorRecoveryRequestModel model)
{
if (!await _userService.RecoverTwoFactorAsync(model.Email, model.MasterPasswordHash, model.RecoveryCode,
_organizationService))
if (!await _userService.RecoverTwoFactorAsync(model.Email, model.MasterPasswordHash, model.RecoveryCode))
{
await Task.Delay(2000);
throw new BadRequestException(string.Empty, "Invalid information. Try again.");

View File

@ -1,4 +1,5 @@
using Bit.Api.Billing.Models.Responses;
#nullable enable
using Bit.Api.Billing.Models.Responses;
using Bit.Core.Billing.Services;
using Bit.Core.Services;
using Bit.Core.Utilities;
@ -43,7 +44,7 @@ public class AccountsBillingController(
}
[HttpGet("invoices")]
public async Task<IResult> GetInvoicesAsync([FromQuery] string startAfter = null)
public async Task<IResult> GetInvoicesAsync([FromQuery] string? status = null, [FromQuery] string? startAfter = null)
{
var user = await userService.GetUserByPrincipalAsync(User);
if (user == null)
@ -54,6 +55,7 @@ public class AccountsBillingController(
var invoices = await paymentHistoryService.GetInvoiceHistoryAsync(
user,
5,
status,
startAfter);
return TypedResults.Ok(invoices);

View File

@ -1,4 +1,5 @@
using Bit.Api.Billing.Models.Requests;
#nullable enable
using Bit.Api.Billing.Models.Requests;
using Bit.Api.Billing.Models.Responses;
using Bit.Core;
using Bit.Core.Billing.Services;
@ -63,7 +64,7 @@ public class OrganizationBillingController(
}
[HttpGet("invoices")]
public async Task<IResult> GetInvoicesAsync([FromRoute] Guid organizationId, [FromQuery] string startAfter = null)
public async Task<IResult> GetInvoicesAsync([FromRoute] Guid organizationId, [FromQuery] string? status = null, [FromQuery] string? startAfter = null)
{
if (!await currentContext.ViewBillingHistory(organizationId))
{
@ -80,6 +81,7 @@ public class OrganizationBillingController(
var invoices = await paymentHistoryService.GetInvoiceHistoryAsync(
organization,
5,
status,
startAfter);
return TypedResults.Ok(invoices);

View File

@ -201,7 +201,10 @@ public class OrganizationsController(
var organizationDetails = await organizationUserRepository.GetDetailsByUserAsync(userId, organization.Id,
OrganizationUserStatusType.Confirmed);
return new ProfileOrganizationResponseModel(organizationDetails);
var organizationManagingActiveUser = await userService.GetOrganizationsManagingUserAsync(userId);
var organizationIdsManagingActiveUser = organizationManagingActiveUser.Select(o => o.Id);
return new ProfileOrganizationResponseModel(organizationDetails, organizationIdsManagingActiveUser);
}
[HttpPost("{id:guid}/seat")]

View File

@ -3,8 +3,11 @@
namespace Bit.Api.Billing.Models.Responses;
public record OrganizationMetadataResponse(
bool IsEligibleForSelfHost,
bool IsOnSecretsManagerStandalone)
{
public static OrganizationMetadataResponse From(OrganizationMetadata metadata)
=> new(metadata.IsOnSecretsManagerStandalone);
=> new(
metadata.IsEligibleForSelfHost,
metadata.IsOnSecretsManagerStandalone);
}

View File

@ -46,7 +46,7 @@ public class PushController : Controller
public async Task PostDelete([FromBody] PushDeviceRequestModel model)
{
CheckUsage();
await _pushRegistrationService.DeleteRegistrationAsync(Prefix(model.Id), model.Type);
await _pushRegistrationService.DeleteRegistrationAsync(Prefix(model.Id));
}
[HttpPut("add-organization")]
@ -54,7 +54,7 @@ public class PushController : Controller
{
CheckUsage();
await _pushRegistrationService.AddUserRegistrationOrganizationAsync(
model.Devices.Select(d => new KeyValuePair<string, Core.Enums.DeviceType>(Prefix(d.Id), d.Type)),
model.Devices.Select(d => Prefix(d.Id)),
Prefix(model.OrganizationId));
}
@ -63,7 +63,7 @@ public class PushController : Controller
{
CheckUsage();
await _pushRegistrationService.DeleteUserRegistrationOrganizationAsync(
model.Devices.Select(d => new KeyValuePair<string, Core.Enums.DeviceType>(Prefix(d.Id), d.Type)),
model.Devices.Select(d => Prefix(d.Id)),
Prefix(model.OrganizationId));
}

View File

@ -1,15 +1,29 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Services;
namespace Bit.Api.Models.Request.Organizations;
public class OrganizationCollectionManagementUpdateRequestModel
{
public bool LimitCollectionCreation { get; set; }
public bool LimitCollectionDeletion { get; set; }
// Deprecated: https://bitwarden.atlassian.net/browse/PM-10863
public bool LimitCreateDeleteOwnerAdmin { get; set; }
public bool AllowAdminAccessToAllCollectionItems { get; set; }
public virtual Organization ToOrganization(Organization existingOrganization)
public virtual Organization ToOrganization(Organization existingOrganization, IFeatureService featureService)
{
existingOrganization.LimitCollectionCreationDeletion = LimitCreateDeleteOwnerAdmin;
if (featureService.IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit))
{
existingOrganization.LimitCollectionCreation = LimitCollectionCreation;
existingOrganization.LimitCollectionDeletion = LimitCollectionDeletion;
}
else
{
existingOrganization.LimitCollectionCreationDeletion = LimitCreateDeleteOwnerAdmin || LimitCollectionCreation || LimitCollectionDeletion;
}
existingOrganization.AllowAdminAccessToAllCollectionItems = AllowAdminAccessToAllCollectionItems;
return existingOrganization;
}

View File

@ -15,7 +15,7 @@ public class ProfileResponseModel : ResponseModel
IEnumerable<ProviderUserOrganizationDetails> providerUserOrganizationDetails,
bool twoFactorEnabled,
bool premiumFromOrganization,
Guid? managedByOrganizationId) : base("profile")
IEnumerable<Guid> organizationIdsManagingUser) : base("profile")
{
if (user == null)
{
@ -37,11 +37,10 @@ public class ProfileResponseModel : ResponseModel
UsesKeyConnector = user.UsesKeyConnector;
AvatarColor = user.AvatarColor;
CreationDate = user.CreationDate;
Organizations = organizationsUserDetails?.Select(o => new ProfileOrganizationResponseModel(o));
Organizations = organizationsUserDetails?.Select(o => new ProfileOrganizationResponseModel(o, organizationIdsManagingUser));
Providers = providerUserDetails?.Select(p => new ProfileProviderResponseModel(p));
ProviderOrganizations =
providerUserOrganizationDetails?.Select(po => new ProfileProviderOrganizationResponseModel(po));
ManagedByOrganizationId = managedByOrganizationId;
}
public ProfileResponseModel() : base("profile")
@ -63,7 +62,6 @@ public class ProfileResponseModel : ResponseModel
public bool UsesKeyConnector { get; set; }
public string AvatarColor { get; set; }
public DateTime CreationDate { get; set; }
public Guid? ManagedByOrganizationId { get; set; }
public IEnumerable<ProfileOrganizationResponseModel> Organizations { get; set; }
public IEnumerable<ProfileProviderResponseModel> Providers { get; set; }
public IEnumerable<ProfileProviderOrganizationResponseModel> ProviderOrganizations { get; set; }

View File

@ -1,5 +1,6 @@
#nullable enable
using System.Diagnostics;
using Bit.Core;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Enums;
@ -101,7 +102,7 @@ public class BulkCollectionAuthorizationHandler : BulkAuthorizationHandler<BulkC
break;
case null:
// requirement isn't actually nullable but since we use the
// requirement isn't actually nullable but since we use the
// not null when trick it makes the compiler think that requirement
// could actually be nullable.
throw new UnreachableException();
@ -123,10 +124,24 @@ public class BulkCollectionAuthorizationHandler : BulkAuthorizationHandler<BulkC
return true;
}
// If the limit collection management setting is disabled, allow any user to create collections
if (await GetOrganizationAbilityAsync(org) is { LimitCollectionCreationDeletion: false })
if (_featureService.IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit))
{
return true;
var userIsMemberOfOrg = org is not null;
var limitCollectionCreationEnabled = await GetOrganizationAbilityAsync(org) is { LimitCollectionCreation: true };
var userIsOrgOwnerOrAdmin = org is { Type: OrganizationUserType.Owner or OrganizationUserType.Admin };
// If the limit collection management setting is disabled, allow any user to create collections
if (userIsMemberOfOrg && (!limitCollectionCreationEnabled || userIsOrgOwnerOrAdmin))
{
return true;
}
}
else
{
// If the limit collection management setting is disabled, allow any user to create collections
if (await GetOrganizationAbilityAsync(org) is { LimitCollectionCreationDeletion: false })
{
return true;
}
}
// Allow provider users to create collections if they are a provider for the target organization
@ -246,21 +261,35 @@ public class BulkCollectionAuthorizationHandler : BulkAuthorizationHandler<BulkC
return true;
}
// If AllowAdminAccessToAllCollectionItems is true, Owners and Admins can delete any collection, regardless of LimitCollectionCreationDeletion setting
// If AllowAdminAccessToAllCollectionItems is true, Owners and Admins can delete any collection, regardless of LimitCollectionDeletion setting
if (await AllowAdminAccessToAllCollectionItems(org) && org is { Type: OrganizationUserType.Owner or OrganizationUserType.Admin })
{
return true;
}
// If LimitCollectionCreationDeletion is false, AllowAdminAccessToAllCollectionItems setting is irrelevant.
// Ensure acting user has manage permissions for all collections being deleted
// If LimitCollectionCreationDeletion is true, only Owners and Admins can delete collections they manage
var organizationAbility = await GetOrganizationAbilityAsync(org);
var canDeleteManagedCollections = organizationAbility is { LimitCollectionCreationDeletion: false } ||
org is { Type: OrganizationUserType.Owner or OrganizationUserType.Admin };
if (canDeleteManagedCollections && await CanManageCollectionsAsync(resources, org))
if (_featureService.IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit))
{
return true;
var userIsMemberOfOrg = org is not null;
var limitCollectionDeletionEnabled = await GetOrganizationAbilityAsync(org) is { LimitCollectionDeletion: true };
var userIsOrgOwnerOrAdmin = org is { Type: OrganizationUserType.Owner or OrganizationUserType.Admin };
// If the limit collection management setting is disabled, allow any user to delete collections
if (userIsMemberOfOrg && (!limitCollectionDeletionEnabled || userIsOrgOwnerOrAdmin) && await CanManageCollectionsAsync(resources, org))
{
return true;
}
}
else
{
// If LimitCollectionCreationDeletion is false, AllowAdminAccessToAllCollectionItems setting is irrelevant.
// Ensure acting user has manage permissions for all collections being deleted
// If LimitCollectionCreationDeletion is true, only Owners and Admins can delete collections they manage
var organizationAbility = await GetOrganizationAbilityAsync(org);
var canDeleteManagedCollections = organizationAbility is { LimitCollectionCreationDeletion: false } ||
org is { Type: OrganizationUserType.Owner or OrganizationUserType.Admin };
if (canDeleteManagedCollections && await CanManageCollectionsAsync(resources, org))
{
return true;
}
}
// Allow providers to delete collections if they are a provider for the target organization

View File

@ -910,6 +910,13 @@ public class CiphersController : Controller
throw new BadRequestException(ModelState);
}
// If Account Deprovisioning is enabled, we need to check if the user is managed by any organization.
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
&& await _userService.IsManagedByAnyOrganizationAsync(user.Id))
{
throw new BadRequestException("Cannot purge accounts owned by an organization. Contact your organization administrator for additional details.");
}
if (string.IsNullOrWhiteSpace(organizationId))
{
await _cipherRepository.DeleteByUserIdAsync(user.Id);

View File

@ -1,5 +1,4 @@
using Bit.Api.Vault.Models.Response;
using Bit.Core;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.Repositories;
@ -7,7 +6,6 @@ using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Data;
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings;
@ -95,23 +93,12 @@ public class SyncController : Controller
var userTwoFactorEnabled = await _userService.TwoFactorIsEnabledAsync(user);
var userHasPremiumFromOrganization = await _userService.HasPremiumFromOrganization(user);
var managedByOrganizationId = await GetManagedByOrganizationIdAsync(user, organizationUserDetails);
var organizationManagingActiveUser = await _userService.GetOrganizationsManagingUserAsync(user.Id);
var organizationIdsManagingActiveUser = organizationManagingActiveUser.Select(o => o.Id);
var response = new SyncResponseModel(_globalSettings, user, userTwoFactorEnabled, userHasPremiumFromOrganization,
managedByOrganizationId, organizationUserDetails, providerUserDetails, providerUserOrganizationDetails,
organizationIdsManagingActiveUser, organizationUserDetails, providerUserDetails, providerUserOrganizationDetails,
folders, collections, ciphers, collectionCiphersGroupDict, excludeDomains, policies, sends);
return response;
}
private async Task<Guid?> GetManagedByOrganizationIdAsync(User user, IEnumerable<OrganizationUserOrganizationDetails> organizationUserDetails)
{
if (!_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning) ||
!organizationUserDetails.Any(o => o.Enabled && o.UseSso))
{
return null;
}
var organizationManagingUser = await _userService.GetOrganizationManagingUserAsync(user.Id);
return organizationManagingUser?.Id;
}
}

View File

@ -21,7 +21,7 @@ public class SyncResponseModel : ResponseModel
User user,
bool userTwoFactorEnabled,
bool userHasPremiumFromOrganization,
Guid? managedByOrganizationId,
IEnumerable<Guid> organizationIdsManagingUser,
IEnumerable<OrganizationUserOrganizationDetails> organizationUserDetails,
IEnumerable<ProviderUserProviderDetails> providerUserDetails,
IEnumerable<ProviderUserOrganizationDetails> providerUserOrganizationDetails,
@ -35,7 +35,7 @@ public class SyncResponseModel : ResponseModel
: base("sync")
{
Profile = new ProfileResponseModel(user, organizationUserDetails, providerUserDetails,
providerUserOrganizationDetails, userTwoFactorEnabled, userHasPremiumFromOrganization, managedByOrganizationId);
providerUserOrganizationDetails, userTwoFactorEnabled, userHasPremiumFromOrganization, organizationIdsManagingUser);
Folders = folders.Select(f => new FolderResponseModel(f));
Ciphers = ciphers.Select(c => new CipherDetailsResponseModel(c, globalSettings, collectionCiphersDict));
Collections = collections?.Select(

View File

@ -32,12 +32,14 @@ public class SubscriptionDeletedHandler : ISubscriptionDeletedHandler
var (organizationId, userId, providerId) = _stripeEventUtilityService.GetIdsFromMetadata(subscription.Metadata);
var subCanceled = subscription.Status == StripeSubscriptionStatus.Canceled;
const string providerMigrationCancellationComment = "Cancelled as part of provider migration to Consolidated Billing";
if (!subCanceled)
{
return;
}
if (organizationId.HasValue)
if (organizationId.HasValue && subscription is not { CancellationDetails.Comment: providerMigrationCancellationComment })
{
await _organizationService.DisableAsync(organizationId.Value, subscription.CurrentPeriodEnd);
}

View File

@ -75,6 +75,7 @@ public class Startup
// Services
services.AddBaseServices(globalSettings);
services.AddDefaultServices(globalSettings);
services.AddDistributedCache(globalSettings);
services.AddBillingOperations();
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();

View File

@ -7,6 +7,7 @@ using Bit.Core.Billing.Enums;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Models.Business;
using Bit.Core.Services;
using Bit.Core.Tools.Entities;
using Bit.Core.Utilities;
@ -93,7 +94,21 @@ public class Organization : ITableObject<Guid>, IStorableSubscriber, IRevisable,
/// If set to false, any organization member can create a collection, and any member can delete a collection that
/// they have Can Manage permissions for.
/// </summary>
public bool LimitCollectionCreationDeletion { get; set; }
public bool LimitCollectionCreation { get; set; }
public bool LimitCollectionDeletion { get; set; }
// Deprecated by https://bitwarden.atlassian.net/browse/PM-10863. This
// was replaced with `LimitCollectionCreation` and
// `LimitCollectionDeletion`.
public bool LimitCollectionCreationDeletion
{
get => LimitCollectionCreation || LimitCollectionDeletion;
set
{
LimitCollectionCreation = value;
LimitCollectionDeletion = value;
}
}
/// <summary>
/// If set to true, admins, owners, and some custom users can read/write all collections and items in the Admin Console.
/// If set to false, users generally need collection-level permissions to read/write a collection or its items.
@ -264,7 +279,7 @@ public class Organization : ITableObject<Guid>, IStorableSubscriber, IRevisable,
return providers[provider];
}
public void UpdateFromLicense(OrganizationLicense license)
public void UpdateFromLicense(OrganizationLicense license, IFeatureService featureService)
{
// The following properties are intentionally excluded from being updated:
// - Id - self-hosted org will have its own unique Guid
@ -299,7 +314,11 @@ public class Organization : ITableObject<Guid>, IStorableSubscriber, IRevisable,
UseSecretsManager = license.UseSecretsManager;
SmSeats = license.SmSeats;
SmServiceAccounts = license.SmServiceAccounts;
LimitCollectionCreationDeletion = license.LimitCollectionCreationDeletion;
AllowAdminAccessToAllCollectionItems = license.AllowAdminAccessToAllCollectionItems;
if (!featureService.IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit))
{
LimitCollectionCreationDeletion = license.LimitCollectionCreationDeletion;
AllowAdminAccessToAllCollectionItems = license.AllowAdminAccessToAllCollectionItems;
}
}
}

View File

@ -16,3 +16,30 @@ public enum PolicyType : byte
ActivateAutofill = 11,
AutomaticAppLogIn = 12,
}
public static class PolicyTypeExtensions
{
/// <summary>
/// Returns the name of the policy for display to the user.
/// Do not include the word "policy" in the return value.
/// </summary>
public static string GetName(this PolicyType type)
{
return type switch
{
PolicyType.TwoFactorAuthentication => "Require two-step login",
PolicyType.MasterPassword => "Master password requirements",
PolicyType.PasswordGenerator => "Password generator",
PolicyType.SingleOrg => "Single organization",
PolicyType.RequireSso => "Require single sign-on authentication",
PolicyType.PersonalOwnership => "Remove individual vault",
PolicyType.DisableSend => "Remove Send",
PolicyType.SendOptions => "Send options",
PolicyType.ResetPassword => "Account recovery administration",
PolicyType.MaximumVaultTimeout => "Vault timeout",
PolicyType.DisablePersonalVaultExport => "Remove individual vault export",
PolicyType.ActivateAutofill => "Active auto-fill",
PolicyType.AutomaticAppLogIn => "Automatically log in users for allowed applications",
};
}
}

View File

@ -4,8 +4,10 @@ namespace Bit.Core.AdminConsole.Enums.Provider;
public enum ProviderType : byte
{
[Display(ShortName = "MSP", Name = "Managed Service Provider", Description = "Access to clients organization")]
[Display(ShortName = "MSP", Name = "Managed Service Provider", Description = "Access to clients organization", Order = 0)]
Msp = 0,
[Display(ShortName = "Reseller", Name = "Reseller", Description = "Access to clients billing")]
[Display(ShortName = "Reseller", Name = "Reseller", Description = "Access to clients billing", Order = 1000)]
Reseller = 1,
[Display(ShortName = "MOE", Name = "Multi-organization Enterprise", Description = "Access to multiple organizations", Order = 1)]
MultiOrganizationEnterprise = 2,
}

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