mirror of
https://github.com/bitwarden/server.git
synced 2025-01-20 21:31:23 +01:00
Merge branch 'BRE-291-ephemeral-env-cleanup-workflow' into ephem-test-01
This commit is contained in:
commit
71f2d806ab
@ -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@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
|
||||
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@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
@ -1,4 +1,3 @@
|
||||
---
|
||||
name: Automatic responses
|
||||
on:
|
||||
issues:
|
||||
|
43
.github/workflows/build.yml
vendored
43
.github/workflows/build.yml
vendored
@ -1,4 +1,3 @@
|
||||
---
|
||||
name: Build
|
||||
|
||||
on:
|
||||
@ -19,7 +18,7 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0
|
||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
|
||||
|
||||
- name: Set up .NET
|
||||
uses: actions/setup-dotnet@6bd8b7f7774af54e05809fcc5431931b3eb1ddee # v4.0.1
|
||||
@ -68,7 +67,7 @@ jobs:
|
||||
node: true
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0
|
||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
|
||||
|
||||
- name: Set up .NET
|
||||
uses: actions/setup-dotnet@6bd8b7f7774af54e05809fcc5431931b3eb1ddee # v4.0.1
|
||||
@ -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@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
|
||||
|
||||
- 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@49e50b215b647c5ec97abb66f69af73c46a4ca08 # v5.0.1
|
||||
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@f779452ac5af1c261dce0346a8f964149f49322b # v3.26.13
|
||||
with:
|
||||
sarif_file: ${{ steps.container-scan.outputs.sarif }}
|
||||
|
||||
@ -292,7 +291,7 @@ jobs:
|
||||
needs: build-docker
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0
|
||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
|
||||
|
||||
- name: Set up .NET
|
||||
uses: actions/setup-dotnet@6bd8b7f7774af54e05809fcc5431931b3eb1ddee # v4.0.1
|
||||
@ -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,7 +402,7 @@ 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
|
||||
@ -437,14 +436,14 @@ 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
|
||||
@ -467,7 +466,7 @@ jobs:
|
||||
- win-x64
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0
|
||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
|
||||
|
||||
- name: Set up .NET
|
||||
uses: actions/setup-dotnet@6bd8b7f7774af54e05809fcc5431931b3eb1ddee # v4.0.1
|
||||
@ -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
|
||||
|
1
.github/workflows/cleanup-after-pr.yml
vendored
1
.github/workflows/cleanup-after-pr.yml
vendored
@ -1,4 +1,3 @@
|
||||
---
|
||||
name: Container registry cleanup
|
||||
|
||||
on:
|
||||
|
40
.github/workflows/cleanup-ephemeral-environment.yml
vendored
Normal file
40
.github/workflows/cleanup-ephemeral-environment.yml
vendored
Normal file
@ -0,0 +1,40 @@
|
||||
name: Ephemeral environment cleanup
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [unlabeled]
|
||||
|
||||
jobs:
|
||||
cleanup-config:
|
||||
name: Cleanup ephemeral environment
|
||||
runs-on: ubuntu-24.04
|
||||
if: ${{ contains(github.event.pull_request.labels.*.name, 'ephemeral-environment') }}
|
||||
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: 'BRE-291-ephemeral-pr-manager',
|
||||
inputs: {
|
||||
ephemeral_env_branch: '$GITHUB_HEAD_REF',
|
||||
cleanup_config: true,
|
||||
project: 'server'
|
||||
}
|
||||
})
|
3
.github/workflows/cleanup-rc-branch.yml
vendored
3
.github/workflows/cleanup-rc-branch.yml
vendored
@ -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@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
|
||||
with:
|
||||
ref: main
|
||||
token: ${{ steps.retrieve-bot-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
|
||||
|
2
.github/workflows/code-references.yml
vendored
2
.github/workflows/code-references.yml
vendored
@ -33,7 +33,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0
|
||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
|
||||
|
||||
- name: Collect
|
||||
id: collect
|
||||
|
7
.github/workflows/enforce-labels.yml
vendored
7
.github/workflows/enforce-labels.yml
vendored
@ -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
|
||||
|
3
.github/workflows/protect-files.yml
vendored
3
.github/workflows/protect-files.yml
vendored
@ -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@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
|
3
.github/workflows/publish.yml
vendored
3
.github/workflows/publish.yml
vendored
@ -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@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
|
||||
|
||||
- name: Set up project name
|
||||
id: setup
|
||||
|
3
.github/workflows/release.yml
vendored
3
.github/workflows/release.yml
vendored
@ -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@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
|
||||
|
||||
- name: Check release version
|
||||
id: version
|
||||
|
6
.github/workflows/repository-management.yml
vendored
6
.github/workflows/repository-management.yml
vendored
@ -27,7 +27,7 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Check out target ref
|
||||
uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0
|
||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
|
||||
with:
|
||||
ref: ${{ inputs.target_ref }}
|
||||
|
||||
@ -62,7 +62,7 @@ jobs:
|
||||
version: ${{ inputs.version_number_override }}
|
||||
|
||||
- name: Check out branch
|
||||
uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0
|
||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
|
||||
with:
|
||||
ref: main
|
||||
|
||||
@ -150,7 +150,7 @@ jobs:
|
||||
needs: bump_version
|
||||
steps:
|
||||
- name: Check out main branch
|
||||
uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0
|
||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
|
||||
with:
|
||||
ref: main
|
||||
|
||||
|
8
.github/workflows/scan.yml
vendored
8
.github/workflows/scan.yml
vendored
@ -26,12 +26,12 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0
|
||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
|
||||
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@f779452ac5af1c261dce0346a8f964149f49322b # v3.26.13
|
||||
with:
|
||||
sarif_file: cx_result.sarif
|
||||
|
||||
@ -66,7 +66,7 @@ jobs:
|
||||
distribution: "zulu"
|
||||
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0
|
||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
1
.github/workflows/stale-bot.yml
vendored
1
.github/workflows/stale-bot.yml
vendored
@ -1,4 +1,3 @@
|
||||
---
|
||||
name: Staleness
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
9
.github/workflows/test-database.yml
vendored
9
.github/workflows/test-database.yml
vendored
@ -1,4 +1,3 @@
|
||||
---
|
||||
name: Database testing
|
||||
|
||||
on:
|
||||
@ -36,7 +35,7 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0
|
||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
|
||||
|
||||
- name: Set up .NET
|
||||
uses: actions/setup-dotnet@6bd8b7f7774af54e05809fcc5431931b3eb1ddee # v4.0.1
|
||||
@ -147,7 +146,7 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0
|
||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
|
||||
|
||||
- name: Set up .NET
|
||||
uses: actions/setup-dotnet@6bd8b7f7774af54e05809fcc5431931b3eb1ddee # v4.0.1
|
||||
@ -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: |
|
||||
|
4
.github/workflows/test.yml
vendored
4
.github/workflows/test.yml
vendored
@ -46,7 +46,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0
|
||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
|
||||
|
||||
- name: Set up .NET
|
||||
uses: actions/setup-dotnet@6bd8b7f7774af54e05809fcc5431931b3eb1ddee # v4.0.1
|
||||
@ -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 }}
|
||||
|
@ -57,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);
|
||||
}
|
||||
|
12
bitwarden_license/src/Scim/Models/GetUserQueryParamModel.cs
Normal file
12
bitwarden_license/src/Scim/Models/GetUserQueryParamModel.cs
Normal 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;
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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()
|
||||
{
|
||||
|
@ -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);
|
||||
|
||||
|
1
dev/.gitignore
vendored
1
dev/.gitignore
vendored
@ -5,7 +5,6 @@ secrets.json
|
||||
# Docker container configurations
|
||||
.env
|
||||
authsources.php
|
||||
directory.ldif
|
||||
|
||||
# Development certificates
|
||||
identity_server_dev.crt
|
||||
|
@ -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
|
||||
|
@ -11,7 +11,6 @@ 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;
|
||||
@ -236,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);
|
||||
|
@ -181,7 +181,6 @@ public class OrganizationEditModel : OrganizationViewModel
|
||||
*/
|
||||
public object GetPlansHelper() =>
|
||||
StaticStore.Plans
|
||||
.Where(p => p.SupportsSecretsManager)
|
||||
.Select(p =>
|
||||
{
|
||||
var plan = new
|
||||
|
@ -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>
|
||||
|
||||
@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>
|
||||
|
@ -101,7 +101,7 @@ public class OrganizationDomainController : Controller
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
organizationDomain = await _verifyOrganizationDomainCommand.VerifyOrganizationDomainAsync(organizationDomain);
|
||||
organizationDomain = await _verifyOrganizationDomainCommand.UserVerifyOrganizationDomainAsync(organizationDomain);
|
||||
|
||||
return new OrganizationDomainResponseModel(organizationDomain);
|
||||
}
|
||||
|
@ -124,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);
|
||||
}
|
||||
|
||||
@ -516,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)
|
||||
{
|
||||
@ -530,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);
|
||||
}
|
||||
}
|
||||
|
@ -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; }
|
||||
}
|
||||
|
@ -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; }
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -443,11 +443,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 +457,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 +477,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 +496,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 +649,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 +939,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);
|
||||
}
|
||||
}
|
||||
|
@ -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")]
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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; }
|
||||
|
@ -1,5 +1,6 @@
|
||||
#nullable enable
|
||||
using System.Diagnostics;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
@ -123,11 +124,25 @@ public class BulkCollectionAuthorizationHandler : BulkAuthorizationHandler<BulkC
|
||||
return true;
|
||||
}
|
||||
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit))
|
||||
{
|
||||
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
|
||||
return await _currentContext.ProviderUserForOrgAsync(_targetOrganizationId);
|
||||
@ -246,12 +261,25 @@ 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 (_featureService.IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit))
|
||||
{
|
||||
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
|
||||
@ -262,6 +290,7 @@ public class BulkCollectionAuthorizationHandler : BulkAuthorizationHandler<BulkC
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Allow providers to delete collections if they are a provider for the target organization
|
||||
return await _currentContext.ProviderUserForOrgAsync(_targetOrganizationId);
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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,20 @@ 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.
|
||||
@ -265,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
|
||||
@ -300,7 +314,11 @@ public class Organization : ITableObject<Guid>, IStorableSubscriber, IRevisable,
|
||||
UseSecretsManager = license.UseSecretsManager;
|
||||
SmSeats = license.SmSeats;
|
||||
SmServiceAccounts = license.SmServiceAccounts;
|
||||
|
||||
if (!featureService.IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit))
|
||||
{
|
||||
LimitCollectionCreationDeletion = license.LimitCollectionCreationDeletion;
|
||||
AllowAdminAccessToAllCollectionItems = license.AllowAdminAccessToAllCollectionItems;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -21,6 +21,9 @@ public class OrganizationAbility
|
||||
UseResetPassword = organization.UseResetPassword;
|
||||
UseCustomPermissions = organization.UseCustomPermissions;
|
||||
UsePolicies = organization.UsePolicies;
|
||||
LimitCollectionCreation = organization.LimitCollectionCreation;
|
||||
LimitCollectionDeletion = organization.LimitCollectionDeletion;
|
||||
// Deprecated: https://bitwarden.atlassian.net/browse/PM-10863
|
||||
LimitCollectionCreationDeletion = organization.LimitCollectionCreationDeletion;
|
||||
AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems;
|
||||
}
|
||||
@ -37,6 +40,9 @@ public class OrganizationAbility
|
||||
public bool UseResetPassword { get; set; }
|
||||
public bool UseCustomPermissions { get; set; }
|
||||
public bool UsePolicies { get; set; }
|
||||
public bool LimitCollectionCreation { get; set; }
|
||||
public bool LimitCollectionDeletion { get; set; }
|
||||
// Deprecated: https://bitwarden.atlassian.net/browse/PM-10863
|
||||
public bool LimitCollectionCreationDeletion { get; set; }
|
||||
public bool AllowAdminAccessToAllCollectionItems { get; set; }
|
||||
}
|
||||
|
@ -54,6 +54,9 @@ public class OrganizationUserOrganizationDetails
|
||||
public bool UsePasswordManager { get; set; }
|
||||
public int? SmSeats { get; set; }
|
||||
public int? SmServiceAccounts { get; set; }
|
||||
public bool LimitCollectionCreation { get; set; }
|
||||
public bool LimitCollectionDeletion { get; set; }
|
||||
// Deprecated: https://bitwarden.atlassian.net/browse/PM-10863
|
||||
public bool LimitCollectionCreationDeletion { get; set; }
|
||||
public bool AllowAdminAccessToAllCollectionItems { get; set; }
|
||||
}
|
||||
|
@ -144,6 +144,9 @@ public class SelfHostedOrganizationDetails : Organization
|
||||
RevisionDate = RevisionDate,
|
||||
MaxAutoscaleSeats = MaxAutoscaleSeats,
|
||||
OwnersNotifiedOfAutoscaling = OwnersNotifiedOfAutoscaling,
|
||||
LimitCollectionCreation = LimitCollectionCreation,
|
||||
LimitCollectionDeletion = LimitCollectionDeletion,
|
||||
// Deprecated: https://bitwarden.atlassian.net/browse/PM-10863
|
||||
LimitCollectionCreationDeletion = LimitCollectionCreationDeletion,
|
||||
AllowAdminAccessToAllCollectionItems = AllowAdminAccessToAllCollectionItems,
|
||||
Status = Status
|
||||
|
@ -40,6 +40,8 @@ public class ProviderUserOrganizationDetails
|
||||
[JsonConverter(typeof(HtmlEncodingStringConverter))]
|
||||
public string ProviderName { get; set; }
|
||||
public PlanType PlanType { get; set; }
|
||||
public bool LimitCollectionCreation { get; set; }
|
||||
public bool LimitCollectionDeletion { get; set; }
|
||||
public bool LimitCollectionCreationDeletion { get; set; }
|
||||
public bool AllowAdminAccessToAllCollectionItems { get; set; }
|
||||
}
|
||||
|
@ -6,7 +6,6 @@ using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains;
|
||||
|
||||
@ -14,21 +13,15 @@ public class CreateOrganizationDomainCommand : ICreateOrganizationDomainCommand
|
||||
{
|
||||
private readonly IOrganizationDomainRepository _organizationDomainRepository;
|
||||
private readonly IEventService _eventService;
|
||||
private readonly IDnsResolverService _dnsResolverService;
|
||||
private readonly ILogger<VerifyOrganizationDomainCommand> _logger;
|
||||
private readonly IGlobalSettings _globalSettings;
|
||||
|
||||
public CreateOrganizationDomainCommand(
|
||||
IOrganizationDomainRepository organizationDomainRepository,
|
||||
IEventService eventService,
|
||||
IDnsResolverService dnsResolverService,
|
||||
ILogger<VerifyOrganizationDomainCommand> logger,
|
||||
IGlobalSettings globalSettings)
|
||||
{
|
||||
_organizationDomainRepository = organizationDomainRepository;
|
||||
_eventService = eventService;
|
||||
_dnsResolverService = dnsResolverService;
|
||||
_logger = logger;
|
||||
_globalSettings = globalSettings;
|
||||
}
|
||||
|
||||
|
@ -4,5 +4,6 @@ namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfa
|
||||
|
||||
public interface IVerifyOrganizationDomainCommand
|
||||
{
|
||||
Task<OrganizationDomain> VerifyOrganizationDomainAsync(OrganizationDomain organizationDomain);
|
||||
Task<OrganizationDomain> UserVerifyOrganizationDomainAsync(OrganizationDomain organizationDomain);
|
||||
Task<OrganizationDomain> SystemVerifyOrganizationDomainAsync(OrganizationDomain organizationDomain);
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains;
|
||||
@ -13,34 +14,85 @@ public class VerifyOrganizationDomainCommand : IVerifyOrganizationDomainCommand
|
||||
private readonly IOrganizationDomainRepository _organizationDomainRepository;
|
||||
private readonly IDnsResolverService _dnsResolverService;
|
||||
private readonly IEventService _eventService;
|
||||
private readonly IGlobalSettings _globalSettings;
|
||||
private readonly ILogger<VerifyOrganizationDomainCommand> _logger;
|
||||
|
||||
public VerifyOrganizationDomainCommand(
|
||||
IOrganizationDomainRepository organizationDomainRepository,
|
||||
IDnsResolverService dnsResolverService,
|
||||
IEventService eventService,
|
||||
IGlobalSettings globalSettings,
|
||||
ILogger<VerifyOrganizationDomainCommand> logger)
|
||||
{
|
||||
_organizationDomainRepository = organizationDomainRepository;
|
||||
_dnsResolverService = dnsResolverService;
|
||||
_eventService = eventService;
|
||||
_globalSettings = globalSettings;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<OrganizationDomain> VerifyOrganizationDomainAsync(OrganizationDomain domain)
|
||||
|
||||
public async Task<OrganizationDomain> UserVerifyOrganizationDomainAsync(OrganizationDomain organizationDomain)
|
||||
{
|
||||
if (domain.VerifiedDate is not null)
|
||||
var domainVerificationResult = await VerifyOrganizationDomainAsync(organizationDomain);
|
||||
|
||||
await _eventService.LogOrganizationDomainEventAsync(domainVerificationResult,
|
||||
domainVerificationResult.VerifiedDate != null
|
||||
? EventType.OrganizationDomain_Verified
|
||||
: EventType.OrganizationDomain_NotVerified);
|
||||
|
||||
await _organizationDomainRepository.ReplaceAsync(domainVerificationResult);
|
||||
|
||||
return domainVerificationResult;
|
||||
}
|
||||
|
||||
public async Task<OrganizationDomain> SystemVerifyOrganizationDomainAsync(OrganizationDomain organizationDomain)
|
||||
{
|
||||
organizationDomain.SetJobRunCount();
|
||||
|
||||
var domainVerificationResult = await VerifyOrganizationDomainAsync(organizationDomain);
|
||||
|
||||
if (domainVerificationResult.VerifiedDate is not null)
|
||||
{
|
||||
_logger.LogInformation(Constants.BypassFiltersEventId, "Successfully validated domain");
|
||||
|
||||
await _eventService.LogOrganizationDomainEventAsync(domainVerificationResult,
|
||||
EventType.OrganizationDomain_Verified,
|
||||
EventSystemUser.DomainVerification);
|
||||
}
|
||||
else
|
||||
{
|
||||
domainVerificationResult.SetNextRunDate(_globalSettings.DomainVerification.VerificationInterval);
|
||||
|
||||
await _eventService.LogOrganizationDomainEventAsync(domainVerificationResult,
|
||||
EventType.OrganizationDomain_NotVerified,
|
||||
EventSystemUser.DomainVerification);
|
||||
|
||||
_logger.LogInformation(Constants.BypassFiltersEventId,
|
||||
"Verification for organization {OrgId} with domain {Domain} failed",
|
||||
domainVerificationResult.OrganizationId, domainVerificationResult.DomainName);
|
||||
}
|
||||
|
||||
await _organizationDomainRepository.ReplaceAsync(domainVerificationResult);
|
||||
|
||||
return domainVerificationResult;
|
||||
}
|
||||
|
||||
private async Task<OrganizationDomain> VerifyOrganizationDomainAsync(OrganizationDomain domain)
|
||||
{
|
||||
domain.SetLastCheckedDate();
|
||||
|
||||
if (domain.VerifiedDate is not null)
|
||||
{
|
||||
await _organizationDomainRepository.ReplaceAsync(domain);
|
||||
throw new ConflictException("Domain has already been verified.");
|
||||
}
|
||||
|
||||
var claimedDomain =
|
||||
await _organizationDomainRepository.GetClaimedDomainsByDomainNameAsync(domain.DomainName);
|
||||
if (claimedDomain.Any())
|
||||
|
||||
if (claimedDomain.Count > 0)
|
||||
{
|
||||
domain.SetLastCheckedDate();
|
||||
await _organizationDomainRepository.ReplaceAsync(domain);
|
||||
throw new ConflictException("The domain is not available to be claimed.");
|
||||
}
|
||||
@ -58,11 +110,6 @@ public class VerifyOrganizationDomainCommand : IVerifyOrganizationDomainCommand
|
||||
domain.DomainName, e.Message);
|
||||
}
|
||||
|
||||
domain.SetLastCheckedDate();
|
||||
await _organizationDomainRepository.ReplaceAsync(domain);
|
||||
|
||||
await _eventService.LogOrganizationDomainEventAsync(domain,
|
||||
domain.VerifiedDate != null ? EventType.OrganizationDomain_Verified : EventType.OrganizationDomain_NotVerified);
|
||||
return domain;
|
||||
}
|
||||
}
|
||||
|
@ -19,7 +19,7 @@ public interface IOrganizationRepository : IRepository<Organization, Guid>
|
||||
Task<IEnumerable<string>> GetOwnerEmailAddressesById(Guid organizationId);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the organization that has a claimed domain matching the user's email domain.
|
||||
/// Gets the organizations that have a verified domain matching the user's email domain.
|
||||
/// </summary>
|
||||
Task<Organization> GetByClaimedUserDomainAsync(Guid userId);
|
||||
Task<ICollection<Organization>> GetByVerifiedUserEmailDomainAsync(Guid userId);
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
@ -10,26 +11,29 @@ public class OrganizationDomainService : IOrganizationDomainService
|
||||
{
|
||||
private readonly IOrganizationDomainRepository _domainRepository;
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
private readonly IDnsResolverService _dnsResolverService;
|
||||
private readonly IEventService _eventService;
|
||||
private readonly IMailService _mailService;
|
||||
private readonly IVerifyOrganizationDomainCommand _verifyOrganizationDomainCommand;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly ILogger<OrganizationDomainService> _logger;
|
||||
private readonly IGlobalSettings _globalSettings;
|
||||
|
||||
public OrganizationDomainService(
|
||||
IOrganizationDomainRepository domainRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IDnsResolverService dnsResolverService,
|
||||
IEventService eventService,
|
||||
IMailService mailService,
|
||||
IVerifyOrganizationDomainCommand verifyOrganizationDomainCommand,
|
||||
TimeProvider timeProvider,
|
||||
ILogger<OrganizationDomainService> logger,
|
||||
IGlobalSettings globalSettings)
|
||||
{
|
||||
_domainRepository = domainRepository;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
_dnsResolverService = dnsResolverService;
|
||||
_eventService = eventService;
|
||||
_mailService = mailService;
|
||||
_verifyOrganizationDomainCommand = verifyOrganizationDomainCommand;
|
||||
_timeProvider = timeProvider;
|
||||
_logger = logger;
|
||||
_globalSettings = globalSettings;
|
||||
}
|
||||
@ -37,7 +41,7 @@ public class OrganizationDomainService : IOrganizationDomainService
|
||||
public async Task ValidateOrganizationsDomainAsync()
|
||||
{
|
||||
//Date should be set 1 hour behind to ensure it selects all domains that should be verified
|
||||
var runDate = DateTime.UtcNow.AddHours(-1);
|
||||
var runDate = _timeProvider.GetUtcNow().UtcDateTime.AddHours(-1);
|
||||
|
||||
var verifiableDomains = await _domainRepository.GetManyByNextRunDateAsync(runDate);
|
||||
|
||||
@ -45,43 +49,17 @@ public class OrganizationDomainService : IOrganizationDomainService
|
||||
|
||||
foreach (var domain in verifiableDomains)
|
||||
{
|
||||
_logger.LogInformation(Constants.BypassFiltersEventId,
|
||||
"Attempting verification for organization {OrgId} with domain {Domain}",
|
||||
domain.OrganizationId,
|
||||
domain.DomainName);
|
||||
|
||||
try
|
||||
{
|
||||
_logger.LogInformation(Constants.BypassFiltersEventId, "Attempting verification for organization {OrgId} with domain {Domain}", domain.OrganizationId, domain.DomainName);
|
||||
|
||||
var status = await _dnsResolverService.ResolveAsync(domain.DomainName, domain.Txt);
|
||||
if (status)
|
||||
{
|
||||
_logger.LogInformation(Constants.BypassFiltersEventId, "Successfully validated domain");
|
||||
|
||||
// Update entry on OrganizationDomain table
|
||||
domain.SetLastCheckedDate();
|
||||
domain.SetVerifiedDate();
|
||||
domain.SetJobRunCount();
|
||||
await _domainRepository.ReplaceAsync(domain);
|
||||
|
||||
await _eventService.LogOrganizationDomainEventAsync(domain, EventType.OrganizationDomain_Verified,
|
||||
EventSystemUser.DomainVerification);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Update entry on OrganizationDomain table
|
||||
domain.SetLastCheckedDate();
|
||||
domain.SetJobRunCount();
|
||||
domain.SetNextRunDate(_globalSettings.DomainVerification.VerificationInterval);
|
||||
await _domainRepository.ReplaceAsync(domain);
|
||||
|
||||
await _eventService.LogOrganizationDomainEventAsync(domain, EventType.OrganizationDomain_NotVerified,
|
||||
EventSystemUser.DomainVerification);
|
||||
_logger.LogInformation(Constants.BypassFiltersEventId, "Verification for organization {OrgId} with domain {Domain} failed",
|
||||
domain.OrganizationId, domain.DomainName);
|
||||
}
|
||||
_ = await _verifyOrganizationDomainCommand.SystemVerifyOrganizationDomainAsync(domain);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Update entry on OrganizationDomain table
|
||||
domain.SetLastCheckedDate();
|
||||
domain.SetJobRunCount();
|
||||
domain.SetNextRunDate(_globalSettings.DomainVerification.VerificationInterval);
|
||||
await _domainRepository.ReplaceAsync(domain);
|
||||
|
||||
|
@ -708,10 +708,16 @@ public class OrganizationService : IOrganizationService
|
||||
UseSecretsManager = license.UseSecretsManager,
|
||||
SmSeats = license.SmSeats,
|
||||
SmServiceAccounts = license.SmServiceAccounts,
|
||||
LimitCollectionCreationDeletion = license.LimitCollectionCreationDeletion,
|
||||
AllowAdminAccessToAllCollectionItems = license.AllowAdminAccessToAllCollectionItems,
|
||||
};
|
||||
|
||||
// These fields are being removed from consideration when processing
|
||||
// licenses.
|
||||
if (!_featureService.IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit))
|
||||
{
|
||||
organization.LimitCollectionCreationDeletion = license.LimitCollectionCreationDeletion;
|
||||
organization.AllowAdminAccessToAllCollectionItems = license.AllowAdminAccessToAllCollectionItems;
|
||||
}
|
||||
|
||||
var result = await SignUpAsync(organization, owner.Id, ownerKey, collectionName, false);
|
||||
|
||||
var dir = $"{_globalSettings.LicenseDirectory}/organization";
|
||||
|
@ -3,17 +3,14 @@
|
||||
public enum ProviderMigrationProgress
|
||||
{
|
||||
Started = 1,
|
||||
ClientsMigrated = 2,
|
||||
TeamsPlanConfigured = 3,
|
||||
EnterprisePlanConfigured = 4,
|
||||
CustomerSetup = 5,
|
||||
SubscriptionSetup = 6,
|
||||
CreditApplied = 7,
|
||||
Completed = 8,
|
||||
|
||||
Reversing = 9,
|
||||
ReversedClientMigrations = 10,
|
||||
RemovedProviderPlans = 11
|
||||
NoClients = 2,
|
||||
ClientsMigrated = 3,
|
||||
TeamsPlanConfigured = 4,
|
||||
EnterprisePlanConfigured = 5,
|
||||
CustomerSetup = 6,
|
||||
SubscriptionSetup = 7,
|
||||
CreditApplied = 8,
|
||||
Completed = 9,
|
||||
}
|
||||
|
||||
public class ProviderMigrationTracker
|
||||
|
@ -41,7 +41,18 @@ public class ProviderMigrator(
|
||||
|
||||
await migrationTrackerCache.StartTracker(provider);
|
||||
|
||||
await MigrateClientsAsync(providerId);
|
||||
var organizations = await GetClientsAsync(provider.Id);
|
||||
|
||||
if (organizations.Count == 0)
|
||||
{
|
||||
logger.LogInformation("CB: Skipping migration for provider ({ProviderID}) with no clients", providerId);
|
||||
|
||||
await migrationTrackerCache.UpdateTrackingStatus(providerId, ProviderMigrationProgress.NoClients);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await MigrateClientsAsync(providerId, organizations);
|
||||
|
||||
await ConfigureTeamsPlanAsync(providerId);
|
||||
|
||||
@ -65,6 +76,16 @@ public class ProviderMigrator(
|
||||
return null;
|
||||
}
|
||||
|
||||
if (providerTracker.Progress == ProviderMigrationProgress.NoClients)
|
||||
{
|
||||
return new ProviderMigrationResult
|
||||
{
|
||||
ProviderId = providerTracker.ProviderId,
|
||||
ProviderName = providerTracker.ProviderName,
|
||||
Result = providerTracker.Progress.ToString()
|
||||
};
|
||||
}
|
||||
|
||||
var clientTrackers = await Task.WhenAll(providerTracker.OrganizationIds.Select(organizationId =>
|
||||
migrationTrackerCache.GetTracker(providerId, organizationId)));
|
||||
|
||||
@ -99,12 +120,10 @@ public class ProviderMigrator(
|
||||
|
||||
#region Steps
|
||||
|
||||
private async Task MigrateClientsAsync(Guid providerId)
|
||||
private async Task MigrateClientsAsync(Guid providerId, List<Organization> organizations)
|
||||
{
|
||||
logger.LogInformation("CB: Migrating clients for provider ({ProviderID})", providerId);
|
||||
|
||||
var organizations = await GetEnabledClientsAsync(providerId);
|
||||
|
||||
var organizationIds = organizations.Select(organization => organization.Id);
|
||||
|
||||
await migrationTrackerCache.SetOrganizationIds(providerId, organizationIds);
|
||||
@ -129,7 +148,7 @@ public class ProviderMigrator(
|
||||
{
|
||||
logger.LogInformation("CB: Configuring Teams plan for provider ({ProviderID})", providerId);
|
||||
|
||||
var organizations = await GetEnabledClientsAsync(providerId);
|
||||
var organizations = await GetClientsAsync(providerId);
|
||||
|
||||
var teamsSeats = organizations
|
||||
.Where(IsTeams)
|
||||
@ -172,7 +191,7 @@ public class ProviderMigrator(
|
||||
{
|
||||
logger.LogInformation("CB: Configuring Enterprise plan for provider ({ProviderID})", providerId);
|
||||
|
||||
var organizations = await GetEnabledClientsAsync(providerId);
|
||||
var organizations = await GetClientsAsync(providerId);
|
||||
|
||||
var enterpriseSeats = organizations
|
||||
.Where(IsEnterprise)
|
||||
@ -215,7 +234,7 @@ public class ProviderMigrator(
|
||||
{
|
||||
if (string.IsNullOrEmpty(provider.GatewayCustomerId))
|
||||
{
|
||||
var organizations = await GetEnabledClientsAsync(provider.Id);
|
||||
var organizations = await GetClientsAsync(provider.Id);
|
||||
|
||||
var sampleOrganization = organizations.FirstOrDefault(organization => !string.IsNullOrEmpty(organization.GatewayCustomerId));
|
||||
|
||||
@ -299,28 +318,43 @@ public class ProviderMigrator(
|
||||
|
||||
private async Task ApplyCreditAsync(Provider provider)
|
||||
{
|
||||
var organizations = await GetEnabledClientsAsync(provider.Id);
|
||||
var organizations = await GetClientsAsync(provider.Id);
|
||||
|
||||
var organizationCustomers =
|
||||
await Task.WhenAll(organizations.Select(organization => stripeAdapter.CustomerGetAsync(organization.GatewayCustomerId)));
|
||||
|
||||
var organizationCancellationCredit = organizationCustomers.Sum(customer => customer.Balance);
|
||||
|
||||
var legacyOrganizations = organizations.Where(organization =>
|
||||
organization.PlanType is
|
||||
PlanType.EnterpriseAnnually2020 or
|
||||
PlanType.EnterpriseMonthly2020 or
|
||||
PlanType.TeamsAnnually2020 or
|
||||
PlanType.TeamsMonthly2020);
|
||||
|
||||
var legacyOrganizationCredit = legacyOrganizations.Sum(organization => organization.Seats ?? 0);
|
||||
|
||||
await stripeAdapter.CustomerUpdateAsync(provider.GatewayCustomerId, new CustomerUpdateOptions
|
||||
await stripeAdapter.CustomerBalanceTransactionCreate(provider.GatewayCustomerId,
|
||||
new CustomerBalanceTransactionCreateOptions
|
||||
{
|
||||
Balance = organizationCancellationCredit + legacyOrganizationCredit
|
||||
Amount = organizationCancellationCredit,
|
||||
Currency = "USD",
|
||||
Description = "Unused, prorated time for client organization subscriptions."
|
||||
});
|
||||
|
||||
logger.LogInformation("CB: Applied {Credit} credit to provider ({ProviderID})", organizationCancellationCredit, provider.Id);
|
||||
var migrationRecords = await Task.WhenAll(organizations.Select(organization =>
|
||||
clientOrganizationMigrationRecordRepository.GetByOrganizationId(organization.Id)));
|
||||
|
||||
var legacyOrganizationMigrationRecords = migrationRecords.Where(migrationRecord =>
|
||||
migrationRecord.PlanType is
|
||||
PlanType.EnterpriseAnnually2020 or
|
||||
PlanType.TeamsAnnually2020);
|
||||
|
||||
var legacyOrganizationCredit = legacyOrganizationMigrationRecords.Sum(migrationRecord => migrationRecord.Seats) * 12 * -100;
|
||||
|
||||
if (legacyOrganizationCredit < 0)
|
||||
{
|
||||
await stripeAdapter.CustomerBalanceTransactionCreate(provider.GatewayCustomerId,
|
||||
new CustomerBalanceTransactionCreateOptions
|
||||
{
|
||||
Amount = legacyOrganizationCredit,
|
||||
Currency = "USD",
|
||||
Description = "1 year rebate for legacy client organizations."
|
||||
});
|
||||
}
|
||||
|
||||
logger.LogInformation("CB: Applied {Credit} credit to provider ({ProviderID})", organizationCancellationCredit + legacyOrganizationCredit, provider.Id);
|
||||
|
||||
await migrationTrackerCache.UpdateTrackingStatus(provider.Id, ProviderMigrationProgress.CreditApplied);
|
||||
}
|
||||
@ -340,13 +374,12 @@ public class ProviderMigrator(
|
||||
|
||||
#region Utilities
|
||||
|
||||
private async Task<List<Organization>> GetEnabledClientsAsync(Guid providerId)
|
||||
private async Task<List<Organization>> GetClientsAsync(Guid providerId)
|
||||
{
|
||||
var providerOrganizations = await providerOrganizationRepository.GetManyDetailsByProviderAsync(providerId);
|
||||
|
||||
return (await Task.WhenAll(providerOrganizations.Select(providerOrganization =>
|
||||
organizationRepository.GetByIdAsync(providerOrganization.OrganizationId))))
|
||||
.Where(organization => organization.Enabled)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
|
@ -146,6 +146,7 @@ public static class FeatureFlagKeys
|
||||
public const string RemoveServerVersionHeader = "remove-server-version-header";
|
||||
public const string AccessIntelligence = "pm-13227-access-intelligence";
|
||||
public const string VerifiedSsoDomainEndpoint = "pm-12337-refactor-sso-details-endpoint";
|
||||
public const string LimitCollectionCreationDeletionSplit = "pm-10863-limit-collection-creation-deletion-split";
|
||||
|
||||
public static List<string> GetAllKeys()
|
||||
{
|
||||
|
@ -53,8 +53,11 @@ public class OrganizationLicense : ILicense
|
||||
UseSecretsManager = org.UseSecretsManager;
|
||||
SmSeats = org.SmSeats;
|
||||
SmServiceAccounts = org.SmServiceAccounts;
|
||||
|
||||
// Deprecated. Left for backwards compatibility with old license versions.
|
||||
LimitCollectionCreationDeletion = org.LimitCollectionCreationDeletion;
|
||||
AllowAdminAccessToAllCollectionItems = org.AllowAdminAccessToAllCollectionItems;
|
||||
//
|
||||
|
||||
if (subscriptionInfo?.Subscription == null)
|
||||
{
|
||||
@ -138,8 +141,12 @@ public class OrganizationLicense : ILicense
|
||||
public bool UseSecretsManager { get; set; }
|
||||
public int? SmSeats { get; set; }
|
||||
public int? SmServiceAccounts { get; set; }
|
||||
|
||||
// Deprecated. Left for backwards compatibility with old license versions.
|
||||
public bool LimitCollectionCreationDeletion { get; set; } = true;
|
||||
public bool AllowAdminAccessToAllCollectionItems { get; set; } = true;
|
||||
//
|
||||
|
||||
public bool Trial { get; set; }
|
||||
public LicenseType? LicenseType { get; set; }
|
||||
public string Hash { get; set; }
|
||||
@ -150,7 +157,8 @@ public class OrganizationLicense : ILicense
|
||||
/// Represents the current version of the license format. Should be updated whenever new fields are added.
|
||||
/// </summary>
|
||||
/// <remarks>Intentionally set one version behind to allow self hosted users some time to update before
|
||||
/// getting out of date license errors</remarks>
|
||||
/// getting out of date license errors
|
||||
/// </remarks>
|
||||
public const int CurrentLicenseFileVersion = 14;
|
||||
private bool ValidLicenseVersion
|
||||
{
|
||||
@ -368,9 +376,10 @@ public class OrganizationLicense : ILicense
|
||||
}
|
||||
|
||||
/*
|
||||
* Version 14 added LimitCollectionCreationDeletion and Version 15 added AllowAdminAccessToAllCollectionItems,
|
||||
* however these are just user settings and it is not worth failing validation if they mismatch.
|
||||
* They are intentionally excluded.
|
||||
* Version 14 added LimitCollectionCreationDeletion and Version
|
||||
* 15 added AllowAdminAccessToAllCollectionItems, however they
|
||||
* are no longer used and are intentionally excluded from
|
||||
* validation.
|
||||
*/
|
||||
|
||||
return valid;
|
||||
|
@ -49,11 +49,11 @@ public class MarkNotificationDeletedCommand : IMarkNotificationDeletedCommand
|
||||
|
||||
if (notificationStatus == null)
|
||||
{
|
||||
notificationStatus = new NotificationStatus()
|
||||
notificationStatus = new NotificationStatus
|
||||
{
|
||||
NotificationId = notificationId,
|
||||
UserId = _currentContext.UserId.Value,
|
||||
DeletedDate = DateTime.Now
|
||||
DeletedDate = DateTime.UtcNow
|
||||
};
|
||||
|
||||
await _authorizationService.AuthorizeOrThrowAsync(_currentContext.HttpContext.User, notificationStatus,
|
||||
|
@ -49,11 +49,11 @@ public class MarkNotificationReadCommand : IMarkNotificationReadCommand
|
||||
|
||||
if (notificationStatus == null)
|
||||
{
|
||||
notificationStatus = new NotificationStatus()
|
||||
notificationStatus = new NotificationStatus
|
||||
{
|
||||
NotificationId = notificationId,
|
||||
UserId = _currentContext.UserId.Value,
|
||||
ReadDate = DateTime.Now
|
||||
ReadDate = DateTime.UtcNow
|
||||
};
|
||||
|
||||
await _authorizationService.AuthorizeOrThrowAsync(_currentContext.HttpContext.User, notificationStatus,
|
||||
|
@ -17,15 +17,18 @@ public class UpdateOrganizationLicenseCommand : IUpdateOrganizationLicenseComman
|
||||
private readonly ILicensingService _licensingService;
|
||||
private readonly IGlobalSettings _globalSettings;
|
||||
private readonly IOrganizationService _organizationService;
|
||||
private readonly IFeatureService _featureService;
|
||||
|
||||
public UpdateOrganizationLicenseCommand(
|
||||
ILicensingService licensingService,
|
||||
IGlobalSettings globalSettings,
|
||||
IOrganizationService organizationService)
|
||||
IOrganizationService organizationService,
|
||||
IFeatureService featureService)
|
||||
{
|
||||
_licensingService = licensingService;
|
||||
_globalSettings = globalSettings;
|
||||
_organizationService = organizationService;
|
||||
_featureService = featureService;
|
||||
}
|
||||
|
||||
public async Task UpdateLicenseAsync(SelfHostedOrganizationDetails selfHostedOrganization,
|
||||
@ -59,7 +62,8 @@ public class UpdateOrganizationLicenseCommand : IUpdateOrganizationLicenseComman
|
||||
private async Task UpdateOrganizationAsync(SelfHostedOrganizationDetails selfHostedOrganizationDetails, OrganizationLicense license)
|
||||
{
|
||||
var organization = selfHostedOrganizationDetails.ToOrganization();
|
||||
organization.UpdateFromLicense(license);
|
||||
|
||||
organization.UpdateFromLicense(license, _featureService);
|
||||
|
||||
await _organizationService.ReplaceAndUpdateCacheAsync(organization);
|
||||
}
|
||||
|
@ -10,6 +10,8 @@ public interface IStripeAdapter
|
||||
Task<Stripe.Customer> CustomerUpdateAsync(string id, Stripe.CustomerUpdateOptions options = null);
|
||||
Task<Stripe.Customer> CustomerDeleteAsync(string id);
|
||||
Task<List<PaymentMethod>> CustomerListPaymentMethods(string id, CustomerListPaymentMethodsOptions options = null);
|
||||
Task<CustomerBalanceTransaction> CustomerBalanceTransactionCreate(string customerId,
|
||||
CustomerBalanceTransactionCreateOptions options);
|
||||
Task<Stripe.Subscription> SubscriptionCreateAsync(Stripe.SubscriptionCreateOptions subscriptionCreateOptions);
|
||||
Task<Stripe.Subscription> SubscriptionGetAsync(string id, Stripe.SubscriptionGetOptions options = null);
|
||||
Task<List<Stripe.Subscription>> SubscriptionListAsync(StripeSubscriptionListOptions subscriptionSearchOptions);
|
||||
|
@ -90,14 +90,20 @@ public interface IUserService
|
||||
/// Indicates if the user is managed by any organization.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A managed user is a user whose email domain matches one of the Organization's verified domains.
|
||||
/// The organization must be enabled and be on an Enterprise plan.
|
||||
/// A user is considered managed by an organization if their email domain matches one of the verified domains of that organization, and the user is a member of it.
|
||||
/// The organization must be enabled and able to have verified domains.
|
||||
/// </remarks>
|
||||
/// <returns>
|
||||
/// False if the Account Deprovisioning feature flag is disabled.
|
||||
/// </returns>
|
||||
Task<bool> IsManagedByAnyOrganizationAsync(Guid userId);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the organization that manages the user.
|
||||
/// Gets the organizations that manage the user.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// An empty collection if the Account Deprovisioning feature flag is disabled.
|
||||
/// </returns>
|
||||
/// <inheritdoc cref="IsManagedByAnyOrganizationAsync(Guid)"/>
|
||||
Task<Organization> GetOrganizationManagingUserAsync(Guid userId);
|
||||
Task<IEnumerable<Organization>> GetOrganizationsManagingUserAsync(Guid userId);
|
||||
}
|
||||
|
@ -18,6 +18,7 @@ public class StripeAdapter : IStripeAdapter
|
||||
private readonly Stripe.PriceService _priceService;
|
||||
private readonly Stripe.SetupIntentService _setupIntentService;
|
||||
private readonly Stripe.TestHelpers.TestClockService _testClockService;
|
||||
private readonly CustomerBalanceTransactionService _customerBalanceTransactionService;
|
||||
|
||||
public StripeAdapter()
|
||||
{
|
||||
@ -34,6 +35,7 @@ public class StripeAdapter : IStripeAdapter
|
||||
_priceService = new Stripe.PriceService();
|
||||
_setupIntentService = new SetupIntentService();
|
||||
_testClockService = new Stripe.TestHelpers.TestClockService();
|
||||
_customerBalanceTransactionService = new CustomerBalanceTransactionService();
|
||||
}
|
||||
|
||||
public Task<Stripe.Customer> CustomerCreateAsync(Stripe.CustomerCreateOptions options)
|
||||
@ -63,6 +65,10 @@ public class StripeAdapter : IStripeAdapter
|
||||
return paymentMethods.Data;
|
||||
}
|
||||
|
||||
public async Task<CustomerBalanceTransaction> CustomerBalanceTransactionCreate(string customerId,
|
||||
CustomerBalanceTransactionCreateOptions options)
|
||||
=> await _customerBalanceTransactionService.CreateAsync(customerId, options);
|
||||
|
||||
public Task<Stripe.Subscription> SubscriptionCreateAsync(Stripe.SubscriptionCreateOptions options)
|
||||
{
|
||||
return _subscriptionService.CreateAsync(options);
|
||||
|
@ -1267,18 +1267,24 @@ public class UserService : UserManager<User>, IUserService, IDisposable
|
||||
|
||||
public async Task<bool> IsManagedByAnyOrganizationAsync(Guid userId)
|
||||
{
|
||||
var managingOrganization = await GetOrganizationManagingUserAsync(userId);
|
||||
return managingOrganization != null;
|
||||
var managingOrganizations = await GetOrganizationsManagingUserAsync(userId);
|
||||
return managingOrganizations.Any();
|
||||
}
|
||||
|
||||
public async Task<Organization> GetOrganizationManagingUserAsync(Guid userId)
|
||||
public async Task<IEnumerable<Organization>> GetOrganizationsManagingUserAsync(Guid userId)
|
||||
{
|
||||
// Users can only be managed by an Organization that is enabled and can have organization domains
|
||||
var organization = await _organizationRepository.GetByClaimedUserDomainAsync(userId);
|
||||
if (!_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning))
|
||||
{
|
||||
return Enumerable.Empty<Organization>();
|
||||
}
|
||||
|
||||
// Get all organizations that have verified the user's email domain.
|
||||
var organizationsWithVerifiedUserEmailDomain = await _organizationRepository.GetByVerifiedUserEmailDomainAsync(userId);
|
||||
|
||||
// Organizations must be enabled and able to have verified domains.
|
||||
// TODO: Replace "UseSso" with a new organization ability like "UseOrganizationDomains" (PM-11622).
|
||||
// Verified domains were tied to SSO, so we currently check the "UseSso" organization ability.
|
||||
return (organization is { Enabled: true, UseSso: true }) ? organization : null;
|
||||
return organizationsWithVerifiedUserEmailDomain.Where(organization => organization is { Enabled: true, UseSso: true });
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IsLegacyUser(string)"/>
|
||||
|
@ -12,8 +12,4 @@
|
||||
<ProjectReference Include="..\Core\Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="MessagePack" Version="2.5.172" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
@ -168,7 +168,7 @@ public class OrganizationRepository : Repository<Organization, Guid>, IOrganizat
|
||||
commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
|
||||
public async Task<Organization> GetByClaimedUserDomainAsync(Guid userId)
|
||||
public async Task<ICollection<Organization>> GetByVerifiedUserEmailDomainAsync(Guid userId)
|
||||
{
|
||||
using (var connection = new SqlConnection(ConnectionString))
|
||||
{
|
||||
@ -177,7 +177,7 @@ public class OrganizationRepository : Repository<Organization, Guid>, IOrganizat
|
||||
new { UserId = userId },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
|
||||
return result.SingleOrDefault();
|
||||
return result.ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,10 +9,6 @@ namespace Bit.Infrastructure.EntityFramework.AdminConsole.Models;
|
||||
|
||||
public class Organization : Core.AdminConsole.Entities.Organization
|
||||
{
|
||||
// Shadow properties - to be introduced by https://bitwarden.atlassian.net/browse/PM-10863
|
||||
public bool LimitCollectionCreation { get => LimitCollectionCreationDeletion; set => LimitCollectionCreationDeletion = value; }
|
||||
public bool LimitCollectionDeletion { get => LimitCollectionCreationDeletion; set => LimitCollectionCreationDeletion = value; }
|
||||
|
||||
public virtual ICollection<Cipher> Ciphers { get; set; }
|
||||
public virtual ICollection<OrganizationUser> OrganizationUsers { get; set; }
|
||||
public virtual ICollection<Group> Groups { get; set; }
|
||||
@ -42,9 +38,6 @@ public class OrganizationMapperProfile : Profile
|
||||
.ForMember(org => org.ApiKeys, opt => opt.Ignore())
|
||||
.ForMember(org => org.Connections, opt => opt.Ignore())
|
||||
.ForMember(org => org.Domains, opt => opt.Ignore())
|
||||
// Shadow properties - to be introduced by https://bitwarden.atlassian.net/browse/PM-10863
|
||||
.ForMember(org => org.LimitCollectionCreation, opt => opt.Ignore())
|
||||
.ForMember(org => org.LimitCollectionDeletion, opt => opt.Ignore())
|
||||
.ReverseMap();
|
||||
|
||||
CreateProjection<Organization, SelfHostedOrganizationDetails>()
|
||||
|
@ -99,6 +99,9 @@ public class OrganizationRepository : Repository<Core.AdminConsole.Entities.Orga
|
||||
UseScim = e.UseScim,
|
||||
UseCustomPermissions = e.UseCustomPermissions,
|
||||
UsePolicies = e.UsePolicies,
|
||||
LimitCollectionCreation = e.LimitCollectionCreation,
|
||||
LimitCollectionDeletion = e.LimitCollectionDeletion,
|
||||
// Deprecated: https://bitwarden.atlassian.net/browse/PM-10863
|
||||
LimitCollectionCreationDeletion = e.LimitCollectionCreationDeletion,
|
||||
AllowAdminAccessToAllCollectionItems = e.AllowAdminAccessToAllCollectionItems
|
||||
}).ToListAsync();
|
||||
@ -276,7 +279,7 @@ public class OrganizationRepository : Repository<Core.AdminConsole.Entities.Orga
|
||||
return await query.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<Core.AdminConsole.Entities.Organization> GetByClaimedUserDomainAsync(Guid userId)
|
||||
public async Task<ICollection<Core.AdminConsole.Entities.Organization>> GetByVerifiedUserEmailDomainAsync(Guid userId)
|
||||
{
|
||||
using (var scope = ServiceScopeFactory.CreateScope())
|
||||
{
|
||||
@ -291,7 +294,7 @@ public class OrganizationRepository : Repository<Core.AdminConsole.Entities.Orga
|
||||
&& u.Email.ToLower().EndsWith("@" + od.DomainName.ToLower())
|
||||
select o;
|
||||
|
||||
return await query.FirstOrDefaultAsync();
|
||||
return await query.ToArrayAsync();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -66,6 +66,9 @@ public class OrganizationUserOrganizationDetailsViewQuery : IQuery<OrganizationU
|
||||
UsePasswordManager = o.UsePasswordManager,
|
||||
SmSeats = o.SmSeats,
|
||||
SmServiceAccounts = o.SmServiceAccounts,
|
||||
LimitCollectionCreation = o.LimitCollectionCreation,
|
||||
LimitCollectionDeletion = o.LimitCollectionDeletion,
|
||||
// Deprecated: https://bitwarden.atlassian.net/browse/PM-10863
|
||||
LimitCollectionCreationDeletion = o.LimitCollectionCreationDeletion,
|
||||
AllowAdminAccessToAllCollectionItems = o.AllowAdminAccessToAllCollectionItems,
|
||||
};
|
||||
|
@ -44,6 +44,9 @@ public class ProviderUserOrganizationDetailsViewQuery : IQuery<ProviderUserOrgan
|
||||
ProviderId = x.p.Id,
|
||||
ProviderName = x.p.Name,
|
||||
PlanType = x.o.PlanType,
|
||||
LimitCollectionCreation = x.o.LimitCollectionCreation,
|
||||
LimitCollectionDeletion = x.o.LimitCollectionDeletion,
|
||||
// Deprecated: https://bitwarden.atlassian.net/browse/PM-10863
|
||||
LimitCollectionCreationDeletion = x.o.LimitCollectionCreationDeletion,
|
||||
AllowAdminAccessToAllCollectionItems = x.o.AllowAdminAccessToAllCollectionItems,
|
||||
});
|
||||
|
@ -12,18 +12,28 @@ BEGIN
|
||||
[dbo].[Collection] S ON S.[Id] = CC.[CollectionId]
|
||||
INNER JOIN
|
||||
[dbo].[OrganizationUser] OU ON OU.[OrganizationId] = S.[OrganizationId] AND OU.[UserId] = @UserId
|
||||
INNER JOIN
|
||||
[dbo].[CollectionUser] CU ON CU.[CollectionId] = S.[Id] AND CU.[OrganizationUserId] = OU.[Id]
|
||||
WHERE
|
||||
OU.[Status] = 2
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT
|
||||
CC.*
|
||||
FROM
|
||||
[dbo].[CollectionCipher] CC
|
||||
INNER JOIN
|
||||
[dbo].[Collection] S ON S.[Id] = CC.[CollectionId]
|
||||
INNER JOIN
|
||||
[dbo].[OrganizationUser] OU ON OU.[OrganizationId] = S.[OrganizationId] AND OU.[UserId] = @UserId
|
||||
INNER JOIN
|
||||
[dbo].[GroupUser] GU ON GU.[OrganizationUserId] = OU.[Id]
|
||||
INNER JOIN
|
||||
[dbo].[CollectionGroup] CG ON CG.[CollectionId] = CC.[CollectionId] AND CG.[GroupId] = GU.[GroupId]
|
||||
LEFT JOIN
|
||||
[dbo].[CollectionUser] CU ON CU.[CollectionId] = S.[Id] AND CU.[OrganizationUserId] = OU.[Id]
|
||||
LEFT JOIN
|
||||
[dbo].[GroupUser] GU ON CU.[CollectionId] IS NULL AND GU.[OrganizationUserId] = OU.[Id]
|
||||
LEFT JOIN
|
||||
[dbo].[Group] G ON G.[Id] = GU.[GroupId]
|
||||
LEFT JOIN
|
||||
[dbo].[CollectionGroup] CG ON CG.[CollectionId] = CC.[CollectionId] AND CG.[GroupId] = GU.[GroupId]
|
||||
WHERE
|
||||
OU.[Status] = 2 -- Confirmed
|
||||
AND (
|
||||
CU.[CollectionId] IS NOT NULL
|
||||
OR CG.[CollectionId] IS NOT NULL
|
||||
)
|
||||
OU.[Status] = 2
|
||||
AND CU.[CollectionId] IS NULL
|
||||
END
|
||||
|
@ -229,13 +229,13 @@ public class OrganizationDomainControllerTests
|
||||
sutProvider.GetDependency<IOrganizationDomainRepository>()
|
||||
.GetDomainByIdOrganizationIdAsync(organizationDomain.Id, organizationDomain.OrganizationId)
|
||||
.Returns(organizationDomain);
|
||||
sutProvider.GetDependency<IVerifyOrganizationDomainCommand>().VerifyOrganizationDomainAsync(organizationDomain)
|
||||
sutProvider.GetDependency<IVerifyOrganizationDomainCommand>().UserVerifyOrganizationDomainAsync(organizationDomain)
|
||||
.Returns(new OrganizationDomain());
|
||||
|
||||
var result = await sutProvider.Sut.Verify(organizationDomain.OrganizationId, organizationDomain.Id);
|
||||
|
||||
await sutProvider.GetDependency<IVerifyOrganizationDomainCommand>().Received(1)
|
||||
.VerifyOrganizationDomainAsync(organizationDomain);
|
||||
.UserVerifyOrganizationDomainAsync(organizationDomain);
|
||||
Assert.IsType<OrganizationDomainResponseModel>(result);
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
using System.Security.Claims;
|
||||
using Bit.Api.Vault.AuthorizationHandlers.Collections;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
@ -32,7 +33,10 @@ public class BulkCollectionAuthorizationHandlerTests
|
||||
organization.Type = userType;
|
||||
organization.Permissions = new Permissions();
|
||||
|
||||
ArrangeOrganizationAbility(sutProvider, organization, true);
|
||||
// `LimitCollectonCreationDeletionSplit` feature flag state isn't
|
||||
// relevant for this test. The flag is never checked for in this
|
||||
// test. This is asserted below.
|
||||
ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureEnabled(sutProvider, organization, true, true);
|
||||
|
||||
var context = new AuthorizationHandlerContext(
|
||||
new[] { BulkCollectionOperations.Create },
|
||||
@ -44,11 +48,12 @@ public class BulkCollectionAuthorizationHandlerTests
|
||||
|
||||
await sutProvider.Sut.HandleAsync(context);
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>().DidNotReceiveWithAnyArgs().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
|
||||
Assert.True(context.HasSucceeded);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData, CollectionCustomization]
|
||||
public async Task CanCreateAsync_WhenUser_WithLimitCollectionCreationDeletionFalse_Success(
|
||||
public async Task CanCreateAsync_WhenUser_WithLimitCollectionCreationFalse_WithLimitCollectionCreationDeletionSplitFeatureDisabled_Success(
|
||||
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
|
||||
ICollection<Collection> collections,
|
||||
CurrentContextOrganization organization)
|
||||
@ -57,7 +62,7 @@ public class BulkCollectionAuthorizationHandlerTests
|
||||
|
||||
organization.Type = OrganizationUserType.User;
|
||||
|
||||
ArrangeOrganizationAbility(sutProvider, organization, false);
|
||||
ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureDisabled(sutProvider, organization, false, false);
|
||||
|
||||
var context = new AuthorizationHandlerContext(
|
||||
new[] { BulkCollectionOperations.Create },
|
||||
@ -66,16 +71,49 @@ public class BulkCollectionAuthorizationHandlerTests
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
|
||||
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit)
|
||||
.Returns(false);
|
||||
|
||||
await sutProvider.Sut.HandleAsync(context);
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
|
||||
Assert.True(context.HasSucceeded);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData, CollectionCustomization]
|
||||
public async Task CanCreateAsync_WhenUser_WithLimitCollectionCreationFalse_WithLimitCollectionCreationDeletionSplitFeatureEnabled_Success(
|
||||
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
|
||||
ICollection<Collection> collections,
|
||||
CurrentContextOrganization organization)
|
||||
{
|
||||
var actingUserId = Guid.NewGuid();
|
||||
|
||||
organization.Type = OrganizationUserType.User;
|
||||
|
||||
ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureEnabled(sutProvider, organization, false, false);
|
||||
|
||||
var context = new AuthorizationHandlerContext(
|
||||
new[] { BulkCollectionOperations.Create },
|
||||
new ClaimsPrincipal(),
|
||||
collections);
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
|
||||
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit)
|
||||
.Returns(true);
|
||||
|
||||
await sutProvider.Sut.HandleAsync(context);
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
|
||||
Assert.True(context.HasSucceeded);
|
||||
}
|
||||
|
||||
[Theory, CollectionCustomization]
|
||||
[BitAutoData(OrganizationUserType.User)]
|
||||
[BitAutoData(OrganizationUserType.Custom)]
|
||||
public async Task CanCreateAsync_WhenMissingPermissions_NoSuccess(
|
||||
public async Task CanCreateAsync_WhenMissingPermissions_WithLimitCollectionCreationDeletionSplitFeatureDisabled_NoSuccess(
|
||||
OrganizationUserType userType,
|
||||
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
|
||||
ICollection<Collection> collections,
|
||||
@ -92,7 +130,7 @@ public class BulkCollectionAuthorizationHandlerTests
|
||||
ManageUsers = false
|
||||
};
|
||||
|
||||
ArrangeOrganizationAbility(sutProvider, organization, true);
|
||||
ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureDisabled(sutProvider, organization, true, true);
|
||||
|
||||
var context = new AuthorizationHandlerContext(
|
||||
new[] { BulkCollectionOperations.Create },
|
||||
@ -102,21 +140,61 @@ public class BulkCollectionAuthorizationHandlerTests
|
||||
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
|
||||
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
|
||||
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false);
|
||||
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(false);
|
||||
|
||||
await sutProvider.Sut.HandleAsync(context);
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
|
||||
Assert.False(context.HasSucceeded);
|
||||
}
|
||||
|
||||
[Theory, CollectionCustomization]
|
||||
[BitAutoData(OrganizationUserType.User)]
|
||||
[BitAutoData(OrganizationUserType.Custom)]
|
||||
public async Task CanCreateAsync_WhenMissingPermissions_WithLimitCollectionCreationDeletionSplitFeatureEnabled_NoSuccess(
|
||||
OrganizationUserType userType,
|
||||
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
|
||||
ICollection<Collection> collections,
|
||||
CurrentContextOrganization organization)
|
||||
{
|
||||
var actingUserId = Guid.NewGuid();
|
||||
|
||||
organization.Type = userType;
|
||||
organization.Permissions = new Permissions
|
||||
{
|
||||
EditAnyCollection = false,
|
||||
DeleteAnyCollection = false,
|
||||
ManageGroups = false,
|
||||
ManageUsers = false
|
||||
};
|
||||
|
||||
ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureEnabled(sutProvider, organization, true, true);
|
||||
|
||||
var context = new AuthorizationHandlerContext(
|
||||
new[] { BulkCollectionOperations.Create },
|
||||
new ClaimsPrincipal(),
|
||||
collections);
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
|
||||
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
|
||||
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false);
|
||||
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(true);
|
||||
|
||||
await sutProvider.Sut.HandleAsync(context);
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
|
||||
Assert.False(context.HasSucceeded);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData, CollectionCustomization]
|
||||
public async Task CanCreateAsync_WhenMissingOrgAccess_NoSuccess(
|
||||
public async Task CanCreateAsync_WhenMissingOrgAccess_WithLimitCollectionCreationDeletionSplitDisabled_NoSuccess(
|
||||
Guid userId,
|
||||
CurrentContextOrganization organization,
|
||||
List<Collection> collections,
|
||||
SutProvider<BulkCollectionAuthorizationHandler> sutProvider)
|
||||
{
|
||||
collections.ForEach(c => c.OrganizationId = organization.Id);
|
||||
ArrangeOrganizationAbility(sutProvider, organization, true);
|
||||
ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureDisabled(sutProvider, organization, true, true);
|
||||
|
||||
var context = new AuthorizationHandlerContext(
|
||||
new[] { BulkCollectionOperations.Create },
|
||||
@ -127,8 +205,38 @@ public class BulkCollectionAuthorizationHandlerTests
|
||||
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);
|
||||
sutProvider.GetDependency<ICurrentContext>().GetOrganization(Arg.Any<Guid>()).Returns((CurrentContextOrganization)null);
|
||||
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false);
|
||||
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(false);
|
||||
|
||||
await sutProvider.Sut.HandleAsync(context);
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
|
||||
Assert.False(context.HasSucceeded);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData, CollectionCustomization]
|
||||
public async Task CanCreateAsync_WhenMissingOrgAccess_WithLimitCollectionCreationDeletionSplitEnabled_NoSuccess(
|
||||
Guid userId,
|
||||
CurrentContextOrganization organization,
|
||||
List<Collection> collections,
|
||||
SutProvider<BulkCollectionAuthorizationHandler> sutProvider)
|
||||
{
|
||||
collections.ForEach(c => c.OrganizationId = organization.Id);
|
||||
ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureEnabled(sutProvider, organization, true, true);
|
||||
|
||||
var context = new AuthorizationHandlerContext(
|
||||
new[] { BulkCollectionOperations.Create },
|
||||
new ClaimsPrincipal(),
|
||||
collections
|
||||
);
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);
|
||||
sutProvider.GetDependency<ICurrentContext>().GetOrganization(Arg.Any<Guid>()).Returns((CurrentContextOrganization)null);
|
||||
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false);
|
||||
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(true);
|
||||
|
||||
await sutProvider.Sut.HandleAsync(context);
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
|
||||
Assert.False(context.HasSucceeded);
|
||||
}
|
||||
|
||||
@ -904,7 +1012,10 @@ public class BulkCollectionAuthorizationHandlerTests
|
||||
DeleteAnyCollection = true
|
||||
};
|
||||
|
||||
ArrangeOrganizationAbility(sutProvider, organization, true);
|
||||
// `LimitCollectonCreationDeletionSplit` feature flag state isn't
|
||||
// relevant for this test. The flag is never checked for in this
|
||||
// test. This is asserted below.
|
||||
ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureEnabled(sutProvider, organization, true, true);
|
||||
|
||||
var context = new AuthorizationHandlerContext(
|
||||
new[] { BulkCollectionOperations.Delete },
|
||||
@ -916,6 +1027,7 @@ public class BulkCollectionAuthorizationHandlerTests
|
||||
|
||||
await sutProvider.Sut.HandleAsync(context);
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>().DidNotReceiveWithAnyArgs().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
|
||||
Assert.True(context.HasSucceeded);
|
||||
}
|
||||
|
||||
@ -931,7 +1043,10 @@ public class BulkCollectionAuthorizationHandlerTests
|
||||
organization.Type = userType;
|
||||
organization.Permissions = new Permissions();
|
||||
|
||||
ArrangeOrganizationAbility(sutProvider, organization, true);
|
||||
// `LimitCollectonCreationDeletionSplit` feature flag state isn't
|
||||
// relevant for this test. The flag is never checked for in this
|
||||
// test. This is asserted below.
|
||||
ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureEnabled(sutProvider, organization, true, true);
|
||||
|
||||
var context = new AuthorizationHandlerContext(
|
||||
new[] { BulkCollectionOperations.Delete },
|
||||
@ -943,11 +1058,12 @@ public class BulkCollectionAuthorizationHandlerTests
|
||||
|
||||
await sutProvider.Sut.HandleAsync(context);
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>().DidNotReceiveWithAnyArgs().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
|
||||
Assert.True(context.HasSucceeded);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData, CollectionCustomization]
|
||||
public async Task CanDeleteAsync_WhenUser_LimitCollectionCreationDeletionFalse_WithCanManagePermission_Success(
|
||||
public async Task CanDeleteAsync_WhenUser_LimitCollectionDeletionFalse_WithCanManagePermission_WithLimitCollectionCreationDeletionSplitFeatureDisabled_Success(
|
||||
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
|
||||
ICollection<CollectionDetails> collections,
|
||||
CurrentContextOrganization organization)
|
||||
@ -957,11 +1073,12 @@ public class BulkCollectionAuthorizationHandlerTests
|
||||
organization.Type = OrganizationUserType.User;
|
||||
organization.Permissions = new Permissions();
|
||||
|
||||
ArrangeOrganizationAbility(sutProvider, organization, false);
|
||||
ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureDisabled(sutProvider, organization, false, false);
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
|
||||
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
|
||||
sutProvider.GetDependency<ICollectionRepository>().GetManyByUserIdAsync(actingUserId).Returns(collections);
|
||||
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(false);
|
||||
|
||||
foreach (var c in collections)
|
||||
{
|
||||
@ -975,6 +1092,41 @@ public class BulkCollectionAuthorizationHandlerTests
|
||||
|
||||
await sutProvider.Sut.HandleAsync(context);
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
|
||||
Assert.True(context.HasSucceeded);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData, CollectionCustomization]
|
||||
public async Task CanDeleteAsync_WhenUser_LimitCollectionDeletionFalse_WithCanManagePermission_WithLimitCollectionCreationDeletionSplitFeatureEnabled_Success(
|
||||
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
|
||||
ICollection<CollectionDetails> collections,
|
||||
CurrentContextOrganization organization)
|
||||
{
|
||||
var actingUserId = Guid.NewGuid();
|
||||
|
||||
organization.Type = OrganizationUserType.User;
|
||||
organization.Permissions = new Permissions();
|
||||
|
||||
ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureEnabled(sutProvider, organization, false, false);
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
|
||||
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
|
||||
sutProvider.GetDependency<ICollectionRepository>().GetManyByUserIdAsync(actingUserId).Returns(collections);
|
||||
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(true);
|
||||
|
||||
foreach (var c in collections)
|
||||
{
|
||||
c.Manage = true;
|
||||
}
|
||||
|
||||
var context = new AuthorizationHandlerContext(
|
||||
new[] { BulkCollectionOperations.Delete },
|
||||
new ClaimsPrincipal(),
|
||||
collections);
|
||||
|
||||
await sutProvider.Sut.HandleAsync(context);
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
|
||||
Assert.True(context.HasSucceeded);
|
||||
}
|
||||
|
||||
@ -982,7 +1134,7 @@ public class BulkCollectionAuthorizationHandlerTests
|
||||
[BitAutoData(OrganizationUserType.Admin)]
|
||||
[BitAutoData(OrganizationUserType.Owner)]
|
||||
[BitAutoData(OrganizationUserType.User)]
|
||||
public async Task CanDeleteAsync_LimitCollectionCreationDeletionFalse_AllowAdminAccessToAllCollectionItemsFalse_WithCanManagePermission_Success(
|
||||
public async Task CanDeleteAsync_LimitCollectionDeletionFalse_AllowAdminAccessToAllCollectionItemsFalse_WithCanManagePermission_WithLimitCollectionCreationDeletionSplitFeatureDisabled_Success(
|
||||
OrganizationUserType userType,
|
||||
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
|
||||
ICollection<CollectionDetails> collections,
|
||||
@ -993,11 +1145,12 @@ public class BulkCollectionAuthorizationHandlerTests
|
||||
organization.Type = userType;
|
||||
organization.Permissions = new Permissions();
|
||||
|
||||
ArrangeOrganizationAbility(sutProvider, organization, false, false);
|
||||
ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureDisabled(sutProvider, organization, false, false, false);
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
|
||||
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
|
||||
sutProvider.GetDependency<ICollectionRepository>().GetManyByUserIdAsync(actingUserId).Returns(collections);
|
||||
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(false);
|
||||
|
||||
foreach (var c in collections)
|
||||
{
|
||||
@ -1011,13 +1164,15 @@ public class BulkCollectionAuthorizationHandlerTests
|
||||
|
||||
await sutProvider.Sut.HandleAsync(context);
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
|
||||
Assert.True(context.HasSucceeded);
|
||||
}
|
||||
|
||||
[Theory, CollectionCustomization]
|
||||
[BitAutoData(OrganizationUserType.Admin)]
|
||||
[BitAutoData(OrganizationUserType.Owner)]
|
||||
public async Task CanDeleteAsync_WhenAdminOrOwner_LimitCollectionCreationDeletionTrue_AllowAdminAccessToAllCollectionItemsFalse_WithCanManagePermission_Success(
|
||||
[BitAutoData(OrganizationUserType.User)]
|
||||
public async Task CanDeleteAsync_LimitCollectionDeletionFalse_AllowAdminAccessToAllCollectionItemsFalse_WithCanManagePermission_WithLimitCollectionCreationDeletionSplitFeatureEnabled_Success(
|
||||
OrganizationUserType userType,
|
||||
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
|
||||
ICollection<CollectionDetails> collections,
|
||||
@ -1028,11 +1183,12 @@ public class BulkCollectionAuthorizationHandlerTests
|
||||
organization.Type = userType;
|
||||
organization.Permissions = new Permissions();
|
||||
|
||||
ArrangeOrganizationAbility(sutProvider, organization, true, false);
|
||||
ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureEnabled(sutProvider, organization, false, false, false);
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
|
||||
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
|
||||
sutProvider.GetDependency<ICollectionRepository>().GetManyByUserIdAsync(actingUserId).Returns(collections);
|
||||
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(true);
|
||||
|
||||
foreach (var c in collections)
|
||||
{
|
||||
@ -1046,13 +1202,14 @@ public class BulkCollectionAuthorizationHandlerTests
|
||||
|
||||
await sutProvider.Sut.HandleAsync(context);
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
|
||||
Assert.True(context.HasSucceeded);
|
||||
}
|
||||
|
||||
[Theory, CollectionCustomization]
|
||||
[BitAutoData(OrganizationUserType.Admin)]
|
||||
[BitAutoData(OrganizationUserType.Owner)]
|
||||
public async Task CanDeleteAsync_WhenAdminOrOwner_LimitCollectionCreationDeletionTrue_AllowAdminAccessToAllCollectionItemsFalse_WithoutCanManagePermission_Failure(
|
||||
public async Task CanDeleteAsync_WhenAdminOrOwner_LimitCollectionDeletionTrue_AllowAdminAccessToAllCollectionItemsFalse_WithCanManagePermission_WithLimitCollectionCreationDeletionSplitFeatureDisabled_Success(
|
||||
OrganizationUserType userType,
|
||||
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
|
||||
ICollection<CollectionDetails> collections,
|
||||
@ -1063,12 +1220,87 @@ public class BulkCollectionAuthorizationHandlerTests
|
||||
organization.Type = userType;
|
||||
organization.Permissions = new Permissions();
|
||||
|
||||
ArrangeOrganizationAbility(sutProvider, organization, true, false);
|
||||
ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureDisabled(sutProvider, organization, true, true, false);
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
|
||||
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
|
||||
sutProvider.GetDependency<ICollectionRepository>().GetManyByUserIdAsync(actingUserId).Returns(collections);
|
||||
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(false);
|
||||
|
||||
foreach (var c in collections)
|
||||
{
|
||||
c.Manage = true;
|
||||
}
|
||||
|
||||
var context = new AuthorizationHandlerContext(
|
||||
new[] { BulkCollectionOperations.Delete },
|
||||
new ClaimsPrincipal(),
|
||||
collections);
|
||||
|
||||
await sutProvider.Sut.HandleAsync(context);
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
|
||||
Assert.True(context.HasSucceeded);
|
||||
}
|
||||
|
||||
[Theory, CollectionCustomization]
|
||||
[BitAutoData(OrganizationUserType.Admin)]
|
||||
[BitAutoData(OrganizationUserType.Owner)]
|
||||
public async Task CanDeleteAsync_WhenAdminOrOwner_LimitCollectionDeletionTrue_AllowAdminAccessToAllCollectionItemsFalse_WithCanManagePermission_WithLimitCollectionCreationDeletionSplitFeatureEnabled_Success(
|
||||
OrganizationUserType userType,
|
||||
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
|
||||
ICollection<CollectionDetails> collections,
|
||||
CurrentContextOrganization organization)
|
||||
{
|
||||
var actingUserId = Guid.NewGuid();
|
||||
|
||||
organization.Type = userType;
|
||||
organization.Permissions = new Permissions();
|
||||
|
||||
ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureEnabled(sutProvider, organization, true, true, false);
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
|
||||
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
|
||||
sutProvider.GetDependency<ICollectionRepository>().GetManyByUserIdAsync(actingUserId).Returns(collections);
|
||||
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(true);
|
||||
|
||||
foreach (var c in collections)
|
||||
{
|
||||
c.Manage = true;
|
||||
}
|
||||
|
||||
var context = new AuthorizationHandlerContext(
|
||||
new[] { BulkCollectionOperations.Delete },
|
||||
new ClaimsPrincipal(),
|
||||
collections);
|
||||
|
||||
await sutProvider.Sut.HandleAsync(context);
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
|
||||
Assert.True(context.HasSucceeded);
|
||||
}
|
||||
|
||||
[Theory, CollectionCustomization]
|
||||
[BitAutoData(OrganizationUserType.Admin)]
|
||||
[BitAutoData(OrganizationUserType.Owner)]
|
||||
public async Task CanDeleteAsync_WhenAdminOrOwner_LimitCollectionDeletionTrue_AllowAdminAccessToAllCollectionItemsFalse_WithoutCanManagePermission_WithLimitCollectionCreationDeletionSplitFeatureDisabled_Failure(
|
||||
OrganizationUserType userType,
|
||||
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
|
||||
ICollection<CollectionDetails> collections,
|
||||
CurrentContextOrganization organization)
|
||||
{
|
||||
var actingUserId = Guid.NewGuid();
|
||||
|
||||
organization.Type = userType;
|
||||
organization.Permissions = new Permissions();
|
||||
|
||||
ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureDisabled(sutProvider, organization, true, true, false);
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
|
||||
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
|
||||
sutProvider.GetDependency<ICollectionRepository>().GetManyByUserIdAsync(actingUserId).Returns(collections);
|
||||
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false);
|
||||
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(false);
|
||||
|
||||
foreach (var c in collections)
|
||||
{
|
||||
@ -1082,11 +1314,50 @@ public class BulkCollectionAuthorizationHandlerTests
|
||||
|
||||
await sutProvider.Sut.HandleAsync(context);
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
|
||||
Assert.False(context.HasSucceeded);
|
||||
}
|
||||
|
||||
[Theory, CollectionCustomization]
|
||||
[BitAutoData(OrganizationUserType.Admin)]
|
||||
[BitAutoData(OrganizationUserType.Owner)]
|
||||
public async Task CanDeleteAsync_WhenAdminOrOwner_LimitCollectionDeletionTrue_AllowAdminAccessToAllCollectionItemsFalse_WithoutCanManagePermission_WithLimitCollectionCreationDeletionSplitFeatureEnabled_Failure(
|
||||
OrganizationUserType userType,
|
||||
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
|
||||
ICollection<CollectionDetails> collections,
|
||||
CurrentContextOrganization organization)
|
||||
{
|
||||
var actingUserId = Guid.NewGuid();
|
||||
|
||||
organization.Type = userType;
|
||||
organization.Permissions = new Permissions();
|
||||
|
||||
ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureEnabled(sutProvider, organization, true, true, false);
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
|
||||
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
|
||||
sutProvider.GetDependency<ICollectionRepository>().GetManyByUserIdAsync(actingUserId).Returns(collections);
|
||||
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false);
|
||||
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(true);
|
||||
|
||||
foreach (var c in collections)
|
||||
{
|
||||
c.Manage = false;
|
||||
}
|
||||
|
||||
var context = new AuthorizationHandlerContext(
|
||||
new[] { BulkCollectionOperations.Delete },
|
||||
new ClaimsPrincipal(),
|
||||
collections);
|
||||
|
||||
await sutProvider.Sut.HandleAsync(context);
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
|
||||
Assert.False(context.HasSucceeded);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData, CollectionCustomization]
|
||||
public async Task CanDeleteAsync_WhenUser_LimitCollectionCreationDeletionTrue_AllowAdminAccessToAllCollectionItemsTrue_Failure(
|
||||
public async Task CanDeleteAsync_WhenUser_LimitCollectionDeletionTrue_AllowAdminAccessToAllCollectionItemsTrue_WithLimitCollectionCreationDeletionSplitFeatureDisabled_Failure(
|
||||
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
|
||||
ICollection<CollectionDetails> collections,
|
||||
CurrentContextOrganization organization)
|
||||
@ -1096,12 +1367,13 @@ public class BulkCollectionAuthorizationHandlerTests
|
||||
organization.Type = OrganizationUserType.User;
|
||||
organization.Permissions = new Permissions();
|
||||
|
||||
ArrangeOrganizationAbility(sutProvider, organization, true);
|
||||
ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureDisabled(sutProvider, organization, true, true);
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
|
||||
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
|
||||
sutProvider.GetDependency<ICollectionRepository>().GetManyByUserIdAsync(actingUserId).Returns(collections);
|
||||
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false);
|
||||
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(false);
|
||||
|
||||
foreach (var c in collections)
|
||||
{
|
||||
@ -1115,11 +1387,12 @@ public class BulkCollectionAuthorizationHandlerTests
|
||||
|
||||
await sutProvider.Sut.HandleAsync(context);
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
|
||||
Assert.False(context.HasSucceeded);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData, CollectionCustomization]
|
||||
public async Task CanDeleteAsync_WhenUser_LimitCollectionCreationDeletionTrue_AllowAdminAccessToAllCollectionItemsFalse_Failure(
|
||||
public async Task CanDeleteAsync_WhenUser_LimitCollectionDeletionTrue_AllowAdminAccessToAllCollectionItemsTrue_WithLimitCollectionCreationDeletionSplitFeatureEnabled_Failure(
|
||||
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
|
||||
ICollection<CollectionDetails> collections,
|
||||
CurrentContextOrganization organization)
|
||||
@ -1129,12 +1402,13 @@ public class BulkCollectionAuthorizationHandlerTests
|
||||
organization.Type = OrganizationUserType.User;
|
||||
organization.Permissions = new Permissions();
|
||||
|
||||
ArrangeOrganizationAbility(sutProvider, organization, true, false);
|
||||
ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureEnabled(sutProvider, organization, true, true);
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
|
||||
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
|
||||
sutProvider.GetDependency<ICollectionRepository>().GetManyByUserIdAsync(actingUserId).Returns(collections);
|
||||
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false);
|
||||
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(true);
|
||||
|
||||
foreach (var c in collections)
|
||||
{
|
||||
@ -1148,13 +1422,88 @@ public class BulkCollectionAuthorizationHandlerTests
|
||||
|
||||
await sutProvider.Sut.HandleAsync(context);
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
|
||||
Assert.False(context.HasSucceeded);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData, CollectionCustomization]
|
||||
public async Task CanDeleteAsync_WhenUser_LimitCollectionDeletionTrue_AllowAdminAccessToAllCollectionItemsFalse_WithLimitCollectionCreationDeletionSplitFeatureDisabled_Failure(
|
||||
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
|
||||
ICollection<CollectionDetails> collections,
|
||||
CurrentContextOrganization organization)
|
||||
{
|
||||
var actingUserId = Guid.NewGuid();
|
||||
|
||||
organization.Type = OrganizationUserType.User;
|
||||
organization.Permissions = new Permissions();
|
||||
|
||||
ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureDisabled(sutProvider, organization, true, true, false);
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
|
||||
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
|
||||
sutProvider.GetDependency<ICollectionRepository>().GetManyByUserIdAsync(actingUserId).Returns(collections);
|
||||
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false);
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit)
|
||||
.Returns(false);
|
||||
|
||||
foreach (var c in collections)
|
||||
{
|
||||
c.Manage = true;
|
||||
}
|
||||
|
||||
var context = new AuthorizationHandlerContext(
|
||||
new[] { BulkCollectionOperations.Delete },
|
||||
new ClaimsPrincipal(),
|
||||
collections);
|
||||
|
||||
await sutProvider.Sut.HandleAsync(context);
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
|
||||
Assert.False(context.HasSucceeded);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData, CollectionCustomization]
|
||||
public async Task CanDeleteAsync_WhenUser_LimitCollectionDeletionTrue_AllowAdminAccessToAllCollectionItemsFalse_WithLimitCollectionCreationDeletionSplitFeatureEnabled_Failure(
|
||||
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
|
||||
ICollection<CollectionDetails> collections,
|
||||
CurrentContextOrganization organization)
|
||||
{
|
||||
var actingUserId = Guid.NewGuid();
|
||||
|
||||
organization.Type = OrganizationUserType.User;
|
||||
organization.Permissions = new Permissions();
|
||||
|
||||
ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureEnabled(sutProvider, organization, true, true, false);
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
|
||||
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
|
||||
sutProvider.GetDependency<ICollectionRepository>().GetManyByUserIdAsync(actingUserId).Returns(collections);
|
||||
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false);
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit)
|
||||
.Returns(true);
|
||||
|
||||
foreach (var c in collections)
|
||||
{
|
||||
c.Manage = true;
|
||||
}
|
||||
|
||||
var context = new AuthorizationHandlerContext(
|
||||
new[] { BulkCollectionOperations.Delete },
|
||||
new ClaimsPrincipal(),
|
||||
collections);
|
||||
|
||||
await sutProvider.Sut.HandleAsync(context);
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
|
||||
Assert.False(context.HasSucceeded);
|
||||
}
|
||||
|
||||
[Theory, CollectionCustomization]
|
||||
[BitAutoData(OrganizationUserType.User)]
|
||||
[BitAutoData(OrganizationUserType.Custom)]
|
||||
public async Task CanDeleteAsync_WhenMissingPermissions_NoSuccess(
|
||||
public async Task CanDeleteAsync_WhenMissingPermissions_WithLimitCollectionCreationDeletionSplitFeatureDisabled_NoSuccess(
|
||||
OrganizationUserType userType,
|
||||
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
|
||||
ICollection<Collection> collections,
|
||||
@ -1171,7 +1520,7 @@ public class BulkCollectionAuthorizationHandlerTests
|
||||
ManageUsers = false
|
||||
};
|
||||
|
||||
ArrangeOrganizationAbility(sutProvider, organization, true);
|
||||
ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureDisabled(sutProvider, organization, true, true);
|
||||
|
||||
var context = new AuthorizationHandlerContext(
|
||||
new[] { BulkCollectionOperations.Delete },
|
||||
@ -1181,14 +1530,54 @@ public class BulkCollectionAuthorizationHandlerTests
|
||||
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
|
||||
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
|
||||
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false);
|
||||
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(false);
|
||||
|
||||
await sutProvider.Sut.HandleAsync(context);
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
|
||||
Assert.False(context.HasSucceeded);
|
||||
}
|
||||
|
||||
[Theory, CollectionCustomization]
|
||||
[BitAutoData(OrganizationUserType.User)]
|
||||
[BitAutoData(OrganizationUserType.Custom)]
|
||||
public async Task CanDeleteAsync_WhenMissingPermissions_WithLimitCollectionCreationDeletionSplitFeatureEnabled_NoSuccess(
|
||||
OrganizationUserType userType,
|
||||
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
|
||||
ICollection<Collection> collections,
|
||||
CurrentContextOrganization organization)
|
||||
{
|
||||
var actingUserId = Guid.NewGuid();
|
||||
|
||||
organization.Type = userType;
|
||||
organization.Permissions = new Permissions
|
||||
{
|
||||
EditAnyCollection = false,
|
||||
DeleteAnyCollection = false,
|
||||
ManageGroups = false,
|
||||
ManageUsers = false
|
||||
};
|
||||
|
||||
ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureEnabled(sutProvider, organization, true, true);
|
||||
|
||||
var context = new AuthorizationHandlerContext(
|
||||
new[] { BulkCollectionOperations.Delete },
|
||||
new ClaimsPrincipal(),
|
||||
collections);
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
|
||||
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
|
||||
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false);
|
||||
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(true);
|
||||
|
||||
await sutProvider.Sut.HandleAsync(context);
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
|
||||
Assert.False(context.HasSucceeded);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData, CollectionCustomization]
|
||||
public async Task CanDeleteAsync_WhenMissingOrgAccess_NoSuccess(
|
||||
public async Task CanDeleteAsync_WhenMissingOrgAccess_WithLimitCollectionCreationDeletionSplitFeatureDisabled_NoSuccess(
|
||||
Guid userId,
|
||||
ICollection<Collection> collections,
|
||||
SutProvider<BulkCollectionAuthorizationHandler> sutProvider)
|
||||
@ -1202,8 +1591,34 @@ public class BulkCollectionAuthorizationHandlerTests
|
||||
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);
|
||||
sutProvider.GetDependency<ICurrentContext>().GetOrganization(Arg.Any<Guid>()).Returns((CurrentContextOrganization)null);
|
||||
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false);
|
||||
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(false);
|
||||
|
||||
await sutProvider.Sut.HandleAsync(context);
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
|
||||
Assert.False(context.HasSucceeded);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData, CollectionCustomization]
|
||||
public async Task CanDeleteAsync_WhenMissingOrgAccess_WithLimitCollectionCreationDeletionSplitFeatureEnabled_NoSuccess(
|
||||
Guid userId,
|
||||
ICollection<Collection> collections,
|
||||
SutProvider<BulkCollectionAuthorizationHandler> sutProvider)
|
||||
{
|
||||
var context = new AuthorizationHandlerContext(
|
||||
new[] { BulkCollectionOperations.Delete },
|
||||
new ClaimsPrincipal(),
|
||||
collections
|
||||
);
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);
|
||||
sutProvider.GetDependency<ICurrentContext>().GetOrganization(Arg.Any<Guid>()).Returns((CurrentContextOrganization)null);
|
||||
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false);
|
||||
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(true);
|
||||
|
||||
await sutProvider.Sut.HandleAsync(context);
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
|
||||
Assert.False(context.HasSucceeded);
|
||||
}
|
||||
|
||||
@ -1224,6 +1639,7 @@ public class BulkCollectionAuthorizationHandlerTests
|
||||
await sutProvider.Sut.HandleAsync(context);
|
||||
Assert.True(context.HasFailed);
|
||||
sutProvider.GetDependency<ICollectionRepository>().DidNotReceiveWithAnyArgs();
|
||||
sutProvider.GetDependency<IFeatureService>().DidNotReceiveWithAnyArgs().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData, CollectionCustomization]
|
||||
@ -1247,10 +1663,11 @@ public class BulkCollectionAuthorizationHandlerTests
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.HandleAsync(context));
|
||||
Assert.Equal("Requested collections must belong to the same organization.", exception.Message);
|
||||
sutProvider.GetDependency<ICurrentContext>().DidNotReceiveWithAnyArgs().GetOrganization(default);
|
||||
sutProvider.GetDependency<IFeatureService>().DidNotReceiveWithAnyArgs().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData, CollectionCustomization]
|
||||
public async Task HandleRequirementAsync_Provider_Success(
|
||||
public async Task HandleRequirementAsync_Provider_WithLimitCollectionCreationDeletionSplitFeatureDisabled_Success(
|
||||
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
|
||||
ICollection<Collection> collections)
|
||||
{
|
||||
@ -1286,6 +1703,63 @@ public class BulkCollectionAuthorizationHandlerTests
|
||||
sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilitiesAsync()
|
||||
.Returns(organizationAbilities);
|
||||
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(true);
|
||||
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(false);
|
||||
|
||||
var context = new AuthorizationHandlerContext(
|
||||
new[] { op },
|
||||
new ClaimsPrincipal(),
|
||||
collections
|
||||
);
|
||||
|
||||
await sutProvider.Sut.HandleAsync(context);
|
||||
|
||||
Assert.True(context.HasSucceeded);
|
||||
await sutProvider.GetDependency<ICurrentContext>().Received().ProviderUserForOrgAsync(orgId);
|
||||
|
||||
// Recreate the SUT to reset the mocks/dependencies between tests
|
||||
sutProvider.Recreate();
|
||||
}
|
||||
}
|
||||
|
||||
[Theory, BitAutoData, CollectionCustomization]
|
||||
public async Task HandleRequirementAsync_Provider_WithLimitCollectionCreationDeletionSplitFeatureEnabled_Success(
|
||||
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
|
||||
ICollection<Collection> collections)
|
||||
{
|
||||
var actingUserId = Guid.NewGuid();
|
||||
var orgId = collections.First().OrganizationId;
|
||||
|
||||
var organizationAbilities = new Dictionary<Guid, OrganizationAbility>
|
||||
{
|
||||
{ collections.First().OrganizationId,
|
||||
new OrganizationAbility
|
||||
{
|
||||
LimitCollectionCreation = true,
|
||||
LimitCollectionDeletion = true,
|
||||
AllowAdminAccessToAllCollectionItems = true
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var operationsToTest = new[]
|
||||
{
|
||||
BulkCollectionOperations.Create,
|
||||
BulkCollectionOperations.Read,
|
||||
BulkCollectionOperations.ReadAccess,
|
||||
BulkCollectionOperations.Update,
|
||||
BulkCollectionOperations.ModifyUserAccess,
|
||||
BulkCollectionOperations.ModifyGroupAccess,
|
||||
BulkCollectionOperations.Delete,
|
||||
};
|
||||
|
||||
foreach (var op in operationsToTest)
|
||||
{
|
||||
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
|
||||
sutProvider.GetDependency<ICurrentContext>().GetOrganization(orgId).Returns((CurrentContextOrganization)null);
|
||||
sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilitiesAsync()
|
||||
.Returns(organizationAbilities);
|
||||
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(true);
|
||||
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(true);
|
||||
|
||||
var context = new AuthorizationHandlerContext(
|
||||
new[] { op },
|
||||
@ -1336,14 +1810,37 @@ public class BulkCollectionAuthorizationHandlerTests
|
||||
await sutProvider.GetDependency<ICollectionRepository>().Received(1).GetManyByUserIdAsync(Arg.Any<Guid>());
|
||||
}
|
||||
|
||||
private static void ArrangeOrganizationAbility(
|
||||
private static void ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureDisabled(
|
||||
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
|
||||
CurrentContextOrganization organization, bool limitCollectionCreationDeletion,
|
||||
CurrentContextOrganization organization,
|
||||
bool limitCollectionCreation,
|
||||
bool limitCollectionDeletion,
|
||||
bool allowAdminAccessToAllCollectionItems = true)
|
||||
{
|
||||
var organizationAbility = new OrganizationAbility();
|
||||
organizationAbility.Id = organization.Id;
|
||||
organizationAbility.LimitCollectionCreationDeletion = limitCollectionCreationDeletion;
|
||||
|
||||
organizationAbility.LimitCollectionCreationDeletion = limitCollectionCreation || limitCollectionDeletion;
|
||||
|
||||
organizationAbility.AllowAdminAccessToAllCollectionItems = allowAdminAccessToAllCollectionItems;
|
||||
|
||||
sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilityAsync(organizationAbility.Id)
|
||||
.Returns(organizationAbility);
|
||||
}
|
||||
|
||||
private static void ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureEnabled(
|
||||
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
|
||||
CurrentContextOrganization organization,
|
||||
bool limitCollectionCreation,
|
||||
bool limitCollectionDeletion,
|
||||
bool allowAdminAccessToAllCollectionItems = true)
|
||||
{
|
||||
var organizationAbility = new OrganizationAbility();
|
||||
organizationAbility.Id = organization.Id;
|
||||
|
||||
organizationAbility.LimitCollectionCreation = limitCollectionCreation;
|
||||
organizationAbility.LimitCollectionDeletion = limitCollectionDeletion;
|
||||
|
||||
organizationAbility.AllowAdminAccessToAllCollectionItems = allowAdminAccessToAllCollectionItems;
|
||||
|
||||
sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilityAsync(organizationAbility.Id)
|
||||
|
@ -15,7 +15,7 @@ namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationDomains;
|
||||
public class VerifyOrganizationDomainCommandTests
|
||||
{
|
||||
[Theory, BitAutoData]
|
||||
public async Task VerifyOrganizationDomain_ShouldThrowConflict_WhenDomainHasBeenClaimed(Guid id,
|
||||
public async Task UserVerifyOrganizationDomain_ShouldThrowConflict_WhenDomainHasBeenClaimed(Guid id,
|
||||
SutProvider<VerifyOrganizationDomainCommand> sutProvider)
|
||||
{
|
||||
var expected = new OrganizationDomain
|
||||
@ -30,14 +30,14 @@ public class VerifyOrganizationDomainCommandTests
|
||||
.GetByIdAsync(id)
|
||||
.Returns(expected);
|
||||
|
||||
var requestAction = async () => await sutProvider.Sut.VerifyOrganizationDomainAsync(expected);
|
||||
var requestAction = async () => await sutProvider.Sut.UserVerifyOrganizationDomainAsync(expected);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<ConflictException>(requestAction);
|
||||
Assert.Contains("Domain has already been verified.", exception.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task VerifyOrganizationDomain_ShouldThrowConflict_WhenDomainHasBeenClaimedByAnotherOrganization(Guid id,
|
||||
public async Task UserVerifyOrganizationDomain_ShouldThrowConflict_WhenDomainHasBeenClaimedByAnotherOrganization(Guid id,
|
||||
SutProvider<VerifyOrganizationDomainCommand> sutProvider)
|
||||
{
|
||||
var expected = new OrganizationDomain
|
||||
@ -54,14 +54,14 @@ public class VerifyOrganizationDomainCommandTests
|
||||
.GetClaimedDomainsByDomainNameAsync(expected.DomainName)
|
||||
.Returns(new List<OrganizationDomain> { expected });
|
||||
|
||||
var requestAction = async () => await sutProvider.Sut.VerifyOrganizationDomainAsync(expected);
|
||||
var requestAction = async () => await sutProvider.Sut.UserVerifyOrganizationDomainAsync(expected);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<ConflictException>(requestAction);
|
||||
Assert.Contains("The domain is not available to be claimed.", exception.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task VerifyOrganizationDomain_ShouldVerifyDomainUpdateAndLogEvent_WhenTxtRecordExists(Guid id,
|
||||
public async Task UserVerifyOrganizationDomain_ShouldVerifyDomainUpdateAndLogEvent_WhenTxtRecordExists(Guid id,
|
||||
SutProvider<VerifyOrganizationDomainCommand> sutProvider)
|
||||
{
|
||||
var expected = new OrganizationDomain
|
||||
@ -81,7 +81,7 @@ public class VerifyOrganizationDomainCommandTests
|
||||
.ResolveAsync(expected.DomainName, Arg.Any<string>())
|
||||
.Returns(true);
|
||||
|
||||
var result = await sutProvider.Sut.VerifyOrganizationDomainAsync(expected);
|
||||
var result = await sutProvider.Sut.UserVerifyOrganizationDomainAsync(expected);
|
||||
|
||||
Assert.NotNull(result.VerifiedDate);
|
||||
await sutProvider.GetDependency<IOrganizationDomainRepository>().Received(1)
|
||||
@ -91,7 +91,7 @@ public class VerifyOrganizationDomainCommandTests
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task VerifyOrganizationDomain_ShouldNotSetVerifiedDate_WhenTxtRecordDoesNotExist(Guid id,
|
||||
public async Task UserVerifyOrganizationDomain_ShouldNotSetVerifiedDate_WhenTxtRecordDoesNotExist(Guid id,
|
||||
SutProvider<VerifyOrganizationDomainCommand> sutProvider)
|
||||
{
|
||||
var expected = new OrganizationDomain
|
||||
@ -111,10 +111,30 @@ public class VerifyOrganizationDomainCommandTests
|
||||
.ResolveAsync(expected.DomainName, Arg.Any<string>())
|
||||
.Returns(false);
|
||||
|
||||
var result = await sutProvider.Sut.VerifyOrganizationDomainAsync(expected);
|
||||
var result = await sutProvider.Sut.UserVerifyOrganizationDomainAsync(expected);
|
||||
|
||||
Assert.Null(result.VerifiedDate);
|
||||
await sutProvider.GetDependency<IEventService>().Received(1)
|
||||
.LogOrganizationDomainEventAsync(Arg.Any<OrganizationDomain>(), EventType.OrganizationDomain_NotVerified);
|
||||
}
|
||||
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task SystemVerifyOrganizationDomain_CallsEventServiceWithUpdatedJobRunCount(SutProvider<VerifyOrganizationDomainCommand> sutProvider)
|
||||
{
|
||||
var domain = new OrganizationDomain()
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
OrganizationId = Guid.NewGuid(),
|
||||
CreationDate = DateTime.UtcNow,
|
||||
DomainName = "test.com",
|
||||
Txt = "btw+12345",
|
||||
};
|
||||
|
||||
_ = await sutProvider.Sut.SystemVerifyOrganizationDomainAsync(domain);
|
||||
|
||||
await sutProvider.GetDependency<IEventService>().ReceivedWithAnyArgs(1)
|
||||
.LogOrganizationDomainEventAsync(default, EventType.OrganizationDomain_NotVerified,
|
||||
EventSystemUser.DomainVerification);
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,7 @@
|
||||
using Bit.Core.AdminConsole.Services.Implementations;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;
|
||||
using Bit.Core.AdminConsole.Services.Implementations;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
@ -36,18 +35,14 @@ public class OrganizationDomainServiceTests
|
||||
Txt = "btw+6789"
|
||||
}
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IOrganizationDomainRepository>().GetManyByNextRunDateAsync(default)
|
||||
.ReturnsForAnyArgs(domains);
|
||||
|
||||
await sutProvider.Sut.ValidateOrganizationsDomainAsync();
|
||||
|
||||
await sutProvider.GetDependency<IDnsResolverService>().ReceivedWithAnyArgs(2)
|
||||
.ResolveAsync(default, default);
|
||||
await sutProvider.GetDependency<IOrganizationDomainRepository>().ReceivedWithAnyArgs(2)
|
||||
.ReplaceAsync(default);
|
||||
await sutProvider.GetDependency<IEventService>().ReceivedWithAnyArgs(2)
|
||||
.LogOrganizationDomainEventAsync(default, EventType.OrganizationDomain_NotVerified,
|
||||
EventSystemUser.DomainVerification);
|
||||
await sutProvider.GetDependency<IVerifyOrganizationDomainCommand>().ReceivedWithAnyArgs(2)
|
||||
.SystemVerifyOrganizationDomainAsync(default);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
|
@ -27,7 +27,6 @@ using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using NSubstitute;
|
||||
using NSubstitute.ReceivedExtensions;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Services;
|
||||
@ -282,45 +281,69 @@ public class UserServiceTests
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task IsManagedByAnyOrganizationAsync_WithManagingEnabledOrganization_ReturnsTrue(
|
||||
SutProvider<UserService> sutProvider, Guid userId, Organization organization)
|
||||
public async Task IsManagedByAnyOrganizationAsync_WithAccountDeprovisioningDisabled_ReturnsFalse(
|
||||
SutProvider<UserService> sutProvider, Guid userId)
|
||||
{
|
||||
organization.Enabled = true;
|
||||
organization.UseSso = true;
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetByClaimedUserDomainAsync(userId)
|
||||
.Returns(organization);
|
||||
|
||||
var result = await sutProvider.Sut.IsManagedByAnyOrganizationAsync(userId);
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task IsManagedByAnyOrganizationAsync_WithManagingDisabledOrganization_ReturnsFalse(
|
||||
SutProvider<UserService> sutProvider, Guid userId, Organization organization)
|
||||
{
|
||||
organization.Enabled = false;
|
||||
organization.UseSso = true;
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetByClaimedUserDomainAsync(userId)
|
||||
.Returns(organization);
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
|
||||
.Returns(false);
|
||||
|
||||
var result = await sutProvider.Sut.IsManagedByAnyOrganizationAsync(userId);
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task IsManagedByAnyOrganizationAsync_WithOrganizationUseSsoFalse_ReturnsFalse(
|
||||
public async Task IsManagedByAnyOrganizationAsync_WithAccountDeprovisioningEnabled_WithManagingEnabledOrganization_ReturnsTrue(
|
||||
SutProvider<UserService> sutProvider, Guid userId, Organization organization)
|
||||
{
|
||||
organization.Enabled = true;
|
||||
organization.UseSso = true;
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetByVerifiedUserEmailDomainAsync(userId)
|
||||
.Returns(new[] { organization });
|
||||
|
||||
var result = await sutProvider.Sut.IsManagedByAnyOrganizationAsync(userId);
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task IsManagedByAnyOrganizationAsync_WithAccountDeprovisioningEnabled_WithManagingDisabledOrganization_ReturnsFalse(
|
||||
SutProvider<UserService> sutProvider, Guid userId, Organization organization)
|
||||
{
|
||||
organization.Enabled = false;
|
||||
organization.UseSso = true;
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetByVerifiedUserEmailDomainAsync(userId)
|
||||
.Returns(new[] { organization });
|
||||
|
||||
var result = await sutProvider.Sut.IsManagedByAnyOrganizationAsync(userId);
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task IsManagedByAnyOrganizationAsync_WithAccountDeprovisioningEnabled_WithOrganizationUseSsoFalse_ReturnsFalse(
|
||||
SutProvider<UserService> sutProvider, Guid userId, Organization organization)
|
||||
{
|
||||
organization.Enabled = true;
|
||||
organization.UseSso = false;
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetByClaimedUserDomainAsync(userId)
|
||||
.Returns(organization);
|
||||
.GetByVerifiedUserEmailDomainAsync(userId)
|
||||
.Returns(new[] { organization });
|
||||
|
||||
var result = await sutProvider.Sut.IsManagedByAnyOrganizationAsync(userId);
|
||||
Assert.False(result);
|
||||
|
@ -97,13 +97,160 @@ public class OrganizationRepositoryTests
|
||||
ResetPasswordKey = "resetpasswordkey1",
|
||||
});
|
||||
|
||||
var user1Response = await organizationRepository.GetByClaimedUserDomainAsync(user1.Id);
|
||||
var user2Response = await organizationRepository.GetByClaimedUserDomainAsync(user2.Id);
|
||||
var user3Response = await organizationRepository.GetByClaimedUserDomainAsync(user3.Id);
|
||||
var user1Response = await organizationRepository.GetByVerifiedUserEmailDomainAsync(user1.Id);
|
||||
var user2Response = await organizationRepository.GetByVerifiedUserEmailDomainAsync(user2.Id);
|
||||
var user3Response = await organizationRepository.GetByVerifiedUserEmailDomainAsync(user3.Id);
|
||||
|
||||
Assert.NotNull(user1Response);
|
||||
Assert.Equal(organization.Id, user1Response.Id);
|
||||
Assert.Null(user2Response);
|
||||
Assert.Null(user3Response);
|
||||
Assert.NotEmpty(user1Response);
|
||||
Assert.Equal(organization.Id, user1Response.First().Id);
|
||||
Assert.Empty(user2Response);
|
||||
Assert.Empty(user3Response);
|
||||
}
|
||||
|
||||
[DatabaseTheory, DatabaseData]
|
||||
public async Task GetByVerifiedUserEmailDomainAsync_WithUnverifiedDomains_ReturnsEmpty(
|
||||
IUserRepository userRepository,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IOrganizationDomainRepository organizationDomainRepository)
|
||||
{
|
||||
var id = Guid.NewGuid();
|
||||
var domainName = $"{id}.example.com";
|
||||
|
||||
var user = await userRepository.CreateAsync(new User
|
||||
{
|
||||
Name = "Test User",
|
||||
Email = $"test+{id}@{domainName}",
|
||||
ApiKey = "TEST",
|
||||
SecurityStamp = "stamp",
|
||||
Kdf = KdfType.PBKDF2_SHA256,
|
||||
KdfIterations = 1,
|
||||
KdfMemory = 2,
|
||||
KdfParallelism = 3
|
||||
});
|
||||
|
||||
var organization = await organizationRepository.CreateAsync(new Organization
|
||||
{
|
||||
Name = $"Test Org {id}",
|
||||
BillingEmail = user.Email,
|
||||
Plan = "Test",
|
||||
PrivateKey = "privatekey",
|
||||
});
|
||||
|
||||
var organizationDomain = new OrganizationDomain
|
||||
{
|
||||
OrganizationId = organization.Id,
|
||||
DomainName = domainName,
|
||||
Txt = "btw+12345",
|
||||
};
|
||||
organizationDomain.SetNextRunDate(12);
|
||||
organizationDomain.SetJobRunCount();
|
||||
await organizationDomainRepository.CreateAsync(organizationDomain);
|
||||
|
||||
await organizationUserRepository.CreateAsync(new OrganizationUser
|
||||
{
|
||||
OrganizationId = organization.Id,
|
||||
UserId = user.Id,
|
||||
Status = OrganizationUserStatusType.Confirmed,
|
||||
ResetPasswordKey = "resetpasswordkey",
|
||||
});
|
||||
|
||||
var result = await organizationRepository.GetByVerifiedUserEmailDomainAsync(user.Id);
|
||||
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
[DatabaseTheory, DatabaseData]
|
||||
public async Task GetByVerifiedUserEmailDomainAsync_WithMultipleVerifiedDomains_ReturnsAllMatchingOrganizations(
|
||||
IUserRepository userRepository,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IOrganizationDomainRepository organizationDomainRepository)
|
||||
{
|
||||
var id = Guid.NewGuid();
|
||||
var domainName = $"{id}.example.com";
|
||||
|
||||
var user = await userRepository.CreateAsync(new User
|
||||
{
|
||||
Name = "Test User",
|
||||
Email = $"test+{id}@{domainName}",
|
||||
ApiKey = "TEST",
|
||||
SecurityStamp = "stamp",
|
||||
Kdf = KdfType.PBKDF2_SHA256,
|
||||
KdfIterations = 1,
|
||||
KdfMemory = 2,
|
||||
KdfParallelism = 3
|
||||
});
|
||||
|
||||
var organization1 = await organizationRepository.CreateAsync(new Organization
|
||||
{
|
||||
Name = $"Test Org 1 {id}",
|
||||
BillingEmail = user.Email,
|
||||
Plan = "Test",
|
||||
PrivateKey = "privatekey1",
|
||||
});
|
||||
|
||||
var organization2 = await organizationRepository.CreateAsync(new Organization
|
||||
{
|
||||
Name = $"Test Org 2 {id}",
|
||||
BillingEmail = user.Email,
|
||||
Plan = "Test",
|
||||
PrivateKey = "privatekey2",
|
||||
});
|
||||
|
||||
var organizationDomain1 = new OrganizationDomain
|
||||
{
|
||||
OrganizationId = organization1.Id,
|
||||
DomainName = domainName,
|
||||
Txt = "btw+12345",
|
||||
};
|
||||
organizationDomain1.SetNextRunDate(12);
|
||||
organizationDomain1.SetJobRunCount();
|
||||
organizationDomain1.SetVerifiedDate();
|
||||
await organizationDomainRepository.CreateAsync(organizationDomain1);
|
||||
|
||||
var organizationDomain2 = new OrganizationDomain
|
||||
{
|
||||
OrganizationId = organization2.Id,
|
||||
DomainName = domainName,
|
||||
Txt = "btw+67890",
|
||||
};
|
||||
organizationDomain2.SetNextRunDate(12);
|
||||
organizationDomain2.SetJobRunCount();
|
||||
organizationDomain2.SetVerifiedDate();
|
||||
await organizationDomainRepository.CreateAsync(organizationDomain2);
|
||||
|
||||
await organizationUserRepository.CreateAsync(new OrganizationUser
|
||||
{
|
||||
OrganizationId = organization1.Id,
|
||||
UserId = user.Id,
|
||||
Status = OrganizationUserStatusType.Confirmed,
|
||||
ResetPasswordKey = "resetpasswordkey1",
|
||||
});
|
||||
|
||||
await organizationUserRepository.CreateAsync(new OrganizationUser
|
||||
{
|
||||
OrganizationId = organization2.Id,
|
||||
UserId = user.Id,
|
||||
Status = OrganizationUserStatusType.Confirmed,
|
||||
ResetPasswordKey = "resetpasswordkey2",
|
||||
});
|
||||
|
||||
var result = await organizationRepository.GetByVerifiedUserEmailDomainAsync(user.Id);
|
||||
|
||||
Assert.Equal(2, result.Count);
|
||||
Assert.Contains(result, org => org.Id == organization1.Id);
|
||||
Assert.Contains(result, org => org.Id == organization2.Id);
|
||||
}
|
||||
|
||||
[DatabaseTheory, DatabaseData]
|
||||
public async Task GetByVerifiedUserEmailDomainAsync_WithNonExistentUser_ReturnsEmpty(
|
||||
IOrganizationRepository organizationRepository)
|
||||
{
|
||||
var nonExistentUserId = Guid.NewGuid();
|
||||
|
||||
var result = await organizationRepository.GetByVerifiedUserEmailDomainAsync(nonExistentUserId);
|
||||
|
||||
Assert.Empty(result);
|
||||
}
|
||||
}
|
||||
|
@ -253,6 +253,9 @@ public class OrganizationUserRepositoryTests
|
||||
Assert.Equal(orgUser1.Permissions, result.Permissions);
|
||||
Assert.Equal(organization.SmSeats, result.SmSeats);
|
||||
Assert.Equal(organization.SmServiceAccounts, result.SmServiceAccounts);
|
||||
Assert.Equal(organization.LimitCollectionCreation, result.LimitCollectionCreation);
|
||||
Assert.Equal(organization.LimitCollectionDeletion, result.LimitCollectionDeletion);
|
||||
// Deprecated: https://bitwarden.atlassian.net/browse/PM-10863
|
||||
Assert.Equal(organization.LimitCollectionCreationDeletion, result.LimitCollectionCreationDeletion);
|
||||
Assert.Equal(organization.AllowAdminAccessToAllCollectionItems, result.AllowAdminAccessToAllCollectionItems);
|
||||
}
|
||||
|
@ -0,0 +1,39 @@
|
||||
CREATE OR ALTER PROCEDURE [dbo].[CollectionCipher_ReadByUserId]
|
||||
@UserId UNIQUEIDENTIFIER
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
SELECT
|
||||
CC.*
|
||||
FROM
|
||||
[dbo].[CollectionCipher] CC
|
||||
INNER JOIN
|
||||
[dbo].[Collection] S ON S.[Id] = CC.[CollectionId]
|
||||
INNER JOIN
|
||||
[dbo].[OrganizationUser] OU ON OU.[OrganizationId] = S.[OrganizationId] AND OU.[UserId] = @UserId
|
||||
INNER JOIN
|
||||
[dbo].[CollectionUser] CU ON CU.[CollectionId] = S.[Id] AND CU.[OrganizationUserId] = OU.[Id]
|
||||
WHERE
|
||||
OU.[Status] = 2
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT
|
||||
CC.*
|
||||
FROM
|
||||
[dbo].[CollectionCipher] CC
|
||||
INNER JOIN
|
||||
[dbo].[Collection] S ON S.[Id] = CC.[CollectionId]
|
||||
INNER JOIN
|
||||
[dbo].[OrganizationUser] OU ON OU.[OrganizationId] = S.[OrganizationId] AND OU.[UserId] = @UserId
|
||||
INNER JOIN
|
||||
[dbo].[GroupUser] GU ON GU.[OrganizationUserId] = OU.[Id]
|
||||
INNER JOIN
|
||||
[dbo].[CollectionGroup] CG ON CG.[CollectionId] = CC.[CollectionId] AND CG.[GroupId] = GU.[GroupId]
|
||||
LEFT JOIN
|
||||
[dbo].[CollectionUser] CU ON CU.[CollectionId] = S.[Id] AND CU.[OrganizationUserId] = OU.[Id]
|
||||
WHERE
|
||||
OU.[Status] = 2
|
||||
AND CU.[CollectionId] IS NULL
|
||||
END
|
Loading…
Reference in New Issue
Block a user