mirror of
https://github.com/bitwarden/server.git
synced 2025-02-17 02:01:53 +01:00
Merge branch 'main' into auth/pm-6631/handle-webauthn-creation-exception
This commit is contained in:
commit
bad8e6b988
9
.github/renovate.json
vendored
9
.github/renovate.json
vendored
@ -40,19 +40,26 @@
|
||||
"commitMessagePrefix": "[deps] Auth:",
|
||||
"reviewers": ["team:team-auth-dev"]
|
||||
},
|
||||
{
|
||||
"matchPackageNames": ["bootstrap", "del", "gulp"],
|
||||
"matchUpdateTypes": ["major"],
|
||||
"description": "Lock bootstrap, del, and gulp major versions due to ASP.NET conflicts",
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"matchPackageNames": [
|
||||
"AspNetCoreRateLimit",
|
||||
"AspNetCoreRateLimit.Redis",
|
||||
"Azure.Data.Tables",
|
||||
"Azure.Extensions.AspNetCore.DataProtection.Blobs",
|
||||
"Azure.Messaging.EventGrid",
|
||||
"Azure.Messaging.ServiceBus",
|
||||
"Azure.Storage.Blobs",
|
||||
"Azure.Storage.Queues",
|
||||
"DuoUniversal",
|
||||
"Fido2.AspNet",
|
||||
"Duende.IdentityServer",
|
||||
"Microsoft.Azure.Cosmos",
|
||||
"Microsoft.Azure.Cosmos.Table",
|
||||
"Microsoft.Extensions.Caching.StackExchangeRedis",
|
||||
"Microsoft.Extensions.Identity.Stores",
|
||||
"Otp.NET",
|
||||
|
33
.github/workflows/build.yml
vendored
33
.github/workflows/build.yml
vendored
@ -540,36 +540,11 @@ jobs:
|
||||
steps:
|
||||
- name: Check if any job failed
|
||||
if: |
|
||||
github.ref == 'refs/heads/main'
|
||||
(github.ref == 'refs/heads/main'
|
||||
|| github.ref == 'refs/heads/rc'
|
||||
|| github.ref == 'refs/heads/hotfix-rc'
|
||||
env:
|
||||
LINT_STATUS: ${{ needs.lint.result }}
|
||||
TESTING_STATUS: ${{ needs.testing.result }}
|
||||
BUILD_ARTIFACTS_STATUS: ${{ needs.build-artifacts.result }}
|
||||
BUILD_DOCKER_STATUS: ${{ needs.build-docker.result }}
|
||||
UPLOAD_STATUS: ${{ needs.upload.result }}
|
||||
BUILD_MSSQLMIGRATORUTILITY_STATUS: ${{ needs.build-mssqlmigratorutility.result }}
|
||||
TRIGGER_SELF_HOST_BUILD_STATUS: ${{ needs.self-host-build.result }}
|
||||
TRIGGER_K8S_DEPLOY_STATUS: ${{ needs.trigger-k8s-deploy.result }}
|
||||
run: |
|
||||
if [ "$LINT_STATUS" = "failure" ]; then
|
||||
exit 1
|
||||
elif [ "$TESTING_STATUS" = "failure" ]; then
|
||||
exit 1
|
||||
elif [ "$BUILD_ARTIFACTS_STATUS" = "failure" ]; then
|
||||
exit 1
|
||||
elif [ "$BUILD_DOCKER_STATUS" = "failure" ]; then
|
||||
exit 1
|
||||
elif [ "$UPLOAD_STATUS" = "failure" ]; then
|
||||
exit 1
|
||||
elif [ "$BUILD_MSSQLMIGRATORUTILITY_STATUS" = "failure" ]; then
|
||||
exit 1
|
||||
elif [ "$TRIGGER_SELF_HOST_BUILD_STATUS" = "failure" ]; then
|
||||
exit 1
|
||||
elif [ "$TRIGGER_K8S_DEPLOY_STATUS" = "failure" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|| github.ref == 'refs/heads/hotfix-rc')
|
||||
&& contains(needs.*.result, 'failure')
|
||||
run: exit 1
|
||||
|
||||
- name: Log in to Azure - CI subscription
|
||||
uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7
|
||||
|
47
.github/workflows/cleanup-after-pr.yml
vendored
47
.github/workflows/cleanup-after-pr.yml
vendored
@ -5,36 +5,26 @@ on:
|
||||
pull_request:
|
||||
types: [closed]
|
||||
|
||||
env:
|
||||
_AZ_REGISTRY: "bitwardenprod.azurecr.io"
|
||||
|
||||
jobs:
|
||||
build-docker:
|
||||
name: Remove branch-specific Docker images
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
########## ACR ##########
|
||||
- name: Log in to Azure - QA Subscription
|
||||
uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7
|
||||
with:
|
||||
creds: ${{ secrets.AZURE_QA_KV_CREDENTIALS }}
|
||||
|
||||
- name: Log in to Azure ACR
|
||||
run: az acr login -n bitwardenqa
|
||||
|
||||
- name: Log in to Azure - production subscription
|
||||
uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7
|
||||
with:
|
||||
creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
|
||||
|
||||
- name: Log in to Azure ACR
|
||||
run: az acr login -n bitwardenprod
|
||||
run: az acr login -n $_AZ_REGISTRY --only-show-errors
|
||||
|
||||
########## Remove Docker images ##########
|
||||
- name: Remove the Docker image from ACR
|
||||
env:
|
||||
REF: ${{ github.event.pull_request.head.ref }}
|
||||
REGISTRIES: |
|
||||
registries:
|
||||
- bitwardenprod
|
||||
- bitwardenqa
|
||||
SERVICES: |
|
||||
services:
|
||||
- Admin
|
||||
@ -54,24 +44,21 @@ jobs:
|
||||
run: |
|
||||
for SERVICE in $(echo "${{ env.SERVICES }}" | yq e ".services[]" - )
|
||||
do
|
||||
for REGISTRY in $( echo "${{ env.REGISTRIES }}" | yq e ".registries[]" - )
|
||||
do
|
||||
SERVICE_NAME=$(echo $SERVICE | awk '{print tolower($0)}')
|
||||
IMAGE_TAG=$(echo "${REF}" | sed "s#/#-#g") # slash safe branch name
|
||||
SERVICE_NAME=$(echo $SERVICE | awk '{print tolower($0)}')
|
||||
IMAGE_TAG=$(echo "${REF}" | sed "s#/#-#g") # slash safe branch name
|
||||
|
||||
echo "[*] Checking if remote exists: $REGISTRY.azurecr.io/$SERVICE_NAME:$IMAGE_TAG"
|
||||
TAG_EXISTS=$(
|
||||
az acr repository show-tags --name $REGISTRY --repository $SERVICE_NAME \
|
||||
| jq --arg $TAG "$IMAGE_TAG" -e '. | any(. == "$TAG")'
|
||||
)
|
||||
echo "[*] Checking if remote exists: $_AZ_REGISTRY/$SERVICE_NAME:$IMAGE_TAG"
|
||||
TAG_EXISTS=$(
|
||||
az acr repository show-tags --name $_AZ_REGISTRY --repository $SERVICE_NAME \
|
||||
| jq --arg $TAG "$IMAGE_TAG" -e '. | any(. == "$TAG")'
|
||||
)
|
||||
|
||||
if [[ "$TAG_EXISTS" == "true" ]]; then
|
||||
echo "[*] Tag exists. Removing tag"
|
||||
az acr repository delete --name $REGISTRY --image $SERVICE_NAME:$IMAGE_TAG --yes
|
||||
else
|
||||
echo "[*] Tag does not exist. No action needed"
|
||||
fi
|
||||
done
|
||||
if [[ "$TAG_EXISTS" == "true" ]]; then
|
||||
echo "[*] Tag exists. Removing tag"
|
||||
az acr repository delete --name $_AZ_REGISTRY --image $SERVICE_NAME:$IMAGE_TAG --yes
|
||||
else
|
||||
echo "[*] Tag does not exist. No action needed"
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Log out of Docker
|
||||
|
53
.github/workflows/cleanup-rc-branch.yml
vendored
Normal file
53
.github/workflows/cleanup-rc-branch.yml
vendored
Normal file
@ -0,0 +1,53 @@
|
||||
---
|
||||
name: Cleanup RC Branch
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- v**
|
||||
|
||||
jobs:
|
||||
delete-rc:
|
||||
name: Delete RC Branch
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Login to Azure - CI Subscription
|
||||
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
|
||||
with:
|
||||
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
||||
|
||||
- name: Retrieve bot secrets
|
||||
id: retrieve-bot-secrets
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@main
|
||||
with:
|
||||
keyvault: bitwarden-ci
|
||||
secrets: "github-pat-bitwarden-devops-bot-repo-scope"
|
||||
|
||||
- name: Checkout main
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
with:
|
||||
ref: main
|
||||
token: ${{ steps.retrieve-bot-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
|
||||
|
||||
- name: Check if a RC branch exists
|
||||
id: branch-check
|
||||
run: |
|
||||
hotfix_rc_branch_check=$(git ls-remote --heads origin hotfix-rc | wc -l)
|
||||
rc_branch_check=$(git ls-remote --heads origin rc | wc -l)
|
||||
|
||||
if [[ "${hotfix_rc_branch_check}" -gt 0 ]]; then
|
||||
echo "hotfix-rc branch exists." | tee -a $GITHUB_STEP_SUMMARY
|
||||
echo "name=hotfix-rc" >> $GITHUB_OUTPUT
|
||||
elif [[ "${rc_branch_check}" -gt 0 ]]; then
|
||||
echo "rc branch exists." | tee -a $GITHUB_STEP_SUMMARY
|
||||
echo "name=rc" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Delete RC branch
|
||||
env:
|
||||
BRANCH_NAME: ${{ steps.branch-check.outputs.name }}
|
||||
run: |
|
||||
if ! [[ -z "$BRANCH_NAME" ]]; then
|
||||
git push --quiet origin --delete $BRANCH_NAME
|
||||
echo "Deleted $BRANCH_NAME branch." | tee -a $GITHUB_STEP_SUMMARY
|
||||
fi
|
15
.github/workflows/container-registry-purge.yml
vendored
15
.github/workflows/container-registry-purge.yml
vendored
@ -69,20 +69,15 @@ jobs:
|
||||
name: Check for failures
|
||||
if: always()
|
||||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
- purge
|
||||
needs: [purge]
|
||||
steps:
|
||||
- name: Check if any job failed
|
||||
if: |
|
||||
github.ref == 'refs/heads/main'
|
||||
(github.ref == 'refs/heads/main'
|
||||
|| github.ref == 'refs/heads/rc'
|
||||
|| github.ref == 'refs/heads/hotfix-rc'
|
||||
env:
|
||||
PURGE_STATUS: ${{ needs.purge.result }}
|
||||
run: |
|
||||
if [ "$PURGE_STATUS" = "failure" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|| github.ref == 'refs/heads/hotfix-rc')
|
||||
&& contains(needs.*.result, 'failure')
|
||||
run: exit 1
|
||||
|
||||
- name: Log in to Azure - CI subscription
|
||||
uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7
|
||||
|
28
.github/workflows/scan.yml
vendored
28
.github/workflows/scan.yml
vendored
@ -7,47 +7,63 @@ on:
|
||||
- "main"
|
||||
- "rc"
|
||||
- "hotfix-rc"
|
||||
pull_request:
|
||||
|
||||
permissions: read-all
|
||||
pull_request_target:
|
||||
types: [opened, synchronize]
|
||||
|
||||
jobs:
|
||||
check-run:
|
||||
name: Check PR run
|
||||
uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main
|
||||
|
||||
sast:
|
||||
name: SAST scan
|
||||
runs-on: ubuntu-22.04
|
||||
needs: check-run
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
security-events: write
|
||||
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Scan with Checkmarx
|
||||
uses: checkmarx/ast-github-action@749fec53e0db0f6404a97e2e0807c3e80e3583a7 #2.0.23
|
||||
env:
|
||||
INCREMENTAL: "${{ github.event_name == 'pull_request' && '--sast-incremental' || '' }}"
|
||||
INCREMENTAL: "${{ contains(github.event_name, 'pull_request') && '--sast-incremental' || '' }}"
|
||||
with:
|
||||
project_name: ${{ github.repository }}
|
||||
cx_tenant: ${{ secrets.CHECKMARX_TENANT }}
|
||||
base_uri: https://ast.checkmarx.net/
|
||||
cx_client_id: ${{ secrets.CHECKMARX_CLIENT_ID }}
|
||||
cx_client_secret: ${{ secrets.CHECKMARX_SECRET }}
|
||||
additional_params: --report-format sarif --output-path . ${{ env.INCREMENTAL }}
|
||||
additional_params: |
|
||||
--report-format sarif \
|
||||
--filter "state=TO_VERIFY;PROPOSED_NOT_EXPLOITABLE;CONFIRMED;URGENT" \
|
||||
--output-path . ${{ env.INCREMENTAL }}
|
||||
|
||||
- name: Upload Checkmarx results to GitHub
|
||||
uses: github/codeql-action/upload-sarif@b7bf0a3ed3ecfa44160715d7c442788f65f0f923 # v3.23.2
|
||||
uses: github/codeql-action/upload-sarif@1b1aada464948af03b950897e5eb522f92603cc2 # v3.24.9
|
||||
with:
|
||||
sarif_file: cx_result.sarif
|
||||
|
||||
quality:
|
||||
name: Quality scan
|
||||
runs-on: ubuntu-22.04
|
||||
needs: check-run
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Scan with SonarCloud
|
||||
uses: sonarsource/sonarcloud-github-action@49e6cd3b187936a73b8280d59ffd9da69df63ec9 # v2.1.1
|
||||
|
12
.github/workflows/test-database.yml
vendored
12
.github/workflows/test-database.yml
vendored
@ -57,9 +57,9 @@ jobs:
|
||||
run: sleep 15s
|
||||
|
||||
- name: Migrate SQL Server
|
||||
working-directory: "dev"
|
||||
run: "./migrate.ps1"
|
||||
shell: pwsh
|
||||
run: 'dotnet run --project util/MsSqlMigratorUtility/ "$CONN_STR"'
|
||||
env:
|
||||
CONN_STR: "Server=localhost;Database=vault_dev;User Id=SA;Password=SET_A_PASSWORD_HERE_123;Encrypt=True;TrustServerCertificate=True;"
|
||||
|
||||
- name: Migrate MySQL
|
||||
working-directory: "util/MySqlMigrations"
|
||||
@ -147,9 +147,9 @@ jobs:
|
||||
shell: pwsh
|
||||
|
||||
- name: Migrate
|
||||
working-directory: "dev"
|
||||
run: "./migrate.ps1"
|
||||
shell: pwsh
|
||||
run: 'dotnet run --project util/MsSqlMigratorUtility/ "$CONN_STR"'
|
||||
env:
|
||||
CONN_STR: "Server=localhost;Database=vault_dev;User Id=SA;Password=SET_A_PASSWORD_HERE_123;Encrypt=True;TrustServerCertificate=True;"
|
||||
|
||||
- name: Diff .sqlproj to migrations
|
||||
run: /usr/local/sqlpackage/sqlpackage /action:DeployReport /SourceFile:"Sql.dacpac" /TargetConnectionString:"Server=localhost;Database=vault_dev;User Id=SA;Password=SET_A_PASSWORD_HERE_123;Encrypt=True;TrustServerCertificate=True;" /OutputPath:"report.xml" /p:IgnoreColumnOrder=True /p:IgnoreComments=True
|
||||
|
116
.github/workflows/version-bump.yml
vendored
116
.github/workflows/version-bump.yml
vendored
@ -1,13 +1,12 @@
|
||||
---
|
||||
name: Bump version
|
||||
run-name: Bump version to ${{ inputs.version_number }}
|
||||
name: Version Bump
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version_number:
|
||||
description: "New version (example: '2024.1.0')"
|
||||
required: true
|
||||
version_number_override:
|
||||
description: "New version override (leave blank for automatic calculation, example: '2024.1.0')"
|
||||
required: false
|
||||
type: string
|
||||
cut_rc_branch:
|
||||
description: "Cut RC branch?"
|
||||
@ -16,22 +15,16 @@ on:
|
||||
|
||||
jobs:
|
||||
bump_version:
|
||||
name: Bump
|
||||
name: Bump Version
|
||||
runs-on: ubuntu-22.04
|
||||
outputs:
|
||||
version: ${{ steps.set-final-version-output.outputs.version }}
|
||||
steps:
|
||||
- name: Log in to Azure - CI subscription
|
||||
uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7
|
||||
- name: Validate version input
|
||||
if: ${{ inputs.version_number_override != '' }}
|
||||
uses: bitwarden/gh-actions/version-check@main
|
||||
with:
|
||||
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
||||
|
||||
- name: Retrieve secrets
|
||||
id: retrieve-secrets
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@main
|
||||
with:
|
||||
keyvault: "bitwarden-ci"
|
||||
secrets: "github-gpg-private-key,
|
||||
github-gpg-private-key-passphrase,
|
||||
github-pat-bitwarden-devops-bot-repo-scope"
|
||||
version: ${{ inputs.version_number_override }}
|
||||
|
||||
- name: Check out branch
|
||||
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
|
||||
@ -48,6 +41,20 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Log in to Azure - CI subscription
|
||||
uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7
|
||||
with:
|
||||
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
||||
|
||||
- name: Retrieve secrets
|
||||
id: retrieve-secrets
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@main
|
||||
with:
|
||||
keyvault: "bitwarden-ci"
|
||||
secrets: "github-gpg-private-key,
|
||||
github-gpg-private-key-passphrase,
|
||||
github-pat-bitwarden-devops-bot-repo-scope"
|
||||
|
||||
- name: Import GPG key
|
||||
uses: crazy-max/ghaction-import-gpg@82a020f1f7f605c65dd2449b392a52c3fcfef7ef # v6.0.0
|
||||
with:
|
||||
@ -56,22 +63,35 @@ jobs:
|
||||
git_user_signingkey: true
|
||||
git_commit_gpgsign: true
|
||||
|
||||
- name: Set up Git
|
||||
run: |
|
||||
git config --local user.email "106330231+bitwarden-devops-bot@users.noreply.github.com"
|
||||
git config --local user.name "bitwarden-devops-bot"
|
||||
|
||||
- name: Create version branch
|
||||
id: create-branch
|
||||
run: |
|
||||
NAME=version_bump_${{ github.ref_name }}_${{ inputs.version_number }}
|
||||
NAME=version_bump_${{ github.ref_name }}_$(date +"%Y-%m-%d")
|
||||
git switch -c $NAME
|
||||
echo "name=$NAME" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Install xmllint
|
||||
run: sudo apt install -y libxml2-utils
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libxml2-utils
|
||||
|
||||
- name: Verify input version
|
||||
env:
|
||||
NEW_VERSION: ${{ inputs.version_number }}
|
||||
- name: Get current version
|
||||
id: current-version
|
||||
run: |
|
||||
CURRENT_VERSION=$(xmllint -xpath "/Project/PropertyGroup/Version/text()" Directory.Build.props)
|
||||
echo "version=$CURRENT_VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Verify input version
|
||||
if: ${{ inputs.version_number_override != '' }}
|
||||
env:
|
||||
CURRENT_VERSION: ${{ steps.current-version.outputs.version }}
|
||||
NEW_VERSION: ${{ inputs.version_number_override }}
|
||||
run: |
|
||||
# Error if version has not changed.
|
||||
if [[ "$NEW_VERSION" == "$CURRENT_VERSION" ]]; then
|
||||
echo "Version has not changed."
|
||||
@ -87,16 +107,37 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Bump version props
|
||||
- name: Calculate next release version
|
||||
if: ${{ inputs.version_number_override == '' }}
|
||||
id: calculate-next-version
|
||||
uses: bitwarden/gh-actions/version-next@main
|
||||
with:
|
||||
version: ${{ steps.current-version.outputs.version }}
|
||||
|
||||
- name: Bump version props - Version Override
|
||||
if: ${{ inputs.version_number_override != '' }}
|
||||
id: bump-version-override
|
||||
uses: bitwarden/gh-actions/version-bump@main
|
||||
with:
|
||||
version: ${{ inputs.version_number }}
|
||||
file_path: "Directory.Build.props"
|
||||
version: ${{ inputs.version_number_override }}
|
||||
|
||||
- name: Set up Git
|
||||
- name: Bump version props - Automatic Calculation
|
||||
if: ${{ inputs.version_number_override == '' }}
|
||||
id: bump-version-automatic
|
||||
uses: bitwarden/gh-actions/version-bump@main
|
||||
with:
|
||||
file_path: "Directory.Build.props"
|
||||
version: ${{ steps.calculate-next-version.outputs.version }}
|
||||
|
||||
- name: Set final version output
|
||||
id: set-final-version-output
|
||||
run: |
|
||||
git config --local user.email "106330231+bitwarden-devops-bot@users.noreply.github.com"
|
||||
git config --local user.name "bitwarden-devops-bot"
|
||||
if [[ "${{ steps.bump-version-override.outcome }}" = "success" ]]; then
|
||||
echo "version=${{ inputs.version_number_override }}" >> $GITHUB_OUTPUT
|
||||
elif [[ "${{ steps.bump-version-automatic.outcome }}" = "success" ]]; then
|
||||
echo "version=${{ steps.calculate-next-version.outputs.version }}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Check if version changed
|
||||
id: version-changed
|
||||
@ -110,7 +151,7 @@ jobs:
|
||||
|
||||
- name: Commit files
|
||||
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
|
||||
run: git commit -m "Bumped version to ${{ inputs.version_number }}" -a
|
||||
run: git commit -m "Bumped version to ${{ steps.set-final-version-output.outputs.version }}" -a
|
||||
|
||||
- name: Push changes
|
||||
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
|
||||
@ -124,7 +165,7 @@ jobs:
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.retrieve-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
|
||||
PR_BRANCH: ${{ steps.create-branch.outputs.name }}
|
||||
TITLE: "Bump version to ${{ inputs.version_number }}"
|
||||
TITLE: "Bump version to ${{ steps.set-final-version-output.outputs.version }}"
|
||||
run: |
|
||||
PR_URL=$(gh pr create --title "$TITLE" \
|
||||
--base "main" \
|
||||
@ -140,38 +181,43 @@ jobs:
|
||||
- [X] Other
|
||||
|
||||
## Objective
|
||||
Automated version bump to ${{ inputs.version_number }}")
|
||||
Automated version bump to ${{ steps.set-final-version-output.outputs.version }}")
|
||||
echo "pr_number=${PR_URL##*/}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Approve PR
|
||||
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
PR_NUMBER: ${{ steps.create-pr.outputs.pr_number }}
|
||||
run: gh pr review $PR_NUMBER --approve
|
||||
|
||||
- name: Merge PR
|
||||
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.retrieve-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
|
||||
PR_NUMBER: ${{ steps.create-pr.outputs.pr_number }}
|
||||
run: gh pr merge $PR_NUMBER --squash --auto --delete-branch
|
||||
|
||||
|
||||
cut_rc:
|
||||
name: Cut RC branch
|
||||
needs: bump_version
|
||||
if: ${{ inputs.cut_rc_branch == true }}
|
||||
needs: bump_version
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Check out branch
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
with:
|
||||
ref: main
|
||||
|
||||
|
||||
- name: Install xmllint
|
||||
run: sudo apt install -y libxml2-utils
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libxml2-utils
|
||||
|
||||
- name: Verify version has been updated
|
||||
env:
|
||||
NEW_VERSION: ${{ inputs.version_number }}
|
||||
NEW_VERSION: ${{ needs.bump_version.outputs.version }}
|
||||
run: |
|
||||
# Wait for version to change.
|
||||
while : ; do
|
||||
|
56
.vscode/launch.json
vendored
56
.vscode/launch.json
vendored
@ -38,6 +38,7 @@
|
||||
"configurations": [
|
||||
"run-Admin",
|
||||
"run-API",
|
||||
"run-Events",
|
||||
"run-EventsProcessor",
|
||||
"run-Identity",
|
||||
"run-Sso",
|
||||
@ -58,6 +59,7 @@
|
||||
"configurations": [
|
||||
"run-Admin-SelfHost",
|
||||
"run-API-SelfHost",
|
||||
"run-Events-SelfHost",
|
||||
"run-EventsProcessor-SelfHost",
|
||||
"run-Identity-SelfHost",
|
||||
"run-Sso-SelfHost",
|
||||
@ -76,6 +78,7 @@
|
||||
"configurations": [
|
||||
"run-Admin-SelfHost",
|
||||
"run-API-SelfHost",
|
||||
"run-Events-SelfHost",
|
||||
"run-EventsProcessor-SelfHost",
|
||||
"run-Identity-SelfHost",
|
||||
],
|
||||
@ -120,6 +123,17 @@
|
||||
},
|
||||
"preLaunchTask": "buildBilling",
|
||||
},
|
||||
{
|
||||
"name": "Events",
|
||||
"configurations": [
|
||||
"run-Events"
|
||||
],
|
||||
"presentation": {
|
||||
"hidden": false,
|
||||
"group": "cloud",
|
||||
},
|
||||
"preLaunchTask": "buildEvents",
|
||||
},
|
||||
{
|
||||
"name": "Events Processor",
|
||||
"configurations": [
|
||||
@ -341,6 +355,25 @@
|
||||
"/Views": "${workspaceFolder}/Views"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "run-Events",
|
||||
"presentation": {
|
||||
"hidden": true,
|
||||
},
|
||||
"requireExactSource": true,
|
||||
"type": "coreclr",
|
||||
"request": "launch",
|
||||
"program": "${workspaceFolder}/src/Events/bin/Debug/net8.0/Events.dll",
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}/src/Events",
|
||||
"stopAtEntry": false,
|
||||
"env": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
},
|
||||
"sourceFileMap": {
|
||||
"/Views": "${workspaceFolder}/Views"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "run-EventsProcessor",
|
||||
"presentation": {
|
||||
@ -505,6 +538,27 @@
|
||||
"/Views": "${workspaceFolder}/Views"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "run-Events-SelfHost",
|
||||
"presentation": {
|
||||
"hidden": true,
|
||||
},
|
||||
"requireExactSource": true,
|
||||
"type": "coreclr",
|
||||
"request": "launch",
|
||||
"program": "${workspaceFolder}/src/Events/bin/Debug/net8.0/Events.dll",
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}/src/Events",
|
||||
"stopAtEntry": false,
|
||||
"env": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"ASPNETCORE_URLS": "http://localhost:46274",
|
||||
"developSelfHosted": "true",
|
||||
},
|
||||
"sourceFileMap": {
|
||||
"/Views": "${workspaceFolder}/Views"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "run-EventsProcessor-SelfHost",
|
||||
"presentation": {
|
||||
@ -519,7 +573,7 @@
|
||||
"stopAtEntry": false,
|
||||
"env": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"ASPNETCORE_URLS": "http://localhost:46274",
|
||||
"ASPNETCORE_URLS": "http://localhost:54103",
|
||||
"developSelfHosted": "true",
|
||||
},
|
||||
"sourceFileMap": {
|
||||
|
12
.vscode/tasks.json
vendored
12
.vscode/tasks.json
vendored
@ -96,6 +96,18 @@
|
||||
],
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
{
|
||||
"label": "buildEvents",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"build",
|
||||
"${workspaceFolder}/src/Events/Events.csproj",
|
||||
"/property:GenerateFullPaths=true",
|
||||
"/consoleloggerparameters:NoSummary"
|
||||
],
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
{
|
||||
"label": "buildEventsProcessor",
|
||||
"command": "dotnet",
|
||||
|
@ -3,7 +3,7 @@
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
|
||||
<Version>2024.2.3</Version>
|
||||
<Version>2024.5.0</Version>
|
||||
|
||||
<RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
|
@ -1,10 +1,15 @@
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.AdminConsole.Providers.Interfaces;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Billing.Entities;
|
||||
using Bit.Core.Billing.Repositories;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
|
||||
namespace Bit.Commercial.Core.AdminConsole.Providers;
|
||||
|
||||
@ -14,20 +19,26 @@ public class CreateProviderCommand : ICreateProviderCommand
|
||||
private readonly IProviderUserRepository _providerUserRepository;
|
||||
private readonly IProviderService _providerService;
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly IProviderPlanRepository _providerPlanRepository;
|
||||
private readonly IFeatureService _featureService;
|
||||
|
||||
public CreateProviderCommand(
|
||||
IProviderRepository providerRepository,
|
||||
IProviderUserRepository providerUserRepository,
|
||||
IProviderService providerService,
|
||||
IUserRepository userRepository)
|
||||
IUserRepository userRepository,
|
||||
IProviderPlanRepository providerPlanRepository,
|
||||
IFeatureService featureService)
|
||||
{
|
||||
_providerRepository = providerRepository;
|
||||
_providerUserRepository = providerUserRepository;
|
||||
_providerService = providerService;
|
||||
_userRepository = userRepository;
|
||||
_providerPlanRepository = providerPlanRepository;
|
||||
_featureService = featureService;
|
||||
}
|
||||
|
||||
public async Task CreateMspAsync(Provider provider, string ownerEmail)
|
||||
public async Task CreateMspAsync(Provider provider, string ownerEmail, int teamsMinimumSeats, int enterpriseMinimumSeats)
|
||||
{
|
||||
var owner = await _userRepository.GetByEmailAsync(ownerEmail);
|
||||
if (owner == null)
|
||||
@ -35,6 +46,13 @@ public class CreateProviderCommand : ICreateProviderCommand
|
||||
throw new BadRequestException("Invalid owner. Owner must be an existing Bitwarden user.");
|
||||
}
|
||||
|
||||
var isConsolidatedBillingEnabled = _featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling);
|
||||
|
||||
if (isConsolidatedBillingEnabled)
|
||||
{
|
||||
provider.Gateway = GatewayType.Stripe;
|
||||
}
|
||||
|
||||
await ProviderRepositoryCreateAsync(provider, ProviderStatusType.Pending);
|
||||
|
||||
var providerUser = new ProviderUser
|
||||
@ -44,6 +62,21 @@ public class CreateProviderCommand : ICreateProviderCommand
|
||||
Type = ProviderUserType.ProviderAdmin,
|
||||
Status = ProviderUserStatusType.Confirmed,
|
||||
};
|
||||
|
||||
if (isConsolidatedBillingEnabled)
|
||||
{
|
||||
var providerPlans = new List<ProviderPlan>
|
||||
{
|
||||
CreateProviderPlan(provider.Id, PlanType.TeamsMonthly, teamsMinimumSeats),
|
||||
CreateProviderPlan(provider.Id, PlanType.EnterpriseMonthly, enterpriseMinimumSeats)
|
||||
};
|
||||
|
||||
foreach (var providerPlan in providerPlans)
|
||||
{
|
||||
await _providerPlanRepository.CreateAsync(providerPlan);
|
||||
}
|
||||
}
|
||||
|
||||
await _providerUserRepository.CreateAsync(providerUser);
|
||||
await _providerService.SendProviderSetupInviteEmailAsync(provider, owner.Email);
|
||||
}
|
||||
@ -60,4 +93,16 @@ public class CreateProviderCommand : ICreateProviderCommand
|
||||
provider.UseEvents = true;
|
||||
await _providerRepository.CreateAsync(provider);
|
||||
}
|
||||
|
||||
private ProviderPlan CreateProviderPlan(Guid providerId, PlanType planType, int seatMinimum)
|
||||
{
|
||||
return new ProviderPlan
|
||||
{
|
||||
ProviderId = providerId,
|
||||
PlanType = planType,
|
||||
SeatMinimum = seatMinimum,
|
||||
PurchasedSeats = 0,
|
||||
AllocatedSeats = 0
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,17 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.AdminConsole.Providers.Interfaces;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Billing.Commands;
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Stripe;
|
||||
|
||||
@ -20,6 +26,8 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
|
||||
private readonly IOrganizationService _organizationService;
|
||||
private readonly IProviderOrganizationRepository _providerOrganizationRepository;
|
||||
private readonly IStripeAdapter _stripeAdapter;
|
||||
private readonly IScaleSeatsCommand _scaleSeatsCommand;
|
||||
private readonly IFeatureService _featureService;
|
||||
|
||||
public RemoveOrganizationFromProviderCommand(
|
||||
IEventService eventService,
|
||||
@ -28,7 +36,9 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
|
||||
IOrganizationRepository organizationRepository,
|
||||
IOrganizationService organizationService,
|
||||
IProviderOrganizationRepository providerOrganizationRepository,
|
||||
IStripeAdapter stripeAdapter)
|
||||
IStripeAdapter stripeAdapter,
|
||||
IScaleSeatsCommand scaleSeatsCommand,
|
||||
IFeatureService featureService)
|
||||
{
|
||||
_eventService = eventService;
|
||||
_logger = logger;
|
||||
@ -37,6 +47,8 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
|
||||
_organizationService = organizationService;
|
||||
_providerOrganizationRepository = providerOrganizationRepository;
|
||||
_stripeAdapter = stripeAdapter;
|
||||
_scaleSeatsCommand = scaleSeatsCommand;
|
||||
_featureService = featureService;
|
||||
}
|
||||
|
||||
public async Task RemoveOrganizationFromProvider(
|
||||
@ -65,8 +77,35 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
|
||||
|
||||
organization.BillingEmail = organizationOwnerEmails.MinBy(email => email);
|
||||
|
||||
await ResetOrganizationBillingAsync(organization, provider, organizationOwnerEmails);
|
||||
|
||||
await _organizationRepository.ReplaceAsync(organization);
|
||||
|
||||
await _providerOrganizationRepository.DeleteAsync(providerOrganization);
|
||||
|
||||
await _eventService.LogProviderOrganizationEventAsync(
|
||||
providerOrganization,
|
||||
EventType.ProviderOrganization_Removed);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// When a client organization is unlinked from a provider, we have to check if they're Stripe-enabled
|
||||
/// and, if they are, we remove their MSP discount and set their Subscription to `send_invoice`. This is because
|
||||
/// the provider's payment method will be removed from their Stripe customer causing ensuing charges to fail. Lastly,
|
||||
/// we email the organization owners letting them know they need to add a new payment method.
|
||||
/// </summary>
|
||||
private async Task ResetOrganizationBillingAsync(
|
||||
Organization organization,
|
||||
Provider provider,
|
||||
IEnumerable<string> organizationOwnerEmails)
|
||||
{
|
||||
if (!organization.IsStripeEnabled())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var isConsolidatedBillingEnabled = _featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling);
|
||||
|
||||
var customerUpdateOptions = new CustomerUpdateOptions
|
||||
{
|
||||
Coupon = string.Empty,
|
||||
@ -75,24 +114,47 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
|
||||
|
||||
await _stripeAdapter.CustomerUpdateAsync(organization.GatewayCustomerId, customerUpdateOptions);
|
||||
|
||||
var subscriptionUpdateOptions = new SubscriptionUpdateOptions
|
||||
if (isConsolidatedBillingEnabled && provider.Status == ProviderStatusType.Billable)
|
||||
{
|
||||
CollectionMethod = "send_invoice",
|
||||
DaysUntilDue = 30
|
||||
};
|
||||
var plan = StaticStore.GetPlan(organization.PlanType).PasswordManager;
|
||||
|
||||
await _stripeAdapter.SubscriptionUpdateAsync(organization.GatewaySubscriptionId, subscriptionUpdateOptions);
|
||||
var subscriptionCreateOptions = new SubscriptionCreateOptions
|
||||
{
|
||||
Customer = organization.GatewayCustomerId,
|
||||
CollectionMethod = StripeConstants.CollectionMethod.SendInvoice,
|
||||
DaysUntilDue = 30,
|
||||
AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true },
|
||||
Metadata = new Dictionary<string, string>
|
||||
{
|
||||
{ "organizationId", organization.Id.ToString() }
|
||||
},
|
||||
OffSession = true,
|
||||
ProrationBehavior = StripeConstants.ProrationBehavior.CreateProrations,
|
||||
Items = [new SubscriptionItemOptions { Price = plan.StripeSeatPlanId, Quantity = organization.Seats }]
|
||||
};
|
||||
|
||||
var subscription = await _stripeAdapter.SubscriptionCreateAsync(subscriptionCreateOptions);
|
||||
|
||||
organization.GatewaySubscriptionId = subscription.Id;
|
||||
|
||||
await _scaleSeatsCommand.ScalePasswordManagerSeats(provider, organization.PlanType,
|
||||
-(organization.Seats ?? 0));
|
||||
}
|
||||
else
|
||||
{
|
||||
var subscriptionUpdateOptions = new SubscriptionUpdateOptions
|
||||
{
|
||||
CollectionMethod = "send_invoice",
|
||||
DaysUntilDue = 30
|
||||
};
|
||||
|
||||
await _stripeAdapter.SubscriptionUpdateAsync(organization.GatewaySubscriptionId, subscriptionUpdateOptions);
|
||||
}
|
||||
|
||||
await _mailService.SendProviderUpdatePaymentMethod(
|
||||
organization.Id,
|
||||
organization.Name,
|
||||
provider.Name,
|
||||
organizationOwnerEmails);
|
||||
|
||||
await _providerOrganizationRepository.DeleteAsync(providerOrganization);
|
||||
|
||||
await _eventService.LogProviderOrganizationEventAsync(
|
||||
providerOrganization,
|
||||
EventType.ProviderOrganization_Removed);
|
||||
}
|
||||
}
|
||||
|
@ -4,8 +4,10 @@ using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.AdminConsole.Models.Business.Provider;
|
||||
using Bit.Core.AdminConsole.Models.Business.Tokenables;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
@ -15,8 +17,10 @@ using Bit.Core.Models.Data;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Tokens;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.AspNetCore.DataProtection;
|
||||
using Stripe;
|
||||
|
||||
namespace Bit.Commercial.Core.AdminConsole.Services;
|
||||
|
||||
@ -37,13 +41,18 @@ public class ProviderService : IProviderService
|
||||
private readonly IOrganizationService _organizationService;
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly IStripeAdapter _stripeAdapter;
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly IDataProtectorTokenFactory<ProviderDeleteTokenable> _providerDeleteTokenDataFactory;
|
||||
private readonly IApplicationCacheService _applicationCacheService;
|
||||
|
||||
public ProviderService(IProviderRepository providerRepository, IProviderUserRepository providerUserRepository,
|
||||
IProviderOrganizationRepository providerOrganizationRepository, IUserRepository userRepository,
|
||||
IUserService userService, IOrganizationService organizationService, IMailService mailService,
|
||||
IDataProtectionProvider dataProtectionProvider, IEventService eventService,
|
||||
IOrganizationRepository organizationRepository, GlobalSettings globalSettings,
|
||||
ICurrentContext currentContext, IStripeAdapter stripeAdapter)
|
||||
ICurrentContext currentContext, IStripeAdapter stripeAdapter, IFeatureService featureService,
|
||||
IDataProtectorTokenFactory<ProviderDeleteTokenable> providerDeleteTokenDataFactory,
|
||||
IApplicationCacheService applicationCacheService)
|
||||
{
|
||||
_providerRepository = providerRepository;
|
||||
_providerUserRepository = providerUserRepository;
|
||||
@ -58,6 +67,9 @@ public class ProviderService : IProviderService
|
||||
_dataProtector = dataProtectionProvider.CreateProtector("ProviderServiceDataProtector");
|
||||
_currentContext = currentContext;
|
||||
_stripeAdapter = stripeAdapter;
|
||||
_featureService = featureService;
|
||||
_providerDeleteTokenDataFactory = providerDeleteTokenDataFactory;
|
||||
_applicationCacheService = applicationCacheService;
|
||||
}
|
||||
|
||||
public async Task<Provider> CompleteSetupAsync(Provider provider, Guid ownerUserId, string token, string key)
|
||||
@ -257,7 +269,7 @@ public class ProviderService : IProviderService
|
||||
|
||||
await _providerUserRepository.ReplaceAsync(providerUser);
|
||||
events.Add((providerUser, EventType.ProviderUser_Confirmed, null));
|
||||
await _mailService.SendProviderConfirmedEmailAsync(provider.Name, user.Email);
|
||||
await _mailService.SendProviderConfirmedEmailAsync(provider.DisplayName(), user.Email);
|
||||
result.Add(Tuple.Create(providerUser, ""));
|
||||
}
|
||||
catch (BadRequestException e)
|
||||
@ -331,7 +343,7 @@ public class ProviderService : IProviderService
|
||||
var email = user == null ? providerUser.Email : user.Email;
|
||||
if (!string.IsNullOrWhiteSpace(email))
|
||||
{
|
||||
await _mailService.SendProviderUserRemoved(provider.Name, email);
|
||||
await _mailService.SendProviderUserRemoved(provider.DisplayName(), email);
|
||||
}
|
||||
|
||||
result.Add(Tuple.Create(providerUser, ""));
|
||||
@ -359,6 +371,7 @@ public class ProviderService : IProviderService
|
||||
}
|
||||
|
||||
var organization = await _organizationRepository.GetByIdAsync(organizationId);
|
||||
|
||||
ThrowOnInvalidPlanType(organization.PlanType);
|
||||
|
||||
if (organization.UseSecretsManager)
|
||||
@ -374,8 +387,22 @@ public class ProviderService : IProviderService
|
||||
Key = key,
|
||||
};
|
||||
|
||||
await ApplyProviderPriceRateAsync(organizationId, providerId);
|
||||
var provider = await _providerRepository.GetByIdAsync(providerId);
|
||||
|
||||
await ApplyProviderPriceRateAsync(organization, provider);
|
||||
await _providerOrganizationRepository.CreateAsync(providerOrganization);
|
||||
|
||||
organization.BillingEmail = provider.BillingEmail;
|
||||
await _organizationRepository.ReplaceAsync(organization);
|
||||
|
||||
if (!string.IsNullOrEmpty(organization.GatewayCustomerId))
|
||||
{
|
||||
await _stripeAdapter.CustomerUpdateAsync(organization.GatewayCustomerId, new CustomerUpdateOptions
|
||||
{
|
||||
Email = provider.BillingEmail
|
||||
});
|
||||
}
|
||||
|
||||
await _eventService.LogProviderOrganizationEventAsync(providerOrganization, EventType.ProviderOrganization_Added);
|
||||
}
|
||||
|
||||
@ -400,16 +427,14 @@ public class ProviderService : IProviderService
|
||||
await _eventService.LogProviderOrganizationEventsAsync(insertedProviderOrganizations.Select(ipo => (ipo, EventType.ProviderOrganization_Added, (DateTime?)null)));
|
||||
}
|
||||
|
||||
private async Task ApplyProviderPriceRateAsync(Guid organizationId, Guid providerId)
|
||||
private async Task ApplyProviderPriceRateAsync(Organization organization, Provider provider)
|
||||
{
|
||||
var provider = await _providerRepository.GetByIdAsync(providerId);
|
||||
// if a provider was created before Nov 6, 2023.If true, the organization plan assigned to that provider is updated to a 2020 plan.
|
||||
if (provider.CreationDate >= Constants.ProviderCreatedPriorNov62023)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var organization = await _organizationRepository.GetByIdAsync(organizationId);
|
||||
var subscriptionItem = await GetSubscriptionItemAsync(organization.GatewaySubscriptionId, GetStripeSeatPlanId(organization.PlanType));
|
||||
var extractedPlanType = PlanTypeMappings(organization);
|
||||
if (subscriptionItem != null)
|
||||
@ -494,9 +519,15 @@ public class ProviderService : IProviderService
|
||||
public async Task<ProviderOrganization> CreateOrganizationAsync(Guid providerId,
|
||||
OrganizationSignup organizationSignup, string clientOwnerEmail, User user)
|
||||
{
|
||||
ThrowOnInvalidPlanType(organizationSignup.Plan);
|
||||
var provider = await _providerRepository.GetByIdAsync(providerId);
|
||||
|
||||
var (organization, _, defaultCollection) = await _organizationService.SignUpAsync(organizationSignup, true);
|
||||
var consolidatedBillingEnabled = _featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling) && provider.IsBillable();
|
||||
|
||||
ThrowOnInvalidPlanType(organizationSignup.Plan, consolidatedBillingEnabled);
|
||||
|
||||
var (organization, _, defaultCollection) = consolidatedBillingEnabled
|
||||
? await _organizationService.SignupClientAsync(organizationSignup)
|
||||
: await _organizationService.SignUpAsync(organizationSignup, true);
|
||||
|
||||
var providerOrganization = new ProviderOrganization
|
||||
{
|
||||
@ -581,12 +612,50 @@ public class ProviderService : IProviderService
|
||||
}
|
||||
}
|
||||
|
||||
public async Task InitiateDeleteAsync(Provider provider, string providerAdminEmail)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(provider.Name))
|
||||
{
|
||||
throw new BadRequestException("Provider name not found.");
|
||||
}
|
||||
var providerAdmin = await _userRepository.GetByEmailAsync(providerAdminEmail);
|
||||
if (providerAdmin == null)
|
||||
{
|
||||
throw new BadRequestException("Provider admin not found.");
|
||||
}
|
||||
|
||||
var providerAdminOrgUser = await _providerUserRepository.GetByProviderUserAsync(provider.Id, providerAdmin.Id);
|
||||
if (providerAdminOrgUser == null || providerAdminOrgUser.Status != ProviderUserStatusType.Confirmed ||
|
||||
providerAdminOrgUser.Type != ProviderUserType.ProviderAdmin)
|
||||
{
|
||||
throw new BadRequestException("Org admin not found.");
|
||||
}
|
||||
|
||||
var token = _providerDeleteTokenDataFactory.Protect(new ProviderDeleteTokenable(provider, 1));
|
||||
await _mailService.SendInitiateDeletProviderEmailAsync(providerAdminEmail, provider, token);
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(Provider provider, string token)
|
||||
{
|
||||
if (!_providerDeleteTokenDataFactory.TryUnprotect(token, out var data) || !data.IsValid(provider))
|
||||
{
|
||||
throw new BadRequestException("Invalid token.");
|
||||
}
|
||||
await DeleteAsync(provider);
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(Provider provider)
|
||||
{
|
||||
await _providerRepository.DeleteAsync(provider);
|
||||
await _applicationCacheService.DeleteProviderAbilityAsync(provider.Id);
|
||||
}
|
||||
|
||||
private async Task SendInviteAsync(ProviderUser providerUser, Provider provider)
|
||||
{
|
||||
var nowMillis = CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow);
|
||||
var token = _dataProtector.Protect(
|
||||
$"ProviderUserInvite {providerUser.Id} {providerUser.Email} {nowMillis}");
|
||||
await _mailService.SendProviderInviteEmailAsync(provider.Name, providerUser, token, providerUser.Email);
|
||||
await _mailService.SendProviderInviteEmailAsync(provider.DisplayName(), providerUser, token, providerUser.Email);
|
||||
}
|
||||
|
||||
private async Task<bool> HasConfirmedProviderAdminExceptAsync(Guid providerId, IEnumerable<Guid> providerUserIds)
|
||||
@ -598,8 +667,13 @@ public class ProviderService : IProviderService
|
||||
return confirmedOwnersIds.Except(providerUserIds).Any();
|
||||
}
|
||||
|
||||
private void ThrowOnInvalidPlanType(PlanType requestedType)
|
||||
private void ThrowOnInvalidPlanType(PlanType requestedType, bool consolidatedBillingEnabled = false)
|
||||
{
|
||||
if (consolidatedBillingEnabled && requestedType is not (PlanType.TeamsMonthly or PlanType.EnterpriseMonthly))
|
||||
{
|
||||
throw new BadRequestException($"Providers cannot manage organizations with the plan type {requestedType}. Only Teams (Monthly) and Enterprise (Monthly) are allowed.");
|
||||
}
|
||||
|
||||
if (ProviderDisallowedOrganizationTypes.Contains(requestedType))
|
||||
{
|
||||
throw new BadRequestException($"Providers cannot manage organizations with the requested plan type ({requestedType}). Only Teams and Enterprise accounts are allowed.");
|
||||
|
@ -1,267 +0,0 @@
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.SecretsManager.AuthorizationRequirements;
|
||||
using Bit.Core.SecretsManager.Entities;
|
||||
using Bit.Core.SecretsManager.Queries.Interfaces;
|
||||
using Bit.Core.SecretsManager.Repositories;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
namespace Bit.Commercial.Core.SecretsManager.AuthorizationHandlers.AccessPolicies;
|
||||
|
||||
public class AccessPolicyAuthorizationHandler : AuthorizationHandler<AccessPolicyOperationRequirement, BaseAccessPolicy>
|
||||
{
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly IAccessClientQuery _accessClientQuery;
|
||||
private readonly IGroupRepository _groupRepository;
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
private readonly IProjectRepository _projectRepository;
|
||||
private readonly IServiceAccountRepository _serviceAccountRepository;
|
||||
|
||||
public AccessPolicyAuthorizationHandler(ICurrentContext currentContext,
|
||||
IAccessClientQuery accessClientQuery,
|
||||
IGroupRepository groupRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IProjectRepository projectRepository,
|
||||
IServiceAccountRepository serviceAccountRepository)
|
||||
{
|
||||
_currentContext = currentContext;
|
||||
_accessClientQuery = accessClientQuery;
|
||||
_groupRepository = groupRepository;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
_projectRepository = projectRepository;
|
||||
_serviceAccountRepository = serviceAccountRepository;
|
||||
}
|
||||
|
||||
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context,
|
||||
AccessPolicyOperationRequirement requirement,
|
||||
BaseAccessPolicy resource)
|
||||
{
|
||||
switch (requirement)
|
||||
{
|
||||
case not null when requirement == AccessPolicyOperations.Create:
|
||||
await CanCreateAccessPolicyAsync(context, requirement, resource);
|
||||
break;
|
||||
case not null when requirement == AccessPolicyOperations.Update:
|
||||
await CanUpdateAccessPolicyAsync(context, requirement, resource);
|
||||
break;
|
||||
case not null when requirement == AccessPolicyOperations.Delete:
|
||||
await CanDeleteAccessPolicyAsync(context, requirement, resource);
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentException("Unsupported operation requirement type provided.",
|
||||
nameof(requirement));
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CanCreateAccessPolicyAsync(AuthorizationHandlerContext context,
|
||||
AccessPolicyOperationRequirement requirement, BaseAccessPolicy resource)
|
||||
{
|
||||
switch (resource)
|
||||
{
|
||||
case UserProjectAccessPolicy ap:
|
||||
await CanCreateAsync(context, requirement, ap);
|
||||
break;
|
||||
case GroupProjectAccessPolicy ap:
|
||||
await CanCreateAsync(context, requirement, ap);
|
||||
break;
|
||||
case ServiceAccountProjectAccessPolicy ap:
|
||||
await CanCreateAsync(context, requirement, ap);
|
||||
break;
|
||||
case UserServiceAccountAccessPolicy ap:
|
||||
await CanCreateAsync(context, requirement, ap);
|
||||
break;
|
||||
case GroupServiceAccountAccessPolicy ap:
|
||||
await CanCreateAsync(context, requirement, ap);
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentException("Unsupported access policy type provided.");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CanUpdateAccessPolicyAsync(AuthorizationHandlerContext context,
|
||||
AccessPolicyOperationRequirement requirement, BaseAccessPolicy resource)
|
||||
{
|
||||
var access = await GetAccessPolicyAccessAsync(context, resource);
|
||||
|
||||
if (access.Write)
|
||||
{
|
||||
context.Succeed(requirement);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CanDeleteAccessPolicyAsync(AuthorizationHandlerContext context,
|
||||
AccessPolicyOperationRequirement requirement, BaseAccessPolicy resource)
|
||||
{
|
||||
var access = await GetAccessPolicyAccessAsync(context, resource);
|
||||
|
||||
if (access.Write)
|
||||
{
|
||||
context.Succeed(requirement);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CanCreateAsync(AuthorizationHandlerContext context,
|
||||
AccessPolicyOperationRequirement requirement, UserProjectAccessPolicy resource)
|
||||
{
|
||||
var user = await _organizationUserRepository.GetByIdAsync(resource.OrganizationUserId!.Value);
|
||||
if (user.OrganizationId != resource.GrantedProject?.OrganizationId)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var access = await GetAccessAsync(context, resource.GrantedProject!.OrganizationId, resource.GrantedProjectId);
|
||||
|
||||
if (access.Write)
|
||||
{
|
||||
context.Succeed(requirement);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CanCreateAsync(AuthorizationHandlerContext context,
|
||||
AccessPolicyOperationRequirement requirement, GroupProjectAccessPolicy resource)
|
||||
{
|
||||
var group = await _groupRepository.GetByIdAsync(resource.GroupId!.Value);
|
||||
if (group.OrganizationId != resource.GrantedProject?.OrganizationId)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var access = await GetAccessAsync(context, resource.GrantedProject!.OrganizationId, resource.GrantedProjectId);
|
||||
|
||||
if (access.Write)
|
||||
{
|
||||
context.Succeed(requirement);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CanCreateAsync(AuthorizationHandlerContext context,
|
||||
AccessPolicyOperationRequirement requirement, ServiceAccountProjectAccessPolicy resource)
|
||||
{
|
||||
var projectOrganizationId = resource.GrantedProject?.OrganizationId;
|
||||
var serviceAccountOrgId = resource.ServiceAccount?.OrganizationId;
|
||||
|
||||
if (projectOrganizationId == null)
|
||||
{
|
||||
var project = await _projectRepository.GetByIdAsync(resource.GrantedProjectId!.Value);
|
||||
projectOrganizationId = project?.OrganizationId;
|
||||
}
|
||||
|
||||
if (serviceAccountOrgId == null)
|
||||
{
|
||||
var serviceAccount = await _serviceAccountRepository.GetByIdAsync(resource.ServiceAccountId!.Value);
|
||||
serviceAccountOrgId = serviceAccount?.OrganizationId;
|
||||
}
|
||||
|
||||
if (!serviceAccountOrgId.HasValue || !projectOrganizationId.HasValue ||
|
||||
serviceAccountOrgId != projectOrganizationId)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var access = await GetAccessAsync(context, projectOrganizationId.Value, resource.GrantedProjectId,
|
||||
resource.ServiceAccountId);
|
||||
|
||||
if (access.Write)
|
||||
{
|
||||
context.Succeed(requirement);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CanCreateAsync(AuthorizationHandlerContext context,
|
||||
AccessPolicyOperationRequirement requirement, UserServiceAccountAccessPolicy resource)
|
||||
{
|
||||
var user = await _organizationUserRepository.GetByIdAsync(resource.OrganizationUserId!.Value);
|
||||
if (user.OrganizationId != resource.GrantedServiceAccount!.OrganizationId)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var access = await GetAccessAsync(context, resource.GrantedServiceAccount!.OrganizationId,
|
||||
serviceAccountIdToCheck: resource.GrantedServiceAccountId);
|
||||
|
||||
if (access.Write)
|
||||
{
|
||||
context.Succeed(requirement);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CanCreateAsync(AuthorizationHandlerContext context,
|
||||
AccessPolicyOperationRequirement requirement, GroupServiceAccountAccessPolicy resource)
|
||||
{
|
||||
var group = await _groupRepository.GetByIdAsync(resource.GroupId!.Value);
|
||||
if (group.OrganizationId != resource.GrantedServiceAccount!.OrganizationId)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var access = await GetAccessAsync(context, resource.GrantedServiceAccount!.OrganizationId,
|
||||
serviceAccountIdToCheck: resource.GrantedServiceAccountId);
|
||||
|
||||
if (access.Write)
|
||||
{
|
||||
context.Succeed(requirement);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<(bool Read, bool Write)> GetAccessPolicyAccessAsync(AuthorizationHandlerContext context,
|
||||
BaseAccessPolicy resource) =>
|
||||
resource switch
|
||||
{
|
||||
UserProjectAccessPolicy ap => await GetAccessAsync(context, ap.GrantedProject!.OrganizationId,
|
||||
ap.GrantedProjectId),
|
||||
GroupProjectAccessPolicy ap => await GetAccessAsync(context, ap.GrantedProject!.OrganizationId,
|
||||
ap.GrantedProjectId),
|
||||
ServiceAccountProjectAccessPolicy ap => await GetAccessAsync(context, ap.GrantedProject!.OrganizationId,
|
||||
ap.GrantedProjectId),
|
||||
UserServiceAccountAccessPolicy ap => await GetAccessAsync(context, ap.GrantedServiceAccount!.OrganizationId,
|
||||
serviceAccountIdToCheck: ap.GrantedServiceAccountId),
|
||||
GroupServiceAccountAccessPolicy ap => await GetAccessAsync(context,
|
||||
ap.GrantedServiceAccount!.OrganizationId, serviceAccountIdToCheck: ap.GrantedServiceAccountId),
|
||||
_ => throw new ArgumentException("Unsupported access policy type provided."),
|
||||
};
|
||||
|
||||
private async Task<(bool Read, bool Write)> GetAccessAsync(AuthorizationHandlerContext context,
|
||||
Guid organizationId, Guid? projectIdToCheck = null,
|
||||
Guid? serviceAccountIdToCheck = null)
|
||||
{
|
||||
if (!_currentContext.AccessSecretsManager(organizationId))
|
||||
{
|
||||
return (false, false);
|
||||
}
|
||||
|
||||
var (accessClient, userId) = await _accessClientQuery.GetAccessClientAsync(context.User, organizationId);
|
||||
|
||||
// Only users and admins should be able to manipulate access policies
|
||||
if (accessClient != AccessClientType.User && accessClient != AccessClientType.NoAccessCheck)
|
||||
{
|
||||
return (false, false);
|
||||
}
|
||||
|
||||
if (projectIdToCheck.HasValue && serviceAccountIdToCheck.HasValue)
|
||||
{
|
||||
var projectAccess =
|
||||
await _projectRepository.AccessToProjectAsync(projectIdToCheck.Value, userId, accessClient);
|
||||
var serviceAccountAccess =
|
||||
await _serviceAccountRepository.AccessToServiceAccountAsync(serviceAccountIdToCheck.Value, userId,
|
||||
accessClient);
|
||||
return (
|
||||
projectAccess.Read && serviceAccountAccess.Read,
|
||||
projectAccess.Write && serviceAccountAccess.Write);
|
||||
}
|
||||
|
||||
if (projectIdToCheck.HasValue)
|
||||
{
|
||||
return await _projectRepository.AccessToProjectAsync(projectIdToCheck.Value, userId, accessClient);
|
||||
}
|
||||
|
||||
if (serviceAccountIdToCheck.HasValue)
|
||||
{
|
||||
return await _serviceAccountRepository.AccessToServiceAccountAsync(serviceAccountIdToCheck.Value, userId,
|
||||
accessClient);
|
||||
}
|
||||
|
||||
throw new ArgumentException("No ID to check provided.");
|
||||
}
|
||||
}
|
@ -0,0 +1,107 @@
|
||||
#nullable enable
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.SecretsManager.AuthorizationRequirements;
|
||||
using Bit.Core.SecretsManager.Enums.AccessPolicies;
|
||||
using Bit.Core.SecretsManager.Models.Data.AccessPolicyUpdates;
|
||||
using Bit.Core.SecretsManager.Queries.Interfaces;
|
||||
using Bit.Core.SecretsManager.Repositories;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
namespace Bit.Commercial.Core.SecretsManager.AuthorizationHandlers.AccessPolicies;
|
||||
|
||||
public class ProjectServiceAccountsAccessPoliciesAuthorizationHandler : AuthorizationHandler<
|
||||
ProjectServiceAccountsAccessPoliciesOperationRequirement,
|
||||
ProjectServiceAccountsAccessPoliciesUpdates>
|
||||
{
|
||||
private readonly IAccessClientQuery _accessClientQuery;
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly IProjectRepository _projectRepository;
|
||||
private readonly IServiceAccountRepository _serviceAccountRepository;
|
||||
|
||||
public ProjectServiceAccountsAccessPoliciesAuthorizationHandler(ICurrentContext currentContext,
|
||||
IAccessClientQuery accessClientQuery,
|
||||
IProjectRepository projectRepository,
|
||||
IServiceAccountRepository serviceAccountRepository)
|
||||
{
|
||||
_currentContext = currentContext;
|
||||
_accessClientQuery = accessClientQuery;
|
||||
_serviceAccountRepository = serviceAccountRepository;
|
||||
_projectRepository = projectRepository;
|
||||
}
|
||||
|
||||
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context,
|
||||
ProjectServiceAccountsAccessPoliciesOperationRequirement requirement,
|
||||
ProjectServiceAccountsAccessPoliciesUpdates resource)
|
||||
{
|
||||
if (!_currentContext.AccessSecretsManager(resource.OrganizationId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Only users and admins should be able to manipulate access policies
|
||||
var (accessClient, userId) =
|
||||
await _accessClientQuery.GetAccessClientAsync(context.User, resource.OrganizationId);
|
||||
if (accessClient != AccessClientType.User && accessClient != AccessClientType.NoAccessCheck)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
switch (requirement)
|
||||
{
|
||||
case not null when requirement == ProjectServiceAccountsAccessPoliciesOperations.Updates:
|
||||
await CanUpdateAsync(context, requirement, resource, accessClient,
|
||||
userId);
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentException("Unsupported operation requirement type provided.",
|
||||
nameof(requirement));
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CanUpdateAsync(AuthorizationHandlerContext context,
|
||||
ProjectServiceAccountsAccessPoliciesOperationRequirement requirement,
|
||||
ProjectServiceAccountsAccessPoliciesUpdates resource,
|
||||
AccessClientType accessClient, Guid userId)
|
||||
{
|
||||
var access =
|
||||
await _projectRepository.AccessToProjectAsync(resource.ProjectId, userId,
|
||||
accessClient);
|
||||
if (!access.Write)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var serviceAccountIds = resource.ServiceAccountAccessPolicyUpdates.Select(update =>
|
||||
update.AccessPolicy.ServiceAccountId!.Value).ToList();
|
||||
|
||||
var inSameOrganization =
|
||||
await _serviceAccountRepository.ServiceAccountsAreInOrganizationAsync(serviceAccountIds,
|
||||
resource.OrganizationId);
|
||||
if (!inSameOrganization)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Users can only create access policies for service accounts they have access to.
|
||||
// User can delete and update any service account access policy if they have write access to the project.
|
||||
var serviceAccountIdsToCheck = resource.ServiceAccountAccessPolicyUpdates
|
||||
.Where(update => update.Operation == AccessPolicyOperation.Create).Select(update =>
|
||||
update.AccessPolicy.ServiceAccountId!.Value).ToList();
|
||||
|
||||
if (serviceAccountIdsToCheck.Count == 0)
|
||||
{
|
||||
context.Succeed(requirement);
|
||||
return;
|
||||
}
|
||||
|
||||
var serviceAccountsAccess =
|
||||
await _serviceAccountRepository.AccessToServiceAccountsAsync(serviceAccountIdsToCheck, userId,
|
||||
accessClient);
|
||||
if (serviceAccountsAccess.Count == serviceAccountIdsToCheck.Count &&
|
||||
serviceAccountsAccess.All(a => a.Value.Write))
|
||||
{
|
||||
context.Succeed(requirement);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,88 @@
|
||||
#nullable enable
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.SecretsManager.AuthorizationRequirements;
|
||||
using Bit.Core.SecretsManager.Models.Data;
|
||||
using Bit.Core.SecretsManager.Queries.Interfaces;
|
||||
using Bit.Core.SecretsManager.Repositories;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
namespace Bit.Commercial.Core.SecretsManager.AuthorizationHandlers.AccessPolicies;
|
||||
|
||||
public class ServiceAccountGrantedPoliciesAuthorizationHandler : AuthorizationHandler<
|
||||
ServiceAccountGrantedPoliciesOperationRequirement,
|
||||
ServiceAccountGrantedPoliciesUpdates>
|
||||
{
|
||||
private readonly IAccessClientQuery _accessClientQuery;
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly IProjectRepository _projectRepository;
|
||||
private readonly IServiceAccountRepository _serviceAccountRepository;
|
||||
|
||||
public ServiceAccountGrantedPoliciesAuthorizationHandler(ICurrentContext currentContext,
|
||||
IAccessClientQuery accessClientQuery,
|
||||
IProjectRepository projectRepository,
|
||||
IServiceAccountRepository serviceAccountRepository)
|
||||
{
|
||||
_currentContext = currentContext;
|
||||
_accessClientQuery = accessClientQuery;
|
||||
_serviceAccountRepository = serviceAccountRepository;
|
||||
_projectRepository = projectRepository;
|
||||
}
|
||||
|
||||
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context,
|
||||
ServiceAccountGrantedPoliciesOperationRequirement requirement,
|
||||
ServiceAccountGrantedPoliciesUpdates resource)
|
||||
{
|
||||
if (!_currentContext.AccessSecretsManager(resource.OrganizationId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Only users and admins should be able to manipulate access policies
|
||||
var (accessClient, userId) =
|
||||
await _accessClientQuery.GetAccessClientAsync(context.User, resource.OrganizationId);
|
||||
if (accessClient != AccessClientType.User && accessClient != AccessClientType.NoAccessCheck)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
switch (requirement)
|
||||
{
|
||||
case not null when requirement == ServiceAccountGrantedPoliciesOperations.Updates:
|
||||
await CanUpdateAsync(context, requirement, resource, accessClient,
|
||||
userId);
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentException("Unsupported operation requirement type provided.",
|
||||
nameof(requirement));
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CanUpdateAsync(AuthorizationHandlerContext context,
|
||||
ServiceAccountGrantedPoliciesOperationRequirement requirement, ServiceAccountGrantedPoliciesUpdates resource,
|
||||
AccessClientType accessClient, Guid userId)
|
||||
{
|
||||
var access =
|
||||
await _serviceAccountRepository.AccessToServiceAccountAsync(resource.ServiceAccountId, userId,
|
||||
accessClient);
|
||||
if (access.Write)
|
||||
{
|
||||
var projectIdsToCheck = resource.ProjectGrantedPolicyUpdates.Select(update =>
|
||||
update.AccessPolicy.GrantedProjectId!.Value).ToList();
|
||||
|
||||
var sameOrganization =
|
||||
await _projectRepository.ProjectsAreInOrganization(projectIdsToCheck, resource.OrganizationId);
|
||||
if (!sameOrganization)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var projectsAccess =
|
||||
await _projectRepository.AccessToProjectsAsync(projectIdsToCheck, userId, accessClient);
|
||||
if (projectsAccess.Count == projectIdsToCheck.Count && projectsAccess.All(a => a.Value.Write))
|
||||
{
|
||||
context.Succeed(requirement);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,33 +0,0 @@
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.SecretsManager.Commands.AccessPolicies.Interfaces;
|
||||
using Bit.Core.SecretsManager.Entities;
|
||||
using Bit.Core.SecretsManager.Repositories;
|
||||
|
||||
namespace Bit.Commercial.Core.SecretsManager.Commands.AccessPolicies;
|
||||
|
||||
public class CreateAccessPoliciesCommand : ICreateAccessPoliciesCommand
|
||||
{
|
||||
private readonly IAccessPolicyRepository _accessPolicyRepository;
|
||||
|
||||
public CreateAccessPoliciesCommand(IAccessPolicyRepository accessPolicyRepository)
|
||||
{
|
||||
_accessPolicyRepository = accessPolicyRepository;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<BaseAccessPolicy>> CreateManyAsync(List<BaseAccessPolicy> accessPolicies)
|
||||
{
|
||||
await CheckAccessPoliciesDoNotExistAsync(accessPolicies);
|
||||
return await _accessPolicyRepository.CreateManyAsync(accessPolicies);
|
||||
}
|
||||
|
||||
private async Task CheckAccessPoliciesDoNotExistAsync(List<BaseAccessPolicy> accessPolicies)
|
||||
{
|
||||
foreach (var accessPolicy in accessPolicies)
|
||||
{
|
||||
if (await _accessPolicyRepository.AccessPolicyExists(accessPolicy))
|
||||
{
|
||||
throw new BadRequestException("Resource already exists");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
using Bit.Core.SecretsManager.Commands.AccessPolicies.Interfaces;
|
||||
using Bit.Core.SecretsManager.Repositories;
|
||||
|
||||
namespace Bit.Commercial.Core.SecretsManager.Commands.AccessPolicies;
|
||||
|
||||
public class DeleteAccessPolicyCommand : IDeleteAccessPolicyCommand
|
||||
{
|
||||
private readonly IAccessPolicyRepository _accessPolicyRepository;
|
||||
|
||||
public DeleteAccessPolicyCommand(IAccessPolicyRepository accessPolicyRepository)
|
||||
{
|
||||
_accessPolicyRepository = accessPolicyRepository;
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(Guid id)
|
||||
{
|
||||
await _accessPolicyRepository.DeleteAsync(id);
|
||||
}
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.SecretsManager.Commands.AccessPolicies.Interfaces;
|
||||
using Bit.Core.SecretsManager.Entities;
|
||||
using Bit.Core.SecretsManager.Repositories;
|
||||
|
||||
namespace Bit.Commercial.Core.SecretsManager.Commands.AccessPolicies;
|
||||
|
||||
public class UpdateAccessPolicyCommand : IUpdateAccessPolicyCommand
|
||||
{
|
||||
private readonly IAccessPolicyRepository _accessPolicyRepository;
|
||||
|
||||
public UpdateAccessPolicyCommand(IAccessPolicyRepository accessPolicyRepository)
|
||||
{
|
||||
_accessPolicyRepository = accessPolicyRepository;
|
||||
}
|
||||
|
||||
public async Task<BaseAccessPolicy> UpdateAsync(Guid id, bool read, bool write)
|
||||
{
|
||||
var accessPolicy = await _accessPolicyRepository.GetByIdAsync(id);
|
||||
if (accessPolicy == null)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
accessPolicy.Read = read;
|
||||
accessPolicy.Write = write;
|
||||
accessPolicy.RevisionDate = DateTime.UtcNow;
|
||||
await _accessPolicyRepository.ReplaceAsync(accessPolicy);
|
||||
return accessPolicy;
|
||||
}
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
#nullable enable
|
||||
using Bit.Core.SecretsManager.Commands.AccessPolicies.Interfaces;
|
||||
using Bit.Core.SecretsManager.Models.Data.AccessPolicyUpdates;
|
||||
using Bit.Core.SecretsManager.Repositories;
|
||||
|
||||
namespace Bit.Commercial.Core.SecretsManager.Commands.AccessPolicies;
|
||||
|
||||
public class UpdateProjectServiceAccountsAccessPoliciesCommand : IUpdateProjectServiceAccountsAccessPoliciesCommand
|
||||
{
|
||||
private readonly IAccessPolicyRepository _accessPolicyRepository;
|
||||
|
||||
public UpdateProjectServiceAccountsAccessPoliciesCommand(IAccessPolicyRepository accessPolicyRepository)
|
||||
{
|
||||
_accessPolicyRepository = accessPolicyRepository;
|
||||
}
|
||||
|
||||
public async Task UpdateAsync(ProjectServiceAccountsAccessPoliciesUpdates accessPoliciesUpdates)
|
||||
{
|
||||
if (!accessPoliciesUpdates.ServiceAccountAccessPolicyUpdates.Any())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await _accessPolicyRepository.UpdateProjectServiceAccountsAccessPoliciesAsync(accessPoliciesUpdates);
|
||||
}
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
#nullable enable
|
||||
using Bit.Core.SecretsManager.Commands.AccessPolicies.Interfaces;
|
||||
using Bit.Core.SecretsManager.Models.Data;
|
||||
using Bit.Core.SecretsManager.Repositories;
|
||||
|
||||
namespace Bit.Commercial.Core.SecretsManager.Commands.AccessPolicies;
|
||||
|
||||
public class UpdateServiceAccountGrantedPoliciesCommand : IUpdateServiceAccountGrantedPoliciesCommand
|
||||
{
|
||||
private readonly IAccessPolicyRepository _accessPolicyRepository;
|
||||
|
||||
public UpdateServiceAccountGrantedPoliciesCommand(IAccessPolicyRepository accessPolicyRepository)
|
||||
{
|
||||
_accessPolicyRepository = accessPolicyRepository;
|
||||
}
|
||||
|
||||
public async Task UpdateAsync(ServiceAccountGrantedPoliciesUpdates grantedPoliciesUpdates)
|
||||
{
|
||||
if (!grantedPoliciesUpdates.ProjectGrantedPolicyUpdates.Any())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await _accessPolicyRepository.UpdateServiceAccountGrantedPoliciesAsync(grantedPoliciesUpdates);
|
||||
}
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
#nullable enable
|
||||
using Bit.Core.SecretsManager.Enums.AccessPolicies;
|
||||
using Bit.Core.SecretsManager.Models.Data;
|
||||
using Bit.Core.SecretsManager.Models.Data.AccessPolicyUpdates;
|
||||
using Bit.Core.SecretsManager.Queries.AccessPolicies.Interfaces;
|
||||
using Bit.Core.SecretsManager.Repositories;
|
||||
|
||||
namespace Bit.Commercial.Core.SecretsManager.Queries.AccessPolicies;
|
||||
|
||||
public class ProjectServiceAccountsAccessPoliciesUpdatesQuery : IProjectServiceAccountsAccessPoliciesUpdatesQuery
|
||||
{
|
||||
private readonly IAccessPolicyRepository _accessPolicyRepository;
|
||||
|
||||
public ProjectServiceAccountsAccessPoliciesUpdatesQuery(IAccessPolicyRepository accessPolicyRepository)
|
||||
{
|
||||
_accessPolicyRepository = accessPolicyRepository;
|
||||
}
|
||||
|
||||
public async Task<ProjectServiceAccountsAccessPoliciesUpdates> GetAsync(
|
||||
ProjectServiceAccountsAccessPolicies projectServiceAccountsAccessPolicies)
|
||||
{
|
||||
var currentPolicies =
|
||||
await _accessPolicyRepository.GetProjectServiceAccountsAccessPoliciesAsync(
|
||||
projectServiceAccountsAccessPolicies.ProjectId);
|
||||
|
||||
if (currentPolicies == null)
|
||||
{
|
||||
return new ProjectServiceAccountsAccessPoliciesUpdates
|
||||
{
|
||||
ProjectId = projectServiceAccountsAccessPolicies.ProjectId,
|
||||
OrganizationId = projectServiceAccountsAccessPolicies.OrganizationId,
|
||||
ServiceAccountAccessPolicyUpdates =
|
||||
projectServiceAccountsAccessPolicies.ServiceAccountAccessPolicies.Select(p =>
|
||||
new ServiceAccountProjectAccessPolicyUpdate
|
||||
{
|
||||
Operation = AccessPolicyOperation.Create,
|
||||
AccessPolicy = p
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
return currentPolicies.GetPolicyUpdates(projectServiceAccountsAccessPolicies);
|
||||
}
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
#nullable enable
|
||||
using Bit.Core.SecretsManager.Enums.AccessPolicies;
|
||||
using Bit.Core.SecretsManager.Models.Data;
|
||||
using Bit.Core.SecretsManager.Models.Data.AccessPolicyUpdates;
|
||||
using Bit.Core.SecretsManager.Queries.AccessPolicies.Interfaces;
|
||||
using Bit.Core.SecretsManager.Repositories;
|
||||
|
||||
namespace Bit.Commercial.Core.SecretsManager.Queries.AccessPolicies;
|
||||
|
||||
public class ServiceAccountGrantedPolicyUpdatesQuery : IServiceAccountGrantedPolicyUpdatesQuery
|
||||
{
|
||||
private readonly IAccessPolicyRepository _accessPolicyRepository;
|
||||
|
||||
public ServiceAccountGrantedPolicyUpdatesQuery(IAccessPolicyRepository accessPolicyRepository)
|
||||
{
|
||||
_accessPolicyRepository = accessPolicyRepository;
|
||||
}
|
||||
|
||||
public async Task<ServiceAccountGrantedPoliciesUpdates> GetAsync(
|
||||
ServiceAccountGrantedPolicies grantedPolicies)
|
||||
{
|
||||
var currentPolicies =
|
||||
await _accessPolicyRepository.GetServiceAccountGrantedPoliciesAsync(grantedPolicies.ServiceAccountId);
|
||||
if (currentPolicies == null)
|
||||
{
|
||||
return new ServiceAccountGrantedPoliciesUpdates
|
||||
{
|
||||
ServiceAccountId = grantedPolicies.ServiceAccountId,
|
||||
OrganizationId = grantedPolicies.OrganizationId,
|
||||
ProjectGrantedPolicyUpdates = grantedPolicies.ProjectGrantedPolicies.Select(p =>
|
||||
new ServiceAccountProjectAccessPolicyUpdate
|
||||
{
|
||||
Operation = AccessPolicyOperation.Create,
|
||||
AccessPolicy = p
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
return currentPolicies.GetPolicyUpdates(grantedPolicies);
|
||||
}
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
#nullable enable
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.SecretsManager.Entities;
|
||||
using Bit.Core.SecretsManager.Models.Data;
|
||||
using Bit.Core.SecretsManager.Queries.Secrets.Interfaces;
|
||||
using Bit.Core.SecretsManager.Repositories;
|
||||
|
||||
namespace Bit.Commercial.Core.SecretsManager.Queries.Secrets;
|
||||
|
||||
public class SecretsSyncQuery : ISecretsSyncQuery
|
||||
{
|
||||
private readonly ISecretRepository _secretRepository;
|
||||
private readonly IServiceAccountRepository _serviceAccountRepository;
|
||||
|
||||
public SecretsSyncQuery(
|
||||
ISecretRepository secretRepository,
|
||||
IServiceAccountRepository serviceAccountRepository)
|
||||
{
|
||||
_secretRepository = secretRepository;
|
||||
_serviceAccountRepository = serviceAccountRepository;
|
||||
}
|
||||
|
||||
public async Task<(bool HasChanges, IEnumerable<Secret>? Secrets)> GetAsync(SecretsSyncRequest syncRequest)
|
||||
{
|
||||
if (syncRequest.LastSyncedDate == null)
|
||||
{
|
||||
return await GetSecretsAsync(syncRequest);
|
||||
}
|
||||
|
||||
var serviceAccount = await _serviceAccountRepository.GetByIdAsync(syncRequest.ServiceAccountId);
|
||||
if (serviceAccount == null)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
if (syncRequest.LastSyncedDate.Value <= serviceAccount.RevisionDate)
|
||||
{
|
||||
return await GetSecretsAsync(syncRequest);
|
||||
}
|
||||
|
||||
return (HasChanges: false, null);
|
||||
}
|
||||
|
||||
private async Task<(bool HasChanges, IEnumerable<Secret>? Secrets)> GetSecretsAsync(SecretsSyncRequest syncRequest)
|
||||
{
|
||||
var secrets = await _secretRepository.GetManyByOrganizationIdAsync(syncRequest.OrganizationId,
|
||||
syncRequest.ServiceAccountId, syncRequest.AccessClientType);
|
||||
return (HasChanges: true, Secrets: secrets);
|
||||
}
|
||||
}
|
@ -12,6 +12,7 @@ using Bit.Commercial.Core.SecretsManager.Commands.Trash;
|
||||
using Bit.Commercial.Core.SecretsManager.Queries;
|
||||
using Bit.Commercial.Core.SecretsManager.Queries.AccessPolicies;
|
||||
using Bit.Commercial.Core.SecretsManager.Queries.Projects;
|
||||
using Bit.Commercial.Core.SecretsManager.Queries.Secrets;
|
||||
using Bit.Commercial.Core.SecretsManager.Queries.ServiceAccounts;
|
||||
using Bit.Core.SecretsManager.Commands.AccessPolicies.Interfaces;
|
||||
using Bit.Core.SecretsManager.Commands.AccessTokens.Interfaces;
|
||||
@ -23,6 +24,7 @@ using Bit.Core.SecretsManager.Commands.Trash.Interfaces;
|
||||
using Bit.Core.SecretsManager.Queries.AccessPolicies.Interfaces;
|
||||
using Bit.Core.SecretsManager.Queries.Interfaces;
|
||||
using Bit.Core.SecretsManager.Queries.Projects.Interfaces;
|
||||
using Bit.Core.SecretsManager.Queries.Secrets.Interfaces;
|
||||
using Bit.Core.SecretsManager.Queries.ServiceAccounts.Interfaces;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
@ -36,13 +38,17 @@ public static class SecretsManagerCollectionExtensions
|
||||
services.AddScoped<IAuthorizationHandler, ProjectAuthorizationHandler>();
|
||||
services.AddScoped<IAuthorizationHandler, SecretAuthorizationHandler>();
|
||||
services.AddScoped<IAuthorizationHandler, ServiceAccountAuthorizationHandler>();
|
||||
services.AddScoped<IAuthorizationHandler, AccessPolicyAuthorizationHandler>();
|
||||
services.AddScoped<IAuthorizationHandler, ProjectPeopleAccessPoliciesAuthorizationHandler>();
|
||||
services.AddScoped<IAuthorizationHandler, ServiceAccountPeopleAccessPoliciesAuthorizationHandler>();
|
||||
services.AddScoped<IAuthorizationHandler, ServiceAccountGrantedPoliciesAuthorizationHandler>();
|
||||
services.AddScoped<IAuthorizationHandler, ProjectServiceAccountsAccessPoliciesAuthorizationHandler>();
|
||||
services.AddScoped<IAccessClientQuery, AccessClientQuery>();
|
||||
services.AddScoped<IMaxProjectsQuery, MaxProjectsQuery>();
|
||||
services.AddScoped<ISameOrganizationQuery, SameOrganizationQuery>();
|
||||
services.AddScoped<IServiceAccountSecretsDetailsQuery, ServiceAccountSecretsDetailsQuery>();
|
||||
services.AddScoped<IServiceAccountGrantedPolicyUpdatesQuery, ServiceAccountGrantedPolicyUpdatesQuery>();
|
||||
services.AddScoped<ISecretsSyncQuery, SecretsSyncQuery>();
|
||||
services.AddScoped<IProjectServiceAccountsAccessPoliciesUpdatesQuery, ProjectServiceAccountsAccessPoliciesUpdatesQuery>();
|
||||
services.AddScoped<ICreateSecretCommand, CreateSecretCommand>();
|
||||
services.AddScoped<IUpdateSecretCommand, UpdateSecretCommand>();
|
||||
services.AddScoped<IDeleteSecretCommand, DeleteSecretCommand>();
|
||||
@ -55,11 +61,10 @@ public static class SecretsManagerCollectionExtensions
|
||||
services.AddScoped<ICountNewServiceAccountSlotsRequiredQuery, CountNewServiceAccountSlotsRequiredQuery>();
|
||||
services.AddScoped<IRevokeAccessTokensCommand, RevokeAccessTokensCommand>();
|
||||
services.AddScoped<ICreateAccessTokenCommand, CreateAccessTokenCommand>();
|
||||
services.AddScoped<ICreateAccessPoliciesCommand, CreateAccessPoliciesCommand>();
|
||||
services.AddScoped<IUpdateAccessPolicyCommand, UpdateAccessPolicyCommand>();
|
||||
services.AddScoped<IDeleteAccessPolicyCommand, DeleteAccessPolicyCommand>();
|
||||
services.AddScoped<IImportCommand, ImportCommand>();
|
||||
services.AddScoped<IEmptyTrashCommand, EmptyTrashCommand>();
|
||||
services.AddScoped<IRestoreTrashCommand, RestoreTrashCommand>();
|
||||
services.AddScoped<IUpdateServiceAccountGrantedPoliciesCommand, UpdateServiceAccountGrantedPoliciesCommand>();
|
||||
services.AddScoped<IUpdateProjectServiceAccountsAccessPoliciesCommand, UpdateProjectServiceAccountsAccessPoliciesCommand>();
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,8 @@
|
||||
using System.Linq.Expressions;
|
||||
using AutoMapper;
|
||||
using AutoMapper;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.SecretsManager.Enums.AccessPolicies;
|
||||
using Bit.Core.SecretsManager.Models.Data;
|
||||
using Bit.Core.SecretsManager.Models.Data.AccessPolicyUpdates;
|
||||
using Bit.Core.SecretsManager.Repositories;
|
||||
using Bit.Infrastructure.EntityFramework.Repositories;
|
||||
using Bit.Infrastructure.EntityFramework.SecretsManager.Discriminators;
|
||||
@ -19,16 +20,13 @@ public class AccessPolicyRepository : BaseEntityFrameworkRepository, IAccessPoli
|
||||
{
|
||||
}
|
||||
|
||||
private static Expression<Func<ServiceAccountProjectAccessPolicy, bool>> UserHasWriteAccessToProject(Guid userId) =>
|
||||
policy =>
|
||||
policy.GrantedProject.UserAccessPolicies.Any(ap => ap.OrganizationUser.User.Id == userId && ap.Write) ||
|
||||
policy.GrantedProject.GroupAccessPolicies.Any(ap =>
|
||||
ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.User.Id == userId && ap.Write));
|
||||
|
||||
public async Task<List<Core.SecretsManager.Entities.BaseAccessPolicy>> CreateManyAsync(List<Core.SecretsManager.Entities.BaseAccessPolicy> baseAccessPolicies)
|
||||
{
|
||||
using var scope = ServiceScopeFactory.CreateScope();
|
||||
await using var scope = ServiceScopeFactory.CreateAsyncScope();
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
await using var transaction = await dbContext.Database.BeginTransactionAsync();
|
||||
|
||||
var serviceAccountIds = new List<Guid>();
|
||||
foreach (var baseAccessPolicy in baseAccessPolicies)
|
||||
{
|
||||
baseAccessPolicy.SetNewId();
|
||||
@ -64,160 +62,25 @@ public class AccessPolicyRepository : BaseEntityFrameworkRepository, IAccessPoli
|
||||
{
|
||||
var entity = Mapper.Map<ServiceAccountProjectAccessPolicy>(accessPolicy);
|
||||
await dbContext.AddAsync(entity);
|
||||
serviceAccountIds.Add(entity.ServiceAccountId!.Value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (serviceAccountIds.Count > 0)
|
||||
{
|
||||
var utcNow = DateTime.UtcNow;
|
||||
await dbContext.ServiceAccount
|
||||
.Where(sa => serviceAccountIds.Contains(sa.Id))
|
||||
.ExecuteUpdateAsync(setters => setters.SetProperty(sa => sa.RevisionDate, utcNow));
|
||||
}
|
||||
|
||||
await dbContext.SaveChangesAsync();
|
||||
await transaction.CommitAsync();
|
||||
return baseAccessPolicies;
|
||||
}
|
||||
|
||||
public async Task<bool> AccessPolicyExists(Core.SecretsManager.Entities.BaseAccessPolicy baseAccessPolicy)
|
||||
{
|
||||
using var scope = ServiceScopeFactory.CreateScope();
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
switch (baseAccessPolicy)
|
||||
{
|
||||
case Core.SecretsManager.Entities.UserProjectAccessPolicy accessPolicy:
|
||||
{
|
||||
var policy = await dbContext.UserProjectAccessPolicy
|
||||
.Where(c => c.OrganizationUserId == accessPolicy.OrganizationUserId &&
|
||||
c.GrantedProjectId == accessPolicy.GrantedProjectId)
|
||||
.FirstOrDefaultAsync();
|
||||
return policy != null;
|
||||
}
|
||||
case Core.SecretsManager.Entities.GroupProjectAccessPolicy accessPolicy:
|
||||
{
|
||||
var policy = await dbContext.GroupProjectAccessPolicy
|
||||
.Where(c => c.GroupId == accessPolicy.GroupId &&
|
||||
c.GrantedProjectId == accessPolicy.GrantedProjectId)
|
||||
.FirstOrDefaultAsync();
|
||||
return policy != null;
|
||||
}
|
||||
case Core.SecretsManager.Entities.ServiceAccountProjectAccessPolicy accessPolicy:
|
||||
{
|
||||
var policy = await dbContext.ServiceAccountProjectAccessPolicy
|
||||
.Where(c => c.ServiceAccountId == accessPolicy.ServiceAccountId &&
|
||||
c.GrantedProjectId == accessPolicy.GrantedProjectId)
|
||||
.FirstOrDefaultAsync();
|
||||
return policy != null;
|
||||
}
|
||||
case Core.SecretsManager.Entities.UserServiceAccountAccessPolicy accessPolicy:
|
||||
{
|
||||
var policy = await dbContext.UserServiceAccountAccessPolicy
|
||||
.Where(c => c.OrganizationUserId == accessPolicy.OrganizationUserId &&
|
||||
c.GrantedServiceAccountId == accessPolicy.GrantedServiceAccountId)
|
||||
.FirstOrDefaultAsync();
|
||||
return policy != null;
|
||||
}
|
||||
case Core.SecretsManager.Entities.GroupServiceAccountAccessPolicy accessPolicy:
|
||||
{
|
||||
var policy = await dbContext.GroupServiceAccountAccessPolicy
|
||||
.Where(c => c.GroupId == accessPolicy.GroupId &&
|
||||
c.GrantedServiceAccountId == accessPolicy.GrantedServiceAccountId)
|
||||
.FirstOrDefaultAsync();
|
||||
return policy != null;
|
||||
}
|
||||
default:
|
||||
throw new ArgumentException("Unsupported access policy type provided.", nameof(baseAccessPolicy));
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Core.SecretsManager.Entities.BaseAccessPolicy?> GetByIdAsync(Guid id)
|
||||
{
|
||||
using var scope = ServiceScopeFactory.CreateScope();
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
var entity = await dbContext.AccessPolicies.Where(ap => ap.Id == id)
|
||||
.Include(ap => ((UserProjectAccessPolicy)ap).OrganizationUser.User)
|
||||
.Include(ap => ((UserProjectAccessPolicy)ap).GrantedProject)
|
||||
.Include(ap => ((GroupProjectAccessPolicy)ap).Group)
|
||||
.Include(ap => ((GroupProjectAccessPolicy)ap).GrantedProject)
|
||||
.Include(ap => ((ServiceAccountProjectAccessPolicy)ap).ServiceAccount)
|
||||
.Include(ap => ((ServiceAccountProjectAccessPolicy)ap).GrantedProject)
|
||||
.Include(ap => ((UserServiceAccountAccessPolicy)ap).OrganizationUser.User)
|
||||
.Include(ap => ((UserServiceAccountAccessPolicy)ap).GrantedServiceAccount)
|
||||
.Include(ap => ((GroupServiceAccountAccessPolicy)ap).Group)
|
||||
.Include(ap => ((GroupServiceAccountAccessPolicy)ap).GrantedServiceAccount)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
return entity == null ? null : MapToCore(entity);
|
||||
}
|
||||
|
||||
public async Task ReplaceAsync(Core.SecretsManager.Entities.BaseAccessPolicy baseAccessPolicy)
|
||||
{
|
||||
using var scope = ServiceScopeFactory.CreateScope();
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
var entity = await dbContext.AccessPolicies.FindAsync(baseAccessPolicy.Id);
|
||||
if (entity != null)
|
||||
{
|
||||
dbContext.AccessPolicies.Attach(entity);
|
||||
entity.Write = baseAccessPolicy.Write;
|
||||
entity.Read = baseAccessPolicy.Read;
|
||||
entity.RevisionDate = baseAccessPolicy.RevisionDate;
|
||||
await dbContext.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Core.SecretsManager.Entities.BaseAccessPolicy>> GetManyByGrantedProjectIdAsync(Guid id, Guid userId)
|
||||
{
|
||||
using var scope = ServiceScopeFactory.CreateScope();
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
|
||||
var entities = await dbContext.AccessPolicies.Where(ap =>
|
||||
((UserProjectAccessPolicy)ap).GrantedProjectId == id ||
|
||||
((GroupProjectAccessPolicy)ap).GrantedProjectId == id ||
|
||||
((ServiceAccountProjectAccessPolicy)ap).GrantedProjectId == id)
|
||||
.Include(ap => ((UserProjectAccessPolicy)ap).OrganizationUser.User)
|
||||
.Include(ap => ((GroupProjectAccessPolicy)ap).Group)
|
||||
.Include(ap => ((ServiceAccountProjectAccessPolicy)ap).ServiceAccount)
|
||||
.Select(ap => new
|
||||
{
|
||||
ap,
|
||||
CurrentUserInGroup = ap is GroupProjectAccessPolicy &&
|
||||
((GroupProjectAccessPolicy)ap).Group.GroupUsers.Any(g =>
|
||||
g.OrganizationUser.User.Id == userId),
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
return entities.Select(e => MapToCore(e.ap, e.CurrentUserInGroup));
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(Guid id)
|
||||
{
|
||||
using var scope = ServiceScopeFactory.CreateScope();
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
var entity = await dbContext.AccessPolicies.FindAsync(id);
|
||||
if (entity != null)
|
||||
{
|
||||
dbContext.Remove(entity);
|
||||
await dbContext.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Core.SecretsManager.Entities.BaseAccessPolicy>> GetManyByServiceAccountIdAsync(Guid id, Guid userId,
|
||||
AccessClientType accessType)
|
||||
{
|
||||
using var scope = ServiceScopeFactory.CreateScope();
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
var query = dbContext.ServiceAccountProjectAccessPolicy.Where(ap =>
|
||||
ap.ServiceAccountId == id);
|
||||
|
||||
query = accessType switch
|
||||
{
|
||||
AccessClientType.NoAccessCheck => query,
|
||||
AccessClientType.User => query.Where(UserHasWriteAccessToProject(userId)),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(accessType), accessType, null),
|
||||
};
|
||||
|
||||
var entities = await query
|
||||
.Include(ap => ap.ServiceAccount)
|
||||
.Include(ap => ap.GrantedProject)
|
||||
.ToListAsync();
|
||||
|
||||
return entities.Select(MapToCore);
|
||||
}
|
||||
|
||||
public async Task<PeopleGrantees> GetPeopleGranteesAsync(Guid organizationId, Guid currentUserId)
|
||||
{
|
||||
using var scope = ServiceScopeFactory.CreateScope();
|
||||
@ -405,6 +268,133 @@ public class AccessPolicyRepository : BaseEntityFrameworkRepository, IAccessPoli
|
||||
return await GetPeoplePoliciesByGrantedServiceAccountIdAsync(peopleAccessPolicies.Id, userId);
|
||||
}
|
||||
|
||||
public async Task<ServiceAccountGrantedPolicies?> GetServiceAccountGrantedPoliciesAsync(Guid serviceAccountId)
|
||||
{
|
||||
await using var scope = ServiceScopeFactory.CreateAsyncScope();
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
var entities = await dbContext.ServiceAccountProjectAccessPolicy
|
||||
.Where(ap => ap.ServiceAccountId == serviceAccountId)
|
||||
.Include(ap => ap.ServiceAccount)
|
||||
.Include(ap => ap.GrantedProject)
|
||||
.ToListAsync();
|
||||
|
||||
if (entities.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return new ServiceAccountGrantedPolicies(serviceAccountId, entities.Select(MapToCore).ToList());
|
||||
}
|
||||
|
||||
public async Task<ServiceAccountGrantedPoliciesPermissionDetails?>
|
||||
GetServiceAccountGrantedPoliciesPermissionDetailsAsync(Guid serviceAccountId, Guid userId,
|
||||
AccessClientType accessClientType)
|
||||
{
|
||||
await using var scope = ServiceScopeFactory.CreateAsyncScope();
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
var accessPolicyQuery = dbContext.ServiceAccountProjectAccessPolicy
|
||||
.Where(ap => ap.ServiceAccountId == serviceAccountId)
|
||||
.Include(ap => ap.ServiceAccount)
|
||||
.Include(ap => ap.GrantedProject);
|
||||
|
||||
var accessPoliciesPermissionDetails =
|
||||
await ToPermissionDetails(accessPolicyQuery, userId, accessClientType).ToListAsync();
|
||||
if (accessPoliciesPermissionDetails.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new ServiceAccountGrantedPoliciesPermissionDetails
|
||||
{
|
||||
ServiceAccountId = serviceAccountId,
|
||||
OrganizationId = accessPoliciesPermissionDetails.First().AccessPolicy.GrantedProject!.OrganizationId,
|
||||
ProjectGrantedPolicies = accessPoliciesPermissionDetails
|
||||
};
|
||||
}
|
||||
|
||||
public async Task UpdateServiceAccountGrantedPoliciesAsync(ServiceAccountGrantedPoliciesUpdates updates)
|
||||
{
|
||||
await using var scope = ServiceScopeFactory.CreateAsyncScope();
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
var currentAccessPolicies = await dbContext.ServiceAccountProjectAccessPolicy
|
||||
.Where(ap => ap.ServiceAccountId == updates.ServiceAccountId)
|
||||
.ToListAsync();
|
||||
|
||||
if (currentAccessPolicies.Count != 0)
|
||||
{
|
||||
var projectIdsToDelete = updates.ProjectGrantedPolicyUpdates
|
||||
.Where(pu => pu.Operation == AccessPolicyOperation.Delete)
|
||||
.Select(pu => pu.AccessPolicy.GrantedProjectId!.Value)
|
||||
.ToList();
|
||||
|
||||
var policiesToDelete = currentAccessPolicies
|
||||
.Where(entity => projectIdsToDelete.Contains(entity.GrantedProjectId!.Value))
|
||||
.ToList();
|
||||
|
||||
dbContext.RemoveRange(policiesToDelete);
|
||||
}
|
||||
|
||||
await UpsertServiceAccountProjectPoliciesAsync(dbContext, currentAccessPolicies,
|
||||
updates.ProjectGrantedPolicyUpdates.Where(pu => pu.Operation != AccessPolicyOperation.Delete).ToList());
|
||||
await UpdateServiceAccountRevisionAsync(dbContext, updates.ServiceAccountId);
|
||||
await dbContext.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task<ProjectServiceAccountsAccessPolicies?> GetProjectServiceAccountsAccessPoliciesAsync(Guid projectId)
|
||||
{
|
||||
await using var scope = ServiceScopeFactory.CreateAsyncScope();
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
var entities = await dbContext.ServiceAccountProjectAccessPolicy
|
||||
.Where(ap => ap.GrantedProjectId == projectId)
|
||||
.Include(ap => ap.ServiceAccount)
|
||||
.Include(ap => ap.GrantedProject)
|
||||
.ToListAsync();
|
||||
|
||||
if (entities.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new ProjectServiceAccountsAccessPolicies(projectId, entities.Select(MapToCore).ToList());
|
||||
}
|
||||
|
||||
public async Task UpdateProjectServiceAccountsAccessPoliciesAsync(
|
||||
ProjectServiceAccountsAccessPoliciesUpdates updates)
|
||||
{
|
||||
await using var scope = ServiceScopeFactory.CreateAsyncScope();
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
await using var transaction = await dbContext.Database.BeginTransactionAsync();
|
||||
|
||||
var currentAccessPolicies = await dbContext.ServiceAccountProjectAccessPolicy
|
||||
.Where(ap => ap.GrantedProjectId == updates.ProjectId)
|
||||
.ToListAsync();
|
||||
|
||||
if (currentAccessPolicies.Count != 0)
|
||||
{
|
||||
var serviceAccountIdsToDelete = updates.ServiceAccountAccessPolicyUpdates
|
||||
.Where(pu => pu.Operation == AccessPolicyOperation.Delete)
|
||||
.Select(pu => pu.AccessPolicy.ServiceAccountId!.Value)
|
||||
.ToList();
|
||||
|
||||
var accessPolicyIdsToDelete = currentAccessPolicies
|
||||
.Where(entity => serviceAccountIdsToDelete.Contains(entity.ServiceAccountId!.Value))
|
||||
.Select(ap => ap.Id)
|
||||
.ToList();
|
||||
|
||||
await dbContext.ServiceAccountProjectAccessPolicy
|
||||
.Where(ap => accessPolicyIdsToDelete.Contains(ap.Id))
|
||||
.ExecuteDeleteAsync();
|
||||
}
|
||||
|
||||
await UpsertServiceAccountProjectPoliciesAsync(dbContext, currentAccessPolicies,
|
||||
updates.ServiceAccountAccessPolicyUpdates.Where(update => update.Operation != AccessPolicyOperation.Delete)
|
||||
.ToList());
|
||||
var effectedServiceAccountIds = updates.ServiceAccountAccessPolicyUpdates
|
||||
.Select(sa => sa.AccessPolicy.ServiceAccountId!.Value).ToList();
|
||||
await UpdateServiceAccountsRevisionAsync(dbContext, effectedServiceAccountIds);
|
||||
await dbContext.SaveChangesAsync();
|
||||
await transaction.CommitAsync();
|
||||
}
|
||||
|
||||
private static async Task UpsertPeoplePoliciesAsync(DatabaseContext dbContext,
|
||||
List<BaseAccessPolicy> policies, IReadOnlyCollection<AccessPolicy> userPolicyEntities,
|
||||
IReadOnlyCollection<AccessPolicy> groupPolicyEntities)
|
||||
@ -440,6 +430,37 @@ public class AccessPolicyRepository : BaseEntityFrameworkRepository, IAccessPoli
|
||||
}
|
||||
}
|
||||
|
||||
private async Task UpsertServiceAccountProjectPoliciesAsync(DatabaseContext dbContext,
|
||||
IReadOnlyCollection<ServiceAccountProjectAccessPolicy> currentPolices,
|
||||
List<ServiceAccountProjectAccessPolicyUpdate> policyUpdates)
|
||||
{
|
||||
var currentDate = DateTime.UtcNow;
|
||||
foreach (var policyUpdate in policyUpdates)
|
||||
{
|
||||
var updatedEntity = MapToEntity(policyUpdate.AccessPolicy);
|
||||
var currentEntity = currentPolices.FirstOrDefault(e =>
|
||||
e.GrantedProjectId == policyUpdate.AccessPolicy.GrantedProjectId!.Value &&
|
||||
e.ServiceAccountId == policyUpdate.AccessPolicy.ServiceAccountId!.Value);
|
||||
|
||||
switch (policyUpdate.Operation)
|
||||
{
|
||||
case AccessPolicyOperation.Create when currentEntity == null:
|
||||
updatedEntity.SetNewId();
|
||||
await dbContext.AddAsync(updatedEntity);
|
||||
break;
|
||||
|
||||
case AccessPolicyOperation.Update when currentEntity != null:
|
||||
dbContext.AccessPolicies.Attach(currentEntity);
|
||||
currentEntity.Read = updatedEntity.Read;
|
||||
currentEntity.Write = updatedEntity.Write;
|
||||
currentEntity.RevisionDate = currentDate;
|
||||
break;
|
||||
default:
|
||||
throw new InvalidOperationException("Policy updates failed due to unexpected state.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Core.SecretsManager.Entities.BaseAccessPolicy MapToCore(
|
||||
BaseAccessPolicy baseAccessPolicyEntity) =>
|
||||
baseAccessPolicyEntity switch
|
||||
@ -494,4 +515,51 @@ public class AccessPolicyRepository : BaseEntityFrameworkRepository, IAccessPoli
|
||||
return MapToCore(baseAccessPolicyEntity);
|
||||
}
|
||||
}
|
||||
|
||||
private IQueryable<ServiceAccountProjectAccessPolicyPermissionDetails> ToPermissionDetails(
|
||||
IQueryable<ServiceAccountProjectAccessPolicy>
|
||||
query, Guid userId, AccessClientType accessClientType)
|
||||
{
|
||||
var permissionDetails = accessClientType switch
|
||||
{
|
||||
AccessClientType.NoAccessCheck => query.Select(ap => new ServiceAccountProjectAccessPolicyPermissionDetails
|
||||
{
|
||||
AccessPolicy =
|
||||
Mapper.Map<Core.SecretsManager.Entities.ServiceAccountProjectAccessPolicy>(ap),
|
||||
HasPermission = true
|
||||
}),
|
||||
AccessClientType.User => query.Select(ap => new ServiceAccountProjectAccessPolicyPermissionDetails
|
||||
{
|
||||
AccessPolicy =
|
||||
Mapper.Map<Core.SecretsManager.Entities.ServiceAccountProjectAccessPolicy>(ap),
|
||||
HasPermission =
|
||||
(ap.GrantedProject.UserAccessPolicies.Any(p => p.OrganizationUser.UserId == userId && p.Write) ||
|
||||
ap.GrantedProject.GroupAccessPolicies.Any(p =>
|
||||
p.Group.GroupUsers.Any(gu => gu.OrganizationUser.UserId == userId && p.Write))) &&
|
||||
(ap.ServiceAccount.UserAccessPolicies.Any(p => p.OrganizationUser.UserId == userId && p.Write) ||
|
||||
ap.ServiceAccount.GroupAccessPolicies.Any(p =>
|
||||
p.Group.GroupUsers.Any(gu => gu.OrganizationUser.UserId == userId && p.Write)))
|
||||
}),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(accessClientType), accessClientType, null)
|
||||
};
|
||||
return permissionDetails;
|
||||
}
|
||||
|
||||
private static async Task UpdateServiceAccountRevisionAsync(DatabaseContext dbContext, Guid serviceAccountId)
|
||||
{
|
||||
var entity = await dbContext.ServiceAccount.FindAsync(serviceAccountId);
|
||||
if (entity != null)
|
||||
{
|
||||
entity.RevisionDate = DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task UpdateServiceAccountsRevisionAsync(DatabaseContext dbContext, List<Guid> serviceAccountIds)
|
||||
{
|
||||
var utcNow = DateTime.UtcNow;
|
||||
await dbContext.ServiceAccount
|
||||
.Where(sa => serviceAccountIds.Contains(sa.Id))
|
||||
.ExecuteUpdateAsync(setters =>
|
||||
setters.SetProperty(sa => sa.RevisionDate, utcNow));
|
||||
}
|
||||
}
|
||||
|
@ -70,23 +70,43 @@ public class ProjectRepository : Repository<Core.SecretsManager.Entities.Project
|
||||
|
||||
public async Task DeleteManyByIdAsync(IEnumerable<Guid> ids)
|
||||
{
|
||||
using var scope = ServiceScopeFactory.CreateScope();
|
||||
var utcNow = DateTime.UtcNow;
|
||||
await using var scope = ServiceScopeFactory.CreateAsyncScope();
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
var projects = dbContext.Project
|
||||
.Where(c => ids.Contains(c.Id))
|
||||
.Include(p => p.Secrets);
|
||||
await projects.ForEachAsync(project =>
|
||||
await using var transaction = await dbContext.Database.BeginTransactionAsync();
|
||||
|
||||
var serviceAccountIds = await dbContext.Project
|
||||
.Where(p => ids.Contains(p.Id))
|
||||
.Include(p => p.ServiceAccountAccessPolicies)
|
||||
.SelectMany(p => p.ServiceAccountAccessPolicies.Select(ap => ap.ServiceAccountId!.Value))
|
||||
.Distinct()
|
||||
.ToListAsync();
|
||||
|
||||
var secretIds = await dbContext.Project
|
||||
.Where(p => ids.Contains(p.Id))
|
||||
.Include(p => p.Secrets)
|
||||
.SelectMany(p => p.Secrets.Select(s => s.Id))
|
||||
.Distinct()
|
||||
.ToListAsync();
|
||||
|
||||
var utcNow = DateTime.UtcNow;
|
||||
if (serviceAccountIds.Count > 0)
|
||||
{
|
||||
foreach (var projectSecret in project.Secrets)
|
||||
{
|
||||
projectSecret.RevisionDate = utcNow;
|
||||
}
|
||||
await dbContext.ServiceAccount
|
||||
.Where(sa => serviceAccountIds.Contains(sa.Id))
|
||||
.ExecuteUpdateAsync(setters =>
|
||||
setters.SetProperty(sa => sa.RevisionDate, utcNow));
|
||||
}
|
||||
|
||||
dbContext.Remove(project);
|
||||
});
|
||||
if (secretIds.Count > 0)
|
||||
{
|
||||
await dbContext.Secret
|
||||
.Where(s => secretIds.Contains(s.Id))
|
||||
.ExecuteUpdateAsync(setters =>
|
||||
setters.SetProperty(s => s.RevisionDate, utcNow));
|
||||
}
|
||||
|
||||
await dbContext.SaveChangesAsync();
|
||||
await dbContext.Project.Where(p => ids.Contains(p.Id)).ExecuteDeleteAsync();
|
||||
await transaction.CommitAsync();
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Core.SecretsManager.Entities.Project>> GetManyWithSecretsByIds(IEnumerable<Guid> ids)
|
||||
@ -120,27 +140,8 @@ public class ProjectRepository : Repository<Core.SecretsManager.Entities.Project
|
||||
var projectQuery = dbContext.Project
|
||||
.Where(s => s.Id == id);
|
||||
|
||||
var query = accessType switch
|
||||
{
|
||||
AccessClientType.NoAccessCheck => projectQuery.Select(_ => new { Read = true, Write = true }),
|
||||
AccessClientType.User => projectQuery.Select(p => new
|
||||
{
|
||||
Read = p.UserAccessPolicies.Any(ap => ap.OrganizationUser.User.Id == userId && ap.Read)
|
||||
|| p.GroupAccessPolicies.Any(ap =>
|
||||
ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.User.Id == userId && ap.Read)),
|
||||
Write = p.UserAccessPolicies.Any(ap => ap.OrganizationUser.User.Id == userId && ap.Write) ||
|
||||
p.GroupAccessPolicies.Any(ap =>
|
||||
ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.User.Id == userId && ap.Write)),
|
||||
}),
|
||||
AccessClientType.ServiceAccount => projectQuery.Select(p => new
|
||||
{
|
||||
Read = p.ServiceAccountAccessPolicies.Any(ap => ap.ServiceAccountId == userId && ap.Read),
|
||||
Write = p.ServiceAccountAccessPolicies.Any(ap => ap.ServiceAccountId == userId && ap.Write),
|
||||
}),
|
||||
_ => projectQuery.Select(_ => new { Read = false, Write = false }),
|
||||
};
|
||||
|
||||
var policy = await query.FirstOrDefaultAsync();
|
||||
var accessQuery = BuildProjectAccessQuery(projectQuery, userId, accessType);
|
||||
var policy = await accessQuery.FirstOrDefaultAsync();
|
||||
|
||||
return policy == null ? (false, false) : (policy.Read, policy.Write);
|
||||
}
|
||||
@ -154,6 +155,46 @@ public class ProjectRepository : Repository<Core.SecretsManager.Entities.Project
|
||||
return projectIds.Count == results.Count;
|
||||
}
|
||||
|
||||
public async Task<Dictionary<Guid, (bool Read, bool Write)>> AccessToProjectsAsync(
|
||||
IEnumerable<Guid> projectIds,
|
||||
Guid userId,
|
||||
AccessClientType accessType)
|
||||
{
|
||||
await using var scope = ServiceScopeFactory.CreateAsyncScope();
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
|
||||
var projectsQuery = dbContext.Project.Where(p => projectIds.Contains(p.Id));
|
||||
var accessQuery = BuildProjectAccessQuery(projectsQuery, userId, accessType);
|
||||
|
||||
return await accessQuery.ToDictionaryAsync(pa => pa.Id, pa => (pa.Read, pa.Write));
|
||||
}
|
||||
|
||||
private record ProjectAccess(Guid Id, bool Read, bool Write);
|
||||
|
||||
private static IQueryable<ProjectAccess> BuildProjectAccessQuery(IQueryable<Project> projectQuery, Guid userId,
|
||||
AccessClientType accessType) =>
|
||||
accessType switch
|
||||
{
|
||||
AccessClientType.NoAccessCheck => projectQuery.Select(p => new ProjectAccess(p.Id, true, true)),
|
||||
AccessClientType.User => projectQuery.Select(p => new ProjectAccess
|
||||
(
|
||||
p.Id,
|
||||
p.UserAccessPolicies.Any(ap => ap.OrganizationUser.User.Id == userId && ap.Read) ||
|
||||
p.GroupAccessPolicies.Any(ap =>
|
||||
ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.User.Id == userId && ap.Read)),
|
||||
p.UserAccessPolicies.Any(ap => ap.OrganizationUser.User.Id == userId && ap.Write) ||
|
||||
p.GroupAccessPolicies.Any(ap =>
|
||||
ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.User.Id == userId && ap.Write))
|
||||
)),
|
||||
AccessClientType.ServiceAccount => projectQuery.Select(p => new ProjectAccess
|
||||
(
|
||||
p.Id,
|
||||
p.ServiceAccountAccessPolicies.Any(ap => ap.ServiceAccountId == userId && ap.Read),
|
||||
p.ServiceAccountAccessPolicies.Any(ap => ap.ServiceAccountId == userId && ap.Write)
|
||||
)),
|
||||
_ => projectQuery.Select(p => new ProjectAccess(p.Id, false, false))
|
||||
};
|
||||
|
||||
private IQueryable<ProjectPermissionDetails> ProjectToPermissionDetails(IQueryable<Project> query, Guid userId, AccessClientType accessType)
|
||||
{
|
||||
var projects = accessType switch
|
||||
@ -199,8 +240,4 @@ public class ProjectRepository : Repository<Core.SecretsManager.Entities.Project
|
||||
|
||||
private static Expression<Func<Project, bool>> ServiceAccountHasReadAccessToProject(Guid serviceAccountId) => p =>
|
||||
p.ServiceAccountAccessPolicies.Any(ap => ap.ServiceAccount.Id == serviceAccountId && ap.Read);
|
||||
|
||||
private static Expression<Func<Project, bool>> ServiceAccountHasWriteAccessToProject(Guid serviceAccountId) => p =>
|
||||
p.ServiceAccountAccessPolicies.Any(ap => ap.ServiceAccount.Id == serviceAccountId && ap.Write);
|
||||
|
||||
}
|
||||
|
@ -43,7 +43,28 @@ public class SecretRepository : Repository<Core.SecretsManager.Entities.Secret,
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<SecretPermissionDetails>> GetManyByOrganizationIdAsync(Guid organizationId, Guid userId, AccessClientType accessType)
|
||||
public async Task<IEnumerable<Core.SecretsManager.Entities.Secret>> GetManyByOrganizationIdAsync(
|
||||
Guid organizationId, Guid userId, AccessClientType accessType)
|
||||
{
|
||||
await using var scope = ServiceScopeFactory.CreateAsyncScope();
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
var query = dbContext.Secret
|
||||
.Include(c => c.Projects)
|
||||
.Where(c => c.OrganizationId == organizationId && c.DeletedDate == null);
|
||||
|
||||
query = accessType switch
|
||||
{
|
||||
AccessClientType.NoAccessCheck => query,
|
||||
AccessClientType.User => query.Where(UserHasReadAccessToSecret(userId)),
|
||||
AccessClientType.ServiceAccount => query.Where(ServiceAccountHasReadAccessToSecret(userId)),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(accessType), accessType, null)
|
||||
};
|
||||
|
||||
var secrets = await query.OrderBy(c => c.RevisionDate).ToListAsync();
|
||||
return Mapper.Map<List<Core.SecretsManager.Entities.Secret>>(secrets);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<SecretPermissionDetails>> GetManyDetailsByOrganizationIdAsync(Guid organizationId, Guid userId, AccessClientType accessType)
|
||||
{
|
||||
using var scope = ServiceScopeFactory.CreateScope();
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
@ -82,7 +103,7 @@ public class SecretRepository : Repository<Core.SecretsManager.Entities.Secret,
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<SecretPermissionDetails>> GetManyByOrganizationIdInTrashAsync(Guid organizationId)
|
||||
public async Task<IEnumerable<SecretPermissionDetails>> GetManyDetailsByOrganizationIdInTrashAsync(Guid organizationId)
|
||||
{
|
||||
using (var scope = ServiceScopeFactory.CreateScope())
|
||||
{
|
||||
@ -103,7 +124,7 @@ public class SecretRepository : Repository<Core.SecretsManager.Entities.Secret,
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<SecretPermissionDetails>> GetManyByProjectIdAsync(Guid projectId, Guid userId, AccessClientType accessType)
|
||||
public async Task<IEnumerable<SecretPermissionDetails>> GetManyDetailsByProjectIdAsync(Guid projectId, Guid userId, AccessClientType accessType)
|
||||
{
|
||||
using var scope = ServiceScopeFactory.CreateScope();
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
@ -115,106 +136,124 @@ public class SecretRepository : Repository<Core.SecretsManager.Entities.Secret,
|
||||
return await secrets.ToListAsync();
|
||||
}
|
||||
|
||||
public override async Task<Core.SecretsManager.Entities.Secret> CreateAsync(Core.SecretsManager.Entities.Secret secret)
|
||||
public override async Task<Core.SecretsManager.Entities.Secret> CreateAsync(
|
||||
Core.SecretsManager.Entities.Secret secret)
|
||||
{
|
||||
using (var scope = ServiceScopeFactory.CreateScope())
|
||||
{
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
secret.SetNewId();
|
||||
var entity = Mapper.Map<Secret>(secret);
|
||||
await using var scope = ServiceScopeFactory.CreateAsyncScope();
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
secret.SetNewId();
|
||||
var entity = Mapper.Map<Secret>(secret);
|
||||
|
||||
if (secret.Projects?.Count > 0)
|
||||
await using var transaction = await dbContext.Database.BeginTransactionAsync();
|
||||
|
||||
if (secret.Projects?.Count > 0)
|
||||
{
|
||||
foreach (var project in entity.Projects)
|
||||
{
|
||||
foreach (var p in entity.Projects)
|
||||
{
|
||||
dbContext.Attach(p);
|
||||
}
|
||||
dbContext.Attach(project);
|
||||
}
|
||||
|
||||
await dbContext.AddAsync(entity);
|
||||
await dbContext.SaveChangesAsync();
|
||||
secret.Id = entity.Id;
|
||||
return secret;
|
||||
var projectIds = entity.Projects.Select(p => p.Id).ToList();
|
||||
await UpdateServiceAccountRevisionsByProjectIdsAsync(dbContext, projectIds);
|
||||
}
|
||||
|
||||
await dbContext.AddAsync(entity);
|
||||
await dbContext.SaveChangesAsync();
|
||||
await transaction.CommitAsync();
|
||||
secret.Id = entity.Id;
|
||||
return secret;
|
||||
}
|
||||
|
||||
public async Task<Core.SecretsManager.Entities.Secret> UpdateAsync(Core.SecretsManager.Entities.Secret secret)
|
||||
{
|
||||
using (var scope = ServiceScopeFactory.CreateScope())
|
||||
await using var scope = ServiceScopeFactory.CreateAsyncScope();
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
var mappedEntity = Mapper.Map<Secret>(secret);
|
||||
await using var transaction = await dbContext.Database.BeginTransactionAsync();
|
||||
|
||||
var entity = await dbContext.Secret
|
||||
.Include(s => s.Projects)
|
||||
.FirstAsync(s => s.Id == secret.Id);
|
||||
|
||||
var projectsToRemove = entity.Projects.Where(p => mappedEntity.Projects.All(mp => mp.Id != p.Id)).ToList();
|
||||
var projectsToAdd = mappedEntity.Projects.Where(p => entity.Projects.All(ep => ep.Id != p.Id)).ToList();
|
||||
|
||||
foreach (var p in projectsToRemove)
|
||||
{
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
var mappedEntity = Mapper.Map<Secret>(secret);
|
||||
|
||||
var entity = await dbContext.Secret
|
||||
.Include("Projects")
|
||||
.FirstAsync(s => s.Id == secret.Id);
|
||||
|
||||
foreach (var p in entity.Projects.Where(p => mappedEntity.Projects.All(mp => mp.Id != p.Id)))
|
||||
{
|
||||
entity.Projects.Remove(p);
|
||||
}
|
||||
|
||||
// Add new relationships
|
||||
foreach (var project in mappedEntity.Projects.Where(p => entity.Projects.All(ep => ep.Id != p.Id)))
|
||||
{
|
||||
var p = dbContext.AttachToOrGet<Project>(_ => _.Id == project.Id, () => project);
|
||||
entity.Projects.Add(p);
|
||||
}
|
||||
|
||||
dbContext.Entry(entity).CurrentValues.SetValues(mappedEntity);
|
||||
await dbContext.SaveChangesAsync();
|
||||
entity.Projects.Remove(p);
|
||||
}
|
||||
|
||||
foreach (var project in projectsToAdd)
|
||||
{
|
||||
var p = dbContext.AttachToOrGet<Project>(x => x.Id == project.Id, () => project);
|
||||
entity.Projects.Add(p);
|
||||
}
|
||||
|
||||
var projectIds = projectsToRemove.Select(p => p.Id).Concat(projectsToAdd.Select(p => p.Id)).ToList();
|
||||
if (projectIds.Count > 0)
|
||||
{
|
||||
await UpdateServiceAccountRevisionsByProjectIdsAsync(dbContext, projectIds);
|
||||
}
|
||||
|
||||
await UpdateServiceAccountRevisionsBySecretIdsAsync(dbContext, [entity.Id]);
|
||||
dbContext.Entry(entity).CurrentValues.SetValues(mappedEntity);
|
||||
await dbContext.SaveChangesAsync();
|
||||
await transaction.CommitAsync();
|
||||
|
||||
return secret;
|
||||
}
|
||||
|
||||
public async Task SoftDeleteManyByIdAsync(IEnumerable<Guid> ids)
|
||||
{
|
||||
using (var scope = ServiceScopeFactory.CreateScope())
|
||||
{
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
var utcNow = DateTime.UtcNow;
|
||||
var secrets = dbContext.Secret.Where(c => ids.Contains(c.Id));
|
||||
await secrets.ForEachAsync(secret =>
|
||||
{
|
||||
dbContext.Attach(secret);
|
||||
secret.DeletedDate = utcNow;
|
||||
secret.RevisionDate = utcNow;
|
||||
});
|
||||
await dbContext.SaveChangesAsync();
|
||||
}
|
||||
await using var scope = ServiceScopeFactory.CreateAsyncScope();
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
await using var transaction = await dbContext.Database.BeginTransactionAsync();
|
||||
|
||||
var secretIds = ids.ToList();
|
||||
await UpdateServiceAccountRevisionsBySecretIdsAsync(dbContext, secretIds);
|
||||
|
||||
var utcNow = DateTime.UtcNow;
|
||||
|
||||
await dbContext.Secret.Where(c => secretIds.Contains(c.Id))
|
||||
.ExecuteUpdateAsync(setters =>
|
||||
setters.SetProperty(s => s.RevisionDate, utcNow)
|
||||
.SetProperty(s => s.DeletedDate, utcNow));
|
||||
|
||||
await transaction.CommitAsync();
|
||||
}
|
||||
|
||||
public async Task HardDeleteManyByIdAsync(IEnumerable<Guid> ids)
|
||||
{
|
||||
using (var scope = ServiceScopeFactory.CreateScope())
|
||||
{
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
var secrets = dbContext.Secret.Where(c => ids.Contains(c.Id));
|
||||
await secrets.ForEachAsync(secret =>
|
||||
{
|
||||
dbContext.Attach(secret);
|
||||
dbContext.Remove(secret);
|
||||
});
|
||||
await dbContext.SaveChangesAsync();
|
||||
}
|
||||
await using var scope = ServiceScopeFactory.CreateAsyncScope();
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
await using var transaction = await dbContext.Database.BeginTransactionAsync();
|
||||
|
||||
var secretIds = ids.ToList();
|
||||
await UpdateServiceAccountRevisionsBySecretIdsAsync(dbContext, secretIds);
|
||||
|
||||
await dbContext.Secret.Where(c => secretIds.Contains(c.Id))
|
||||
.ExecuteDeleteAsync();
|
||||
|
||||
await transaction.CommitAsync();
|
||||
}
|
||||
|
||||
public async Task RestoreManyByIdAsync(IEnumerable<Guid> ids)
|
||||
{
|
||||
using (var scope = ServiceScopeFactory.CreateScope())
|
||||
{
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
var utcNow = DateTime.UtcNow;
|
||||
var secrets = dbContext.Secret.Where(c => ids.Contains(c.Id));
|
||||
await secrets.ForEachAsync(secret =>
|
||||
{
|
||||
dbContext.Attach(secret);
|
||||
secret.DeletedDate = null;
|
||||
secret.RevisionDate = utcNow;
|
||||
});
|
||||
await dbContext.SaveChangesAsync();
|
||||
}
|
||||
await using var scope = ServiceScopeFactory.CreateAsyncScope();
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
await using var transaction = await dbContext.Database.BeginTransactionAsync();
|
||||
|
||||
var secretIds = ids.ToList();
|
||||
await UpdateServiceAccountRevisionsBySecretIdsAsync(dbContext, secretIds);
|
||||
|
||||
var utcNow = DateTime.UtcNow;
|
||||
|
||||
await dbContext.Secret.Where(c => secretIds.Contains(c.Id))
|
||||
.ExecuteUpdateAsync(setters =>
|
||||
setters.SetProperty(s => s.RevisionDate, utcNow)
|
||||
.SetProperty(s => s.DeletedDate, (DateTime?)null));
|
||||
|
||||
await transaction.CommitAsync();
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Core.SecretsManager.Entities.Secret>> ImportAsync(IEnumerable<Core.SecretsManager.Entities.Secret> secrets)
|
||||
@ -248,24 +287,6 @@ public class SecretRepository : Repository<Core.SecretsManager.Entities.Secret,
|
||||
return secrets;
|
||||
}
|
||||
|
||||
public async Task UpdateRevisionDates(IEnumerable<Guid> ids)
|
||||
{
|
||||
using (var scope = ServiceScopeFactory.CreateScope())
|
||||
{
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
var utcNow = DateTime.UtcNow;
|
||||
var secrets = dbContext.Secret.Where(s => ids.Contains(s.Id));
|
||||
|
||||
await secrets.ForEachAsync(secret =>
|
||||
{
|
||||
dbContext.Attach(secret);
|
||||
secret.RevisionDate = utcNow;
|
||||
});
|
||||
|
||||
await dbContext.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<(bool Read, bool Write)> AccessToSecretAsync(Guid id, Guid userId, AccessClientType accessType)
|
||||
{
|
||||
using var scope = ServiceScopeFactory.CreateScope();
|
||||
@ -357,4 +378,60 @@ public class SecretRepository : Repository<Core.SecretsManager.Entities.Secret,
|
||||
p.UserAccessPolicies.Any(ap => ap.OrganizationUser.UserId == userId && ap.Read) ||
|
||||
p.GroupAccessPolicies.Any(ap =>
|
||||
ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.UserId == userId && ap.Read)));
|
||||
|
||||
private static async Task UpdateServiceAccountRevisionsByProjectIdsAsync(DatabaseContext dbContext,
|
||||
List<Guid> projectIds)
|
||||
{
|
||||
if (projectIds.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var serviceAccountIds = await dbContext.Project.Where(p => projectIds.Contains(p.Id))
|
||||
.Include(p => p.ServiceAccountAccessPolicies)
|
||||
.SelectMany(p => p.ServiceAccountAccessPolicies.Select(ap => ap.ServiceAccountId!.Value))
|
||||
.Distinct()
|
||||
.ToListAsync();
|
||||
|
||||
await UpdateServiceAccountRevisionsAsync(dbContext, serviceAccountIds);
|
||||
}
|
||||
|
||||
private static async Task UpdateServiceAccountRevisionsBySecretIdsAsync(DatabaseContext dbContext,
|
||||
List<Guid> secretIds)
|
||||
{
|
||||
if (secretIds.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var projectAccessServiceAccountIds = await dbContext.Secret
|
||||
.Where(s => secretIds.Contains(s.Id))
|
||||
.SelectMany(s =>
|
||||
s.Projects.SelectMany(p => p.ServiceAccountAccessPolicies.Select(ap => ap.ServiceAccountId!.Value)))
|
||||
.Distinct()
|
||||
.ToListAsync();
|
||||
|
||||
var directAccessServiceAccountIds = await dbContext.Secret
|
||||
.Where(s => secretIds.Contains(s.Id))
|
||||
.SelectMany(s => s.ServiceAccountAccessPolicies.Select(ap => ap.ServiceAccountId!.Value))
|
||||
.Distinct()
|
||||
.ToListAsync();
|
||||
|
||||
var serviceAccountIds =
|
||||
directAccessServiceAccountIds.Concat(projectAccessServiceAccountIds).Distinct().ToList();
|
||||
|
||||
await UpdateServiceAccountRevisionsAsync(dbContext, serviceAccountIds);
|
||||
}
|
||||
|
||||
private static async Task UpdateServiceAccountRevisionsAsync(DatabaseContext dbContext,
|
||||
List<Guid> serviceAccountIds)
|
||||
{
|
||||
if (serviceAccountIds.Count > 0)
|
||||
{
|
||||
var utcNow = DateTime.UtcNow;
|
||||
await dbContext.ServiceAccount
|
||||
.Where(sa => serviceAccountIds.Contains(sa.Id))
|
||||
.ExecuteUpdateAsync(setters => setters.SetProperty(b => b.RevisionDate, utcNow));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -43,28 +43,6 @@ public class ServiceAccountRepository : Repository<Core.SecretsManager.Entities.
|
||||
return Mapper.Map<List<Core.SecretsManager.Entities.ServiceAccount>>(serviceAccounts);
|
||||
}
|
||||
|
||||
public async Task<bool> UserHasReadAccessToServiceAccount(Guid id, Guid userId)
|
||||
{
|
||||
using var scope = ServiceScopeFactory.CreateScope();
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
var query = dbContext.ServiceAccount
|
||||
.Where(sa => sa.Id == id)
|
||||
.Where(UserHasReadAccessToServiceAccount(userId));
|
||||
|
||||
return await query.AnyAsync();
|
||||
}
|
||||
|
||||
public async Task<bool> UserHasWriteAccessToServiceAccount(Guid id, Guid userId)
|
||||
{
|
||||
using var scope = ServiceScopeFactory.CreateScope();
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
var query = dbContext.ServiceAccount
|
||||
.Where(sa => sa.Id == id)
|
||||
.Where(UserHasWriteAccessToServiceAccount(userId));
|
||||
|
||||
return await query.AnyAsync();
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Core.SecretsManager.Entities.ServiceAccount>> GetManyByOrganizationIdWriteAccessAsync(Guid organizationId, Guid userId, AccessClientType accessType)
|
||||
{
|
||||
using var scope = ServiceScopeFactory.CreateScope();
|
||||
@ -112,30 +90,29 @@ public class ServiceAccountRepository : Repository<Core.SecretsManager.Entities.
|
||||
public async Task<(bool Read, bool Write)> AccessToServiceAccountAsync(Guid id, Guid userId,
|
||||
AccessClientType accessType)
|
||||
{
|
||||
using var scope = ServiceScopeFactory.CreateScope();
|
||||
await using var scope = ServiceScopeFactory.CreateAsyncScope();
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
|
||||
var serviceAccount = dbContext.ServiceAccount.Where(sa => sa.Id == id);
|
||||
var serviceAccountQuery = dbContext.ServiceAccount.Where(sa => sa.Id == id);
|
||||
|
||||
var query = accessType switch
|
||||
{
|
||||
AccessClientType.NoAccessCheck => serviceAccount.Select(_ => new { Read = true, Write = true }),
|
||||
AccessClientType.User => serviceAccount.Select(sa => new
|
||||
{
|
||||
Read = sa.UserAccessPolicies.Any(ap => ap.OrganizationUser.User.Id == userId && ap.Read) ||
|
||||
sa.GroupAccessPolicies.Any(ap =>
|
||||
ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.User.Id == userId && ap.Read)),
|
||||
Write = sa.UserAccessPolicies.Any(ap => ap.OrganizationUser.User.Id == userId && ap.Write) ||
|
||||
sa.GroupAccessPolicies.Any(ap =>
|
||||
ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.User.Id == userId && ap.Write)),
|
||||
}),
|
||||
AccessClientType.ServiceAccount => serviceAccount.Select(_ => new { Read = false, Write = false }),
|
||||
_ => serviceAccount.Select(_ => new { Read = false, Write = false }),
|
||||
};
|
||||
var accessQuery = BuildServiceAccountAccessQuery(serviceAccountQuery, userId, accessType);
|
||||
var access = await accessQuery.FirstOrDefaultAsync();
|
||||
|
||||
var policy = await query.FirstOrDefaultAsync();
|
||||
return access == null ? (false, false) : (access.Read, access.Write);
|
||||
}
|
||||
|
||||
return policy == null ? (false, false) : (policy.Read, policy.Write);
|
||||
public async Task<Dictionary<Guid, (bool Read, bool Write)>> AccessToServiceAccountsAsync(
|
||||
IEnumerable<Guid> ids,
|
||||
Guid userId,
|
||||
AccessClientType accessType)
|
||||
{
|
||||
await using var scope = ServiceScopeFactory.CreateAsyncScope();
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
|
||||
var serviceAccountsQuery = dbContext.ServiceAccount.Where(p => ids.Contains(p.Id));
|
||||
var accessQuery = BuildServiceAccountAccessQuery(serviceAccountsQuery, userId, accessType);
|
||||
|
||||
return await accessQuery.ToDictionaryAsync(access => access.Id, access => (access.Read, access.Write));
|
||||
}
|
||||
|
||||
public async Task<int> GetServiceAccountCountByOrganizationIdAsync(Guid organizationId)
|
||||
@ -148,6 +125,15 @@ public class ServiceAccountRepository : Repository<Core.SecretsManager.Entities.
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> ServiceAccountsAreInOrganizationAsync(List<Guid> serviceAccountIds, Guid organizationId)
|
||||
{
|
||||
await using var scope = ServiceScopeFactory.CreateAsyncScope();
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
var result = await dbContext.ServiceAccount.CountAsync(sa =>
|
||||
sa.OrganizationId == organizationId && serviceAccountIds.Contains(sa.Id));
|
||||
return serviceAccountIds.Count == result;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<ServiceAccountSecretsDetails>> GetManyByOrganizationIdWithSecretsDetailsAsync(
|
||||
Guid organizationId, Guid userId, AccessClientType accessType)
|
||||
{
|
||||
@ -186,6 +172,27 @@ public class ServiceAccountRepository : Repository<Core.SecretsManager.Entities.
|
||||
return results;
|
||||
}
|
||||
|
||||
private record ServiceAccountAccess(Guid Id, bool Read, bool Write);
|
||||
|
||||
private static IQueryable<ServiceAccountAccess> BuildServiceAccountAccessQuery(IQueryable<ServiceAccount> serviceAccountQuery, Guid userId,
|
||||
AccessClientType accessType) =>
|
||||
accessType switch
|
||||
{
|
||||
AccessClientType.NoAccessCheck => serviceAccountQuery.Select(sa => new ServiceAccountAccess(sa.Id, true, true)),
|
||||
AccessClientType.User => serviceAccountQuery.Select(sa => new ServiceAccountAccess
|
||||
(
|
||||
sa.Id,
|
||||
sa.UserAccessPolicies.Any(ap => ap.OrganizationUser.User.Id == userId && ap.Read) ||
|
||||
sa.GroupAccessPolicies.Any(ap =>
|
||||
ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.User.Id == userId && ap.Read)),
|
||||
sa.UserAccessPolicies.Any(ap => ap.OrganizationUser.User.Id == userId && ap.Write) ||
|
||||
sa.GroupAccessPolicies.Any(ap =>
|
||||
ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.User.Id == userId && ap.Write))
|
||||
)),
|
||||
AccessClientType.ServiceAccount => serviceAccountQuery.Select(sa => new ServiceAccountAccess(sa.Id, false, false)),
|
||||
_ => serviceAccountQuery.Select(sa => new ServiceAccountAccess(sa.Id, false, false))
|
||||
};
|
||||
|
||||
private static Expression<Func<ServiceAccount, bool>> UserHasReadAccessToServiceAccount(Guid userId) => sa =>
|
||||
sa.UserAccessPolicies.Any(ap => ap.OrganizationUser.User.Id == userId && ap.Read) ||
|
||||
sa.GroupAccessPolicies.Any(ap => ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.User.Id == userId && ap.Read));
|
||||
|
@ -30,10 +30,6 @@
|
||||
"connectionString": "SECRET",
|
||||
"applicationCacheTopicName": "SECRET"
|
||||
},
|
||||
"documentDb": {
|
||||
"uri": "SECRET",
|
||||
"key": "SECRET"
|
||||
},
|
||||
"sentry": {
|
||||
"dsn": "SECRET"
|
||||
},
|
||||
@ -58,6 +54,5 @@
|
||||
"region": "SECRET"
|
||||
}
|
||||
},
|
||||
"scimSettings": {
|
||||
}
|
||||
"scimSettings": {}
|
||||
}
|
||||
|
@ -19,13 +19,13 @@ using Bit.Core.Utilities;
|
||||
using Bit.Sso.Models;
|
||||
using Bit.Sso.Utilities;
|
||||
using Duende.IdentityServer;
|
||||
using Duende.IdentityServer.Extensions;
|
||||
using Duende.IdentityServer.Services;
|
||||
using Duende.IdentityServer.Stores;
|
||||
using IdentityModel;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using AuthenticationSchemes = Bit.Core.AuthenticationSchemes;
|
||||
using DIM = Duende.IdentityServer.Models;
|
||||
|
||||
namespace Bit.Sso.Controllers;
|
||||
@ -483,7 +483,7 @@ public class AccountController : Controller
|
||||
if (orgUser.Status == OrganizationUserStatusType.Invited)
|
||||
{
|
||||
// Org User is invited - they must manually accept the invite via email and authenticate with MP
|
||||
throw new Exception(_i18nService.T("UserAlreadyInvited", email, organization.Name));
|
||||
throw new Exception(_i18nService.T("UserAlreadyInvited", email, organization.DisplayName()));
|
||||
}
|
||||
|
||||
// Accepted or Confirmed - create SSO link and return;
|
||||
@ -516,7 +516,7 @@ public class AccountController : Controller
|
||||
await _organizationService.AdjustSeatsAsync(orgId, initialSeatCount - organization.Seats.Value, prorationDate);
|
||||
}
|
||||
_logger.LogInformation(e, "SSO auto provisioning failed");
|
||||
throw new Exception(_i18nService.T("NoSeatsAvailable", organization.Name));
|
||||
throw new Exception(_i18nService.T("NoSeatsAvailable", organization.DisplayName()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -703,8 +703,10 @@ public class AccountController : Controller
|
||||
var idp = User.FindFirst(JwtClaimTypes.IdentityProvider)?.Value;
|
||||
if (idp != null && idp != IdentityServerConstants.LocalIdentityProvider)
|
||||
{
|
||||
var providerSupportsSignout = await HttpContext.GetSchemeSupportsSignOutAsync(idp);
|
||||
if (providerSupportsSignout)
|
||||
var provider = HttpContext.RequestServices.GetRequiredService<IAuthenticationHandlerProvider>();
|
||||
var handler = await provider.GetHandlerAsync(HttpContext, idp);
|
||||
|
||||
if (handler is IAuthenticationSignOutHandler)
|
||||
{
|
||||
if (logoutId == null)
|
||||
{
|
||||
|
@ -8,7 +8,7 @@
|
||||
<PropertyGroup Condition=" '$(RunConfiguration)' == 'Sso-SelfHost' " />
|
||||
<ItemGroup>
|
||||
<!-- This is a transitive dependency to Sustainsys.Saml2.AspNetCore2 -->
|
||||
<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.1.22" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.2.2" />
|
||||
|
||||
<PackageReference Include="Sustainsys.Saml2.AspNetCore2" Version="2.9.2" />
|
||||
</ItemGroup>
|
||||
|
@ -6,7 +6,7 @@ using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.SharedWeb.Utilities;
|
||||
using Bit.Sso.Utilities;
|
||||
using Duende.IdentityServer.Extensions;
|
||||
using Duende.IdentityServer.Services;
|
||||
using Microsoft.IdentityModel.Logging;
|
||||
using Stripe;
|
||||
|
||||
@ -108,7 +108,7 @@ public class Startup
|
||||
var uri = new Uri(globalSettings.BaseServiceUri.Sso);
|
||||
app.Use(async (ctx, next) =>
|
||||
{
|
||||
ctx.SetIdentityServerOrigin($"{uri.Scheme}://{uri.Host}");
|
||||
ctx.RequestServices.GetRequiredService<IServerUrls>().Origin = $"{uri.Scheme}://{uri.Host}";
|
||||
await next();
|
||||
});
|
||||
}
|
||||
|
@ -31,10 +31,6 @@
|
||||
"connectionString": "SECRET",
|
||||
"applicationCacheTopicName": "SECRET"
|
||||
},
|
||||
"documentDb": {
|
||||
"uri": "SECRET",
|
||||
"key": "SECRET"
|
||||
},
|
||||
"notificationHub": {
|
||||
"connectionString": "SECRET",
|
||||
"hubName": "SECRET"
|
||||
|
74
bitwarden_license/src/Sso/package-lock.json
generated
74
bitwarden_license/src/Sso/package-lock.json
generated
@ -9,15 +9,15 @@
|
||||
"version": "0.0.0",
|
||||
"license": "-",
|
||||
"devDependencies": {
|
||||
"bootstrap": "4.5.0",
|
||||
"del": "6.0.0",
|
||||
"bootstrap": "4.6.2",
|
||||
"del": "6.1.1",
|
||||
"font-awesome": "4.7.0",
|
||||
"gulp": "4.0.2",
|
||||
"gulp-sass": "5.1.0",
|
||||
"jquery": "3.5.1",
|
||||
"jquery": "3.7.1",
|
||||
"merge-stream": "2.0.0",
|
||||
"popper.js": "1.16.1",
|
||||
"sass": "1.49.7"
|
||||
"sass": "1.75.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@nodelib/fs.scandir": {
|
||||
@ -598,17 +598,23 @@
|
||||
}
|
||||
},
|
||||
"node_modules/bootstrap": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.5.0.tgz",
|
||||
"integrity": "sha512-Z93QoXvodoVslA+PWNdk23Hze4RBYIkpb5h8I2HY2Tu2h7A0LpAgLcyrhrSUyo2/Oxm2l1fRZPs1e5hnxnliXA==",
|
||||
"version": "4.6.2",
|
||||
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.6.2.tgz",
|
||||
"integrity": "sha512-51Bbp/Uxr9aTuy6ca/8FbFloBUJZLHwnhTcnjIeRn2suQWsWzcuJhGjKDB5eppVte/8oCdOL3VuwxvZDUggwGQ==",
|
||||
"dev": true,
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/bootstrap"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/twbs"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/bootstrap"
|
||||
}
|
||||
],
|
||||
"peerDependencies": {
|
||||
"jquery": "1.9.1 - 3",
|
||||
"popper.js": "^1.16.0"
|
||||
"popper.js": "^1.16.1"
|
||||
}
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
@ -1028,9 +1034,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/del": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/del/-/del-6.0.0.tgz",
|
||||
"integrity": "sha512-1shh9DQ23L16oXSZKB2JxpL7iMy2E0S9d517ptA1P8iw0alkPtQcrKH7ru31rYtKwF499HkTu+DRzq3TCKDFRQ==",
|
||||
"version": "6.1.1",
|
||||
"resolved": "https://registry.npmjs.org/del/-/del-6.1.1.tgz",
|
||||
"integrity": "sha512-ua8BhapfP0JUJKC/zV9yHHDW/rDoDxP4Zhn3AkA6/xT6gY7jYXJiaeyBZznYVujhZZET+UgcbZiQ7sN3WqcImg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"globby": "^11.0.1",
|
||||
@ -2383,9 +2389,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/jquery": {
|
||||
"version": "3.5.1",
|
||||
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.5.1.tgz",
|
||||
"integrity": "sha512-XwIBPqcMn57FxfT+Go5pzySnm4KWkT1Tv7gjrpT1srtf8Weynl6R273VJ5GjkRb51IzMp5nbaPjJXMWeju2MKg==",
|
||||
"version": "3.7.1",
|
||||
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz",
|
||||
"integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/json-stable-stringify-without-jsonify": {
|
||||
@ -3946,9 +3952,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/sass": {
|
||||
"version": "1.49.7",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.49.7.tgz",
|
||||
"integrity": "sha512-13dml55EMIR2rS4d/RDHHP0sXMY3+30e1TKsyXaSz3iLWVoDWEoboY8WzJd5JMnxrRHffKO3wq2mpJ0jxRJiEQ==",
|
||||
"version": "1.75.0",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.75.0.tgz",
|
||||
"integrity": "sha512-ShMYi3WkrDWxExyxSZPst4/okE9ts46xZmJDSawJQrnte7M1V9fScVB+uNXOVKRBt0PggHOwoZcn8mYX4trnBw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"chokidar": ">=3.0.0 <4.0.0",
|
||||
@ -3959,7 +3965,7 @@
|
||||
"sass": "sass.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/sass/node_modules/anymatch": {
|
||||
@ -3976,12 +3982,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/sass/node_modules/binary-extensions": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
|
||||
"integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
||||
"integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/sass/node_modules/braces": {
|
||||
@ -3997,16 +4006,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/sass/node_modules/chokidar": {
|
||||
"version": "3.5.3",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
|
||||
"integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
||||
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"anymatch": "~3.1.2",
|
||||
"braces": "~3.0.2",
|
||||
@ -4019,6 +4022,9 @@
|
||||
"engines": {
|
||||
"node": ">= 8.10.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
|
@ -8,14 +8,14 @@
|
||||
"build": "gulp build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"bootstrap": "4.5.0",
|
||||
"del": "6.0.0",
|
||||
"bootstrap": "4.6.2",
|
||||
"del": "6.1.1",
|
||||
"font-awesome": "4.7.0",
|
||||
"gulp": "4.0.2",
|
||||
"gulp-sass": "5.1.0",
|
||||
"jquery": "3.5.1",
|
||||
"jquery": "3.7.1",
|
||||
"merge-stream": "2.0.0",
|
||||
"popper.js": "1.16.1",
|
||||
"sass": "1.49.7"
|
||||
"sass": "1.75.0"
|
||||
}
|
||||
}
|
||||
|
@ -22,7 +22,7 @@ public class CreateProviderCommandTests
|
||||
provider.Type = ProviderType.Msp;
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.CreateMspAsync(provider, default));
|
||||
() => sutProvider.Sut.CreateMspAsync(provider, default, default, default));
|
||||
Assert.Contains("Invalid owner.", exception.Message);
|
||||
}
|
||||
|
||||
@ -34,7 +34,7 @@ public class CreateProviderCommandTests
|
||||
var userRepository = sutProvider.GetDependency<IUserRepository>();
|
||||
userRepository.GetByEmailAsync(user.Email).Returns(user);
|
||||
|
||||
await sutProvider.Sut.CreateMspAsync(provider, user.Email);
|
||||
await sutProvider.Sut.CreateMspAsync(provider, user.Email, default, default);
|
||||
|
||||
await sutProvider.GetDependency<IProviderRepository>().ReceivedWithAnyArgs().CreateAsync(default);
|
||||
await sutProvider.GetDependency<IProviderService>().Received(1).SendProviderSetupInviteEmailAsync(provider, user.Email);
|
||||
|
@ -1,7 +1,10 @@
|
||||
using Bit.Commercial.Core.AdminConsole.Providers;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Billing.Commands;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
@ -11,6 +14,7 @@ using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using Stripe;
|
||||
using Xunit;
|
||||
using IMailService = Bit.Core.Services.IMailService;
|
||||
|
||||
namespace Bit.Commercial.Core.Test.AdminConsole.ProviderFeatures;
|
||||
|
||||
@ -81,12 +85,15 @@ public class RemoveOrganizationFromProviderCommandTests
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task RemoveOrganizationFromProvider_MakesCorrectInvocations(
|
||||
public async Task RemoveOrganizationFromProvider_NoStripeObjects_MakesCorrectInvocations(
|
||||
Provider provider,
|
||||
ProviderOrganization providerOrganization,
|
||||
Organization organization,
|
||||
SutProvider<RemoveOrganizationFromProviderCommand> sutProvider)
|
||||
{
|
||||
organization.GatewayCustomerId = null;
|
||||
organization.GatewaySubscriptionId = null;
|
||||
|
||||
providerOrganization.ProviderId = provider.Id;
|
||||
|
||||
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
||||
@ -108,19 +115,125 @@ public class RemoveOrganizationFromProviderCommandTests
|
||||
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
|
||||
await stripeAdapter.DidNotReceiveWithAnyArgs().CustomerUpdateAsync(Arg.Any<string>(), Arg.Any<CustomerUpdateOptions>());
|
||||
|
||||
await stripeAdapter.DidNotReceiveWithAnyArgs().SubscriptionUpdateAsync(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>());
|
||||
|
||||
await sutProvider.GetDependency<IMailService>().DidNotReceiveWithAnyArgs().SendProviderUpdatePaymentMethod(
|
||||
Arg.Any<Guid>(),
|
||||
Arg.Any<string>(),
|
||||
Arg.Any<string>(),
|
||||
Arg.Any<IEnumerable<string>>());
|
||||
|
||||
await sutProvider.GetDependency<IProviderOrganizationRepository>().Received(1)
|
||||
.DeleteAsync(providerOrganization);
|
||||
|
||||
await sutProvider.GetDependency<IEventService>().Received(1).LogProviderOrganizationEventAsync(
|
||||
providerOrganization,
|
||||
EventType.ProviderOrganization_Removed);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task RemoveOrganizationFromProvider_MakesCorrectInvocations__FeatureFlagOff(
|
||||
Provider provider,
|
||||
ProviderOrganization providerOrganization,
|
||||
Organization organization,
|
||||
SutProvider<RemoveOrganizationFromProviderCommand> sutProvider)
|
||||
{
|
||||
providerOrganization.ProviderId = provider.Id;
|
||||
|
||||
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
||||
|
||||
sutProvider.GetDependency<IOrganizationService>().HasConfirmedOwnersExceptAsync(
|
||||
providerOrganization.OrganizationId,
|
||||
Array.Empty<Guid>(),
|
||||
includeProvider: false)
|
||||
.Returns(true);
|
||||
|
||||
var organizationOwnerEmails = new List<string> { "a@example.com", "b@example.com" };
|
||||
|
||||
organizationRepository.GetOwnerEmailAddressesById(organization.Id).Returns(organizationOwnerEmails);
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
stripeAdapter.SubscriptionCreateAsync(default).ReturnsForAnyArgs(new Stripe.Subscription
|
||||
{
|
||||
Id = "S-1",
|
||||
CurrentPeriodEnd = DateTime.Today.AddDays(10),
|
||||
});
|
||||
|
||||
await sutProvider.Sut.RemoveOrganizationFromProvider(provider, providerOrganization, organization);
|
||||
|
||||
await organizationRepository.Received(1).ReplaceAsync(Arg.Is<Organization>(
|
||||
org => org.Id == organization.Id && org.BillingEmail == "a@example.com"));
|
||||
|
||||
await stripeAdapter.Received(1).CustomerUpdateAsync(
|
||||
organization.GatewayCustomerId, Arg.Is<CustomerUpdateOptions>(
|
||||
options => options.Coupon == string.Empty && options.Email == "a@gmail.com"));
|
||||
|
||||
await stripeAdapter.Received(1).SubscriptionUpdateAsync(
|
||||
organization.GatewaySubscriptionId, Arg.Is<SubscriptionUpdateOptions>(
|
||||
options => options.CollectionMethod == "send_invoice" && options.DaysUntilDue == 30));
|
||||
options => options.Coupon == string.Empty && options.Email == "a@example.com"));
|
||||
|
||||
await sutProvider.GetDependency<IMailService>().Received(1).SendProviderUpdatePaymentMethod(
|
||||
organization.Id,
|
||||
organization.Name,
|
||||
provider.Name,
|
||||
Arg.Is<IEnumerable<string>>(emails => emails.Contains("a@gmail.com") && emails.Contains("b@gmail.com")));
|
||||
Arg.Is<IEnumerable<string>>(emails => emails.Contains("a@example.com") && emails.Contains("b@example.com")));
|
||||
|
||||
await sutProvider.GetDependency<IProviderOrganizationRepository>().Received(1)
|
||||
.DeleteAsync(providerOrganization);
|
||||
|
||||
await sutProvider.GetDependency<IEventService>().Received(1).LogProviderOrganizationEventAsync(
|
||||
providerOrganization,
|
||||
EventType.ProviderOrganization_Removed);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task RemoveOrganizationFromProvider_CreatesSubscriptionAndScalesSeats_FeatureFlagON(Provider provider,
|
||||
ProviderOrganization providerOrganization,
|
||||
Organization organization,
|
||||
SutProvider<RemoveOrganizationFromProviderCommand> sutProvider)
|
||||
{
|
||||
providerOrganization.ProviderId = provider.Id;
|
||||
provider.Status = ProviderStatusType.Billable;
|
||||
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
||||
sutProvider.GetDependency<IOrganizationService>().HasConfirmedOwnersExceptAsync(
|
||||
providerOrganization.OrganizationId,
|
||||
Array.Empty<Guid>(),
|
||||
includeProvider: false)
|
||||
.Returns(true);
|
||||
|
||||
var organizationOwnerEmails = new List<string> { "a@example.com", "b@example.com" };
|
||||
|
||||
organizationRepository.GetOwnerEmailAddressesById(organization.Id).Returns(organizationOwnerEmails);
|
||||
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
stripeAdapter.SubscriptionCreateAsync(default).ReturnsForAnyArgs(new Stripe.Subscription
|
||||
{
|
||||
Id = "S-1",
|
||||
CurrentPeriodEnd = DateTime.Today.AddDays(10),
|
||||
});
|
||||
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling).Returns(true);
|
||||
await sutProvider.Sut.RemoveOrganizationFromProvider(provider, providerOrganization, organization);
|
||||
await stripeAdapter.Received(1).CustomerUpdateAsync(
|
||||
organization.GatewayCustomerId, Arg.Is<CustomerUpdateOptions>(
|
||||
options => options.Coupon == string.Empty && options.Email == "a@example.com"));
|
||||
|
||||
await stripeAdapter.Received(1).SubscriptionCreateAsync(Arg.Is<SubscriptionCreateOptions>(c =>
|
||||
c.Customer == organization.GatewayCustomerId &&
|
||||
c.CollectionMethod == "send_invoice" &&
|
||||
c.DaysUntilDue == 30 &&
|
||||
c.Items.Count == 1
|
||||
));
|
||||
|
||||
await sutProvider.GetDependency<IScaleSeatsCommand>().Received(1)
|
||||
.ScalePasswordManagerSeats(provider, organization.PlanType, -(int)organization.Seats);
|
||||
|
||||
await organizationRepository.Received(1).ReplaceAsync(Arg.Is<Organization>(
|
||||
org => org.Id == organization.Id && org.BillingEmail == "a@example.com" &&
|
||||
org.GatewaySubscriptionId == "S-1"));
|
||||
|
||||
await sutProvider.GetDependency<IMailService>().Received(1).SendProviderUpdatePaymentMethod(
|
||||
organization.Id,
|
||||
organization.Name,
|
||||
provider.Name,
|
||||
Arg.Is<IEnumerable<string>>(emails =>
|
||||
emails.Contains("a@example.com") && emails.Contains("b@example.com")));
|
||||
|
||||
await sutProvider.GetDependency<IProviderOrganizationRepository>().Received(1)
|
||||
.DeleteAsync(providerOrganization);
|
||||
|
@ -1,9 +1,11 @@
|
||||
using Bit.Commercial.Core.AdminConsole.Services;
|
||||
using Bit.Commercial.Core.Test.AdminConsole.AutoFixture;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.AdminConsole.Models.Business.Provider;
|
||||
using Bit.Core.AdminConsole.Models.Business.Tokenables;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
@ -13,6 +15,7 @@ using Bit.Core.Models.Business;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Test.AutoFixture.OrganizationFixtures;
|
||||
using Bit.Core.Tokens;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
@ -458,17 +461,112 @@ public class ProviderServiceTests
|
||||
{
|
||||
organization.PlanType = PlanType.EnterpriseAnnually;
|
||||
|
||||
var providerRepository = sutProvider.GetDependency<IProviderRepository>();
|
||||
providerRepository.GetByIdAsync(provider.Id).Returns(provider);
|
||||
|
||||
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
|
||||
providerOrganizationRepository.GetByOrganizationId(organization.Id).ReturnsNull();
|
||||
|
||||
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
||||
organizationRepository.GetByIdAsync(organization.Id).Returns(organization);
|
||||
|
||||
await sutProvider.Sut.AddOrganization(provider.Id, organization.Id, key);
|
||||
|
||||
await providerOrganizationRepository.Received(1)
|
||||
.CreateAsync(Arg.Is<ProviderOrganization>(providerOrganization =>
|
||||
providerOrganization.ProviderId == provider.Id &&
|
||||
providerOrganization.OrganizationId == organization.Id &&
|
||||
providerOrganization.Key == key));
|
||||
|
||||
await organizationRepository.Received(1)
|
||||
.ReplaceAsync(Arg.Is<Organization>(org => org.BillingEmail == provider.BillingEmail));
|
||||
|
||||
await sutProvider.GetDependency<IStripeAdapter>().Received(1).CustomerUpdateAsync(
|
||||
organization.GatewayCustomerId,
|
||||
Arg.Is<CustomerUpdateOptions>(options => options.Email == provider.BillingEmail));
|
||||
|
||||
await sutProvider.GetDependency<IEventService>()
|
||||
.Received().LogProviderOrganizationEventAsync(Arg.Is<ProviderOrganization>(providerOrganization =>
|
||||
providerOrganization.ProviderId == provider.Id &&
|
||||
providerOrganization.OrganizationId == organization.Id &&
|
||||
providerOrganization.Key == key),
|
||||
EventType.ProviderOrganization_Added);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task AddOrganization_CreateAfterNov62023_PlanTypeDoesNotUpdated(Provider provider, Organization organization, string key,
|
||||
SutProvider<ProviderService> sutProvider)
|
||||
{
|
||||
provider.Type = ProviderType.Msp;
|
||||
|
||||
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
|
||||
|
||||
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
|
||||
var expectedPlanType = PlanType.EnterpriseAnnually;
|
||||
organization.PlanType = PlanType.EnterpriseAnnually;
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
||||
|
||||
await sutProvider.Sut.AddOrganization(provider.Id, organization.Id, key);
|
||||
|
||||
await providerOrganizationRepository.Received(1)
|
||||
.CreateAsync(Arg.Is<ProviderOrganization>(providerOrganization =>
|
||||
providerOrganization.ProviderId == provider.Id &&
|
||||
providerOrganization.OrganizationId == organization.Id &&
|
||||
providerOrganization.Key == key));
|
||||
|
||||
await sutProvider.GetDependency<IEventService>()
|
||||
.Received().LogProviderOrganizationEventAsync(Arg.Is<ProviderOrganization>(providerOrganization =>
|
||||
providerOrganization.ProviderId == provider.Id &&
|
||||
providerOrganization.OrganizationId == organization.Id &&
|
||||
providerOrganization.Key == key),
|
||||
EventType.ProviderOrganization_Added);
|
||||
|
||||
Assert.Equal(organization.PlanType, expectedPlanType);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task AddOrganization_CreateBeforeNov62023_PlanTypeUpdated(Provider provider, Organization organization, string key,
|
||||
SutProvider<ProviderService> sutProvider)
|
||||
{
|
||||
var newCreationDate = new DateTime(2023, 11, 5);
|
||||
BackdateProviderCreationDate(provider, newCreationDate);
|
||||
provider.Type = ProviderType.Msp;
|
||||
|
||||
organization.PlanType = PlanType.EnterpriseAnnually;
|
||||
organization.Plan = "Enterprise (Annually)";
|
||||
|
||||
var expectedPlanType = PlanType.EnterpriseAnnually2020;
|
||||
|
||||
var expectedPlanId = "2020-enterprise-org-seat-annually";
|
||||
|
||||
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
|
||||
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
|
||||
providerOrganizationRepository.GetByOrganizationId(organization.Id).ReturnsNull();
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
||||
var subscriptionItem = GetSubscription(organization.GatewaySubscriptionId);
|
||||
sutProvider.GetDependency<IStripeAdapter>().SubscriptionGetAsync(organization.GatewaySubscriptionId)
|
||||
.Returns(GetSubscription(organization.GatewaySubscriptionId));
|
||||
await sutProvider.GetDependency<IStripeAdapter>().SubscriptionUpdateAsync(
|
||||
organization.GatewaySubscriptionId, SubscriptionUpdateRequest(expectedPlanId, subscriptionItem));
|
||||
|
||||
await sutProvider.Sut.AddOrganization(provider.Id, organization.Id, key);
|
||||
|
||||
await providerOrganizationRepository.ReceivedWithAnyArgs().CreateAsync(default);
|
||||
await providerOrganizationRepository.Received(1)
|
||||
.CreateAsync(Arg.Is<ProviderOrganization>(providerOrganization =>
|
||||
providerOrganization.ProviderId == provider.Id &&
|
||||
providerOrganization.OrganizationId == organization.Id &&
|
||||
providerOrganization.Key == key));
|
||||
|
||||
await sutProvider.GetDependency<IEventService>()
|
||||
.Received().LogProviderOrganizationEventAsync(Arg.Any<ProviderOrganization>(),
|
||||
.Received().LogProviderOrganizationEventAsync(Arg.Is<ProviderOrganization>(providerOrganization =>
|
||||
providerOrganization.ProviderId == provider.Id &&
|
||||
providerOrganization.OrganizationId == organization.Id &&
|
||||
providerOrganization.Key == key),
|
||||
EventType.ProviderOrganization_Added);
|
||||
|
||||
Assert.Equal(organization.PlanType, expectedPlanType);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
@ -543,6 +641,85 @@ public class ProviderServiceTests
|
||||
t.First().Item2 == null));
|
||||
}
|
||||
|
||||
[Theory, OrganizationCustomize(FlexibleCollections = false), BitAutoData]
|
||||
public async Task CreateOrganizationAsync_ConsolidatedBillingEnabled_InvalidPlanType_ThrowsBadRequestException(
|
||||
Provider provider,
|
||||
OrganizationSignup organizationSignup,
|
||||
Organization organization,
|
||||
string clientOwnerEmail,
|
||||
User user,
|
||||
SutProvider<ProviderService> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling).Returns(true);
|
||||
|
||||
provider.Type = ProviderType.Msp;
|
||||
provider.Status = ProviderStatusType.Billable;
|
||||
|
||||
organizationSignup.Plan = PlanType.EnterpriseAnnually;
|
||||
|
||||
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
|
||||
|
||||
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
|
||||
|
||||
sutProvider.GetDependency<IOrganizationService>().SignupClientAsync(organizationSignup)
|
||||
.Returns((organization, null as OrganizationUser, new Collection()));
|
||||
|
||||
await Assert.ThrowsAsync<BadRequestException>(() =>
|
||||
sutProvider.Sut.CreateOrganizationAsync(provider.Id, organizationSignup, clientOwnerEmail, user));
|
||||
|
||||
await providerOrganizationRepository.DidNotReceiveWithAnyArgs().CreateAsync(default);
|
||||
}
|
||||
|
||||
[Theory, OrganizationCustomize(FlexibleCollections = false), BitAutoData]
|
||||
public async Task CreateOrganizationAsync_ConsolidatedBillingEnabled_InvokeSignupClientAsync(
|
||||
Provider provider,
|
||||
OrganizationSignup organizationSignup,
|
||||
Organization organization,
|
||||
string clientOwnerEmail,
|
||||
User user,
|
||||
SutProvider<ProviderService> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling).Returns(true);
|
||||
|
||||
provider.Type = ProviderType.Msp;
|
||||
provider.Status = ProviderStatusType.Billable;
|
||||
|
||||
organizationSignup.Plan = PlanType.EnterpriseMonthly;
|
||||
|
||||
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
|
||||
|
||||
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
|
||||
|
||||
sutProvider.GetDependency<IOrganizationService>().SignupClientAsync(organizationSignup)
|
||||
.Returns((organization, null as OrganizationUser, new Collection()));
|
||||
|
||||
var providerOrganization = await sutProvider.Sut.CreateOrganizationAsync(provider.Id, organizationSignup, clientOwnerEmail, user);
|
||||
|
||||
await providerOrganizationRepository.Received(1).CreateAsync(Arg.Is<ProviderOrganization>(
|
||||
po =>
|
||||
po.ProviderId == provider.Id &&
|
||||
po.OrganizationId == organization.Id));
|
||||
|
||||
await sutProvider.GetDependency<IEventService>()
|
||||
.Received()
|
||||
.LogProviderOrganizationEventAsync(providerOrganization, EventType.ProviderOrganization_Created);
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationService>()
|
||||
.Received()
|
||||
.InviteUsersAsync(
|
||||
organization.Id,
|
||||
user.Id,
|
||||
Arg.Is<IEnumerable<(OrganizationUserInvite, string)>>(
|
||||
t =>
|
||||
t.Count() == 1 &&
|
||||
t.First().Item1.Emails.Count() == 1 &&
|
||||
t.First().Item1.Emails.First() == clientOwnerEmail &&
|
||||
t.First().Item1.Type == OrganizationUserType.Owner &&
|
||||
t.First().Item1.AccessAll &&
|
||||
!t.First().Item1.Collections.Any() &&
|
||||
t.First().Item2 == null));
|
||||
}
|
||||
|
||||
[Theory, OrganizationCustomize(FlexibleCollections = true), BitAutoData]
|
||||
public async Task CreateOrganizationAsync_WithFlexibleCollections_SetsAccessAllToFalse
|
||||
(Provider provider, OrganizationSignup organizationSignup, Organization organization, string clientOwnerEmail,
|
||||
@ -577,62 +754,92 @@ public class ProviderServiceTests
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task AddOrganization_CreateAfterNov62023_PlanTypeDoesNotUpdated(Provider provider, Organization organization, string key,
|
||||
SutProvider<ProviderService> sutProvider)
|
||||
public async Task Delete_Success(Provider provider, SutProvider<ProviderService> sutProvider)
|
||||
{
|
||||
provider.Type = ProviderType.Msp;
|
||||
var providerRepository = sutProvider.GetDependency<IProviderRepository>();
|
||||
var applicationCacheService = sutProvider.GetDependency<IApplicationCacheService>();
|
||||
|
||||
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
|
||||
await sutProvider.Sut.DeleteAsync(provider);
|
||||
|
||||
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
|
||||
var expectedPlanType = PlanType.EnterpriseAnnually;
|
||||
organization.PlanType = PlanType.EnterpriseAnnually;
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
||||
|
||||
await sutProvider.Sut.AddOrganization(provider.Id, organization.Id, key);
|
||||
|
||||
await providerOrganizationRepository.ReceivedWithAnyArgs().CreateAsync(default);
|
||||
await sutProvider.GetDependency<IEventService>()
|
||||
.Received().LogProviderOrganizationEventAsync(Arg.Any<ProviderOrganization>(),
|
||||
EventType.ProviderOrganization_Added);
|
||||
Assert.Equal(organization.PlanType, expectedPlanType);
|
||||
await providerRepository.Received().DeleteAsync(provider);
|
||||
await applicationCacheService.Received().DeleteProviderAbilityAsync(provider.Id);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task AddOrganization_CreateBeforeNov62023_PlanTypeUpdated(Provider provider, Organization organization, string key,
|
||||
SutProvider<ProviderService> sutProvider)
|
||||
public async Task InitiateDeleteAsync_ThrowsBadRequestException_WhenProviderNameIsEmpty(string providerAdminEmail, SutProvider<ProviderService> sutProvider)
|
||||
{
|
||||
var newCreationDate = new DateTime(2023, 11, 5);
|
||||
BackdateProviderCreationDate(provider, newCreationDate);
|
||||
provider.Type = ProviderType.Msp;
|
||||
var provider = new Provider { Name = "" };
|
||||
await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.InitiateDeleteAsync(provider, providerAdminEmail));
|
||||
}
|
||||
|
||||
organization.PlanType = PlanType.EnterpriseAnnually;
|
||||
organization.Plan = "Enterprise (Annually)";
|
||||
[Theory, BitAutoData]
|
||||
public async Task InitiateDeleteAsync_ThrowsBadRequestException_WhenProviderAdminNotFound(Provider provider, SutProvider<ProviderService> sutProvider)
|
||||
{
|
||||
var providerAdminEmail = "nonexistent@example.com";
|
||||
var userRepository = sutProvider.GetDependency<IUserRepository>();
|
||||
userRepository.GetByEmailAsync(providerAdminEmail).Returns(Task.FromResult<User>(null));
|
||||
|
||||
var expectedPlanType = PlanType.EnterpriseAnnually2020;
|
||||
await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.InitiateDeleteAsync(provider, providerAdminEmail));
|
||||
}
|
||||
|
||||
var expectedPlanId = "2020-enterprise-org-seat-annually";
|
||||
[Theory, BitAutoData]
|
||||
public async Task InitiateDeleteAsync_ThrowsBadRequestException_WhenProviderAdminStatusIsNotConfirmed(
|
||||
Provider provider
|
||||
, User providerAdmin
|
||||
, ProviderUser providerUser
|
||||
, SutProvider<ProviderService> sutProvider)
|
||||
{
|
||||
var providerAdminEmail = "nonexistent@example.com";
|
||||
providerUser.Status = ProviderUserStatusType.Confirmed;
|
||||
providerUser.Type = ProviderUserType.ServiceUser;
|
||||
|
||||
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
|
||||
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
|
||||
providerOrganizationRepository.GetByOrganizationId(organization.Id).ReturnsNull();
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
||||
var userRepository = sutProvider.GetDependency<IUserRepository>();
|
||||
userRepository.GetByEmailAsync(providerAdminEmail).Returns(Task.FromResult<User>(providerAdmin));
|
||||
var providerUserRepository = sutProvider.GetDependency<IProviderUserRepository>();
|
||||
providerUserRepository.GetByProviderUserAsync(provider.Id, providerAdmin.Id).Returns(providerUser);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
||||
var subscriptionItem = GetSubscription(organization.GatewaySubscriptionId);
|
||||
sutProvider.GetDependency<IStripeAdapter>().SubscriptionGetAsync(organization.GatewaySubscriptionId)
|
||||
.Returns(GetSubscription(organization.GatewaySubscriptionId));
|
||||
await sutProvider.GetDependency<IStripeAdapter>().SubscriptionUpdateAsync(
|
||||
organization.GatewaySubscriptionId, SubscriptionUpdateRequest(expectedPlanId, subscriptionItem));
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.InitiateDeleteAsync(provider, providerAdminEmail));
|
||||
Assert.Contains("Org admin not found.", exception.Message);
|
||||
|
||||
await sutProvider.Sut.AddOrganization(provider.Id, organization.Id, key);
|
||||
}
|
||||
|
||||
await providerOrganizationRepository.ReceivedWithAnyArgs().CreateAsync(default);
|
||||
await sutProvider.GetDependency<IEventService>()
|
||||
.Received().LogProviderOrganizationEventAsync(Arg.Any<ProviderOrganization>(),
|
||||
EventType.ProviderOrganization_Added);
|
||||
[Theory, BitAutoData]
|
||||
public async Task InitiateDeleteAsync_SendsInitiateDeleteProviderEmail(Provider provider, User providerAdmin
|
||||
, ProviderUser providerUser, SutProvider<ProviderService> sutProvider)
|
||||
{
|
||||
var providerAdminEmail = providerAdmin.Email;
|
||||
providerUser.Status = ProviderUserStatusType.Confirmed;
|
||||
providerUser.Type = ProviderUserType.ProviderAdmin;
|
||||
|
||||
Assert.Equal(organization.PlanType, expectedPlanType);
|
||||
var userRepository = sutProvider.GetDependency<IUserRepository>();
|
||||
userRepository.GetByEmailAsync(providerAdminEmail).Returns(Task.FromResult<User>(providerAdmin));
|
||||
var providerUserRepository = sutProvider.GetDependency<IProviderUserRepository>();
|
||||
providerUserRepository.GetByProviderUserAsync(provider.Id, providerAdmin.Id).Returns(providerUser);
|
||||
var mailService = sutProvider.GetDependency<IMailService>();
|
||||
|
||||
await sutProvider.Sut.InitiateDeleteAsync(provider, providerAdminEmail);
|
||||
await mailService.Received().SendInitiateDeletProviderEmailAsync(providerAdminEmail, provider, Arg.Any<string>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task DeleteAsync_ThrowsBadRequestException_WhenInvalidToken(Provider provider, string invalidToken
|
||||
, SutProvider<ProviderService> sutProvider)
|
||||
{
|
||||
var providerDeleteTokenDataFactory = sutProvider.GetDependency<IDataProtectorTokenFactory<ProviderDeleteTokenable>>();
|
||||
providerDeleteTokenDataFactory.TryUnprotect(invalidToken, out Arg.Any<ProviderDeleteTokenable>()).Returns(false);
|
||||
|
||||
await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.DeleteAsync(provider, invalidToken));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task DeleteAsync_ThrowsBadRequestException_WhenInvalidTokenData(Provider provider, string validToken
|
||||
, SutProvider<ProviderService> sutProvider)
|
||||
{
|
||||
var validTokenData = new ProviderDeleteTokenable();
|
||||
var providerDeleteTokenDataFactory = sutProvider.GetDependency<IDataProtectorTokenFactory<ProviderDeleteTokenable>>();
|
||||
providerDeleteTokenDataFactory.TryUnprotect(validToken, out validTokenData).Returns(false);
|
||||
|
||||
await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.DeleteAsync(provider, validToken));
|
||||
}
|
||||
|
||||
private static SubscriptionUpdateOptions SubscriptionUpdateRequest(string expectedPlanId, Subscription subscriptionItem) =>
|
||||
|
@ -1,763 +0,0 @@
|
||||
using System.Reflection;
|
||||
using System.Security.Claims;
|
||||
using Bit.Commercial.Core.SecretsManager.AuthorizationHandlers.AccessPolicies;
|
||||
using Bit.Commercial.Core.Test.SecretsManager.Enums;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.SecretsManager.AuthorizationRequirements;
|
||||
using Bit.Core.SecretsManager.Entities;
|
||||
using Bit.Core.SecretsManager.Queries.Interfaces;
|
||||
using Bit.Core.SecretsManager.Repositories;
|
||||
using Bit.Core.Test.SecretsManager.AutoFixture.ProjectsFixture;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Commercial.Core.Test.SecretsManager.AuthorizationHandlers.AccessPolicies;
|
||||
|
||||
[SutProviderCustomize]
|
||||
[ProjectCustomize]
|
||||
public class AccessPolicyAuthorizationHandlerTests
|
||||
{
|
||||
private static void SetupCurrentUserPermission(SutProvider<AccessPolicyAuthorizationHandler> sutProvider,
|
||||
PermissionType permissionType, Guid organizationId, Guid userId = new())
|
||||
{
|
||||
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(organizationId)
|
||||
.Returns(true);
|
||||
|
||||
switch (permissionType)
|
||||
{
|
||||
case PermissionType.RunAsAdmin:
|
||||
sutProvider.GetDependency<IAccessClientQuery>().GetAccessClientAsync(default, organizationId).ReturnsForAnyArgs(
|
||||
(AccessClientType.NoAccessCheck, userId));
|
||||
break;
|
||||
case PermissionType.RunAsUserWithPermission:
|
||||
sutProvider.GetDependency<IAccessClientQuery>().GetAccessClientAsync(default, organizationId).ReturnsForAnyArgs(
|
||||
(AccessClientType.User, userId));
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(permissionType), permissionType, null);
|
||||
}
|
||||
}
|
||||
|
||||
private static BaseAccessPolicy CreatePolicy(AccessPolicyType accessPolicyType, Project grantedProject,
|
||||
ServiceAccount grantedServiceAccount, Guid? serviceAccountId = null)
|
||||
{
|
||||
switch (accessPolicyType)
|
||||
{
|
||||
case AccessPolicyType.UserProjectAccessPolicy:
|
||||
return
|
||||
new UserProjectAccessPolicy
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
OrganizationUserId = Guid.NewGuid(),
|
||||
Read = true,
|
||||
Write = true,
|
||||
GrantedProjectId = grantedProject.Id,
|
||||
GrantedProject = grantedProject,
|
||||
};
|
||||
case AccessPolicyType.GroupProjectAccessPolicy:
|
||||
return
|
||||
new GroupProjectAccessPolicy
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
GroupId = Guid.NewGuid(),
|
||||
GrantedProjectId = grantedProject.Id,
|
||||
Read = true,
|
||||
Write = true,
|
||||
GrantedProject = grantedProject,
|
||||
};
|
||||
case AccessPolicyType.ServiceAccountProjectAccessPolicy:
|
||||
return new ServiceAccountProjectAccessPolicy
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
ServiceAccountId = serviceAccountId,
|
||||
GrantedProjectId = grantedProject.Id,
|
||||
Read = true,
|
||||
Write = true,
|
||||
GrantedProject = grantedProject,
|
||||
};
|
||||
case AccessPolicyType.UserServiceAccountAccessPolicy:
|
||||
return
|
||||
new UserServiceAccountAccessPolicy
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
OrganizationUserId = Guid.NewGuid(),
|
||||
Read = true,
|
||||
Write = true,
|
||||
GrantedServiceAccountId = grantedServiceAccount.Id,
|
||||
GrantedServiceAccount = grantedServiceAccount,
|
||||
};
|
||||
case AccessPolicyType.GroupServiceAccountAccessPolicy:
|
||||
return new GroupServiceAccountAccessPolicy
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
GroupId = Guid.NewGuid(),
|
||||
GrantedServiceAccountId = grantedServiceAccount.Id,
|
||||
GrantedServiceAccount = grantedServiceAccount,
|
||||
Read = true,
|
||||
Write = true,
|
||||
};
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(accessPolicyType), accessPolicyType, null);
|
||||
}
|
||||
}
|
||||
|
||||
private static void SetupMockAccess(SutProvider<AccessPolicyAuthorizationHandler> sutProvider,
|
||||
Guid userId, BaseAccessPolicy accessPolicy, bool read, bool write)
|
||||
{
|
||||
switch (accessPolicy)
|
||||
{
|
||||
case UserProjectAccessPolicy ap:
|
||||
sutProvider.GetDependency<IProjectRepository>()
|
||||
.AccessToProjectAsync(ap.GrantedProjectId!.Value, userId, Arg.Any<AccessClientType>())
|
||||
.Returns((read, write));
|
||||
break;
|
||||
case GroupProjectAccessPolicy ap:
|
||||
sutProvider.GetDependency<IProjectRepository>()
|
||||
.AccessToProjectAsync(ap.GrantedProjectId!.Value, userId, Arg.Any<AccessClientType>())
|
||||
.Returns((read, write));
|
||||
break;
|
||||
case UserServiceAccountAccessPolicy ap:
|
||||
sutProvider.GetDependency<IServiceAccountRepository>()
|
||||
.AccessToServiceAccountAsync(ap.GrantedServiceAccountId!.Value, userId, Arg.Any<AccessClientType>())
|
||||
.Returns((read, write));
|
||||
break;
|
||||
case GroupServiceAccountAccessPolicy ap:
|
||||
sutProvider.GetDependency<IServiceAccountRepository>()
|
||||
.AccessToServiceAccountAsync(ap.GrantedServiceAccountId!.Value, userId, Arg.Any<AccessClientType>())
|
||||
.Returns((read, write));
|
||||
break;
|
||||
case ServiceAccountProjectAccessPolicy ap:
|
||||
sutProvider.GetDependency<IProjectRepository>()
|
||||
.AccessToProjectAsync(ap.GrantedProjectId!.Value, userId, Arg.Any<AccessClientType>())
|
||||
.Returns((read, write));
|
||||
sutProvider.GetDependency<IServiceAccountRepository>()
|
||||
.AccessToServiceAccountAsync(ap.ServiceAccountId!.Value, userId, Arg.Any<AccessClientType>())
|
||||
.Returns((read, write));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static void SetupOrganizationMismatch(SutProvider<AccessPolicyAuthorizationHandler> sutProvider,
|
||||
BaseAccessPolicy accessPolicy)
|
||||
{
|
||||
switch (accessPolicy)
|
||||
{
|
||||
case UserProjectAccessPolicy resource:
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetByIdAsync(resource.OrganizationUserId!.Value)
|
||||
.Returns(new OrganizationUser
|
||||
{
|
||||
Id = resource.OrganizationUserId!.Value,
|
||||
OrganizationId = Guid.NewGuid()
|
||||
});
|
||||
break;
|
||||
case GroupProjectAccessPolicy resource:
|
||||
sutProvider.GetDependency<IGroupRepository>().GetByIdAsync(resource.GroupId!.Value)
|
||||
.Returns(new Group { Id = resource.GroupId!.Value, OrganizationId = Guid.NewGuid() });
|
||||
break;
|
||||
case UserServiceAccountAccessPolicy resource:
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetByIdAsync(resource.OrganizationUserId!.Value)
|
||||
.Returns(new OrganizationUser
|
||||
{
|
||||
Id = resource.OrganizationUserId!.Value,
|
||||
OrganizationId = Guid.NewGuid()
|
||||
});
|
||||
break;
|
||||
case GroupServiceAccountAccessPolicy resource:
|
||||
sutProvider.GetDependency<IGroupRepository>().GetByIdAsync(resource.GroupId!.Value)
|
||||
.Returns(new Group { Id = resource.GroupId!.Value, OrganizationId = Guid.NewGuid() });
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(accessPolicy), accessPolicy, null);
|
||||
}
|
||||
}
|
||||
|
||||
private static void SetupOrganizationMatch(SutProvider<AccessPolicyAuthorizationHandler> sutProvider,
|
||||
BaseAccessPolicy accessPolicy, Guid organizationId)
|
||||
{
|
||||
switch (accessPolicy)
|
||||
{
|
||||
case UserProjectAccessPolicy resource:
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetByIdAsync(resource.OrganizationUserId!.Value)
|
||||
.Returns(new OrganizationUser
|
||||
{
|
||||
Id = resource.OrganizationUserId!.Value,
|
||||
OrganizationId = organizationId
|
||||
});
|
||||
break;
|
||||
case GroupProjectAccessPolicy resource:
|
||||
sutProvider.GetDependency<IGroupRepository>().GetByIdAsync(resource.GroupId!.Value)
|
||||
.Returns(new Group { Id = resource.GroupId!.Value, OrganizationId = organizationId });
|
||||
break;
|
||||
case UserServiceAccountAccessPolicy resource:
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetByIdAsync(resource.OrganizationUserId!.Value)
|
||||
.Returns(new OrganizationUser
|
||||
{
|
||||
Id = resource.OrganizationUserId!.Value,
|
||||
OrganizationId = organizationId
|
||||
});
|
||||
break;
|
||||
case GroupServiceAccountAccessPolicy resource:
|
||||
sutProvider.GetDependency<IGroupRepository>().GetByIdAsync(resource.GroupId!.Value)
|
||||
.Returns(new Group { Id = resource.GroupId!.Value, OrganizationId = organizationId });
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(accessPolicy), accessPolicy, null);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AccessPolicyOperations_OnlyPublicStatic()
|
||||
{
|
||||
var publicStaticFields = typeof(AccessPolicyOperations).GetFields(BindingFlags.Public | BindingFlags.Static);
|
||||
var allFields = typeof(AccessPolicyOperations).GetFields();
|
||||
Assert.Equal(publicStaticFields.Length, allFields.Length);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task Handler_UnsupportedAccessPolicyOperationRequirement_Throws(
|
||||
SutProvider<AccessPolicyAuthorizationHandler> sutProvider, UserProjectAccessPolicy resource,
|
||||
ClaimsPrincipal claimsPrincipal)
|
||||
{
|
||||
var requirement = new AccessPolicyOperationRequirement();
|
||||
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
|
||||
claimsPrincipal, resource);
|
||||
|
||||
await Assert.ThrowsAsync<ArgumentException>(() => sutProvider.Sut.HandleAsync(authzContext));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(AccessPolicyType.UserProjectAccessPolicy)]
|
||||
[BitAutoData(AccessPolicyType.GroupProjectAccessPolicy)]
|
||||
[BitAutoData(AccessPolicyType.UserServiceAccountAccessPolicy)]
|
||||
[BitAutoData(AccessPolicyType.GroupServiceAccountAccessPolicy)]
|
||||
public async Task CanCreate_OrgMismatch_DoesNotSucceed(
|
||||
AccessPolicyType accessPolicyType,
|
||||
SutProvider<AccessPolicyAuthorizationHandler> sutProvider,
|
||||
Project mockGrantedProject,
|
||||
ServiceAccount mockGrantedServiceAccount,
|
||||
ClaimsPrincipal claimsPrincipal)
|
||||
{
|
||||
var requirement = AccessPolicyOperations.Create;
|
||||
var resource = CreatePolicy(accessPolicyType, mockGrantedProject, mockGrantedServiceAccount);
|
||||
SetupOrganizationMismatch(sutProvider, resource);
|
||||
|
||||
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
|
||||
claimsPrincipal, resource);
|
||||
|
||||
await sutProvider.Sut.HandleAsync(authzContext);
|
||||
|
||||
Assert.False(authzContext.HasSucceeded);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(AccessPolicyType.UserProjectAccessPolicy)]
|
||||
[BitAutoData(AccessPolicyType.GroupProjectAccessPolicy)]
|
||||
[BitAutoData(AccessPolicyType.UserServiceAccountAccessPolicy)]
|
||||
[BitAutoData(AccessPolicyType.GroupServiceAccountAccessPolicy)]
|
||||
public async Task CanCreate_AccessToSecretsManagerFalse_DoesNotSucceed(
|
||||
AccessPolicyType accessPolicyType,
|
||||
SutProvider<AccessPolicyAuthorizationHandler> sutProvider,
|
||||
Guid organizationId,
|
||||
Project mockGrantedProject,
|
||||
ServiceAccount mockGrantedServiceAccount,
|
||||
ClaimsPrincipal claimsPrincipal)
|
||||
{
|
||||
var requirement = AccessPolicyOperations.Create;
|
||||
mockGrantedProject.OrganizationId = organizationId;
|
||||
mockGrantedServiceAccount.OrganizationId = organizationId;
|
||||
var resource = CreatePolicy(accessPolicyType, mockGrantedProject, mockGrantedServiceAccount);
|
||||
SetupOrganizationMatch(sutProvider, resource, organizationId);
|
||||
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(organizationId)
|
||||
.Returns(false);
|
||||
|
||||
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
|
||||
claimsPrincipal, resource);
|
||||
|
||||
await sutProvider.Sut.HandleAsync(authzContext);
|
||||
|
||||
Assert.False(authzContext.HasSucceeded);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(AccessClientType.ServiceAccount, AccessPolicyType.UserProjectAccessPolicy)]
|
||||
[BitAutoData(AccessClientType.ServiceAccount, AccessPolicyType.GroupProjectAccessPolicy)]
|
||||
[BitAutoData(AccessClientType.ServiceAccount, AccessPolicyType.UserServiceAccountAccessPolicy)]
|
||||
[BitAutoData(AccessClientType.ServiceAccount, AccessPolicyType.GroupServiceAccountAccessPolicy)]
|
||||
[BitAutoData(AccessClientType.Organization, AccessPolicyType.UserProjectAccessPolicy)]
|
||||
[BitAutoData(AccessClientType.Organization, AccessPolicyType.GroupProjectAccessPolicy)]
|
||||
[BitAutoData(AccessClientType.Organization, AccessPolicyType.UserServiceAccountAccessPolicy)]
|
||||
[BitAutoData(AccessClientType.Organization, AccessPolicyType.GroupServiceAccountAccessPolicy)]
|
||||
public async Task CanCreate_UnsupportedClientTypes_DoesNotSucceed(
|
||||
AccessClientType clientType,
|
||||
AccessPolicyType accessPolicyType,
|
||||
SutProvider<AccessPolicyAuthorizationHandler> sutProvider,
|
||||
Guid organizationId,
|
||||
Project mockGrantedProject,
|
||||
ServiceAccount mockGrantedServiceAccount,
|
||||
ClaimsPrincipal claimsPrincipal)
|
||||
{
|
||||
var requirement = AccessPolicyOperations.Create;
|
||||
mockGrantedProject.OrganizationId = organizationId;
|
||||
mockGrantedServiceAccount.OrganizationId = organizationId;
|
||||
var resource = CreatePolicy(accessPolicyType, mockGrantedProject, mockGrantedServiceAccount);
|
||||
SetupOrganizationMatch(sutProvider, resource, organizationId);
|
||||
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(organizationId)
|
||||
.Returns(true);
|
||||
sutProvider.GetDependency<IAccessClientQuery>().GetAccessClientAsync(default, organizationId).ReturnsForAnyArgs(
|
||||
(clientType, Guid.NewGuid()));
|
||||
|
||||
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
|
||||
claimsPrincipal, resource);
|
||||
|
||||
await sutProvider.Sut.HandleAsync(authzContext);
|
||||
|
||||
Assert.False(authzContext.HasSucceeded);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(AccessPolicyType.UserProjectAccessPolicy, PermissionType.RunAsAdmin, true, true, true)]
|
||||
[BitAutoData(AccessPolicyType.UserProjectAccessPolicy, PermissionType.RunAsUserWithPermission, false, false, false)]
|
||||
[BitAutoData(AccessPolicyType.UserProjectAccessPolicy, PermissionType.RunAsUserWithPermission, false, true, true)]
|
||||
[BitAutoData(AccessPolicyType.UserProjectAccessPolicy, PermissionType.RunAsUserWithPermission, true, false, false)]
|
||||
[BitAutoData(AccessPolicyType.UserProjectAccessPolicy, PermissionType.RunAsUserWithPermission, true, true, true)]
|
||||
[BitAutoData(AccessPolicyType.GroupProjectAccessPolicy, PermissionType.RunAsAdmin, true, true, true)]
|
||||
[BitAutoData(AccessPolicyType.GroupProjectAccessPolicy, PermissionType.RunAsUserWithPermission, false, false, false)]
|
||||
[BitAutoData(AccessPolicyType.GroupProjectAccessPolicy, PermissionType.RunAsUserWithPermission, false, true, true)]
|
||||
[BitAutoData(AccessPolicyType.GroupProjectAccessPolicy, PermissionType.RunAsUserWithPermission, true, false, false)]
|
||||
[BitAutoData(AccessPolicyType.GroupProjectAccessPolicy, PermissionType.RunAsUserWithPermission, true, true, true)]
|
||||
[BitAutoData(AccessPolicyType.UserServiceAccountAccessPolicy, PermissionType.RunAsAdmin, true, true, true)]
|
||||
[BitAutoData(AccessPolicyType.UserServiceAccountAccessPolicy, PermissionType.RunAsUserWithPermission, false, false, false)]
|
||||
[BitAutoData(AccessPolicyType.UserServiceAccountAccessPolicy, PermissionType.RunAsUserWithPermission, false, true, true)]
|
||||
[BitAutoData(AccessPolicyType.UserServiceAccountAccessPolicy, PermissionType.RunAsUserWithPermission, true, false, false)]
|
||||
[BitAutoData(AccessPolicyType.UserServiceAccountAccessPolicy, PermissionType.RunAsUserWithPermission, true, true, true)]
|
||||
[BitAutoData(AccessPolicyType.GroupServiceAccountAccessPolicy, PermissionType.RunAsAdmin, true, true, true)]
|
||||
[BitAutoData(AccessPolicyType.GroupServiceAccountAccessPolicy, PermissionType.RunAsUserWithPermission, false, false, false)]
|
||||
[BitAutoData(AccessPolicyType.GroupServiceAccountAccessPolicy, PermissionType.RunAsUserWithPermission, false, true, true)]
|
||||
[BitAutoData(AccessPolicyType.GroupServiceAccountAccessPolicy, PermissionType.RunAsUserWithPermission, true, false, false)]
|
||||
[BitAutoData(AccessPolicyType.GroupServiceAccountAccessPolicy, PermissionType.RunAsUserWithPermission, true, true, true)]
|
||||
public async Task CanCreate_AccessCheck(
|
||||
AccessPolicyType accessPolicyType,
|
||||
PermissionType permissionType,
|
||||
bool read, bool write, bool expected,
|
||||
SutProvider<AccessPolicyAuthorizationHandler> sutProvider,
|
||||
Guid organizationId,
|
||||
Guid userId,
|
||||
Guid serviceAccountId,
|
||||
Project mockGrantedProject,
|
||||
ServiceAccount mockGrantedServiceAccount,
|
||||
ClaimsPrincipal claimsPrincipal)
|
||||
{
|
||||
var requirement = AccessPolicyOperations.Create;
|
||||
mockGrantedProject.OrganizationId = organizationId;
|
||||
mockGrantedServiceAccount.OrganizationId = organizationId;
|
||||
var resource = CreatePolicy(accessPolicyType, mockGrantedProject, mockGrantedServiceAccount, serviceAccountId);
|
||||
SetupCurrentUserPermission(sutProvider, permissionType, organizationId, userId);
|
||||
SetupOrganizationMatch(sutProvider, resource, organizationId);
|
||||
SetupMockAccess(sutProvider, userId, resource, read, write);
|
||||
|
||||
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
|
||||
claimsPrincipal, resource);
|
||||
|
||||
await sutProvider.Sut.HandleAsync(authzContext);
|
||||
|
||||
Assert.Equal(expected, authzContext.HasSucceeded);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(false, false)]
|
||||
[BitAutoData(false, true)]
|
||||
[BitAutoData(true, false)]
|
||||
public async Task CanCreate_ServiceAccountProjectAccessPolicy_TargetsDontExist_DoesNotSucceed(bool projectExists,
|
||||
bool serviceAccountExists,
|
||||
SutProvider<AccessPolicyAuthorizationHandler> sutProvider, ServiceAccountProjectAccessPolicy resource,
|
||||
Project mockProject, ServiceAccount mockServiceAccount,
|
||||
ClaimsPrincipal claimsPrincipal)
|
||||
{
|
||||
var requirement = AccessPolicyOperations.Create;
|
||||
resource.GrantedProject = null;
|
||||
resource.ServiceAccount = null;
|
||||
|
||||
if (projectExists)
|
||||
{
|
||||
resource.GrantedProject = null;
|
||||
mockProject.Id = resource.GrantedProjectId!.Value;
|
||||
sutProvider.GetDependency<IProjectRepository>().GetByIdAsync(resource.GrantedProjectId!.Value)
|
||||
.Returns(mockProject);
|
||||
}
|
||||
|
||||
if (serviceAccountExists)
|
||||
{
|
||||
resource.ServiceAccount = null;
|
||||
sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(resource.ServiceAccountId!.Value)
|
||||
.Returns(mockServiceAccount);
|
||||
}
|
||||
|
||||
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
|
||||
claimsPrincipal, resource);
|
||||
|
||||
await sutProvider.Sut.HandleAsync(authzContext);
|
||||
|
||||
Assert.False(authzContext.HasSucceeded);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(false, false)]
|
||||
[BitAutoData(false, true)]
|
||||
[BitAutoData(true, false)]
|
||||
[BitAutoData(true, true)]
|
||||
public async Task CanCreate_ServiceAccountProjectAccessPolicy_OrgMismatch_DoesNotSucceed(bool fetchProject,
|
||||
bool fetchSa,
|
||||
SutProvider<AccessPolicyAuthorizationHandler> sutProvider, ServiceAccountProjectAccessPolicy resource,
|
||||
Project mockProject, ServiceAccount mockServiceAccount,
|
||||
ClaimsPrincipal claimsPrincipal)
|
||||
{
|
||||
var requirement = AccessPolicyOperations.Create;
|
||||
|
||||
if (fetchProject)
|
||||
{
|
||||
resource.GrantedProject = null;
|
||||
mockProject.Id = resource.GrantedProjectId!.Value;
|
||||
sutProvider.GetDependency<IProjectRepository>().GetByIdAsync(resource.GrantedProjectId!.Value)
|
||||
.Returns(mockProject);
|
||||
}
|
||||
|
||||
if (fetchSa)
|
||||
{
|
||||
resource.ServiceAccount = null;
|
||||
mockServiceAccount.Id = resource.ServiceAccountId!.Value;
|
||||
sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(resource.ServiceAccountId!.Value)
|
||||
.Returns(mockServiceAccount);
|
||||
}
|
||||
|
||||
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
|
||||
claimsPrincipal, resource);
|
||||
|
||||
await sutProvider.Sut.HandleAsync(authzContext);
|
||||
|
||||
Assert.False(authzContext.HasSucceeded);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task CanCreate_ServiceAccountProjectAccessPolicy_AccessToSecretsManagerFalse_DoesNotSucceed(
|
||||
SutProvider<AccessPolicyAuthorizationHandler> sutProvider, ServiceAccountProjectAccessPolicy resource,
|
||||
ClaimsPrincipal claimsPrincipal)
|
||||
{
|
||||
var requirement = AccessPolicyOperations.Create;
|
||||
resource.ServiceAccount!.OrganizationId = resource.GrantedProject!.OrganizationId;
|
||||
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(resource.GrantedProject!.OrganizationId)
|
||||
.Returns(false);
|
||||
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
|
||||
claimsPrincipal, resource);
|
||||
|
||||
await sutProvider.Sut.HandleAsync(authzContext);
|
||||
|
||||
Assert.False(authzContext.HasSucceeded);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(AccessClientType.ServiceAccount)]
|
||||
[BitAutoData(AccessClientType.Organization)]
|
||||
public async Task CanCreate_ServiceAccountProjectAccessPolicy_UnsupportedClientTypes_DoesNotSucceed(
|
||||
AccessClientType clientType,
|
||||
SutProvider<AccessPolicyAuthorizationHandler> sutProvider, ServiceAccountProjectAccessPolicy resource,
|
||||
ClaimsPrincipal claimsPrincipal)
|
||||
{
|
||||
var requirement = AccessPolicyOperations.Create;
|
||||
resource.ServiceAccount!.OrganizationId = resource.GrantedProject!.OrganizationId;
|
||||
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(resource.GrantedProject!.OrganizationId)
|
||||
.Returns(true);
|
||||
sutProvider.GetDependency<IAccessClientQuery>().GetAccessClientAsync(default, resource.ServiceAccount!.OrganizationId).ReturnsForAnyArgs(
|
||||
(clientType, new Guid()));
|
||||
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
|
||||
claimsPrincipal, resource);
|
||||
|
||||
await sutProvider.Sut.HandleAsync(authzContext);
|
||||
|
||||
Assert.False(authzContext.HasSucceeded);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(PermissionType.RunAsAdmin, true, true, true, true, true)]
|
||||
[BitAutoData(PermissionType.RunAsUserWithPermission, false, false, false, false, false)]
|
||||
[BitAutoData(PermissionType.RunAsUserWithPermission, false, false, false, true, false)]
|
||||
[BitAutoData(PermissionType.RunAsUserWithPermission, false, false, true, false, false)]
|
||||
[BitAutoData(PermissionType.RunAsUserWithPermission, false, false, true, true, false)]
|
||||
[BitAutoData(PermissionType.RunAsUserWithPermission, false, true, false, false, false)]
|
||||
[BitAutoData(PermissionType.RunAsUserWithPermission, false, true, false, true, true)]
|
||||
[BitAutoData(PermissionType.RunAsUserWithPermission, false, true, true, false, false)]
|
||||
[BitAutoData(PermissionType.RunAsUserWithPermission, false, true, true, true, true)]
|
||||
[BitAutoData(PermissionType.RunAsUserWithPermission, true, false, false, false, false)]
|
||||
[BitAutoData(PermissionType.RunAsUserWithPermission, true, false, false, true, false)]
|
||||
[BitAutoData(PermissionType.RunAsUserWithPermission, true, false, true, false, false)]
|
||||
[BitAutoData(PermissionType.RunAsUserWithPermission, true, false, true, true, false)]
|
||||
[BitAutoData(PermissionType.RunAsUserWithPermission, true, true, false, false, false)]
|
||||
[BitAutoData(PermissionType.RunAsUserWithPermission, true, true, false, true, true)]
|
||||
[BitAutoData(PermissionType.RunAsUserWithPermission, true, true, true, false, false)]
|
||||
[BitAutoData(PermissionType.RunAsUserWithPermission, true, true, true, true, true)]
|
||||
public async Task CanCreate_ServiceAccountProjectAccessPolicy_AccessCheck(PermissionType permissionType,
|
||||
bool projectRead,
|
||||
bool projectWrite, bool saRead, bool saWrite, bool expected,
|
||||
SutProvider<AccessPolicyAuthorizationHandler> sutProvider, ServiceAccountProjectAccessPolicy resource,
|
||||
ClaimsPrincipal claimsPrincipal, Guid userId)
|
||||
{
|
||||
var requirement = AccessPolicyOperations.Create;
|
||||
resource.ServiceAccount!.OrganizationId = resource.GrantedProject!.OrganizationId;
|
||||
SetupCurrentUserPermission(sutProvider, permissionType, resource.GrantedProject!.OrganizationId, userId);
|
||||
sutProvider.GetDependency<IProjectRepository>()
|
||||
.AccessToProjectAsync(resource.GrantedProjectId!.Value, userId, Arg.Any<AccessClientType>())
|
||||
.Returns((projectRead, projectWrite));
|
||||
sutProvider.GetDependency<IServiceAccountRepository>()
|
||||
.AccessToServiceAccountAsync(resource.ServiceAccountId!.Value, userId, Arg.Any<AccessClientType>())
|
||||
.Returns((saRead, saWrite));
|
||||
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
|
||||
claimsPrincipal, resource);
|
||||
|
||||
await sutProvider.Sut.HandleAsync(authzContext);
|
||||
|
||||
Assert.Equal(expected, authzContext.HasSucceeded);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(AccessPolicyType.UserProjectAccessPolicy)]
|
||||
[BitAutoData(AccessPolicyType.GroupProjectAccessPolicy)]
|
||||
[BitAutoData(AccessPolicyType.ServiceAccountProjectAccessPolicy)]
|
||||
[BitAutoData(AccessPolicyType.UserServiceAccountAccessPolicy)]
|
||||
[BitAutoData(AccessPolicyType.GroupServiceAccountAccessPolicy)]
|
||||
public async Task CanUpdate_AccessToSecretsManagerFalse_DoesNotSucceed(AccessPolicyType accessPolicyType,
|
||||
SutProvider<AccessPolicyAuthorizationHandler> sutProvider,
|
||||
Guid organizationId,
|
||||
Project mockGrantedProject,
|
||||
ServiceAccount mockGrantedServiceAccount,
|
||||
ClaimsPrincipal claimsPrincipal)
|
||||
{
|
||||
var requirement = AccessPolicyOperations.Update;
|
||||
mockGrantedProject.OrganizationId = organizationId;
|
||||
mockGrantedServiceAccount.OrganizationId = organizationId;
|
||||
var resource = CreatePolicy(accessPolicyType, mockGrantedProject, mockGrantedServiceAccount);
|
||||
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(organizationId).Returns(false);
|
||||
|
||||
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
|
||||
claimsPrincipal, resource);
|
||||
|
||||
await sutProvider.Sut.HandleAsync(authzContext);
|
||||
|
||||
Assert.False(authzContext.HasSucceeded);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(AccessClientType.ServiceAccount, AccessPolicyType.UserProjectAccessPolicy)]
|
||||
[BitAutoData(AccessClientType.ServiceAccount, AccessPolicyType.GroupProjectAccessPolicy)]
|
||||
[BitAutoData(AccessClientType.ServiceAccount, AccessPolicyType.ServiceAccountProjectAccessPolicy)]
|
||||
[BitAutoData(AccessClientType.ServiceAccount, AccessPolicyType.UserServiceAccountAccessPolicy)]
|
||||
[BitAutoData(AccessClientType.ServiceAccount, AccessPolicyType.GroupServiceAccountAccessPolicy)]
|
||||
[BitAutoData(AccessClientType.Organization, AccessPolicyType.UserProjectAccessPolicy)]
|
||||
[BitAutoData(AccessClientType.Organization, AccessPolicyType.GroupProjectAccessPolicy)]
|
||||
[BitAutoData(AccessClientType.Organization, AccessPolicyType.ServiceAccountProjectAccessPolicy)]
|
||||
[BitAutoData(AccessClientType.Organization, AccessPolicyType.UserServiceAccountAccessPolicy)]
|
||||
[BitAutoData(AccessClientType.Organization, AccessPolicyType.GroupServiceAccountAccessPolicy)]
|
||||
public async Task CanUpdate_UnsupportedClientTypes_DoesNotSucceed(
|
||||
AccessClientType clientType,
|
||||
AccessPolicyType accessPolicyType,
|
||||
SutProvider<AccessPolicyAuthorizationHandler> sutProvider,
|
||||
Guid organizationId,
|
||||
Project mockGrantedProject,
|
||||
ServiceAccount mockGrantedServiceAccount,
|
||||
ClaimsPrincipal claimsPrincipal)
|
||||
{
|
||||
var requirement = AccessPolicyOperations.Update;
|
||||
mockGrantedProject.OrganizationId = organizationId;
|
||||
mockGrantedServiceAccount.OrganizationId = organizationId;
|
||||
var resource = CreatePolicy(accessPolicyType, mockGrantedProject, mockGrantedServiceAccount);
|
||||
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(organizationId).Returns(true);
|
||||
sutProvider.GetDependency<IAccessClientQuery>().GetAccessClientAsync(default, organizationId).ReturnsForAnyArgs(
|
||||
(clientType, new Guid()));
|
||||
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
|
||||
claimsPrincipal, resource);
|
||||
|
||||
await sutProvider.Sut.HandleAsync(authzContext);
|
||||
|
||||
Assert.False(authzContext.HasSucceeded);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(AccessPolicyType.UserProjectAccessPolicy, PermissionType.RunAsAdmin, true, true, true)]
|
||||
[BitAutoData(AccessPolicyType.UserProjectAccessPolicy, PermissionType.RunAsUserWithPermission, false, false, false)]
|
||||
[BitAutoData(AccessPolicyType.UserProjectAccessPolicy, PermissionType.RunAsUserWithPermission, false, true, true)]
|
||||
[BitAutoData(AccessPolicyType.UserProjectAccessPolicy, PermissionType.RunAsUserWithPermission, true, false, false)]
|
||||
[BitAutoData(AccessPolicyType.UserProjectAccessPolicy, PermissionType.RunAsUserWithPermission, true, true, true)]
|
||||
[BitAutoData(AccessPolicyType.GroupProjectAccessPolicy, PermissionType.RunAsAdmin, true, true, true)]
|
||||
[BitAutoData(AccessPolicyType.GroupProjectAccessPolicy, PermissionType.RunAsUserWithPermission, false, false, false)]
|
||||
[BitAutoData(AccessPolicyType.GroupProjectAccessPolicy, PermissionType.RunAsUserWithPermission, false, true, true)]
|
||||
[BitAutoData(AccessPolicyType.GroupProjectAccessPolicy, PermissionType.RunAsUserWithPermission, true, false, false)]
|
||||
[BitAutoData(AccessPolicyType.GroupProjectAccessPolicy, PermissionType.RunAsUserWithPermission, true, true, true)]
|
||||
[BitAutoData(AccessPolicyType.ServiceAccountProjectAccessPolicy, PermissionType.RunAsAdmin, true, true, true)]
|
||||
[BitAutoData(AccessPolicyType.ServiceAccountProjectAccessPolicy, PermissionType.RunAsUserWithPermission, false, false, false)]
|
||||
[BitAutoData(AccessPolicyType.ServiceAccountProjectAccessPolicy, PermissionType.RunAsUserWithPermission, false, true, true)]
|
||||
[BitAutoData(AccessPolicyType.ServiceAccountProjectAccessPolicy, PermissionType.RunAsUserWithPermission, true, false, false)]
|
||||
[BitAutoData(AccessPolicyType.ServiceAccountProjectAccessPolicy, PermissionType.RunAsUserWithPermission, true, true, true)]
|
||||
[BitAutoData(AccessPolicyType.UserServiceAccountAccessPolicy, PermissionType.RunAsAdmin, true, true, true)]
|
||||
[BitAutoData(AccessPolicyType.UserServiceAccountAccessPolicy, PermissionType.RunAsUserWithPermission, false, false, false)]
|
||||
[BitAutoData(AccessPolicyType.UserServiceAccountAccessPolicy, PermissionType.RunAsUserWithPermission, false, true, true)]
|
||||
[BitAutoData(AccessPolicyType.UserServiceAccountAccessPolicy, PermissionType.RunAsUserWithPermission, true, false, false)]
|
||||
[BitAutoData(AccessPolicyType.UserServiceAccountAccessPolicy, PermissionType.RunAsUserWithPermission, true, true, true)]
|
||||
[BitAutoData(AccessPolicyType.GroupServiceAccountAccessPolicy, PermissionType.RunAsAdmin, true, true, true)]
|
||||
[BitAutoData(AccessPolicyType.GroupServiceAccountAccessPolicy, PermissionType.RunAsUserWithPermission, false, false, false)]
|
||||
[BitAutoData(AccessPolicyType.GroupServiceAccountAccessPolicy, PermissionType.RunAsUserWithPermission, false, true, true)]
|
||||
[BitAutoData(AccessPolicyType.GroupServiceAccountAccessPolicy, PermissionType.RunAsUserWithPermission, true, false, false)]
|
||||
[BitAutoData(AccessPolicyType.GroupServiceAccountAccessPolicy, PermissionType.RunAsUserWithPermission, true, true, true)]
|
||||
public async Task CanUpdate_AccessCheck(
|
||||
AccessPolicyType accessPolicyType,
|
||||
PermissionType permissionType, bool read,
|
||||
bool write, bool expected,
|
||||
SutProvider<AccessPolicyAuthorizationHandler> sutProvider,
|
||||
Guid organizationId,
|
||||
Project mockGrantedProject,
|
||||
ServiceAccount mockGrantedServiceAccount,
|
||||
ClaimsPrincipal claimsPrincipal, Guid userId, Guid serviceAccountId)
|
||||
{
|
||||
var requirement = AccessPolicyOperations.Update;
|
||||
mockGrantedProject.OrganizationId = organizationId;
|
||||
mockGrantedServiceAccount.OrganizationId = organizationId;
|
||||
|
||||
var resource = CreatePolicy(accessPolicyType, mockGrantedProject, mockGrantedServiceAccount,
|
||||
serviceAccountId);
|
||||
SetupCurrentUserPermission(sutProvider, permissionType, organizationId, userId);
|
||||
SetupMockAccess(sutProvider, userId, resource, read, write);
|
||||
|
||||
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
|
||||
claimsPrincipal, resource);
|
||||
|
||||
await sutProvider.Sut.HandleAsync(authzContext);
|
||||
|
||||
Assert.Equal(expected, authzContext.HasSucceeded);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(AccessPolicyType.UserProjectAccessPolicy)]
|
||||
[BitAutoData(AccessPolicyType.GroupProjectAccessPolicy)]
|
||||
[BitAutoData(AccessPolicyType.ServiceAccountProjectAccessPolicy)]
|
||||
[BitAutoData(AccessPolicyType.UserServiceAccountAccessPolicy)]
|
||||
[BitAutoData(AccessPolicyType.GroupServiceAccountAccessPolicy)]
|
||||
public async Task CanDelete_AccessToSecretsManagerFalse_DoesNotSucceed(AccessPolicyType accessPolicyType,
|
||||
SutProvider<AccessPolicyAuthorizationHandler> sutProvider,
|
||||
Guid organizationId,
|
||||
Project mockGrantedProject,
|
||||
ServiceAccount mockGrantedServiceAccount,
|
||||
ClaimsPrincipal claimsPrincipal)
|
||||
{
|
||||
var requirement = AccessPolicyOperations.Delete;
|
||||
mockGrantedProject.OrganizationId = organizationId;
|
||||
mockGrantedServiceAccount.OrganizationId = organizationId;
|
||||
var resource = CreatePolicy(accessPolicyType, mockGrantedProject, mockGrantedServiceAccount);
|
||||
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(organizationId).Returns(false);
|
||||
|
||||
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
|
||||
claimsPrincipal, resource);
|
||||
|
||||
await sutProvider.Sut.HandleAsync(authzContext);
|
||||
|
||||
Assert.False(authzContext.HasSucceeded);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(AccessClientType.ServiceAccount, AccessPolicyType.UserProjectAccessPolicy)]
|
||||
[BitAutoData(AccessClientType.ServiceAccount, AccessPolicyType.GroupProjectAccessPolicy)]
|
||||
[BitAutoData(AccessClientType.ServiceAccount, AccessPolicyType.ServiceAccountProjectAccessPolicy)]
|
||||
[BitAutoData(AccessClientType.ServiceAccount, AccessPolicyType.UserServiceAccountAccessPolicy)]
|
||||
[BitAutoData(AccessClientType.ServiceAccount, AccessPolicyType.GroupServiceAccountAccessPolicy)]
|
||||
[BitAutoData(AccessClientType.Organization, AccessPolicyType.UserProjectAccessPolicy)]
|
||||
[BitAutoData(AccessClientType.Organization, AccessPolicyType.GroupProjectAccessPolicy)]
|
||||
[BitAutoData(AccessClientType.Organization, AccessPolicyType.ServiceAccountProjectAccessPolicy)]
|
||||
[BitAutoData(AccessClientType.Organization, AccessPolicyType.UserServiceAccountAccessPolicy)]
|
||||
[BitAutoData(AccessClientType.Organization, AccessPolicyType.GroupServiceAccountAccessPolicy)]
|
||||
public async Task CanDelete_UnsupportedClientTypes_DoesNotSucceed(
|
||||
AccessClientType clientType,
|
||||
AccessPolicyType accessPolicyType,
|
||||
SutProvider<AccessPolicyAuthorizationHandler> sutProvider,
|
||||
Guid organizationId,
|
||||
Project mockGrantedProject,
|
||||
ServiceAccount mockGrantedServiceAccount,
|
||||
ClaimsPrincipal claimsPrincipal)
|
||||
{
|
||||
var requirement = AccessPolicyOperations.Delete;
|
||||
mockGrantedProject.OrganizationId = organizationId;
|
||||
mockGrantedServiceAccount.OrganizationId = organizationId;
|
||||
var resource = CreatePolicy(accessPolicyType, mockGrantedProject, mockGrantedServiceAccount);
|
||||
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(organizationId).Returns(true);
|
||||
sutProvider.GetDependency<IAccessClientQuery>().GetAccessClientAsync(default, organizationId).ReturnsForAnyArgs(
|
||||
(clientType, new Guid()));
|
||||
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
|
||||
claimsPrincipal, resource);
|
||||
|
||||
await sutProvider.Sut.HandleAsync(authzContext);
|
||||
|
||||
Assert.False(authzContext.HasSucceeded);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(AccessPolicyType.UserProjectAccessPolicy, PermissionType.RunAsAdmin, true, true, true)]
|
||||
[BitAutoData(AccessPolicyType.UserProjectAccessPolicy, PermissionType.RunAsUserWithPermission, false, false, false)]
|
||||
[BitAutoData(AccessPolicyType.UserProjectAccessPolicy, PermissionType.RunAsUserWithPermission, false, true, true)]
|
||||
[BitAutoData(AccessPolicyType.UserProjectAccessPolicy, PermissionType.RunAsUserWithPermission, true, false, false)]
|
||||
[BitAutoData(AccessPolicyType.UserProjectAccessPolicy, PermissionType.RunAsUserWithPermission, true, true, true)]
|
||||
[BitAutoData(AccessPolicyType.GroupProjectAccessPolicy, PermissionType.RunAsAdmin, true, true, true)]
|
||||
[BitAutoData(AccessPolicyType.GroupProjectAccessPolicy, PermissionType.RunAsUserWithPermission, false, false, false)]
|
||||
[BitAutoData(AccessPolicyType.GroupProjectAccessPolicy, PermissionType.RunAsUserWithPermission, false, true, true)]
|
||||
[BitAutoData(AccessPolicyType.GroupProjectAccessPolicy, PermissionType.RunAsUserWithPermission, true, false, false)]
|
||||
[BitAutoData(AccessPolicyType.GroupProjectAccessPolicy, PermissionType.RunAsUserWithPermission, true, true, true)]
|
||||
[BitAutoData(AccessPolicyType.ServiceAccountProjectAccessPolicy, PermissionType.RunAsAdmin, true, true, true)]
|
||||
[BitAutoData(AccessPolicyType.ServiceAccountProjectAccessPolicy, PermissionType.RunAsUserWithPermission, false, false, false)]
|
||||
[BitAutoData(AccessPolicyType.ServiceAccountProjectAccessPolicy, PermissionType.RunAsUserWithPermission, false, true, true)]
|
||||
[BitAutoData(AccessPolicyType.ServiceAccountProjectAccessPolicy, PermissionType.RunAsUserWithPermission, true, false, false)]
|
||||
[BitAutoData(AccessPolicyType.ServiceAccountProjectAccessPolicy, PermissionType.RunAsUserWithPermission, true, true, true)]
|
||||
[BitAutoData(AccessPolicyType.UserServiceAccountAccessPolicy, PermissionType.RunAsAdmin, true, true, true)]
|
||||
[BitAutoData(AccessPolicyType.UserServiceAccountAccessPolicy, PermissionType.RunAsUserWithPermission, false, false, false)]
|
||||
[BitAutoData(AccessPolicyType.UserServiceAccountAccessPolicy, PermissionType.RunAsUserWithPermission, false, true, true)]
|
||||
[BitAutoData(AccessPolicyType.UserServiceAccountAccessPolicy, PermissionType.RunAsUserWithPermission, true, false, false)]
|
||||
[BitAutoData(AccessPolicyType.UserServiceAccountAccessPolicy, PermissionType.RunAsUserWithPermission, true, true, true)]
|
||||
[BitAutoData(AccessPolicyType.GroupServiceAccountAccessPolicy, PermissionType.RunAsAdmin, true, true, true)]
|
||||
[BitAutoData(AccessPolicyType.GroupServiceAccountAccessPolicy, PermissionType.RunAsUserWithPermission, false, false, false)]
|
||||
[BitAutoData(AccessPolicyType.GroupServiceAccountAccessPolicy, PermissionType.RunAsUserWithPermission, false, true, true)]
|
||||
[BitAutoData(AccessPolicyType.GroupServiceAccountAccessPolicy, PermissionType.RunAsUserWithPermission, true, false, false)]
|
||||
[BitAutoData(AccessPolicyType.GroupServiceAccountAccessPolicy, PermissionType.RunAsUserWithPermission, true, true, true)]
|
||||
public async Task CanDelete_AccessCheck(
|
||||
AccessPolicyType accessPolicyType,
|
||||
PermissionType permissionType,
|
||||
bool read, bool write, bool expected,
|
||||
SutProvider<AccessPolicyAuthorizationHandler> sutProvider,
|
||||
Guid organizationId,
|
||||
Project mockGrantedProject,
|
||||
ServiceAccount mockGrantedServiceAccount,
|
||||
ClaimsPrincipal claimsPrincipal, Guid userId, Guid serviceAccountId)
|
||||
{
|
||||
var requirement = AccessPolicyOperations.Delete;
|
||||
mockGrantedProject.OrganizationId = organizationId;
|
||||
mockGrantedServiceAccount.OrganizationId = organizationId;
|
||||
|
||||
var resource = CreatePolicy(accessPolicyType, mockGrantedProject, mockGrantedServiceAccount,
|
||||
serviceAccountId);
|
||||
SetupCurrentUserPermission(sutProvider, permissionType, organizationId, userId);
|
||||
SetupMockAccess(sutProvider, userId, resource, read, write);
|
||||
|
||||
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
|
||||
claimsPrincipal, resource);
|
||||
|
||||
await sutProvider.Sut.HandleAsync(authzContext);
|
||||
|
||||
Assert.Equal(expected, authzContext.HasSucceeded);
|
||||
}
|
||||
}
|
@ -0,0 +1,342 @@
|
||||
#nullable enable
|
||||
using System.Reflection;
|
||||
using System.Security.Claims;
|
||||
using Bit.Commercial.Core.SecretsManager.AuthorizationHandlers.AccessPolicies;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.SecretsManager.AuthorizationRequirements;
|
||||
using Bit.Core.SecretsManager.Entities;
|
||||
using Bit.Core.SecretsManager.Enums.AccessPolicies;
|
||||
using Bit.Core.SecretsManager.Models.Data.AccessPolicyUpdates;
|
||||
using Bit.Core.SecretsManager.Queries.Interfaces;
|
||||
using Bit.Core.SecretsManager.Repositories;
|
||||
using Bit.Core.Test.SecretsManager.AutoFixture.ProjectsFixture;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Commercial.Core.Test.SecretsManager.AuthorizationHandlers.AccessPolicies;
|
||||
|
||||
[SutProviderCustomize]
|
||||
[ProjectCustomize]
|
||||
public class ProjectServiceAccountsAccessPoliciesAuthorizationHandlerTests
|
||||
{
|
||||
[Fact]
|
||||
public void ServiceAccountGrantedPoliciesOperations_OnlyPublicStatic()
|
||||
{
|
||||
var publicStaticFields =
|
||||
typeof(ProjectServiceAccountsAccessPoliciesOperations).GetFields(BindingFlags.Public | BindingFlags.Static);
|
||||
var allFields = typeof(ProjectServiceAccountsAccessPoliciesOperations).GetFields();
|
||||
Assert.Equal(publicStaticFields.Length, allFields.Length);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task Handler_AccessSecretsManagerFalse_DoesNotSucceed(
|
||||
SutProvider<ProjectServiceAccountsAccessPoliciesAuthorizationHandler> sutProvider,
|
||||
ProjectServiceAccountsAccessPoliciesUpdates resource,
|
||||
ClaimsPrincipal claimsPrincipal)
|
||||
{
|
||||
var requirement = ProjectServiceAccountsAccessPoliciesOperations.Updates;
|
||||
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(resource.OrganizationId)
|
||||
.Returns(false);
|
||||
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
|
||||
claimsPrincipal, resource);
|
||||
|
||||
await sutProvider.Sut.HandleAsync(authzContext);
|
||||
|
||||
Assert.False(authzContext.HasSucceeded);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(AccessClientType.ServiceAccount)]
|
||||
[BitAutoData(AccessClientType.Organization)]
|
||||
public async Task Handler_UnsupportedClientTypes_DoesNotSucceed(
|
||||
AccessClientType accessClientType,
|
||||
SutProvider<ProjectServiceAccountsAccessPoliciesAuthorizationHandler> sutProvider,
|
||||
ProjectServiceAccountsAccessPoliciesUpdates resource,
|
||||
ClaimsPrincipal claimsPrincipal)
|
||||
{
|
||||
var requirement = ProjectServiceAccountsAccessPoliciesOperations.Updates;
|
||||
SetupUserSubstitutes(sutProvider, accessClientType, resource);
|
||||
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
|
||||
claimsPrincipal, resource);
|
||||
|
||||
await sutProvider.Sut.HandleAsync(authzContext);
|
||||
|
||||
Assert.False(authzContext.HasSucceeded);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task Handler_UnsupportedProjectServiceAccountsPoliciesOperationRequirement_Throws(
|
||||
SutProvider<ProjectServiceAccountsAccessPoliciesAuthorizationHandler> sutProvider,
|
||||
ProjectServiceAccountsAccessPoliciesUpdates resource,
|
||||
ClaimsPrincipal claimsPrincipal)
|
||||
{
|
||||
var requirement = new ProjectServiceAccountsAccessPoliciesOperationRequirement();
|
||||
SetupUserSubstitutes(sutProvider, AccessClientType.NoAccessCheck, resource);
|
||||
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
|
||||
claimsPrincipal, resource);
|
||||
|
||||
await Assert.ThrowsAsync<ArgumentException>(() => sutProvider.Sut.HandleAsync(authzContext));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(AccessClientType.NoAccessCheck, false, false)]
|
||||
[BitAutoData(AccessClientType.NoAccessCheck, true, false)]
|
||||
[BitAutoData(AccessClientType.User, false, false)]
|
||||
[BitAutoData(AccessClientType.User, true, false)]
|
||||
public async Task Handler_UserHasNoWriteAccessToProject_DoesNotSucceed(
|
||||
AccessClientType accessClientType,
|
||||
bool projectReadAccess,
|
||||
bool projectWriteAccess,
|
||||
SutProvider<ProjectServiceAccountsAccessPoliciesAuthorizationHandler> sutProvider,
|
||||
ProjectServiceAccountsAccessPoliciesUpdates resource,
|
||||
Guid userId,
|
||||
ClaimsPrincipal claimsPrincipal)
|
||||
{
|
||||
var requirement = ProjectServiceAccountsAccessPoliciesOperations.Updates;
|
||||
SetupUserSubstitutes(sutProvider, accessClientType, resource, userId);
|
||||
sutProvider.GetDependency<IProjectRepository>()
|
||||
.AccessToProjectAsync(resource.ProjectId, userId, accessClientType)
|
||||
.Returns((projectReadAccess, projectWriteAccess));
|
||||
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
|
||||
claimsPrincipal, resource);
|
||||
|
||||
await sutProvider.Sut.HandleAsync(authzContext);
|
||||
|
||||
Assert.False(authzContext.HasSucceeded);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task Handler_ServiceAccountsInDifferentOrganization_DoesNotSucceed(
|
||||
SutProvider<ProjectServiceAccountsAccessPoliciesAuthorizationHandler> sutProvider,
|
||||
ProjectServiceAccountsAccessPoliciesUpdates resource,
|
||||
Guid userId,
|
||||
ClaimsPrincipal claimsPrincipal)
|
||||
{
|
||||
var requirement = ProjectServiceAccountsAccessPoliciesOperations.Updates;
|
||||
SetupUserSubstitutes(sutProvider, AccessClientType.NoAccessCheck, resource, userId);
|
||||
sutProvider.GetDependency<IProjectRepository>()
|
||||
.AccessToProjectAsync(resource.ProjectId, userId, AccessClientType.NoAccessCheck)
|
||||
.Returns((true, true));
|
||||
sutProvider.GetDependency<IServiceAccountRepository>()
|
||||
.ServiceAccountsAreInOrganizationAsync(Arg.Any<List<Guid>>(), resource.OrganizationId)
|
||||
.Returns(false);
|
||||
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
|
||||
claimsPrincipal, resource);
|
||||
|
||||
await sutProvider.Sut.HandleAsync(authzContext);
|
||||
|
||||
Assert.False(authzContext.HasSucceeded);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(AccessClientType.NoAccessCheck)]
|
||||
[BitAutoData(AccessClientType.User)]
|
||||
public async Task Handler_UserHasAccessToProject_NoCreatesRequested_Success(
|
||||
AccessClientType accessClientType,
|
||||
SutProvider<ProjectServiceAccountsAccessPoliciesAuthorizationHandler> sutProvider,
|
||||
ProjectServiceAccountsAccessPoliciesUpdates resource,
|
||||
Guid userId,
|
||||
ClaimsPrincipal claimsPrincipal)
|
||||
{
|
||||
var requirement = ProjectServiceAccountsAccessPoliciesOperations.Updates;
|
||||
resource = RemoveAllCreates(resource);
|
||||
SetupServiceAccountsAccessTest(sutProvider, accessClientType, resource, userId);
|
||||
|
||||
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
|
||||
claimsPrincipal, resource);
|
||||
|
||||
await sutProvider.Sut.HandleAsync(authzContext);
|
||||
|
||||
Assert.True(authzContext.HasSucceeded);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(AccessClientType.NoAccessCheck)]
|
||||
[BitAutoData(AccessClientType.User)]
|
||||
public async Task Handler_UserHasNoAccessToCreateServiceAccounts_DoesNotSucceed(
|
||||
AccessClientType accessClientType,
|
||||
SutProvider<ProjectServiceAccountsAccessPoliciesAuthorizationHandler> sutProvider,
|
||||
ProjectServiceAccountsAccessPoliciesUpdates resource,
|
||||
Guid userId,
|
||||
ClaimsPrincipal claimsPrincipal)
|
||||
{
|
||||
var requirement = ProjectServiceAccountsAccessPoliciesOperations.Updates;
|
||||
resource = AddServiceAccountCreateUpdate(resource);
|
||||
SetupServiceAccountsAccessTest(sutProvider, accessClientType, resource, userId);
|
||||
var accessResult = resource.ServiceAccountAccessPolicyUpdates
|
||||
.Where(x => x.Operation == AccessPolicyOperation.Create)
|
||||
.Select(x => x.AccessPolicy.ServiceAccountId!.Value)
|
||||
.ToDictionary(id => id, _ => (false, false));
|
||||
|
||||
sutProvider.GetDependency<IServiceAccountRepository>()
|
||||
.AccessToServiceAccountsAsync(Arg.Any<List<Guid>>(), userId, accessClientType)
|
||||
.Returns(accessResult);
|
||||
|
||||
|
||||
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
|
||||
claimsPrincipal, resource);
|
||||
|
||||
await sutProvider.Sut.HandleAsync(authzContext);
|
||||
|
||||
Assert.False(authzContext.HasSucceeded);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(AccessClientType.NoAccessCheck)]
|
||||
[BitAutoData(AccessClientType.User)]
|
||||
public async Task Handler_AccessResultsPartial_DoesNotSucceed(
|
||||
AccessClientType accessClientType,
|
||||
SutProvider<ProjectServiceAccountsAccessPoliciesAuthorizationHandler> sutProvider,
|
||||
ProjectServiceAccountsAccessPoliciesUpdates resource,
|
||||
Guid userId,
|
||||
ClaimsPrincipal claimsPrincipal)
|
||||
{
|
||||
var requirement = ProjectServiceAccountsAccessPoliciesOperations.Updates;
|
||||
resource = AddServiceAccountCreateUpdate(resource);
|
||||
SetupServiceAccountsAccessTest(sutProvider, accessClientType, resource, userId);
|
||||
|
||||
var accessResult = resource.ServiceAccountAccessPolicyUpdates
|
||||
.Where(x => x.Operation == AccessPolicyOperation.Create)
|
||||
.Select(x => x.AccessPolicy.ServiceAccountId!.Value)
|
||||
.ToDictionary(id => id, _ => (false, false));
|
||||
|
||||
accessResult[accessResult.First().Key] = (true, true);
|
||||
accessResult.Remove(accessResult.Last().Key);
|
||||
sutProvider.GetDependency<IServiceAccountRepository>()
|
||||
.AccessToServiceAccountsAsync(Arg.Any<List<Guid>>(), userId, accessClientType)
|
||||
.Returns(accessResult);
|
||||
|
||||
|
||||
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
|
||||
claimsPrincipal, resource);
|
||||
|
||||
await sutProvider.Sut.HandleAsync(authzContext);
|
||||
|
||||
Assert.False(authzContext.HasSucceeded);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(AccessClientType.NoAccessCheck)]
|
||||
[BitAutoData(AccessClientType.User)]
|
||||
public async Task Handler_UserHasAccessToSomeCreateServiceAccounts_DoesNotSucceed(
|
||||
AccessClientType accessClientType,
|
||||
SutProvider<ProjectServiceAccountsAccessPoliciesAuthorizationHandler> sutProvider,
|
||||
ProjectServiceAccountsAccessPoliciesUpdates resource,
|
||||
Guid userId,
|
||||
ClaimsPrincipal claimsPrincipal)
|
||||
{
|
||||
var requirement = ProjectServiceAccountsAccessPoliciesOperations.Updates;
|
||||
resource = AddServiceAccountCreateUpdate(resource);
|
||||
SetupServiceAccountsAccessTest(sutProvider, accessClientType, resource, userId);
|
||||
|
||||
var accessResult = resource.ServiceAccountAccessPolicyUpdates
|
||||
.Where(x => x.Operation == AccessPolicyOperation.Create)
|
||||
.Select(x => x.AccessPolicy.ServiceAccountId!.Value)
|
||||
.ToDictionary(id => id, _ => (false, false));
|
||||
|
||||
accessResult[accessResult.First().Key] = (true, true);
|
||||
sutProvider.GetDependency<IServiceAccountRepository>()
|
||||
.AccessToServiceAccountsAsync(Arg.Any<List<Guid>>(), userId, accessClientType)
|
||||
.Returns(accessResult);
|
||||
|
||||
|
||||
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
|
||||
claimsPrincipal, resource);
|
||||
|
||||
await sutProvider.Sut.HandleAsync(authzContext);
|
||||
|
||||
Assert.False(authzContext.HasSucceeded);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(AccessClientType.NoAccessCheck)]
|
||||
[BitAutoData(AccessClientType.User)]
|
||||
public async Task Handler_UserHasAccessToAllCreateServiceAccounts_Success(
|
||||
AccessClientType accessClientType,
|
||||
SutProvider<ProjectServiceAccountsAccessPoliciesAuthorizationHandler> sutProvider,
|
||||
ProjectServiceAccountsAccessPoliciesUpdates resource,
|
||||
Guid userId,
|
||||
ClaimsPrincipal claimsPrincipal)
|
||||
{
|
||||
var requirement = ProjectServiceAccountsAccessPoliciesOperations.Updates;
|
||||
resource = AddServiceAccountCreateUpdate(resource);
|
||||
SetupServiceAccountsAccessTest(sutProvider, accessClientType, resource, userId);
|
||||
|
||||
var accessResult = resource.ServiceAccountAccessPolicyUpdates
|
||||
.Where(x => x.Operation == AccessPolicyOperation.Create)
|
||||
.Select(x => x.AccessPolicy.ServiceAccountId!.Value)
|
||||
.ToDictionary(id => id, _ => (true, true));
|
||||
|
||||
sutProvider.GetDependency<IServiceAccountRepository>()
|
||||
.AccessToServiceAccountsAsync(Arg.Any<List<Guid>>(), userId, accessClientType)
|
||||
.Returns(accessResult);
|
||||
|
||||
|
||||
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
|
||||
claimsPrincipal, resource);
|
||||
|
||||
await sutProvider.Sut.HandleAsync(authzContext);
|
||||
|
||||
Assert.True(authzContext.HasSucceeded);
|
||||
}
|
||||
|
||||
private static void SetupUserSubstitutes(
|
||||
SutProvider<ProjectServiceAccountsAccessPoliciesAuthorizationHandler> sutProvider,
|
||||
AccessClientType accessClientType,
|
||||
ProjectServiceAccountsAccessPoliciesUpdates resource,
|
||||
Guid userId = new())
|
||||
{
|
||||
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(resource.OrganizationId)
|
||||
.Returns(true);
|
||||
sutProvider.GetDependency<IAccessClientQuery>().GetAccessClientAsync(default, resource.OrganizationId)
|
||||
.ReturnsForAnyArgs((accessClientType, userId));
|
||||
}
|
||||
|
||||
private static void SetupServiceAccountsAccessTest(
|
||||
SutProvider<ProjectServiceAccountsAccessPoliciesAuthorizationHandler> sutProvider,
|
||||
AccessClientType accessClientType,
|
||||
ProjectServiceAccountsAccessPoliciesUpdates resource,
|
||||
Guid userId = new())
|
||||
{
|
||||
SetupUserSubstitutes(sutProvider, accessClientType, resource, userId);
|
||||
|
||||
sutProvider.GetDependency<IProjectRepository>()
|
||||
.AccessToProjectAsync(resource.ProjectId, userId, accessClientType)
|
||||
.Returns((true, true));
|
||||
sutProvider.GetDependency<IServiceAccountRepository>()
|
||||
.ServiceAccountsAreInOrganizationAsync(Arg.Any<List<Guid>>(), resource.OrganizationId)
|
||||
.Returns(true);
|
||||
}
|
||||
|
||||
private static ProjectServiceAccountsAccessPoliciesUpdates AddServiceAccountCreateUpdate(
|
||||
ProjectServiceAccountsAccessPoliciesUpdates resource)
|
||||
{
|
||||
resource.ServiceAccountAccessPolicyUpdates = resource.ServiceAccountAccessPolicyUpdates.Append(
|
||||
new ServiceAccountProjectAccessPolicyUpdate
|
||||
{
|
||||
AccessPolicy = new ServiceAccountProjectAccessPolicy
|
||||
{
|
||||
ServiceAccountId = Guid.NewGuid(),
|
||||
GrantedProjectId = resource.ProjectId,
|
||||
Read = true,
|
||||
Write = true
|
||||
}
|
||||
});
|
||||
return resource;
|
||||
}
|
||||
|
||||
private static ProjectServiceAccountsAccessPoliciesUpdates RemoveAllCreates(
|
||||
ProjectServiceAccountsAccessPoliciesUpdates resource)
|
||||
{
|
||||
resource.ServiceAccountAccessPolicyUpdates =
|
||||
resource.ServiceAccountAccessPolicyUpdates.Where(x => x.Operation != AccessPolicyOperation.Create);
|
||||
return resource;
|
||||
}
|
||||
}
|
@ -0,0 +1,273 @@
|
||||
#nullable enable
|
||||
using System.Reflection;
|
||||
using System.Security.Claims;
|
||||
using Bit.Commercial.Core.SecretsManager.AuthorizationHandlers.AccessPolicies;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.SecretsManager.AuthorizationRequirements;
|
||||
using Bit.Core.SecretsManager.Models.Data;
|
||||
using Bit.Core.SecretsManager.Queries.Interfaces;
|
||||
using Bit.Core.SecretsManager.Repositories;
|
||||
using Bit.Core.Test.SecretsManager.AutoFixture.ProjectsFixture;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Commercial.Core.Test.SecretsManager.AuthorizationHandlers.AccessPolicies;
|
||||
|
||||
[SutProviderCustomize]
|
||||
[ProjectCustomize]
|
||||
public class ServiceAccountGrantedPoliciesAuthorizationHandlerTests
|
||||
{
|
||||
[Fact]
|
||||
public void ServiceAccountGrantedPoliciesOperations_OnlyPublicStatic()
|
||||
{
|
||||
var publicStaticFields =
|
||||
typeof(ServiceAccountGrantedPoliciesOperations).GetFields(BindingFlags.Public | BindingFlags.Static);
|
||||
var allFields = typeof(ServiceAccountGrantedPoliciesOperations).GetFields();
|
||||
Assert.Equal(publicStaticFields.Length, allFields.Length);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task Handler_AccessSecretsManagerFalse_DoesNotSucceed(
|
||||
SutProvider<ServiceAccountGrantedPoliciesAuthorizationHandler> sutProvider,
|
||||
ServiceAccountGrantedPoliciesUpdates resource,
|
||||
ClaimsPrincipal claimsPrincipal)
|
||||
{
|
||||
var requirement = ServiceAccountGrantedPoliciesOperations.Updates;
|
||||
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(resource.OrganizationId)
|
||||
.Returns(false);
|
||||
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
|
||||
claimsPrincipal, resource);
|
||||
|
||||
await sutProvider.Sut.HandleAsync(authzContext);
|
||||
|
||||
Assert.False(authzContext.HasSucceeded);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(AccessClientType.ServiceAccount)]
|
||||
[BitAutoData(AccessClientType.Organization)]
|
||||
public async Task Handler_UnsupportedClientTypes_DoesNotSucceed(
|
||||
AccessClientType accessClientType,
|
||||
SutProvider<ServiceAccountGrantedPoliciesAuthorizationHandler> sutProvider,
|
||||
ServiceAccountGrantedPoliciesUpdates resource,
|
||||
ClaimsPrincipal claimsPrincipal)
|
||||
{
|
||||
var requirement = ServiceAccountGrantedPoliciesOperations.Updates;
|
||||
SetupUserSubstitutes(sutProvider, accessClientType, resource);
|
||||
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
|
||||
claimsPrincipal, resource);
|
||||
|
||||
await sutProvider.Sut.HandleAsync(authzContext);
|
||||
|
||||
Assert.False(authzContext.HasSucceeded);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task Handler_UnsupportedServiceAccountGrantedPoliciesOperationRequirement_Throws(
|
||||
SutProvider<ServiceAccountGrantedPoliciesAuthorizationHandler> sutProvider,
|
||||
ServiceAccountGrantedPoliciesUpdates resource,
|
||||
ClaimsPrincipal claimsPrincipal)
|
||||
{
|
||||
var requirement = new ServiceAccountGrantedPoliciesOperationRequirement();
|
||||
SetupUserSubstitutes(sutProvider, AccessClientType.NoAccessCheck, resource);
|
||||
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
|
||||
claimsPrincipal, resource);
|
||||
|
||||
await Assert.ThrowsAsync<ArgumentException>(() => sutProvider.Sut.HandleAsync(authzContext));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(AccessClientType.NoAccessCheck, false, false)]
|
||||
[BitAutoData(AccessClientType.NoAccessCheck, true, false)]
|
||||
[BitAutoData(AccessClientType.User, false, false)]
|
||||
[BitAutoData(AccessClientType.User, true, false)]
|
||||
public async Task Handler_UserHasNoWriteAccessToServiceAccount_DoesNotSucceed(
|
||||
AccessClientType accessClientType,
|
||||
bool saReadAccess,
|
||||
bool saWriteAccess,
|
||||
SutProvider<ServiceAccountGrantedPoliciesAuthorizationHandler> sutProvider,
|
||||
ServiceAccountGrantedPoliciesUpdates resource,
|
||||
Guid userId,
|
||||
ClaimsPrincipal claimsPrincipal)
|
||||
{
|
||||
var requirement = ServiceAccountGrantedPoliciesOperations.Updates;
|
||||
SetupUserSubstitutes(sutProvider, accessClientType, resource, userId);
|
||||
sutProvider.GetDependency<IServiceAccountRepository>()
|
||||
.AccessToServiceAccountAsync(resource.ServiceAccountId, userId, accessClientType)
|
||||
.Returns((saReadAccess, saWriteAccess));
|
||||
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
|
||||
claimsPrincipal, resource);
|
||||
|
||||
await sutProvider.Sut.HandleAsync(authzContext);
|
||||
|
||||
Assert.False(authzContext.HasSucceeded);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task Handler_GrantedProjectsInDifferentOrganization_DoesNotSucceed(
|
||||
SutProvider<ServiceAccountGrantedPoliciesAuthorizationHandler> sutProvider,
|
||||
ServiceAccountGrantedPoliciesUpdates resource,
|
||||
Guid userId,
|
||||
ClaimsPrincipal claimsPrincipal)
|
||||
{
|
||||
var requirement = ServiceAccountGrantedPoliciesOperations.Updates;
|
||||
SetupUserSubstitutes(sutProvider, AccessClientType.NoAccessCheck, resource, userId);
|
||||
sutProvider.GetDependency<IServiceAccountRepository>()
|
||||
.AccessToServiceAccountAsync(resource.ServiceAccountId, userId, AccessClientType.NoAccessCheck)
|
||||
.Returns((true, true));
|
||||
sutProvider.GetDependency<IProjectRepository>()
|
||||
.ProjectsAreInOrganization(Arg.Any<List<Guid>>(), resource.OrganizationId)
|
||||
.Returns(false);
|
||||
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
|
||||
claimsPrincipal, resource);
|
||||
|
||||
await sutProvider.Sut.HandleAsync(authzContext);
|
||||
|
||||
Assert.False(authzContext.HasSucceeded);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(AccessClientType.NoAccessCheck)]
|
||||
[BitAutoData(AccessClientType.User)]
|
||||
public async Task Handler_UserHasNoAccessToGrantedProjects_DoesNotSucceed(
|
||||
AccessClientType accessClientType,
|
||||
SutProvider<ServiceAccountGrantedPoliciesAuthorizationHandler> sutProvider,
|
||||
ServiceAccountGrantedPoliciesUpdates resource,
|
||||
Guid userId,
|
||||
ClaimsPrincipal claimsPrincipal)
|
||||
{
|
||||
var requirement = ServiceAccountGrantedPoliciesOperations.Updates;
|
||||
var projectIds = SetupProjectAccessTest(sutProvider, accessClientType, resource, userId);
|
||||
|
||||
sutProvider.GetDependency<IProjectRepository>()
|
||||
.AccessToProjectsAsync(Arg.Any<List<Guid>>(), userId, accessClientType)
|
||||
.Returns(projectIds.ToDictionary(projectId => projectId, _ => (false, false)));
|
||||
|
||||
|
||||
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
|
||||
claimsPrincipal, resource);
|
||||
|
||||
await sutProvider.Sut.HandleAsync(authzContext);
|
||||
|
||||
Assert.False(authzContext.HasSucceeded);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(AccessClientType.NoAccessCheck)]
|
||||
[BitAutoData(AccessClientType.User)]
|
||||
public async Task Handler_UserHasAccessToSomeGrantedProjects_DoesNotSucceed(
|
||||
AccessClientType accessClientType,
|
||||
SutProvider<ServiceAccountGrantedPoliciesAuthorizationHandler> sutProvider,
|
||||
ServiceAccountGrantedPoliciesUpdates resource,
|
||||
Guid userId,
|
||||
ClaimsPrincipal claimsPrincipal)
|
||||
{
|
||||
var requirement = ServiceAccountGrantedPoliciesOperations.Updates;
|
||||
var projectIds = SetupProjectAccessTest(sutProvider, accessClientType, resource, userId);
|
||||
|
||||
var accessResult = projectIds.ToDictionary(projectId => projectId, _ => (false, false));
|
||||
accessResult[projectIds.First()] = (true, true);
|
||||
sutProvider.GetDependency<IProjectRepository>()
|
||||
.AccessToProjectsAsync(Arg.Any<List<Guid>>(), userId, accessClientType)
|
||||
.Returns(accessResult);
|
||||
|
||||
|
||||
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
|
||||
claimsPrincipal, resource);
|
||||
|
||||
await sutProvider.Sut.HandleAsync(authzContext);
|
||||
|
||||
Assert.False(authzContext.HasSucceeded);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(AccessClientType.NoAccessCheck)]
|
||||
[BitAutoData(AccessClientType.User)]
|
||||
public async Task Handler_AccessResultsPartial_DoesNotSucceed(
|
||||
AccessClientType accessClientType,
|
||||
SutProvider<ServiceAccountGrantedPoliciesAuthorizationHandler> sutProvider,
|
||||
ServiceAccountGrantedPoliciesUpdates resource,
|
||||
Guid userId,
|
||||
ClaimsPrincipal claimsPrincipal)
|
||||
{
|
||||
var requirement = ServiceAccountGrantedPoliciesOperations.Updates;
|
||||
var projectIds = SetupProjectAccessTest(sutProvider, accessClientType, resource, userId);
|
||||
|
||||
var accessResult = projectIds.ToDictionary(projectId => projectId, _ => (false, false));
|
||||
accessResult.Remove(projectIds.First());
|
||||
sutProvider.GetDependency<IProjectRepository>()
|
||||
.AccessToProjectsAsync(Arg.Any<List<Guid>>(), userId, accessClientType)
|
||||
.Returns(accessResult);
|
||||
|
||||
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
|
||||
claimsPrincipal, resource);
|
||||
|
||||
await sutProvider.Sut.HandleAsync(authzContext);
|
||||
|
||||
Assert.False(authzContext.HasSucceeded);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(AccessClientType.NoAccessCheck)]
|
||||
[BitAutoData(AccessClientType.User)]
|
||||
public async Task Handler_UserHasAccessToAllGrantedProjects_Success(
|
||||
AccessClientType accessClientType,
|
||||
SutProvider<ServiceAccountGrantedPoliciesAuthorizationHandler> sutProvider,
|
||||
ServiceAccountGrantedPoliciesUpdates resource,
|
||||
Guid userId,
|
||||
ClaimsPrincipal claimsPrincipal)
|
||||
{
|
||||
var requirement = ServiceAccountGrantedPoliciesOperations.Updates;
|
||||
var projectIds = SetupProjectAccessTest(sutProvider, accessClientType, resource, userId);
|
||||
|
||||
sutProvider.GetDependency<IProjectRepository>()
|
||||
.AccessToProjectsAsync(Arg.Any<List<Guid>>(), userId, accessClientType)
|
||||
.Returns(projectIds.ToDictionary(projectId => projectId, _ => (true, true)));
|
||||
|
||||
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
|
||||
claimsPrincipal, resource);
|
||||
|
||||
await sutProvider.Sut.HandleAsync(authzContext);
|
||||
|
||||
Assert.True(authzContext.HasSucceeded);
|
||||
}
|
||||
|
||||
private static void SetupUserSubstitutes(
|
||||
SutProvider<ServiceAccountGrantedPoliciesAuthorizationHandler> sutProvider,
|
||||
AccessClientType accessClientType,
|
||||
ServiceAccountGrantedPoliciesUpdates resource,
|
||||
Guid userId = new())
|
||||
{
|
||||
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(resource.OrganizationId)
|
||||
.Returns(true);
|
||||
sutProvider.GetDependency<IAccessClientQuery>().GetAccessClientAsync(default, resource.OrganizationId)
|
||||
.ReturnsForAnyArgs((accessClientType, userId));
|
||||
}
|
||||
|
||||
private static List<Guid> SetupProjectAccessTest(
|
||||
SutProvider<ServiceAccountGrantedPoliciesAuthorizationHandler> sutProvider,
|
||||
AccessClientType accessClientType,
|
||||
ServiceAccountGrantedPoliciesUpdates resource,
|
||||
Guid userId = new())
|
||||
{
|
||||
SetupUserSubstitutes(sutProvider, accessClientType, resource, userId);
|
||||
|
||||
sutProvider.GetDependency<IServiceAccountRepository>()
|
||||
.AccessToServiceAccountAsync(resource.ServiceAccountId, userId, accessClientType)
|
||||
.Returns((true, true));
|
||||
sutProvider.GetDependency<IProjectRepository>()
|
||||
.ProjectsAreInOrganization(Arg.Any<List<Guid>>(), resource.OrganizationId)
|
||||
.Returns(true);
|
||||
|
||||
return resource.ProjectGrantedPolicyUpdates
|
||||
.Select(pu => pu.AccessPolicy.GrantedProjectId!.Value)
|
||||
.ToList();
|
||||
}
|
||||
}
|
@ -1,136 +0,0 @@
|
||||
using Bit.Commercial.Core.SecretsManager.Commands.AccessPolicies;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.SecretsManager.Entities;
|
||||
using Bit.Core.SecretsManager.Repositories;
|
||||
using Bit.Core.Test.SecretsManager.AutoFixture.ProjectsFixture;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Bit.Test.Common.Helpers;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Commercial.Core.Test.SecretsManager.Commands.AccessPolicies;
|
||||
|
||||
[SutProviderCustomize]
|
||||
[ProjectCustomize]
|
||||
public class CreateAccessPoliciesCommandTests
|
||||
{
|
||||
private static List<BaseAccessPolicy> MakeGrantedProjectAccessPolicies(Guid grantedProjectId, List<UserProjectAccessPolicy> userProjectAccessPolicies,
|
||||
List<GroupProjectAccessPolicy> groupProjectAccessPolicies,
|
||||
List<ServiceAccountProjectAccessPolicy> serviceAccountProjectAccessPolicies)
|
||||
{
|
||||
var data = new List<BaseAccessPolicy>();
|
||||
foreach (var ap in userProjectAccessPolicies)
|
||||
{
|
||||
ap.GrantedProjectId = grantedProjectId;
|
||||
ap.GrantedProject = null;
|
||||
ap.User = null;
|
||||
}
|
||||
foreach (var ap in groupProjectAccessPolicies)
|
||||
{
|
||||
ap.GrantedProjectId = grantedProjectId;
|
||||
ap.GrantedProject = null;
|
||||
ap.Group = null;
|
||||
}
|
||||
foreach (var ap in serviceAccountProjectAccessPolicies)
|
||||
{
|
||||
ap.GrantedProjectId = grantedProjectId;
|
||||
ap.GrantedProject = null;
|
||||
ap.ServiceAccount = null;
|
||||
}
|
||||
data.AddRange(userProjectAccessPolicies);
|
||||
data.AddRange(groupProjectAccessPolicies);
|
||||
data.AddRange(serviceAccountProjectAccessPolicies);
|
||||
return data;
|
||||
}
|
||||
|
||||
private static List<BaseAccessPolicy> MakeGrantedServiceAccountAccessPolicies(Guid grantedServiceAccountId, List<UserServiceAccountAccessPolicy> userServiceAccountAccessPolicies,
|
||||
List<GroupServiceAccountAccessPolicy> groupServiceAccountAccessPolicies)
|
||||
{
|
||||
var data = new List<BaseAccessPolicy>();
|
||||
foreach (var ap in userServiceAccountAccessPolicies)
|
||||
{
|
||||
ap.GrantedServiceAccountId = grantedServiceAccountId;
|
||||
ap.GrantedServiceAccount = null;
|
||||
ap.User = null;
|
||||
}
|
||||
foreach (var ap in groupServiceAccountAccessPolicies)
|
||||
{
|
||||
ap.GrantedServiceAccountId = grantedServiceAccountId;
|
||||
ap.GrantedServiceAccount = null;
|
||||
ap.Group = null;
|
||||
}
|
||||
data.AddRange(userServiceAccountAccessPolicies);
|
||||
data.AddRange(groupServiceAccountAccessPolicies);
|
||||
return data;
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task CreateMany_AlreadyExists_Throws_BadRequestException(
|
||||
Project project,
|
||||
ServiceAccount serviceAccount,
|
||||
List<UserProjectAccessPolicy> userProjectAccessPolicies,
|
||||
List<GroupProjectAccessPolicy> groupProjectAccessPolicies,
|
||||
List<ServiceAccountProjectAccessPolicy> serviceAccountProjectAccessPolicies,
|
||||
List<UserServiceAccountAccessPolicy> userServiceAccountAccessPolicies,
|
||||
List<GroupServiceAccountAccessPolicy> groupServiceAccountAccessPolicies,
|
||||
SutProvider<CreateAccessPoliciesCommand> sutProvider)
|
||||
{
|
||||
var data = MakeGrantedProjectAccessPolicies(project.Id, userProjectAccessPolicies, groupProjectAccessPolicies,
|
||||
serviceAccountProjectAccessPolicies);
|
||||
var saData = MakeGrantedServiceAccountAccessPolicies(serviceAccount.Id, userServiceAccountAccessPolicies, groupServiceAccountAccessPolicies);
|
||||
data = data.Concat(saData).ToList();
|
||||
|
||||
sutProvider.GetDependency<IAccessPolicyRepository>().AccessPolicyExists(Arg.Any<BaseAccessPolicy>())
|
||||
.Returns(true);
|
||||
|
||||
await Assert.ThrowsAsync<BadRequestException>(() =>
|
||||
sutProvider.Sut.CreateManyAsync(data));
|
||||
|
||||
await sutProvider.GetDependency<IAccessPolicyRepository>().DidNotReceiveWithAnyArgs().CreateManyAsync(default!);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task CreateMany_ClearsReferences(SutProvider<CreateAccessPoliciesCommand> sutProvider, Guid projectId)
|
||||
{
|
||||
var userProjectAp = new UserProjectAccessPolicy
|
||||
{
|
||||
GrantedProjectId = projectId,
|
||||
OrganizationUserId = new Guid(),
|
||||
};
|
||||
var data = new List<BaseAccessPolicy>() { userProjectAp, };
|
||||
|
||||
userProjectAp.GrantedProject = new Project() { Id = new Guid() };
|
||||
var expectedCall = new List<BaseAccessPolicy>() { userProjectAp, };
|
||||
|
||||
await sutProvider.Sut.CreateManyAsync(data);
|
||||
|
||||
await sutProvider.GetDependency<IAccessPolicyRepository>().Received(1)
|
||||
.CreateManyAsync(Arg.Is(AssertHelper.AssertPropertyEqual(expectedCall)));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task CreateMany_Success(
|
||||
Project project,
|
||||
ServiceAccount serviceAccount,
|
||||
List<UserProjectAccessPolicy> userProjectAccessPolicies,
|
||||
List<GroupProjectAccessPolicy> groupProjectAccessPolicies,
|
||||
List<ServiceAccountProjectAccessPolicy> serviceAccountProjectAccessPolicies,
|
||||
List<UserServiceAccountAccessPolicy> userServiceAccountAccessPolicies,
|
||||
List<GroupServiceAccountAccessPolicy> groupServiceAccountAccessPolicies,
|
||||
SutProvider<CreateAccessPoliciesCommand> sutProvider)
|
||||
{
|
||||
var data = MakeGrantedProjectAccessPolicies(project.Id, userProjectAccessPolicies, groupProjectAccessPolicies,
|
||||
serviceAccountProjectAccessPolicies);
|
||||
var saData = MakeGrantedServiceAccountAccessPolicies(serviceAccount.Id, userServiceAccountAccessPolicies, groupServiceAccountAccessPolicies);
|
||||
data = data.Concat(saData).ToList();
|
||||
|
||||
await sutProvider.Sut.CreateManyAsync(data);
|
||||
|
||||
await sutProvider.GetDependency<IAccessPolicyRepository>().Received(1)
|
||||
.CreateManyAsync(Arg.Is(AssertHelper.AssertPropertyEqual(data)));
|
||||
}
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
using Bit.Commercial.Core.SecretsManager.Commands.AccessPolicies;
|
||||
using Bit.Core.SecretsManager.Repositories;
|
||||
using Bit.Core.Test.SecretsManager.AutoFixture.ProjectsFixture;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Bit.Test.Common.Helpers;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Commercial.Core.Test.SecretsManager.Commands.AccessPolicies;
|
||||
|
||||
[SutProviderCustomize]
|
||||
[ProjectCustomize]
|
||||
public class DeleteAccessPolicyCommandTests
|
||||
{
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task DeleteAsync_Success(SutProvider<DeleteAccessPolicyCommand> sutProvider, Guid data)
|
||||
{
|
||||
await sutProvider.Sut.DeleteAsync(data);
|
||||
|
||||
await sutProvider.GetDependency<IAccessPolicyRepository>().Received(1)
|
||||
.DeleteAsync(Arg.Is(AssertHelper.AssertPropertyEqual(data)));
|
||||
}
|
||||
}
|
@ -1,44 +0,0 @@
|
||||
using Bit.Commercial.Core.SecretsManager.Commands.AccessPolicies;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.SecretsManager.Entities;
|
||||
using Bit.Core.SecretsManager.Repositories;
|
||||
using Bit.Core.Test.SecretsManager.AutoFixture.ProjectsFixture;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Bit.Test.Common.Helpers;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Commercial.Core.Test.SecretsManager.Commands.AccessPolicies;
|
||||
|
||||
[SutProviderCustomize]
|
||||
[ProjectCustomize]
|
||||
public class UpdateAccessPolicyCommandTests
|
||||
{
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task UpdateAsync_DoesNotExist_ThrowsNotFound(Guid data, bool read, bool write,
|
||||
SutProvider<UpdateAccessPolicyCommand> sutProvider)
|
||||
{
|
||||
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.UpdateAsync(data, read, write));
|
||||
await sutProvider.GetDependency<IAccessPolicyRepository>().DidNotReceiveWithAnyArgs().ReplaceAsync(Arg.Any<BaseAccessPolicy>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task UpdateAsync_Success(Guid data, bool read, bool write, UserProjectAccessPolicy accessPolicy,
|
||||
SutProvider<UpdateAccessPolicyCommand> sutProvider)
|
||||
{
|
||||
accessPolicy.Id = data;
|
||||
sutProvider.GetDependency<IAccessPolicyRepository>().GetByIdAsync(data).Returns(accessPolicy);
|
||||
|
||||
var result = await sutProvider.Sut.UpdateAsync(data, read, write);
|
||||
|
||||
await sutProvider.GetDependency<IAccessPolicyRepository>().Received(1)
|
||||
.ReplaceAsync(Arg.Any<BaseAccessPolicy>());
|
||||
|
||||
AssertHelper.AssertRecent(result.RevisionDate);
|
||||
Assert.Equal(read, result.Read);
|
||||
Assert.Equal(write, result.Write);
|
||||
}
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
using Bit.Commercial.Core.SecretsManager.Commands.AccessPolicies;
|
||||
using Bit.Core.SecretsManager.Models.Data.AccessPolicyUpdates;
|
||||
using Bit.Core.SecretsManager.Repositories;
|
||||
using Bit.Core.Test.SecretsManager.AutoFixture.ProjectsFixture;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Commercial.Core.Test.SecretsManager.Commands.AccessPolicies;
|
||||
|
||||
[SutProviderCustomize]
|
||||
[ProjectCustomize]
|
||||
public class UpdateProjectServiceAccountsAccessPoliciesCommandTests
|
||||
{
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task UpdateAsync_NoUpdates_DoesNotCallRepository(
|
||||
SutProvider<UpdateProjectServiceAccountsAccessPoliciesCommand> sutProvider,
|
||||
ProjectServiceAccountsAccessPoliciesUpdates data)
|
||||
{
|
||||
data.ServiceAccountAccessPolicyUpdates = [];
|
||||
await sutProvider.Sut.UpdateAsync(data);
|
||||
|
||||
await sutProvider.GetDependency<IAccessPolicyRepository>()
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.UpdateProjectServiceAccountsAccessPoliciesAsync(Arg.Any<ProjectServiceAccountsAccessPoliciesUpdates>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task UpdateAsync_HasUpdates_CallsRepository(
|
||||
SutProvider<UpdateProjectServiceAccountsAccessPoliciesCommand> sutProvider,
|
||||
ProjectServiceAccountsAccessPoliciesUpdates data)
|
||||
{
|
||||
await sutProvider.Sut.UpdateAsync(data);
|
||||
|
||||
await sutProvider.GetDependency<IAccessPolicyRepository>()
|
||||
.Received(1)
|
||||
.UpdateProjectServiceAccountsAccessPoliciesAsync(Arg.Any<ProjectServiceAccountsAccessPoliciesUpdates>());
|
||||
}
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
#nullable enable
|
||||
using Bit.Commercial.Core.SecretsManager.Commands.AccessPolicies;
|
||||
using Bit.Core.SecretsManager.Models.Data;
|
||||
using Bit.Core.SecretsManager.Repositories;
|
||||
using Bit.Core.Test.SecretsManager.AutoFixture.ProjectsFixture;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Commercial.Core.Test.SecretsManager.Commands.AccessPolicies;
|
||||
|
||||
[SutProviderCustomize]
|
||||
[ProjectCustomize]
|
||||
public class UpdateServiceAccountGrantedPoliciesCommandTests
|
||||
{
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task UpdateAsync_NoUpdates_DoesNotCallRepository(
|
||||
SutProvider<UpdateServiceAccountGrantedPoliciesCommand> sutProvider,
|
||||
ServiceAccountGrantedPoliciesUpdates data)
|
||||
{
|
||||
data.ProjectGrantedPolicyUpdates = [];
|
||||
await sutProvider.Sut.UpdateAsync(data);
|
||||
|
||||
await sutProvider.GetDependency<IAccessPolicyRepository>()
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.UpdateServiceAccountGrantedPoliciesAsync(Arg.Any<ServiceAccountGrantedPoliciesUpdates>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task UpdateAsync_HasUpdates_CallsRepository(
|
||||
SutProvider<UpdateServiceAccountGrantedPoliciesCommand> sutProvider,
|
||||
ServiceAccountGrantedPoliciesUpdates data)
|
||||
{
|
||||
await sutProvider.Sut.UpdateAsync(data);
|
||||
|
||||
await sutProvider.GetDependency<IAccessPolicyRepository>()
|
||||
.Received(1)
|
||||
.UpdateServiceAccountGrantedPoliciesAsync(Arg.Any<ServiceAccountGrantedPoliciesUpdates>());
|
||||
}
|
||||
}
|
@ -0,0 +1,86 @@
|
||||
#nullable enable
|
||||
using Bit.Commercial.Core.SecretsManager.Queries.AccessPolicies;
|
||||
using Bit.Core.SecretsManager.Entities;
|
||||
using Bit.Core.SecretsManager.Enums.AccessPolicies;
|
||||
using Bit.Core.SecretsManager.Models.Data;
|
||||
using Bit.Core.SecretsManager.Repositories;
|
||||
using Bit.Core.Test.SecretsManager.AutoFixture.ProjectsFixture;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using NSubstitute.ReturnsExtensions;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Commercial.Core.Test.SecretsManager.Queries.AccessPolicies;
|
||||
|
||||
[SutProviderCustomize]
|
||||
[ProjectCustomize]
|
||||
public class ProjectServiceAccountsAccessPoliciesUpdatesQueryTests
|
||||
{
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetAsync_NoCurrentAccessPolicies_ReturnsAllCreates(
|
||||
SutProvider<ProjectServiceAccountsAccessPoliciesUpdatesQuery> sutProvider,
|
||||
ProjectServiceAccountsAccessPolicies data)
|
||||
{
|
||||
sutProvider.GetDependency<IAccessPolicyRepository>()
|
||||
.GetProjectServiceAccountsAccessPoliciesAsync(data.ProjectId)
|
||||
.ReturnsNullForAnyArgs();
|
||||
|
||||
var result = await sutProvider.Sut.GetAsync(data);
|
||||
|
||||
Assert.Equal(data.ProjectId, result.ProjectId);
|
||||
Assert.Equal(data.OrganizationId, result.OrganizationId);
|
||||
Assert.Equal(data.ServiceAccountAccessPolicies.Count(), result.ServiceAccountAccessPolicyUpdates.Count());
|
||||
Assert.All(result.ServiceAccountAccessPolicyUpdates, p =>
|
||||
{
|
||||
Assert.Equal(AccessPolicyOperation.Create, p.Operation);
|
||||
Assert.Contains(data.ServiceAccountAccessPolicies, x => x == p.AccessPolicy);
|
||||
});
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetAsync_CurrentAccessPolicies_ReturnsChanges(
|
||||
SutProvider<ProjectServiceAccountsAccessPoliciesUpdatesQuery> sutProvider,
|
||||
ProjectServiceAccountsAccessPolicies data, ServiceAccountProjectAccessPolicy currentPolicyToDelete)
|
||||
{
|
||||
foreach (var policy in data.ServiceAccountAccessPolicies)
|
||||
{
|
||||
policy.GrantedProjectId = data.ProjectId;
|
||||
}
|
||||
|
||||
currentPolicyToDelete.GrantedProjectId = data.ProjectId;
|
||||
|
||||
var updatePolicy = new ServiceAccountProjectAccessPolicy
|
||||
{
|
||||
ServiceAccountId = data.ServiceAccountAccessPolicies.First().ServiceAccountId,
|
||||
GrantedProjectId = data.ProjectId,
|
||||
Read = !data.ServiceAccountAccessPolicies.First().Read,
|
||||
Write = !data.ServiceAccountAccessPolicies.First().Write
|
||||
};
|
||||
|
||||
var currentPolicies = new ProjectServiceAccountsAccessPolicies
|
||||
{
|
||||
ProjectId = data.ProjectId,
|
||||
OrganizationId = data.OrganizationId,
|
||||
ServiceAccountAccessPolicies = [updatePolicy, currentPolicyToDelete]
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IAccessPolicyRepository>()
|
||||
.GetProjectServiceAccountsAccessPoliciesAsync(data.ProjectId)
|
||||
.ReturnsForAnyArgs(currentPolicies);
|
||||
|
||||
var result = await sutProvider.Sut.GetAsync(data);
|
||||
|
||||
Assert.Equal(data.ProjectId, result.ProjectId);
|
||||
Assert.Equal(data.OrganizationId, result.OrganizationId);
|
||||
Assert.Single(result.ServiceAccountAccessPolicyUpdates.Where(x =>
|
||||
x.Operation == AccessPolicyOperation.Delete && x.AccessPolicy == currentPolicyToDelete));
|
||||
Assert.Single(result.ServiceAccountAccessPolicyUpdates.Where(x =>
|
||||
x.Operation == AccessPolicyOperation.Update &&
|
||||
x.AccessPolicy.GrantedProjectId == updatePolicy.GrantedProjectId));
|
||||
Assert.Equal(result.ServiceAccountAccessPolicyUpdates.Count() - 2,
|
||||
result.ServiceAccountAccessPolicyUpdates.Count(x => x.Operation == AccessPolicyOperation.Create));
|
||||
}
|
||||
}
|
@ -0,0 +1,86 @@
|
||||
#nullable enable
|
||||
using Bit.Commercial.Core.SecretsManager.Queries.AccessPolicies;
|
||||
using Bit.Core.SecretsManager.Entities;
|
||||
using Bit.Core.SecretsManager.Enums.AccessPolicies;
|
||||
using Bit.Core.SecretsManager.Models.Data;
|
||||
using Bit.Core.SecretsManager.Repositories;
|
||||
using Bit.Core.Test.SecretsManager.AutoFixture.ProjectsFixture;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using NSubstitute.ReturnsExtensions;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Commercial.Core.Test.SecretsManager.Queries.AccessPolicies;
|
||||
|
||||
[SutProviderCustomize]
|
||||
[ProjectCustomize]
|
||||
public class ServiceAccountGrantedPolicyUpdatesQueryTests
|
||||
{
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetAsync_NoCurrentGrantedPolicies_ReturnsAllCreates(
|
||||
SutProvider<ServiceAccountGrantedPolicyUpdatesQuery> sutProvider,
|
||||
ServiceAccountGrantedPolicies data)
|
||||
{
|
||||
sutProvider.GetDependency<IAccessPolicyRepository>()
|
||||
.GetServiceAccountGrantedPoliciesAsync(data.ServiceAccountId)
|
||||
.ReturnsNullForAnyArgs();
|
||||
|
||||
var result = await sutProvider.Sut.GetAsync(data);
|
||||
|
||||
Assert.Equal(data.ServiceAccountId, result.ServiceAccountId);
|
||||
Assert.Equal(data.OrganizationId, result.OrganizationId);
|
||||
Assert.Equal(data.ProjectGrantedPolicies.Count(), result.ProjectGrantedPolicyUpdates.Count());
|
||||
Assert.All(result.ProjectGrantedPolicyUpdates, p =>
|
||||
{
|
||||
Assert.Equal(AccessPolicyOperation.Create, p.Operation);
|
||||
Assert.Contains(data.ProjectGrantedPolicies, x => x == p.AccessPolicy);
|
||||
});
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetAsync_CurrentGrantedPolicies_ReturnsChanges(
|
||||
SutProvider<ServiceAccountGrantedPolicyUpdatesQuery> sutProvider,
|
||||
ServiceAccountGrantedPolicies data, ServiceAccountProjectAccessPolicy currentPolicyToDelete)
|
||||
{
|
||||
foreach (var grantedPolicy in data.ProjectGrantedPolicies)
|
||||
{
|
||||
grantedPolicy.ServiceAccountId = data.ServiceAccountId;
|
||||
}
|
||||
|
||||
currentPolicyToDelete.ServiceAccountId = data.ServiceAccountId;
|
||||
|
||||
var updatePolicy = new ServiceAccountProjectAccessPolicy
|
||||
{
|
||||
ServiceAccountId = data.ServiceAccountId,
|
||||
GrantedProjectId = data.ProjectGrantedPolicies.First().GrantedProjectId,
|
||||
Read = !data.ProjectGrantedPolicies.First().Read,
|
||||
Write = !data.ProjectGrantedPolicies.First().Write
|
||||
};
|
||||
|
||||
var currentPolicies = new ServiceAccountGrantedPolicies
|
||||
{
|
||||
ServiceAccountId = data.ServiceAccountId,
|
||||
OrganizationId = data.OrganizationId,
|
||||
ProjectGrantedPolicies = [updatePolicy, currentPolicyToDelete]
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IAccessPolicyRepository>()
|
||||
.GetServiceAccountGrantedPoliciesAsync(data.ServiceAccountId)
|
||||
.ReturnsForAnyArgs(currentPolicies);
|
||||
|
||||
var result = await sutProvider.Sut.GetAsync(data);
|
||||
|
||||
Assert.Equal(data.ServiceAccountId, result.ServiceAccountId);
|
||||
Assert.Equal(data.OrganizationId, result.OrganizationId);
|
||||
Assert.Single(result.ProjectGrantedPolicyUpdates.Where(x =>
|
||||
x.Operation == AccessPolicyOperation.Delete && x.AccessPolicy == currentPolicyToDelete));
|
||||
Assert.Single(result.ProjectGrantedPolicyUpdates.Where(x =>
|
||||
x.Operation == AccessPolicyOperation.Update &&
|
||||
x.AccessPolicy.GrantedProjectId == updatePolicy.GrantedProjectId));
|
||||
Assert.Equal(result.ProjectGrantedPolicyUpdates.Count() - 2,
|
||||
result.ProjectGrantedPolicyUpdates.Count(x => x.Operation == AccessPolicyOperation.Create));
|
||||
}
|
||||
}
|
@ -0,0 +1,96 @@
|
||||
#nullable enable
|
||||
using Bit.Commercial.Core.SecretsManager.Queries.Secrets;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.SecretsManager.Entities;
|
||||
using Bit.Core.SecretsManager.Models.Data;
|
||||
using Bit.Core.SecretsManager.Repositories;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Commercial.Core.Test.SecretsManager.Queries.Secrets;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class SecretsSyncQueryTests
|
||||
{
|
||||
[Theory, BitAutoData]
|
||||
public async Task GetAsync_NullLastSyncedDate_ReturnsHasChanges(
|
||||
SutProvider<SecretsSyncQuery> sutProvider,
|
||||
SecretsSyncRequest data)
|
||||
{
|
||||
data.LastSyncedDate = null;
|
||||
|
||||
var result = await sutProvider.Sut.GetAsync(data);
|
||||
|
||||
Assert.True(result.HasChanges);
|
||||
await sutProvider.GetDependency<ISecretRepository>().Received(1)
|
||||
.GetManyByOrganizationIdAsync(Arg.Is(data.OrganizationId),
|
||||
Arg.Is(data.ServiceAccountId),
|
||||
Arg.Is(data.AccessClientType));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task GetAsync_HasLastSyncedDateServiceAccountNotFound_Throws(
|
||||
SutProvider<SecretsSyncQuery> sutProvider,
|
||||
SecretsSyncRequest data)
|
||||
{
|
||||
data.LastSyncedDate = DateTime.UtcNow;
|
||||
sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(data.ServiceAccountId)
|
||||
.Returns((ServiceAccount?)null);
|
||||
|
||||
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.GetAsync(data));
|
||||
|
||||
await sutProvider.GetDependency<ISecretRepository>()
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.GetManyByOrganizationIdAsync(Arg.Any<Guid>(), Arg.Any<Guid>(), Arg.Any<AccessClientType>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(true)]
|
||||
[BitAutoData(false)]
|
||||
public async Task GetAsync_HasLastSyncedDateServiceAccountWithLaterOrEqualRevisionDate_ReturnsChanges(
|
||||
bool datesEqual,
|
||||
SutProvider<SecretsSyncQuery> sutProvider,
|
||||
SecretsSyncRequest data,
|
||||
ServiceAccount serviceAccount)
|
||||
{
|
||||
data.LastSyncedDate = DateTime.UtcNow.AddDays(-1);
|
||||
serviceAccount.Id = data.ServiceAccountId;
|
||||
serviceAccount.RevisionDate = datesEqual ? data.LastSyncedDate.Value : data.LastSyncedDate.Value.AddSeconds(600);
|
||||
|
||||
sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(data.ServiceAccountId)
|
||||
.Returns(serviceAccount);
|
||||
|
||||
var result = await sutProvider.Sut.GetAsync(data);
|
||||
|
||||
Assert.True(result.HasChanges);
|
||||
await sutProvider.GetDependency<ISecretRepository>().Received(1)
|
||||
.GetManyByOrganizationIdAsync(Arg.Is(data.OrganizationId),
|
||||
Arg.Is(data.ServiceAccountId),
|
||||
Arg.Is(data.AccessClientType));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task GetAsync_HasLastSyncedDateServiceAccountWithEarlierRevisionDate_ReturnsNoChanges(
|
||||
SutProvider<SecretsSyncQuery> sutProvider,
|
||||
SecretsSyncRequest data,
|
||||
ServiceAccount serviceAccount)
|
||||
{
|
||||
data.LastSyncedDate = DateTime.UtcNow.AddDays(-1);
|
||||
serviceAccount.Id = data.ServiceAccountId;
|
||||
serviceAccount.RevisionDate = data.LastSyncedDate.Value.AddDays(-2);
|
||||
|
||||
sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(data.ServiceAccountId)
|
||||
.Returns(serviceAccount);
|
||||
|
||||
var result = await sutProvider.Sut.GetAsync(data);
|
||||
|
||||
Assert.False(result.HasChanges);
|
||||
Assert.Null(result.Secrets);
|
||||
await sutProvider.GetDependency<ISecretRepository>()
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.GetManyByOrganizationIdAsync(Arg.Any<Guid>(), Arg.Any<Guid>(), Arg.Any<AccessClientType>());
|
||||
}
|
||||
}
|
@ -54,7 +54,7 @@ services:
|
||||
- postgres
|
||||
|
||||
mysql:
|
||||
image: mysql:8
|
||||
image: mysql:8.0
|
||||
container_name: bw-mysql
|
||||
ports:
|
||||
- "3306:3306"
|
||||
|
@ -1,12 +1,12 @@
|
||||
#!/bin/bash
|
||||
|
||||
# There seems to be [a bug with docker-compose](https://github.com/docker/compose/issues/4076#issuecomment-324932294)
|
||||
#
|
||||
# !!! UPDATED 2024 for MsSqlMigratorUtility !!!
|
||||
#
|
||||
# There seems to be [a bug with docker-compose](https://github.com/docker/compose/issues/4076#issuecomment-324932294)
|
||||
# where it takes ~40ms to connect to the terminal output of the container, so stuff logged to the terminal in this time is lost.
|
||||
# The best workaround seems to be adding tiny delay like so:
|
||||
sleep 0.1;
|
||||
|
||||
MIGRATE_DIRECTORY="/mnt/migrator/DbScripts"
|
||||
LAST_MIGRATION_FILE="/mnt/data/last_migration"
|
||||
SERVER='mssql'
|
||||
DATABASE="vault_dev"
|
||||
USER="SA"
|
||||
@ -16,58 +16,33 @@ while getopts "s" arg; do
|
||||
case $arg in
|
||||
s)
|
||||
echo "Running for self-host environment"
|
||||
LAST_MIGRATION_FILE="/mnt/data/last_self_host_migration"
|
||||
DATABASE="vault_dev_self_host"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ ! -f "$LAST_MIGRATION_FILE" ]; then
|
||||
echo "No migration file, nothing to migrate to a database store"
|
||||
exit 1
|
||||
else
|
||||
LAST_MIGRATION=$(cat $LAST_MIGRATION_FILE)
|
||||
rm $LAST_MIGRATION_FILE
|
||||
fi
|
||||
|
||||
[ -z "$LAST_MIGRATION" ]
|
||||
PERFORM_MIGRATION=$?
|
||||
|
||||
# Create database if it does not already exist
|
||||
QUERY="IF NOT EXISTS (SELECT * FROM sys.databases WHERE name = 'migrations_$DATABASE')
|
||||
QUERY="IF OBJECT_ID('[$DATABASE].[dbo].[Migration]') IS NULL AND OBJECT_ID('[migrations_$DATABASE].[dbo].[migrations]') IS NOT NULL
|
||||
BEGIN
|
||||
CREATE DATABASE migrations_$DATABASE;
|
||||
END;
|
||||
-- Create [database].dbo.Migration with the schema expected by MsSqlMigratorUtility
|
||||
SET ANSI_NULLS ON;
|
||||
SET QUOTED_IDENTIFIER ON;
|
||||
|
||||
CREATE TABLE [$DATABASE].[dbo].[Migration](
|
||||
[Id] [int] IDENTITY(1,1) NOT NULL,
|
||||
[ScriptName] [nvarchar](255) NOT NULL,
|
||||
[Applied] [datetime] NOT NULL
|
||||
) ON [PRIMARY];
|
||||
|
||||
ALTER TABLE [$DATABASE].[dbo].[Migration] ADD CONSTRAINT [PK_Migration_Id] PRIMARY KEY CLUSTERED
|
||||
(
|
||||
[Id] ASC
|
||||
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY];
|
||||
|
||||
-- Copy across old data
|
||||
INSERT INTO [$DATABASE].[dbo].[Migration] (ScriptName, Applied)
|
||||
SELECT CONCAT('Bit.Migrator.DbScripts.', [Filename]), CreationDate
|
||||
FROM [migrations_$DATABASE].[dbo].[migrations];
|
||||
END
|
||||
"
|
||||
|
||||
/opt/mssql-tools/bin/sqlcmd -S $SERVER -d master -U $USER -P $PASSWD -I -Q "$QUERY"
|
||||
|
||||
QUERY="IF OBJECT_ID('[dbo].[migrations_$DATABASE]') IS NULL
|
||||
BEGIN
|
||||
CREATE TABLE [migrations_$DATABASE].[dbo].[migrations] (
|
||||
[Id] INT IDENTITY(1,1) PRIMARY KEY,
|
||||
[Filename] NVARCHAR(MAX) NOT NULL,
|
||||
[CreationDate] DATETIME2 (7) NULL,
|
||||
);
|
||||
END;"
|
||||
|
||||
/opt/mssql-tools/bin/sqlcmd -S $SERVER -d master -U $USER -P $PASSWD -I -Q "$QUERY"
|
||||
|
||||
record_migration () {
|
||||
echo "recording $1"
|
||||
local file=$(basename $1)
|
||||
echo $file
|
||||
local query="INSERT INTO [migrations] ([Filename], [CreationDate]) VALUES ('$file', GETUTCDATE())"
|
||||
/opt/mssql-tools/bin/sqlcmd -S $SERVER -d migrations_$DATABASE -U $USER -P $PASSWD -I -Q "$query"
|
||||
}
|
||||
|
||||
for f in `ls -v $MIGRATE_DIRECTORY/*.sql`; do
|
||||
if (( PERFORM_MIGRATION == 0 )); then
|
||||
echo "Still need to migrate $f"
|
||||
else
|
||||
record_migration $f
|
||||
if [ "$LAST_MIGRATION" == "$f" ]; then
|
||||
PERFORM_MIGRATION=0
|
||||
fi
|
||||
fi
|
||||
done;
|
||||
|
@ -1,94 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# There seems to be [a bug with docker-compose](https://github.com/docker/compose/issues/4076#issuecomment-324932294)
|
||||
# where it takes ~40ms to connect to the terminal output of the container, so stuff logged to the terminal in this time is lost.
|
||||
# The best workaround seems to be adding tiny delay like so:
|
||||
sleep 0.1;
|
||||
|
||||
MIGRATE_DIRECTORY="/mnt/migrator/DbScripts"
|
||||
SERVER='mssql'
|
||||
DATABASE="vault_dev"
|
||||
USER="SA"
|
||||
PASSWD=$MSSQL_PASSWORD
|
||||
|
||||
while getopts "sp" arg; do
|
||||
case $arg in
|
||||
s)
|
||||
echo "Running for self-host environment"
|
||||
DATABASE="vault_dev_self_host"
|
||||
;;
|
||||
p)
|
||||
echo "Running for pipeline"
|
||||
MIGRATE_DIRECTORY=$MSSQL_MIGRATIONS_DIRECTORY
|
||||
SERVER=$MSSQL_HOST
|
||||
DATABASE=$MSSQL_DATABASE
|
||||
USER=$MSSQL_USER
|
||||
PASSWD=$MSSQL_PASS
|
||||
esac
|
||||
done
|
||||
|
||||
# Create databases if they do not already exist
|
||||
QUERY="IF NOT EXISTS (SELECT * FROM sys.databases WHERE name = '$DATABASE')
|
||||
BEGIN
|
||||
CREATE DATABASE $DATABASE;
|
||||
END;
|
||||
|
||||
GO
|
||||
IF NOT EXISTS (SELECT * FROM sys.databases WHERE name = 'migrations_$DATABASE')
|
||||
BEGIN
|
||||
CREATE DATABASE migrations_$DATABASE;
|
||||
END;
|
||||
|
||||
GO
|
||||
"
|
||||
/opt/mssql-tools/bin/sqlcmd -S $SERVER -d master -U $USER -P $PASSWD -I -Q "$QUERY"
|
||||
echo "Return code: $?"
|
||||
|
||||
# Create migrations table if it does not already exist
|
||||
QUERY="IF OBJECT_ID('[migrations_$DATABASE].[dbo].[migrations]') IS NULL
|
||||
BEGIN
|
||||
CREATE TABLE [migrations_$DATABASE].[dbo].[migrations] (
|
||||
[Id] INT IDENTITY(1,1) PRIMARY KEY,
|
||||
[Filename] NVARCHAR(MAX) NOT NULL,
|
||||
[CreationDate] DATETIME2 (7) NULL,
|
||||
);
|
||||
END;
|
||||
GO
|
||||
"
|
||||
/opt/mssql-tools/bin/sqlcmd -S $SERVER -d migrations_$DATABASE -U $USER -P $PASSWD -I -Q "$QUERY"
|
||||
echo "Return code: $?"
|
||||
|
||||
should_migrate () {
|
||||
local file=$(basename $1)
|
||||
local query="SELECT * FROM [migrations] WHERE [Filename] = '$file'"
|
||||
local result=$(/opt/mssql-tools/bin/sqlcmd -S $SERVER -d migrations_$DATABASE -U $USER -P $PASSWD -I -Q "$query")
|
||||
if [[ "$result" =~ .*"$file".* ]]; then
|
||||
return 1;
|
||||
else
|
||||
return 0;
|
||||
fi
|
||||
}
|
||||
|
||||
record_migration () {
|
||||
echo "recording $1"
|
||||
local file=$(basename $1)
|
||||
echo $file
|
||||
local query="INSERT INTO [migrations] ([Filename], [CreationDate]) VALUES ('$file', GETUTCDATE())"
|
||||
/opt/mssql-tools/bin/sqlcmd -S $SERVER -d migrations_$DATABASE -U $USER -P $PASSWD -I -Q "$query"
|
||||
}
|
||||
|
||||
migrate () {
|
||||
local file=$1
|
||||
echo "Performing $file"
|
||||
/opt/mssql-tools/bin/sqlcmd -S $SERVER -d $DATABASE -U $USER -P $PASSWD -I -i $file
|
||||
}
|
||||
|
||||
for f in `ls -v $MIGRATE_DIRECTORY/*.sql`; do
|
||||
BASENAME=$(basename $f)
|
||||
if should_migrate $f == 1 ; then
|
||||
migrate $f
|
||||
record_migration $f
|
||||
else
|
||||
echo "Skipping $f, $BASENAME"
|
||||
fi
|
||||
done;
|
@ -2,20 +2,20 @@
|
||||
# Creates the vault_dev database, and runs all the migrations.
|
||||
|
||||
# Due to azure-edge-sql not containing the mssql-tools on ARM, we manually use
|
||||
# the mssql-tools container which runs under x86_64. We should monitor this
|
||||
# in the future and investigate if we can migrate back.
|
||||
# docker-compose --profile mssql exec mssql bash /mnt/helpers/run_migrations.sh @args
|
||||
# the mssql-tools container which runs under x86_64.
|
||||
|
||||
param(
|
||||
[switch]$all = $false,
|
||||
[switch]$postgres = $false,
|
||||
[switch]$mysql = $false,
|
||||
[switch]$mssql = $false,
|
||||
[switch]$sqlite = $false,
|
||||
[switch]$selfhost = $false,
|
||||
[switch]$pipeline = $false
|
||||
[switch]$all,
|
||||
[switch]$postgres,
|
||||
[switch]$mysql,
|
||||
[switch]$mssql,
|
||||
[switch]$sqlite,
|
||||
[switch]$selfhost
|
||||
)
|
||||
|
||||
# Abort on any error
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
if (!$all -and !$postgres -and !$mysql -and !$sqlite) {
|
||||
$mssql = $true;
|
||||
}
|
||||
@ -29,22 +29,27 @@ if ($all -or $postgres -or $mysql -or $sqlite) {
|
||||
}
|
||||
|
||||
if ($all -or $mssql) {
|
||||
if ($selfhost) {
|
||||
$migrationArgs = "-s"
|
||||
} elseif ($pipeline) {
|
||||
$migrationArgs = "-p"
|
||||
function Get-UserSecrets {
|
||||
return dotnet user-secrets list --json --project ../src/Api | ConvertFrom-Json
|
||||
}
|
||||
|
||||
Write-Host "Starting Microsoft SQL Server Migrations"
|
||||
docker run `
|
||||
-v "$(pwd)/helpers/mssql:/mnt/helpers" `
|
||||
-v "$(pwd)/../util/Migrator:/mnt/migrator/" `
|
||||
-v "$(pwd)/.data/mssql:/mnt/data" `
|
||||
--env-file .env `
|
||||
--network=bitwardenserver_default `
|
||||
--rm `
|
||||
mcr.microsoft.com/mssql-tools `
|
||||
/mnt/helpers/run_migrations.sh $migrationArgs
|
||||
if ($selfhost) {
|
||||
$msSqlConnectionString = $(Get-UserSecrets).'dev:selfHostOverride:globalSettings:sqlServer:connectionString'
|
||||
$envName = "self-host"
|
||||
|
||||
Write-Output "Migrating your migrations to use MsSqlMigratorUtility (if needed)"
|
||||
./migrate_migration_record.ps1 -s
|
||||
} else {
|
||||
$msSqlConnectionString = $(Get-UserSecrets).'globalSettings:sqlServer:connectionString'
|
||||
$envName = "cloud"
|
||||
|
||||
Write-Output "Migrating your migrations to use MsSqlMigratorUtility (if needed)"
|
||||
./migrate_migration_record.ps1
|
||||
}
|
||||
|
||||
Write-Host "Starting Microsoft SQL Server Migrations for $envName"
|
||||
|
||||
dotnet run --project ../util/MsSqlMigratorUtility/ "$msSqlConnectionString"
|
||||
}
|
||||
|
||||
$currentDir = Get-Location
|
||||
|
@ -1,15 +1,13 @@
|
||||
#!/usr/bin/env pwsh
|
||||
# This script need only be run once
|
||||
# !!! UPDATED 2024 for MsSqlMigratorUtility !!!
|
||||
#
|
||||
# This is a migration script for updating recording the last migration run
|
||||
# in a file to recording migrations in a database table. It will create a
|
||||
# migrations_vault table and store all of the previously run migrations as
|
||||
# indicated by a last_migrations file. It will then delete this file.
|
||||
# This is a migration script to move data from [migrations_vault_dev].[dbo].[migrations] (used by our custom
|
||||
# migrator script) to [vault_dev].[dbo].[Migration] (used by MsSqlMigratorUtility). It is safe to run multiple
|
||||
# times because it will not perform any migration if it detects that the new table is already present.
|
||||
# This will be deleted after a few months after everyone has (presumably) migrated to the new schema.
|
||||
|
||||
# Due to azure-edge-sql not containing the mssql-tools on ARM, we manually use
|
||||
# the mssql-tools container which runs under x86_64. We should monitor this
|
||||
# in the future and investigate if we can migrate back.
|
||||
# docker-compose --profile mssql exec mssql bash /mnt/helpers/run_migrations.sh @args
|
||||
# the mssql-tools container which runs under x86_64.
|
||||
|
||||
docker run `
|
||||
-v "$(pwd)/helpers/mssql:/mnt/helpers" `
|
||||
|
@ -24,8 +24,9 @@ $projects = @{
|
||||
Icons = "../src/Icons"
|
||||
Identity = "../src/Identity"
|
||||
Notifications = "../src/Notifications"
|
||||
Sso = "../bitwarden_license/src/Sso"
|
||||
Scim = "../bitwarden_license/src/Scim"
|
||||
Sso = "../bitwarden_license/src/Sso"
|
||||
Scim = "../bitwarden_license/src/Scim"
|
||||
IntegrationTests = "../test/Infrastructure.IntegrationTest"
|
||||
}
|
||||
|
||||
foreach ($key in $projects.keys) {
|
||||
|
@ -1,11 +1,14 @@
|
||||
using Bit.Admin.AdminConsole.Models;
|
||||
using System.Net;
|
||||
using Bit.Admin.AdminConsole.Models;
|
||||
using Bit.Admin.Enums;
|
||||
using Bit.Admin.Services;
|
||||
using Bit.Admin.Utilities;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Providers.Interfaces;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Billing.Commands;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
@ -52,6 +55,8 @@ public class OrganizationsController : Controller
|
||||
private readonly IProviderOrganizationRepository _providerOrganizationRepository;
|
||||
private readonly IRemoveOrganizationFromProviderCommand _removeOrganizationFromProviderCommand;
|
||||
private readonly IRemovePaymentMethodCommand _removePaymentMethodCommand;
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly IScaleSeatsCommand _scaleSeatsCommand;
|
||||
|
||||
public OrganizationsController(
|
||||
IOrganizationService organizationService,
|
||||
@ -77,7 +82,9 @@ public class OrganizationsController : Controller
|
||||
IServiceAccountRepository serviceAccountRepository,
|
||||
IProviderOrganizationRepository providerOrganizationRepository,
|
||||
IRemoveOrganizationFromProviderCommand removeOrganizationFromProviderCommand,
|
||||
IRemovePaymentMethodCommand removePaymentMethodCommand)
|
||||
IRemovePaymentMethodCommand removePaymentMethodCommand,
|
||||
IFeatureService featureService,
|
||||
IScaleSeatsCommand scaleSeatsCommand)
|
||||
{
|
||||
_organizationService = organizationService;
|
||||
_organizationRepository = organizationRepository;
|
||||
@ -103,6 +110,8 @@ public class OrganizationsController : Controller
|
||||
_providerOrganizationRepository = providerOrganizationRepository;
|
||||
_removeOrganizationFromProviderCommand = removeOrganizationFromProviderCommand;
|
||||
_removePaymentMethodCommand = removePaymentMethodCommand;
|
||||
_featureService = featureService;
|
||||
_scaleSeatsCommand = scaleSeatsCommand;
|
||||
}
|
||||
|
||||
[RequirePermission(Permission.Org_List_View)]
|
||||
@ -119,8 +128,9 @@ public class OrganizationsController : Controller
|
||||
count = 1;
|
||||
}
|
||||
|
||||
var encodedName = WebUtility.HtmlEncode(name);
|
||||
var skip = (page - 1) * count;
|
||||
var organizations = await _organizationRepository.SearchAsync(name, userEmail, paid, skip, count);
|
||||
var organizations = await _organizationRepository.SearchAsync(encodedName, userEmail, paid, skip, count);
|
||||
return View(new OrganizationsModel
|
||||
{
|
||||
Items = organizations as List<Organization>,
|
||||
@ -232,12 +242,30 @@ public class OrganizationsController : Controller
|
||||
public async Task<IActionResult> Delete(Guid id)
|
||||
{
|
||||
var organization = await _organizationRepository.GetByIdAsync(id);
|
||||
if (organization != null)
|
||||
|
||||
if (organization == null)
|
||||
{
|
||||
await _organizationRepository.DeleteAsync(organization);
|
||||
await _applicationCacheService.DeleteOrganizationAbilityAsync(organization.Id);
|
||||
return RedirectToAction("Index");
|
||||
}
|
||||
|
||||
var consolidatedBillingEnabled = _featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling);
|
||||
|
||||
if (consolidatedBillingEnabled && organization.IsValidClient())
|
||||
{
|
||||
var provider = await _providerRepository.GetByOrganizationIdAsync(organization.Id);
|
||||
|
||||
if (provider.IsBillable())
|
||||
{
|
||||
await _scaleSeatsCommand.ScalePasswordManagerSeats(
|
||||
provider,
|
||||
organization.PlanType,
|
||||
-organization.Seats ?? 0);
|
||||
}
|
||||
}
|
||||
|
||||
await _organizationRepository.DeleteAsync(organization);
|
||||
await _applicationCacheService.DeleteOrganizationAbilityAsync(organization.Id);
|
||||
|
||||
return RedirectToAction("Index");
|
||||
}
|
||||
|
||||
@ -321,7 +349,10 @@ public class OrganizationsController : Controller
|
||||
providerOrganization,
|
||||
organization);
|
||||
|
||||
await _removePaymentMethodCommand.RemovePaymentMethod(organization);
|
||||
if (organization.IsStripeEnabled())
|
||||
{
|
||||
await _removePaymentMethodCommand.RemovePaymentMethod(organization);
|
||||
}
|
||||
|
||||
return Json(null);
|
||||
}
|
||||
|
@ -3,6 +3,8 @@ using Bit.Admin.Utilities;
|
||||
using Bit.Core.AdminConsole.Providers.Interfaces;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Billing.Commands;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
@ -55,12 +57,23 @@ public class ProviderOrganizationsController : Controller
|
||||
return RedirectToAction("View", "Providers", new { id = providerId });
|
||||
}
|
||||
|
||||
await _removeOrganizationFromProviderCommand.RemoveOrganizationFromProvider(
|
||||
provider,
|
||||
providerOrganization,
|
||||
organization);
|
||||
try
|
||||
{
|
||||
await _removeOrganizationFromProviderCommand.RemoveOrganizationFromProvider(
|
||||
provider,
|
||||
providerOrganization,
|
||||
organization);
|
||||
}
|
||||
catch (BadRequestException ex)
|
||||
{
|
||||
return BadRequest(ex.Message);
|
||||
}
|
||||
|
||||
await _removePaymentMethodCommand.RemovePaymentMethod(organization);
|
||||
|
||||
if (organization.IsStripeEnabled())
|
||||
{
|
||||
await _removePaymentMethodCommand.RemovePaymentMethod(organization);
|
||||
}
|
||||
|
||||
return Json(null);
|
||||
}
|
||||
|
@ -1,4 +1,6 @@
|
||||
using Bit.Admin.AdminConsole.Models;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Net;
|
||||
using Bit.Admin.AdminConsole.Models;
|
||||
using Bit.Admin.Enums;
|
||||
using Bit.Admin.Utilities;
|
||||
using Bit.Core;
|
||||
@ -7,10 +9,14 @@ using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.AdminConsole.Providers.Interfaces;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Billing.Entities;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Billing.Repositories;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Tools.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
@ -29,10 +35,10 @@ public class ProvidersController : Controller
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
private readonly IApplicationCacheService _applicationCacheService;
|
||||
private readonly IProviderService _providerService;
|
||||
private readonly IReferenceEventService _referenceEventService;
|
||||
private readonly IUserService _userService;
|
||||
private readonly ICreateProviderCommand _createProviderCommand;
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly IProviderPlanRepository _providerPlanRepository;
|
||||
|
||||
public ProvidersController(
|
||||
IOrganizationRepository organizationRepository,
|
||||
@ -43,10 +49,10 @@ public class ProvidersController : Controller
|
||||
IProviderService providerService,
|
||||
GlobalSettings globalSettings,
|
||||
IApplicationCacheService applicationCacheService,
|
||||
IReferenceEventService referenceEventService,
|
||||
IUserService userService,
|
||||
ICreateProviderCommand createProviderCommand,
|
||||
IFeatureService featureService)
|
||||
IFeatureService featureService,
|
||||
IProviderPlanRepository providerPlanRepository)
|
||||
{
|
||||
_organizationRepository = organizationRepository;
|
||||
_organizationService = organizationService;
|
||||
@ -56,10 +62,10 @@ public class ProvidersController : Controller
|
||||
_providerService = providerService;
|
||||
_globalSettings = globalSettings;
|
||||
_applicationCacheService = applicationCacheService;
|
||||
_referenceEventService = referenceEventService;
|
||||
_userService = userService;
|
||||
_createProviderCommand = createProviderCommand;
|
||||
_featureService = featureService;
|
||||
_providerPlanRepository = providerPlanRepository;
|
||||
}
|
||||
|
||||
[RequirePermission(Permission.Provider_List_View)]
|
||||
@ -89,11 +95,13 @@ public class ProvidersController : Controller
|
||||
});
|
||||
}
|
||||
|
||||
public IActionResult Create(string ownerEmail = null)
|
||||
public IActionResult Create(int teamsMinimumSeats, int enterpriseMinimumSeats, string ownerEmail = null)
|
||||
{
|
||||
return View(new CreateProviderModel
|
||||
{
|
||||
OwnerEmail = ownerEmail
|
||||
OwnerEmail = ownerEmail,
|
||||
TeamsMonthlySeatMinimum = teamsMinimumSeats,
|
||||
EnterpriseMonthlySeatMinimum = enterpriseMinimumSeats
|
||||
});
|
||||
}
|
||||
|
||||
@ -111,7 +119,11 @@ public class ProvidersController : Controller
|
||||
switch (provider.Type)
|
||||
{
|
||||
case ProviderType.Msp:
|
||||
await _createProviderCommand.CreateMspAsync(provider, model.OwnerEmail);
|
||||
await _createProviderCommand.CreateMspAsync(
|
||||
provider,
|
||||
model.OwnerEmail,
|
||||
model.TeamsMonthlySeatMinimum,
|
||||
model.EnterpriseMonthlySeatMinimum);
|
||||
break;
|
||||
case ProviderType.Reseller:
|
||||
await _createProviderCommand.CreateResellerAsync(provider);
|
||||
@ -146,7 +158,17 @@ public class ProvidersController : Controller
|
||||
|
||||
var users = await _providerUserRepository.GetManyDetailsByProviderAsync(id);
|
||||
var providerOrganizations = await _providerOrganizationRepository.GetManyDetailsByProviderAsync(id);
|
||||
return View(new ProviderEditModel(provider, users, providerOrganizations));
|
||||
|
||||
var isConsolidatedBillingEnabled = _featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling);
|
||||
|
||||
if (!isConsolidatedBillingEnabled || !provider.IsBillable())
|
||||
{
|
||||
return View(new ProviderEditModel(provider, users, providerOrganizations, new List<ProviderPlan>()));
|
||||
}
|
||||
|
||||
var providerPlans = await _providerPlanRepository.GetByProviderId(id);
|
||||
|
||||
return View(new ProviderEditModel(provider, users, providerOrganizations, providerPlans.ToList()));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
@ -156,14 +178,56 @@ public class ProvidersController : Controller
|
||||
public async Task<IActionResult> Edit(Guid id, ProviderEditModel model)
|
||||
{
|
||||
var provider = await _providerRepository.GetByIdAsync(id);
|
||||
|
||||
if (provider == null)
|
||||
{
|
||||
return RedirectToAction("Index");
|
||||
}
|
||||
|
||||
model.ToProvider(provider);
|
||||
|
||||
await _providerRepository.ReplaceAsync(provider);
|
||||
await _applicationCacheService.UpsertProviderAbilityAsync(provider);
|
||||
|
||||
var isConsolidatedBillingEnabled = _featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling);
|
||||
|
||||
if (!isConsolidatedBillingEnabled || !provider.IsBillable())
|
||||
{
|
||||
return RedirectToAction("Edit", new { id });
|
||||
}
|
||||
|
||||
var providerPlans = await _providerPlanRepository.GetByProviderId(id);
|
||||
|
||||
if (providerPlans.Count == 0)
|
||||
{
|
||||
var newProviderPlans = new List<ProviderPlan>
|
||||
{
|
||||
new () { ProviderId = provider.Id, PlanType = PlanType.TeamsMonthly, SeatMinimum = model.TeamsMonthlySeatMinimum, PurchasedSeats = 0, AllocatedSeats = 0 },
|
||||
new () { ProviderId = provider.Id, PlanType = PlanType.EnterpriseMonthly, SeatMinimum = model.EnterpriseMonthlySeatMinimum, PurchasedSeats = 0, AllocatedSeats = 0 }
|
||||
};
|
||||
|
||||
foreach (var newProviderPlan in newProviderPlans)
|
||||
{
|
||||
await _providerPlanRepository.CreateAsync(newProviderPlan);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var providerPlan in providerPlans)
|
||||
{
|
||||
if (providerPlan.PlanType == PlanType.EnterpriseMonthly)
|
||||
{
|
||||
providerPlan.SeatMinimum = model.EnterpriseMonthlySeatMinimum;
|
||||
}
|
||||
else if (providerPlan.PlanType == PlanType.TeamsMonthly)
|
||||
{
|
||||
providerPlan.SeatMinimum = model.TeamsMonthlySeatMinimum;
|
||||
}
|
||||
|
||||
await _providerPlanRepository.ReplaceAsync(providerPlan);
|
||||
}
|
||||
}
|
||||
|
||||
return RedirectToAction("Edit", new { id });
|
||||
}
|
||||
|
||||
@ -188,8 +252,9 @@ public class ProvidersController : Controller
|
||||
count = 1;
|
||||
}
|
||||
|
||||
var encodedName = WebUtility.HtmlEncode(name);
|
||||
var skip = (page - 1) * count;
|
||||
var unassignedOrganizations = await _organizationRepository.SearchUnassignedToProviderAsync(name, ownerEmail, skip, count);
|
||||
var unassignedOrganizations = await _organizationRepository.SearchUnassignedToProviderAsync(encodedName, ownerEmail, skip, count);
|
||||
var viewModel = new OrganizationUnassignedToProviderSearchViewModel
|
||||
{
|
||||
OrganizationName = string.IsNullOrWhiteSpace(name) ? null : name,
|
||||
@ -199,7 +264,7 @@ public class ProvidersController : Controller
|
||||
Items = unassignedOrganizations.Select(uo => new OrganizationSelectableViewModel
|
||||
{
|
||||
Id = uo.Id,
|
||||
Name = uo.Name,
|
||||
Name = uo.DisplayName(),
|
||||
PlanType = uo.PlanType
|
||||
}).ToList()
|
||||
};
|
||||
@ -248,4 +313,64 @@ public class ProvidersController : Controller
|
||||
|
||||
return RedirectToAction("Edit", "Providers", new { id = providerId });
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
[RequirePermission(Permission.Provider_Edit)]
|
||||
public async Task<IActionResult> Delete(Guid id, string providerName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(providerName))
|
||||
{
|
||||
return BadRequest("Invalid provider name");
|
||||
}
|
||||
|
||||
var providerOrganizations = await _providerOrganizationRepository.GetManyDetailsByProviderAsync(id);
|
||||
|
||||
if (providerOrganizations.Count > 0)
|
||||
{
|
||||
return BadRequest("You must unlink all clients before you can delete a provider");
|
||||
}
|
||||
|
||||
var provider = await _providerRepository.GetByIdAsync(id);
|
||||
|
||||
if (provider is null)
|
||||
{
|
||||
return BadRequest("Provider does not exist");
|
||||
}
|
||||
|
||||
if (!string.Equals(providerName.Trim(), provider.Name, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return BadRequest("Invalid provider name");
|
||||
}
|
||||
|
||||
await _providerService.DeleteAsync(provider);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
[RequirePermission(Permission.Provider_Edit)]
|
||||
public async Task<IActionResult> DeleteInitiation(Guid id, string providerEmail)
|
||||
{
|
||||
var emailAttribute = new EmailAddressAttribute();
|
||||
if (!emailAttribute.IsValid(providerEmail))
|
||||
{
|
||||
return BadRequest("Invalid provider admin email");
|
||||
}
|
||||
|
||||
var provider = await _providerRepository.GetByIdAsync(id);
|
||||
if (provider != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _providerService.InitiateDeleteAsync(provider, providerEmail);
|
||||
}
|
||||
catch (BadRequestException ex)
|
||||
{
|
||||
return BadRequest(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
|
@ -24,6 +24,12 @@ public class CreateProviderModel : IValidatableObject
|
||||
[Display(Name = "Primary Billing Email")]
|
||||
public string BillingEmail { get; set; }
|
||||
|
||||
[Display(Name = "Teams (Monthly) Seat Minimum")]
|
||||
public int TeamsMonthlySeatMinimum { get; set; }
|
||||
|
||||
[Display(Name = "Enterprise (Monthly) Seat Minimum")]
|
||||
public int EnterpriseMonthlySeatMinimum { get; set; }
|
||||
|
||||
public virtual Provider ToProvider()
|
||||
{
|
||||
return new Provider()
|
||||
@ -45,6 +51,16 @@ public class CreateProviderModel : IValidatableObject
|
||||
var ownerEmailDisplayName = nameof(OwnerEmail).GetDisplayAttribute<CreateProviderModel>()?.GetName() ?? nameof(OwnerEmail);
|
||||
yield return new ValidationResult($"The {ownerEmailDisplayName} field is required.");
|
||||
}
|
||||
if (TeamsMonthlySeatMinimum < 0)
|
||||
{
|
||||
var teamsMinimumSeatsDisplayName = nameof(TeamsMonthlySeatMinimum).GetDisplayAttribute<CreateProviderModel>()?.GetName() ?? nameof(TeamsMonthlySeatMinimum);
|
||||
yield return new ValidationResult($"The {teamsMinimumSeatsDisplayName} field can not be negative.");
|
||||
}
|
||||
if (EnterpriseMonthlySeatMinimum < 0)
|
||||
{
|
||||
var enterpriseMinimumSeatsDisplayName = nameof(EnterpriseMonthlySeatMinimum).GetDisplayAttribute<CreateProviderModel>()?.GetName() ?? nameof(EnterpriseMonthlySeatMinimum);
|
||||
yield return new ValidationResult($"The {enterpriseMinimumSeatsDisplayName} field can not be negative.");
|
||||
}
|
||||
break;
|
||||
case ProviderType.Reseller:
|
||||
if (string.IsNullOrWhiteSpace(Name))
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Net;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
@ -36,8 +37,7 @@ public class OrganizationEditModel : OrganizationViewModel
|
||||
BillingInfo = billingInfo;
|
||||
BraintreeMerchantId = globalSettings.Braintree.MerchantId;
|
||||
|
||||
Name = org.Name;
|
||||
BusinessName = org.BusinessName;
|
||||
Name = org.DisplayName();
|
||||
BillingEmail = provider?.Type == ProviderType.Reseller ? provider.BillingEmail : org.BillingEmail;
|
||||
PlanType = org.PlanType;
|
||||
Plan = org.Plan;
|
||||
@ -80,8 +80,6 @@ public class OrganizationEditModel : OrganizationViewModel
|
||||
[Required]
|
||||
[Display(Name = "Organization Name")]
|
||||
public string Name { get; set; }
|
||||
[Display(Name = "Business Name")]
|
||||
public string BusinessName { get; set; }
|
||||
[Display(Name = "Billing Email")]
|
||||
public string BillingEmail { get; set; }
|
||||
[Required]
|
||||
@ -145,9 +143,9 @@ public class OrganizationEditModel : OrganizationViewModel
|
||||
public int? SmSeats { get; set; }
|
||||
[Display(Name = "Max Autoscale Seats")]
|
||||
public int? MaxAutoscaleSmSeats { get; set; }
|
||||
[Display(Name = "Service Accounts")]
|
||||
[Display(Name = "Machine Accounts")]
|
||||
public int? SmServiceAccounts { get; set; }
|
||||
[Display(Name = "Max Autoscale Service Accounts")]
|
||||
[Display(Name = "Max Autoscale Machine Accounts")]
|
||||
public int? MaxAutoscaleSmServiceAccounts { get; set; }
|
||||
|
||||
/**
|
||||
@ -184,8 +182,7 @@ public class OrganizationEditModel : OrganizationViewModel
|
||||
|
||||
public Organization ToOrganization(Organization existingOrganization)
|
||||
{
|
||||
existingOrganization.Name = Name;
|
||||
existingOrganization.BusinessName = BusinessName;
|
||||
existingOrganization.Name = WebUtility.HtmlEncode(Name.Trim());
|
||||
existingOrganization.BillingEmail = BillingEmail?.ToLowerInvariant()?.Trim();
|
||||
existingOrganization.PlanType = PlanType.Value;
|
||||
existingOrganization.Plan = Plan;
|
||||
|
@ -1,6 +1,8 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Models.Data.Provider;
|
||||
using Bit.Core.Billing.Entities;
|
||||
using Bit.Core.Enums;
|
||||
|
||||
namespace Bit.Admin.AdminConsole.Models;
|
||||
|
||||
@ -8,13 +10,21 @@ public class ProviderEditModel : ProviderViewModel
|
||||
{
|
||||
public ProviderEditModel() { }
|
||||
|
||||
public ProviderEditModel(Provider provider, IEnumerable<ProviderUserUserDetails> providerUsers, IEnumerable<ProviderOrganizationOrganizationDetails> organizations)
|
||||
: base(provider, providerUsers, organizations)
|
||||
public ProviderEditModel(
|
||||
Provider provider,
|
||||
IEnumerable<ProviderUserUserDetails> providerUsers,
|
||||
IEnumerable<ProviderOrganizationOrganizationDetails> organizations,
|
||||
IReadOnlyCollection<ProviderPlan> providerPlans) : base(provider, providerUsers, organizations)
|
||||
{
|
||||
Name = provider.Name;
|
||||
BusinessName = provider.BusinessName;
|
||||
Name = provider.DisplayName();
|
||||
BusinessName = provider.DisplayBusinessName();
|
||||
BillingEmail = provider.BillingEmail;
|
||||
BillingPhone = provider.BillingPhone;
|
||||
TeamsMonthlySeatMinimum = GetSeatMinimum(providerPlans, PlanType.TeamsMonthly);
|
||||
EnterpriseMonthlySeatMinimum = GetSeatMinimum(providerPlans, PlanType.EnterpriseMonthly);
|
||||
Gateway = provider.Gateway;
|
||||
GatewayCustomerId = provider.GatewayCustomerId;
|
||||
GatewaySubscriptionId = provider.GatewaySubscriptionId;
|
||||
}
|
||||
|
||||
[Display(Name = "Billing Email")]
|
||||
@ -24,12 +34,28 @@ public class ProviderEditModel : ProviderViewModel
|
||||
[Display(Name = "Business Name")]
|
||||
public string BusinessName { get; set; }
|
||||
public string Name { get; set; }
|
||||
[Display(Name = "Events")]
|
||||
[Display(Name = "Teams (Monthly) Seat Minimum")]
|
||||
public int TeamsMonthlySeatMinimum { get; set; }
|
||||
|
||||
public Provider ToProvider(Provider existingProvider)
|
||||
[Display(Name = "Enterprise (Monthly) Seat Minimum")]
|
||||
public int EnterpriseMonthlySeatMinimum { get; set; }
|
||||
[Display(Name = "Gateway")]
|
||||
public GatewayType? Gateway { get; set; }
|
||||
[Display(Name = "Gateway Customer Id")]
|
||||
public string GatewayCustomerId { get; set; }
|
||||
[Display(Name = "Gateway Subscription Id")]
|
||||
public string GatewaySubscriptionId { get; set; }
|
||||
|
||||
public virtual Provider ToProvider(Provider existingProvider)
|
||||
{
|
||||
existingProvider.BillingEmail = BillingEmail?.ToLowerInvariant()?.Trim();
|
||||
existingProvider.BillingPhone = BillingPhone?.ToLowerInvariant()?.Trim();
|
||||
existingProvider.BillingEmail = BillingEmail?.ToLowerInvariant().Trim();
|
||||
existingProvider.BillingPhone = BillingPhone?.ToLowerInvariant().Trim();
|
||||
existingProvider.Gateway = Gateway;
|
||||
existingProvider.GatewayCustomerId = GatewayCustomerId;
|
||||
existingProvider.GatewaySubscriptionId = GatewaySubscriptionId;
|
||||
return existingProvider;
|
||||
}
|
||||
|
||||
private static int GetSeatMinimum(IEnumerable<ProviderPlan> providerPlans, PlanType planType)
|
||||
=> providerPlans.FirstOrDefault(providerPlan => providerPlan.PlanType == planType)?.SeatMinimum ?? 0;
|
||||
}
|
||||
|
@ -4,7 +4,7 @@
|
||||
@inject Bit.Admin.Services.IAccessControlService AccessControlService
|
||||
@model OrganizationEditModel
|
||||
@{
|
||||
ViewData["Title"] = (Model.Provider != null ? "Client " : string.Empty) + "Organization: " + Model.Organization.Name;
|
||||
ViewData["Title"] = (Model.Provider != null ? "Client " : string.Empty) + "Organization: " + Model.Name;
|
||||
|
||||
var canViewOrganizationInformation = AccessControlService.UserHasPermission(Permission.Org_OrgInformation_View);
|
||||
var canViewBillingInformation = AccessControlService.UserHasPermission(Permission.Org_BillingInformation_View);
|
||||
@ -58,7 +58,7 @@
|
||||
</script>
|
||||
}
|
||||
|
||||
<h1>@(Model.Provider != null ? "Client " : string.Empty)Organization <small>@Model.Organization.Name</small></h1>
|
||||
<h1>@(Model.Provider != null ? "Client " : string.Empty)Organization <small>@Model.Name</small></h1>
|
||||
|
||||
@if (Model.Provider != null)
|
||||
{
|
||||
|
@ -46,7 +46,7 @@
|
||||
{
|
||||
<tr>
|
||||
<td>
|
||||
<a asp-action="@Model.Action" asp-route-id="@org.Id">@org.Name</a>
|
||||
<a asp-action="@Model.Action" asp-route-id="@org.Id">@org.DisplayName()</a>
|
||||
</td>
|
||||
<td>
|
||||
@org.Plan
|
||||
|
@ -1,10 +1,10 @@
|
||||
@inject Bit.Core.Settings.GlobalSettings GlobalSettings
|
||||
@model OrganizationViewModel
|
||||
@{
|
||||
ViewData["Title"] = "Organization: " + Model.Organization.Name;
|
||||
ViewData["Title"] = "Organization: " + Model.Organization.DisplayName();
|
||||
}
|
||||
|
||||
<h1>Organization <small>@Model.Organization.Name</small></h1>
|
||||
<h1>Organization <small>@Model.Organization.DisplayName()</small></h1>
|
||||
|
||||
@if (Model.Provider != null)
|
||||
{
|
||||
|
@ -2,8 +2,8 @@
|
||||
@model Bit.Core.AdminConsole.Entities.Provider.Provider
|
||||
<dl class="row">
|
||||
<dt class="col-sm-4 col-lg-3">Provider Name</dt>
|
||||
<dd class="col-sm-8 col-lg-9">@Model.Name</dd>
|
||||
|
||||
<dd class="col-sm-8 col-lg-9">@Model.DisplayName()</dd>
|
||||
|
||||
<dt class="col-sm-4 col-lg-3">Provider Type</dt>
|
||||
<dd class="col-sm-8 col-lg-9">@(Model.Type.GetDisplayAttribute()?.GetName())</dd>
|
||||
</dl>
|
||||
</dl>
|
||||
|
@ -68,7 +68,7 @@
|
||||
<dt class="col-sm-4 col-lg-3">Projects</dt>
|
||||
<dd class="col-sm-8 col-lg-9">@(Model.UseSecretsManager ? Model.ProjectsCount: "N/A")</dd>
|
||||
|
||||
<dt class="col-sm-4 col-lg-3">Service Accounts</dt>
|
||||
<dt class="col-sm-4 col-lg-3">Machine Accounts</dt>
|
||||
<dd class="col-sm-8 col-lg-9">@(Model.UseSecretsManager ? Model.ServiceAccountsCount: "N/A")</dd>
|
||||
|
||||
<dt class="col-sm-4 col-lg-3">Secrets Manager Seats</dt>
|
||||
|
@ -45,7 +45,7 @@
|
||||
@Html.HiddenFor(m => Model.Items[i].Id, new { @readonly = "readonly", autocomplete = "off" })
|
||||
@Html.CheckBoxFor(m => Model.Items[i].Selected)
|
||||
</td>
|
||||
<td>@Html.ActionLink(Model.Items[i].Name, "Edit", "Organizations", new { id = Model.Items[i].Id }, new { target = "_blank" })</td>
|
||||
<td>@Html.ActionLink(Model.Items[i].DisplayName(), "Edit", "Organizations", new { id = Model.Items[i].Id }, new { target = "_blank" })</td>
|
||||
<td>@(Model.Items[i].PlanType.GetDisplayAttribute()?.Name ?? Model.Items[i].PlanType.ToString())</td>
|
||||
</tr>
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
@using Bit.SharedWeb.Utilities
|
||||
@using Bit.Core.AdminConsole.Enums.Provider
|
||||
@using Bit.Core
|
||||
@model CreateProviderModel
|
||||
@inject Bit.Core.Services.IFeatureService FeatureService
|
||||
@{
|
||||
ViewData["Title"] = "Create Provider";
|
||||
}
|
||||
@ -39,6 +41,23 @@
|
||||
<label asp-for="OwnerEmail"></label>
|
||||
<input type="text" class="form-control" asp-for="OwnerEmail">
|
||||
</div>
|
||||
@if (FeatureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling))
|
||||
{
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
<div class="form-group">
|
||||
<label asp-for="TeamsMonthlySeatMinimum"></label>
|
||||
<input type="number" class="form-control" asp-for="TeamsMonthlySeatMinimum">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm">
|
||||
<div class="form-group">
|
||||
<label asp-for="EnterpriseMonthlySeatMinimum"></label>
|
||||
<input type="number" class="form-control" asp-for="EnterpriseMonthlySeatMinimum">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div id="@($"info-{(int)ProviderType.Reseller}")" class="form-group @(Model.Type != ProviderType.Reseller ? "d-none" : string.Empty)">
|
||||
|
@ -1,14 +1,16 @@
|
||||
@using Bit.Admin.Enums;
|
||||
@using Bit.Core
|
||||
@using Bit.Core.Billing.Extensions
|
||||
@inject Bit.Admin.Services.IAccessControlService AccessControlService
|
||||
@inject Bit.Core.Services.IFeatureService FeatureService
|
||||
|
||||
@model ProviderEditModel
|
||||
@{
|
||||
ViewData["Title"] = "Provider: " + Model.Provider.Name;
|
||||
|
||||
ViewData["Title"] = "Provider: " + Model.Provider.DisplayName();
|
||||
var canEdit = AccessControlService.UserHasPermission(Permission.Provider_Edit);
|
||||
}
|
||||
|
||||
<h1>Provider <small>@Model.Provider.Name</small></h1>
|
||||
<h1>Provider <small>@Model.Provider.DisplayName()</small></h1>
|
||||
|
||||
<h2>Provider Information</h2>
|
||||
@await Html.PartialAsync("_ViewInformation", Model)
|
||||
@ -17,12 +19,12 @@
|
||||
<h2>General</h2>
|
||||
<dl class="row">
|
||||
<dt class="col-sm-4 col-lg-3">Name</dt>
|
||||
<dd class="col-sm-8 col-lg-9">@Model.Provider.Name</dd>
|
||||
<dd class="col-sm-8 col-lg-9">@Model.Provider.DisplayName()</dd>
|
||||
</dl>
|
||||
<h2>Business Information</h2>
|
||||
<dl class="row">
|
||||
<dt class="col-sm-4 col-lg-3">Business Name</dt>
|
||||
<dd class="col-sm-8 col-lg-9">@Model.Provider.BusinessName</dd>
|
||||
<dd class="col-sm-8 col-lg-9">@Model.Provider.DisplayBusinessName()</dd>
|
||||
</dl>
|
||||
<h2>Billing</h2>
|
||||
<div class="row">
|
||||
@ -41,12 +43,147 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@if (FeatureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling) && Model.Provider.IsBillable())
|
||||
{
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
<div class="form-group">
|
||||
<label asp-for="TeamsMonthlySeatMinimum"></label>
|
||||
<input type="number" class="form-control" asp-for="TeamsMonthlySeatMinimum">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm">
|
||||
<div class="form-group">
|
||||
<label asp-for="EnterpriseMonthlySeatMinimum"></label>
|
||||
<input type="number" class="form-control" asp-for="EnterpriseMonthlySeatMinimum">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
<div class="form-group">
|
||||
<div class="form-group">
|
||||
<label asp-for="Gateway"></label>
|
||||
<select class="form-control" asp-for="Gateway" asp-items="Html.GetEnumSelectList<Bit.Core.Enums.GatewayType>()">
|
||||
<option value="">--</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
<div class="form-group">
|
||||
<label asp-for="GatewayCustomerId"></label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" asp-for="GatewayCustomerId">
|
||||
<div class="input-group-append">
|
||||
<button class="btn btn-secondary" type="button" id="gateway-customer-link">
|
||||
<i class="fa fa-external-link"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm">
|
||||
<div class="form-group">
|
||||
<label asp-for="GatewaySubscriptionId"></label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" asp-for="GatewaySubscriptionId">
|
||||
<div class="input-group-append">
|
||||
<button class="btn btn-secondary" type="button" id="gateway-subscription-link">
|
||||
<i class="fa fa-external-link"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</form>
|
||||
@await Html.PartialAsync("Organizations", Model)
|
||||
@if (canEdit)
|
||||
{
|
||||
<!-- Modals -->
|
||||
<div class="modal fade rounded" id="requestDeletionModal" tabindex="-1" aria-labelledby="requestDeletionModal" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content rounded">
|
||||
<div class="p-3">
|
||||
<h4 class="font-weight-bolder" id="exampleModalLabel">Request provider deletion</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<span class="font-weight-light">
|
||||
Enter the email of the provider admin that will receive the request to delete the provider portal.
|
||||
</span>
|
||||
<form>
|
||||
<div class="form-group">
|
||||
<label for="provider-email" class="col-form-label">Provider email</label>
|
||||
<input type="email" class="form-control" id="provider-email">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-primary btn-pill" data-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-danger btn-pill" onclick="initiateDeleteProvider('@Model.Provider.Id')">Send email request</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal fade" id="DeleteModal" tabindex="-1" aria-labelledby="DeleteModal" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content rounded">
|
||||
<div class="p-3">
|
||||
<h4 class="font-weight-bolder" id="exampleModalLabel">Delete provider</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<span class="font-weight-light">
|
||||
This action is permanent and irreversible. Enter the provider name to complete deletion of the provider and associated data.
|
||||
</span>
|
||||
<form>
|
||||
<div class="form-group">
|
||||
<label for="provider-name" class="col-form-label">Provider name</label>
|
||||
<input type="text" class="form-control" id="provider-name">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-primary btn-pill" data-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-danger btn-pill" onclick="deleteProvider('@Model.Provider.Id');">Delete provider</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal fade" id="linkedWarningModal" tabindex="-1" role="dialog" aria-labelledby="linkedWarningModal" aria-hidden="true">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content rounded">
|
||||
<div class="modal-body">
|
||||
<h4 class="font-weight-bolder">Cannot Delete @Model.Name</h4>
|
||||
<p class="font-weight-lighter">You must unlink all clients before you can delete @Model.Name.</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-primary btn-pill" data-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-primary btn-pill" data-dismiss="modal">Ok</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- End of Modal Section -->
|
||||
|
||||
<div class="d-flex mt-4">
|
||||
<button type="submit" class="btn btn-primary" form="edit-form">Save</button>
|
||||
@if (FeatureService.IsEnabled(FeatureFlagKeys.EnableDeleteProvider))
|
||||
{
|
||||
<div class="ml-auto d-flex">
|
||||
<button class="btn btn-danger" onclick="openRequestDeleteModal(@Model.ProviderOrganizations.Count())">Request Delete</button>
|
||||
<button id="requestDeletionBtn" hidden="hidden" data-toggle="modal" data-target="#requestDeletionModal"></button>
|
||||
|
||||
<button class="btn btn-outline-danger ml-2" onclick="openDeleteModal(@Model.ProviderOrganizations.Count())">Delete</button>
|
||||
<button id="deleteBtn" hidden="hidden" data-toggle="modal" data-target="#DeleteModal"></button>
|
||||
|
||||
<button id="linkAccWarningBtn" hidden="hidden" data-toggle="modal" data-target="#linkedWarningModal"></button>
|
||||
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
|
@ -52,7 +52,7 @@
|
||||
{
|
||||
<tr>
|
||||
<td>
|
||||
<a asp-action="@Model.Action" asp-route-id="@provider.Id">@(provider.Name ?? "Pending")</a>
|
||||
<a asp-action="@Model.Action" asp-route-id="@provider.Id">@(!string.IsNullOrEmpty(provider.DisplayName()) ? provider.DisplayName() : "Pending")</a>
|
||||
</td>
|
||||
<td>@provider.Type.GetDisplayAttribute()?.GetShortName()</td>
|
||||
<td>@provider.Status</td>
|
||||
|
@ -45,7 +45,7 @@
|
||||
{
|
||||
<tr>
|
||||
<td class="align-middle">
|
||||
<a asp-controller="Organizations" asp-action="Edit" asp-route-id="@providerOrganization.OrganizationId">@providerOrganization.OrganizationName</a>
|
||||
<a asp-controller="Organizations" asp-action="Edit" asp-route-id="@providerOrganization.OrganizationId">@providerOrganization.DisplayName()</a>
|
||||
</td>
|
||||
<td>
|
||||
@providerOrganization.Status
|
||||
|
@ -1,9 +1,9 @@
|
||||
@model ProviderViewModel
|
||||
@{
|
||||
ViewData["Title"] = "Provider: " + Model.Provider.Name;
|
||||
ViewData["Title"] = "Provider: " + Model.Provider.DisplayName();
|
||||
}
|
||||
|
||||
<h1>Provider <small>@Model.Provider.Name</small></h1>
|
||||
<h1>Provider <small>@Model.Provider.DisplayName()</small></h1>
|
||||
|
||||
<h2>Information</h2>
|
||||
@await Html.PartialAsync("_ViewInformation", Model)
|
||||
|
@ -12,7 +12,7 @@
|
||||
window.location.href = `@Url.Action("Edit", "Providers")?id=${providerId}`;
|
||||
},
|
||||
error: function (response) {
|
||||
alert("Error!");
|
||||
alert("Error!: " + response.responseText);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -17,4 +17,60 @@
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function deleteProvider(id) {
|
||||
const providerName = $('#DeleteModal input#provider-name').val();
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: `@Url.Action("Delete", "Providers")?id=${id}&providerName=${providerName}`,
|
||||
dataType: 'json',
|
||||
contentType: false,
|
||||
processData: false,
|
||||
success: function () {
|
||||
$('#DeleteModal').modal('hide');
|
||||
window.location.href = `@Url.Action("Index", "Providers")`;
|
||||
},
|
||||
error: function (response) {
|
||||
alert("Error!: " + response.responseText);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function initiateDeleteProvider(id) {
|
||||
const email = $('#requestDeletionModal input#provider-email').val();
|
||||
const providerEmail = encodeURIComponent(email);
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: `@Url.Action("DeleteInitiation", "Providers")?id=${id}&providerEmail=${providerEmail}`,
|
||||
dataType: 'json',
|
||||
contentType: false,
|
||||
processData: false,
|
||||
success: function () {
|
||||
$('#requestDeletionModal').modal('hide');
|
||||
window.location.href = `@Url.Action("Index", "Providers")`;
|
||||
},
|
||||
error: function (response) {
|
||||
alert("Error!: " + response.responseText);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function openDeleteModal(providerOrganizations) {
|
||||
|
||||
if (providerOrganizations > 0){
|
||||
$('#linkAccWarningBtn').click()
|
||||
} else {
|
||||
$('#deleteBtn').click()
|
||||
}
|
||||
}
|
||||
|
||||
function openRequestDeleteModal(providerOrganizations) {
|
||||
|
||||
if (providerOrganizations > 0){
|
||||
$('#linkAccWarningBtn').click()
|
||||
} else {
|
||||
$('#requestDeletionBtn').click()
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
@ -9,7 +9,6 @@
|
||||
@{
|
||||
var canViewGeneralDetails = AccessControlService.UserHasPermission(Permission.Org_GeneralDetails_View);
|
||||
var canViewBilling = AccessControlService.UserHasPermission(Permission.Org_Billing_View);
|
||||
var canViewBusinessInformation = AccessControlService.UserHasPermission(Permission.Org_BusinessInformation_View);
|
||||
var canViewPlan = AccessControlService.UserHasPermission(Permission.Org_Plan_View);
|
||||
var canViewLicensing = AccessControlService.UserHasPermission(Permission.Org_Licensing_View);
|
||||
var canCheckEnabled = AccessControlService.UserHasPermission(Permission.Org_CheckEnabledBox);
|
||||
@ -28,7 +27,7 @@
|
||||
<div class="col-sm">
|
||||
<div class="form-group">
|
||||
<label asp-for="Name"></label>
|
||||
<input type="text" class="form-control" asp-for="Name" required>
|
||||
<input type="text" class="form-control" asp-for="Name" value="@Model.Name" required>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -61,19 +60,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
@if (canViewBusinessInformation)
|
||||
{
|
||||
<h2>Business Information</h2>
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
<div class="form-group">
|
||||
<label asp-for="BusinessName"></label>
|
||||
<input type="text" class="form-control" asp-for="BusinessName">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (canViewPlan)
|
||||
{
|
||||
<h2>Plan</h2>
|
||||
|
@ -57,8 +57,11 @@
|
||||
case '@((byte)PlanType.TeamsAnnually2019)':
|
||||
case '@((byte)PlanType.TeamsMonthly2020)':
|
||||
case '@((byte)PlanType.TeamsAnnually2020)':
|
||||
case '@((byte)PlanType.TeamsMonthly2023)':
|
||||
case '@((byte)PlanType.TeamsAnnually2023)':
|
||||
case '@((byte)PlanType.TeamsMonthly)':
|
||||
case '@((byte)PlanType.TeamsAnnually)':
|
||||
case '@((byte)PlanType.TeamsStarter2023)':
|
||||
case '@((byte)PlanType.TeamsStarter)':
|
||||
document.getElementById('@(nameof(Model.UsePolicies))').checked = false;
|
||||
document.getElementById('@(nameof(Model.UseSso))').checked = false;
|
||||
@ -79,6 +82,8 @@
|
||||
case '@((byte)PlanType.EnterpriseAnnually2019)':
|
||||
case '@((byte)PlanType.EnterpriseMonthly2020)':
|
||||
case '@((byte)PlanType.EnterpriseAnnually2020)':
|
||||
case '@((byte)PlanType.EnterpriseMonthly2023)':
|
||||
case '@((byte)PlanType.EnterpriseAnnually2023)':
|
||||
case '@((byte)PlanType.EnterpriseMonthly)':
|
||||
case '@((byte)PlanType.EnterpriseAnnually)':
|
||||
document.getElementById('@(nameof(Model.UsePolicies))').checked = true;
|
||||
|
@ -76,14 +76,18 @@ public class JobsHostedService : BaseJobsHostedService
|
||||
{
|
||||
new Tuple<Type, ITrigger>(typeof(DeleteSendsJob), everyFiveMinutesTrigger),
|
||||
new Tuple<Type, ITrigger>(typeof(DatabaseExpiredGrantsJob), everyFridayAt10pmTrigger),
|
||||
new Tuple<Type, ITrigger>(typeof(DatabaseUpdateStatisticsJob), everySaturdayAtMidnightTrigger),
|
||||
new Tuple<Type, ITrigger>(typeof(DatabaseRebuildlIndexesJob), everySundayAtMidnightTrigger),
|
||||
new Tuple<Type, ITrigger>(typeof(DeleteCiphersJob), everyDayAtMidnightUtc),
|
||||
new Tuple<Type, ITrigger>(typeof(DatabaseExpiredSponsorshipsJob), everyMondayAtMidnightTrigger),
|
||||
new Tuple<Type, ITrigger>(typeof(DeleteAuthRequestsJob), everyFifteenMinutesTrigger),
|
||||
new Tuple<Type, ITrigger>(typeof(DeleteUnverifiedOrganizationDomainsJob), everyDayAtTwoAmUtcTrigger),
|
||||
};
|
||||
|
||||
if (!(_globalSettings.SqlServer?.DisableDatabaseMaintenanceJobs ?? false))
|
||||
{
|
||||
jobs.Add(new Tuple<Type, ITrigger>(typeof(DatabaseUpdateStatisticsJob), everySaturdayAtMidnightTrigger));
|
||||
jobs.Add(new Tuple<Type, ITrigger>(typeof(DatabaseRebuildlIndexesJob), everySundayAtMidnightTrigger));
|
||||
}
|
||||
|
||||
if (!_globalSettings.SelfHosted)
|
||||
{
|
||||
jobs.Add(new Tuple<Type, ITrigger>(typeof(AliveJob), everyTopOfTheHourTrigger));
|
||||
|
@ -88,7 +88,7 @@ public class Startup
|
||||
services.AddBaseServices(globalSettings);
|
||||
services.AddDefaultServices(globalSettings);
|
||||
services.AddScoped<IAccessControlService, AccessControlService>();
|
||||
services.AddBillingCommands();
|
||||
services.AddBillingOperations();
|
||||
|
||||
#if OSS
|
||||
services.AddOosServices();
|
||||
|
@ -87,6 +87,7 @@ public static class RolePermissionMapping
|
||||
Permission.Provider_List_View,
|
||||
Permission.Provider_Create,
|
||||
Permission.Provider_View,
|
||||
Permission.Provider_Edit,
|
||||
Permission.Provider_ResendEmailInvite,
|
||||
Permission.Tools_ChargeBrainTreeCustomer,
|
||||
Permission.Tools_PromoteAdmin,
|
||||
|
@ -30,10 +30,6 @@
|
||||
"connectionString": "SECRET",
|
||||
"applicationCacheTopicName": "SECRET"
|
||||
},
|
||||
"documentDb": {
|
||||
"uri": "SECRET",
|
||||
"key": "SECRET"
|
||||
},
|
||||
"notificationHub": {
|
||||
"connectionString": "SECRET",
|
||||
"hubName": "SECRET"
|
||||
|
74
src/Admin/package-lock.json
generated
74
src/Admin/package-lock.json
generated
@ -9,15 +9,15 @@
|
||||
"version": "0.0.0",
|
||||
"license": "GPL-3.0",
|
||||
"devDependencies": {
|
||||
"bootstrap": "4.5.0",
|
||||
"del": "6.0.0",
|
||||
"bootstrap": "4.6.2",
|
||||
"del": "6.1.1",
|
||||
"font-awesome": "4.7.0",
|
||||
"gulp": "4.0.2",
|
||||
"gulp-sass": "5.1.0",
|
||||
"jquery": "3.5.1",
|
||||
"jquery": "3.7.1",
|
||||
"merge-stream": "2.0.0",
|
||||
"popper.js": "1.16.1",
|
||||
"sass": "1.49.7",
|
||||
"sass": "1.75.0",
|
||||
"toastr": "2.1.4"
|
||||
}
|
||||
},
|
||||
@ -599,17 +599,23 @@
|
||||
}
|
||||
},
|
||||
"node_modules/bootstrap": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.5.0.tgz",
|
||||
"integrity": "sha512-Z93QoXvodoVslA+PWNdk23Hze4RBYIkpb5h8I2HY2Tu2h7A0LpAgLcyrhrSUyo2/Oxm2l1fRZPs1e5hnxnliXA==",
|
||||
"version": "4.6.2",
|
||||
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.6.2.tgz",
|
||||
"integrity": "sha512-51Bbp/Uxr9aTuy6ca/8FbFloBUJZLHwnhTcnjIeRn2suQWsWzcuJhGjKDB5eppVte/8oCdOL3VuwxvZDUggwGQ==",
|
||||
"dev": true,
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/bootstrap"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/twbs"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/bootstrap"
|
||||
}
|
||||
],
|
||||
"peerDependencies": {
|
||||
"jquery": "1.9.1 - 3",
|
||||
"popper.js": "^1.16.0"
|
||||
"popper.js": "^1.16.1"
|
||||
}
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
@ -1029,9 +1035,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/del": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/del/-/del-6.0.0.tgz",
|
||||
"integrity": "sha512-1shh9DQ23L16oXSZKB2JxpL7iMy2E0S9d517ptA1P8iw0alkPtQcrKH7ru31rYtKwF499HkTu+DRzq3TCKDFRQ==",
|
||||
"version": "6.1.1",
|
||||
"resolved": "https://registry.npmjs.org/del/-/del-6.1.1.tgz",
|
||||
"integrity": "sha512-ua8BhapfP0JUJKC/zV9yHHDW/rDoDxP4Zhn3AkA6/xT6gY7jYXJiaeyBZznYVujhZZET+UgcbZiQ7sN3WqcImg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"globby": "^11.0.1",
|
||||
@ -2384,9 +2390,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/jquery": {
|
||||
"version": "3.5.1",
|
||||
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.5.1.tgz",
|
||||
"integrity": "sha512-XwIBPqcMn57FxfT+Go5pzySnm4KWkT1Tv7gjrpT1srtf8Weynl6R273VJ5GjkRb51IzMp5nbaPjJXMWeju2MKg==",
|
||||
"version": "3.7.1",
|
||||
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz",
|
||||
"integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/json-stable-stringify-without-jsonify": {
|
||||
@ -3947,9 +3953,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/sass": {
|
||||
"version": "1.49.7",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.49.7.tgz",
|
||||
"integrity": "sha512-13dml55EMIR2rS4d/RDHHP0sXMY3+30e1TKsyXaSz3iLWVoDWEoboY8WzJd5JMnxrRHffKO3wq2mpJ0jxRJiEQ==",
|
||||
"version": "1.75.0",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.75.0.tgz",
|
||||
"integrity": "sha512-ShMYi3WkrDWxExyxSZPst4/okE9ts46xZmJDSawJQrnte7M1V9fScVB+uNXOVKRBt0PggHOwoZcn8mYX4trnBw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"chokidar": ">=3.0.0 <4.0.0",
|
||||
@ -3960,7 +3966,7 @@
|
||||
"sass": "sass.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/sass/node_modules/anymatch": {
|
||||
@ -3977,12 +3983,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/sass/node_modules/binary-extensions": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
|
||||
"integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
||||
"integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/sass/node_modules/braces": {
|
||||
@ -3998,16 +4007,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/sass/node_modules/chokidar": {
|
||||
"version": "3.5.3",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
|
||||
"integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
||||
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"anymatch": "~3.1.2",
|
||||
"braces": "~3.0.2",
|
||||
@ -4020,6 +4023,9 @@
|
||||
"engines": {
|
||||
"node": ">= 8.10.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
|
@ -8,15 +8,15 @@
|
||||
"build": "gulp build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"bootstrap": "4.5.0",
|
||||
"del": "6.0.0",
|
||||
"bootstrap": "4.6.2",
|
||||
"del": "6.1.1",
|
||||
"font-awesome": "4.7.0",
|
||||
"gulp": "4.0.2",
|
||||
"gulp-sass": "5.1.0",
|
||||
"jquery": "3.5.1",
|
||||
"jquery": "3.7.1",
|
||||
"merge-stream": "2.0.0",
|
||||
"popper.js": "1.16.1",
|
||||
"sass": "1.49.7",
|
||||
"sass": "1.75.0",
|
||||
"toastr": "2.1.4"
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,9 @@
|
||||
using Bit.Api.AdminConsole.Models.Response;
|
||||
using Bit.Api.Models.Response;
|
||||
using Bit.Api.Utilities;
|
||||
using Bit.Api.Vault.AuthorizationHandlers.Collections;
|
||||
using Bit.Api.Vault.AuthorizationHandlers.Groups;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Interfaces;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
@ -28,6 +30,10 @@ public class GroupsController : Controller
|
||||
private readonly IUpdateGroupCommand _updateGroupCommand;
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
private readonly IApplicationCacheService _applicationCacheService;
|
||||
private readonly IUserService _userService;
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
private readonly ICollectionRepository _collectionRepository;
|
||||
|
||||
public GroupsController(
|
||||
IGroupRepository groupRepository,
|
||||
@ -38,7 +44,11 @@ public class GroupsController : Controller
|
||||
IUpdateGroupCommand updateGroupCommand,
|
||||
IDeleteGroupCommand deleteGroupCommand,
|
||||
IAuthorizationService authorizationService,
|
||||
IApplicationCacheService applicationCacheService)
|
||||
IApplicationCacheService applicationCacheService,
|
||||
IUserService userService,
|
||||
IFeatureService featureService,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
ICollectionRepository collectionRepository)
|
||||
{
|
||||
_groupRepository = groupRepository;
|
||||
_groupService = groupService;
|
||||
@ -49,6 +59,10 @@ public class GroupsController : Controller
|
||||
_deleteGroupCommand = deleteGroupCommand;
|
||||
_authorizationService = authorizationService;
|
||||
_applicationCacheService = applicationCacheService;
|
||||
_userService = userService;
|
||||
_featureService = featureService;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
_collectionRepository = collectionRepository;
|
||||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
@ -115,16 +129,30 @@ public class GroupsController : Controller
|
||||
}
|
||||
|
||||
[HttpPost("")]
|
||||
public async Task<GroupResponseModel> Post(string orgId, [FromBody] GroupRequestModel model)
|
||||
public async Task<GroupResponseModel> Post(Guid orgId, [FromBody] GroupRequestModel model)
|
||||
{
|
||||
var orgIdGuid = new Guid(orgId);
|
||||
if (!await _currentContext.ManageGroups(orgIdGuid))
|
||||
if (!await _currentContext.ManageGroups(orgId))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var organization = await _organizationRepository.GetByIdAsync(orgIdGuid);
|
||||
var group = model.ToGroup(orgIdGuid);
|
||||
// Flexible Collections - check the user has permission to grant access to the collections for the new group
|
||||
if (await FlexibleCollectionsIsEnabledAsync(orgId) &&
|
||||
_featureService.IsEnabled(FeatureFlagKeys.FlexibleCollectionsV1) &&
|
||||
model.Collections?.Any() == true)
|
||||
{
|
||||
var collections = await _collectionRepository.GetManyByManyIdsAsync(model.Collections.Select(a => a.Id));
|
||||
var authorized =
|
||||
(await _authorizationService.AuthorizeAsync(User, collections, BulkCollectionOperations.ModifyGroupAccess))
|
||||
.Succeeded;
|
||||
if (!authorized)
|
||||
{
|
||||
throw new NotFoundException("You are not authorized to grant access to these collections.");
|
||||
}
|
||||
}
|
||||
|
||||
var organization = await _organizationRepository.GetByIdAsync(orgId);
|
||||
var group = model.ToGroup(orgId);
|
||||
await _createGroupCommand.CreateGroupAsync(group, organization, model.Collections?.Select(c => c.ToSelectionReadOnly()).ToList(), model.Users);
|
||||
|
||||
return new GroupResponseModel(group);
|
||||
@ -132,30 +160,85 @@ public class GroupsController : Controller
|
||||
|
||||
[HttpPut("{id}")]
|
||||
[HttpPost("{id}")]
|
||||
public async Task<GroupResponseModel> Put(string orgId, string id, [FromBody] GroupRequestModel model)
|
||||
public async Task<GroupResponseModel> Put(Guid orgId, Guid id, [FromBody] GroupRequestModel model)
|
||||
{
|
||||
var group = await _groupRepository.GetByIdAsync(new Guid(id));
|
||||
if (await FlexibleCollectionsIsEnabledAsync(orgId) && _featureService.IsEnabled(FeatureFlagKeys.FlexibleCollectionsV1))
|
||||
{
|
||||
// Use new Flexible Collections v1 logic
|
||||
return await Put_vNext(orgId, id, model);
|
||||
}
|
||||
|
||||
// Pre-Flexible Collections v1 logic follows
|
||||
var group = await _groupRepository.GetByIdAsync(id);
|
||||
if (group == null || !await _currentContext.ManageGroups(group.OrganizationId))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var orgIdGuid = new Guid(orgId);
|
||||
var organization = await _organizationRepository.GetByIdAsync(orgIdGuid);
|
||||
var organization = await _organizationRepository.GetByIdAsync(orgId);
|
||||
|
||||
await _updateGroupCommand.UpdateGroupAsync(model.ToGroup(group), organization, model.Collections?.Select(c => c.ToSelectionReadOnly()).ToList(), model.Users);
|
||||
await _updateGroupCommand.UpdateGroupAsync(model.ToGroup(group), organization,
|
||||
model.Collections.Select(c => c.ToSelectionReadOnly()).ToList(), model.Users);
|
||||
return new GroupResponseModel(group);
|
||||
}
|
||||
|
||||
[HttpPut("{id}/users")]
|
||||
public async Task PutUsers(string orgId, string id, [FromBody] IEnumerable<Guid> model)
|
||||
/// <summary>
|
||||
/// Put logic for Flexible Collections v1
|
||||
/// </summary>
|
||||
private async Task<GroupResponseModel> Put_vNext(Guid orgId, Guid id, [FromBody] GroupRequestModel model)
|
||||
{
|
||||
var group = await _groupRepository.GetByIdAsync(new Guid(id));
|
||||
var (group, currentAccess) = await _groupRepository.GetByIdWithCollectionsAsync(id);
|
||||
if (group == null || !await _currentContext.ManageGroups(group.OrganizationId))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
await _groupRepository.UpdateUsersAsync(group.Id, model);
|
||||
|
||||
// Check whether the user is permitted to add themselves to the group
|
||||
var orgAbility = await _applicationCacheService.GetOrganizationAbilityAsync(orgId);
|
||||
if (!orgAbility.AllowAdminAccessToAllCollectionItems)
|
||||
{
|
||||
var userId = _userService.GetProperUserId(User).Value;
|
||||
var organizationUser = await _organizationUserRepository.GetByOrganizationAsync(orgId, userId);
|
||||
var currentGroupUsers = await _groupRepository.GetManyUserIdsByIdAsync(id);
|
||||
if (!currentGroupUsers.Contains(organizationUser.Id) && model.Users.Contains(organizationUser.Id))
|
||||
{
|
||||
throw new BadRequestException("You cannot add yourself to groups.");
|
||||
}
|
||||
}
|
||||
|
||||
// The client only sends collections that the saving user has permissions to edit.
|
||||
// On the server side, we need to (1) confirm this and (2) concat these with the collections that the user
|
||||
// can't edit before saving to the database.
|
||||
var currentCollections = await _collectionRepository
|
||||
.GetManyByManyIdsAsync(currentAccess.Select(cas => cas.Id));
|
||||
|
||||
var readonlyCollectionIds = new HashSet<Guid>();
|
||||
foreach (var collection in currentCollections)
|
||||
{
|
||||
if (!(await _authorizationService.AuthorizeAsync(User, collection, BulkCollectionOperations.ModifyGroupAccess))
|
||||
.Succeeded)
|
||||
{
|
||||
readonlyCollectionIds.Add(collection.Id);
|
||||
}
|
||||
}
|
||||
|
||||
if (model.Collections.Any(c => readonlyCollectionIds.Contains(c.Id)))
|
||||
{
|
||||
throw new BadRequestException("You must have Can Manage permissions to edit a collection's membership");
|
||||
}
|
||||
|
||||
var editedCollectionAccess = model.Collections
|
||||
.Select(c => c.ToSelectionReadOnly());
|
||||
var readonlyCollectionAccess = currentAccess
|
||||
.Where(ca => readonlyCollectionIds.Contains(ca.Id));
|
||||
var collectionsToSave = editedCollectionAccess
|
||||
.Concat(readonlyCollectionAccess)
|
||||
.ToList();
|
||||
|
||||
var organization = await _organizationRepository.GetByIdAsync(orgId);
|
||||
|
||||
await _updateGroupCommand.UpdateGroupAsync(model.ToGroup(group), organization, collectionsToSave, model.Users);
|
||||
return new GroupResponseModel(group);
|
||||
}
|
||||
|
||||
[HttpDelete("{id}")]
|
||||
|
@ -80,7 +80,6 @@ public class OrganizationDomainController : Controller
|
||||
var organizationDomain = new OrganizationDomain
|
||||
{
|
||||
OrganizationId = orgId,
|
||||
Txt = model.Txt,
|
||||
DomainName = model.DomainName.ToLower()
|
||||
};
|
||||
|
||||
|
@ -3,7 +3,9 @@ using Bit.Api.AdminConsole.Models.Response.Organizations;
|
||||
using Bit.Api.Models.Request.Organizations;
|
||||
using Bit.Api.Models.Response;
|
||||
using Bit.Api.Utilities;
|
||||
using Bit.Api.Vault.AuthorizationHandlers.Collections;
|
||||
using Bit.Api.Vault.AuthorizationHandlers.OrganizationUsers;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
@ -37,10 +39,12 @@ public class OrganizationUsersController : Controller
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly ICountNewSmSeatsRequiredQuery _countNewSmSeatsRequiredQuery;
|
||||
private readonly IUpdateSecretsManagerSubscriptionCommand _updateSecretsManagerSubscriptionCommand;
|
||||
private readonly IUpdateOrganizationUserCommand _updateOrganizationUserCommand;
|
||||
private readonly IUpdateOrganizationUserGroupsCommand _updateOrganizationUserGroupsCommand;
|
||||
private readonly IAcceptOrgUserCommand _acceptOrgUserCommand;
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
private readonly IApplicationCacheService _applicationCacheService;
|
||||
private readonly IFeatureService _featureService;
|
||||
|
||||
public OrganizationUsersController(
|
||||
IOrganizationRepository organizationRepository,
|
||||
@ -53,10 +57,12 @@ public class OrganizationUsersController : Controller
|
||||
ICurrentContext currentContext,
|
||||
ICountNewSmSeatsRequiredQuery countNewSmSeatsRequiredQuery,
|
||||
IUpdateSecretsManagerSubscriptionCommand updateSecretsManagerSubscriptionCommand,
|
||||
IUpdateOrganizationUserCommand updateOrganizationUserCommand,
|
||||
IUpdateOrganizationUserGroupsCommand updateOrganizationUserGroupsCommand,
|
||||
IAcceptOrgUserCommand acceptOrgUserCommand,
|
||||
IAuthorizationService authorizationService,
|
||||
IApplicationCacheService applicationCacheService)
|
||||
IApplicationCacheService applicationCacheService,
|
||||
IFeatureService featureService)
|
||||
{
|
||||
_organizationRepository = organizationRepository;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
@ -68,10 +74,12 @@ public class OrganizationUsersController : Controller
|
||||
_currentContext = currentContext;
|
||||
_countNewSmSeatsRequiredQuery = countNewSmSeatsRequiredQuery;
|
||||
_updateSecretsManagerSubscriptionCommand = updateSecretsManagerSubscriptionCommand;
|
||||
_updateOrganizationUserCommand = updateOrganizationUserCommand;
|
||||
_updateOrganizationUserGroupsCommand = updateOrganizationUserGroupsCommand;
|
||||
_acceptOrgUserCommand = acceptOrgUserCommand;
|
||||
_authorizationService = authorizationService;
|
||||
_applicationCacheService = applicationCacheService;
|
||||
_featureService = featureService;
|
||||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
@ -90,8 +98,11 @@ public class OrganizationUsersController : Controller
|
||||
response.Type = GetFlexibleCollectionsUserType(response.Type, response.Permissions);
|
||||
|
||||
// Set 'Edit/Delete Assigned Collections' custom permissions to false
|
||||
response.Permissions.EditAssignedCollections = false;
|
||||
response.Permissions.DeleteAssignedCollections = false;
|
||||
if (response.Permissions is not null)
|
||||
{
|
||||
response.Permissions.EditAssignedCollections = false;
|
||||
response.Permissions.DeleteAssignedCollections = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (includeGroups)
|
||||
@ -176,16 +187,30 @@ public class OrganizationUsersController : Controller
|
||||
}
|
||||
|
||||
[HttpPost("invite")]
|
||||
public async Task Invite(string orgId, [FromBody] OrganizationUserInviteRequestModel model)
|
||||
public async Task Invite(Guid orgId, [FromBody] OrganizationUserInviteRequestModel model)
|
||||
{
|
||||
var orgGuidId = new Guid(orgId);
|
||||
if (!await _currentContext.ManageUsers(orgGuidId))
|
||||
if (!await _currentContext.ManageUsers(orgId))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
// Flexible Collections - check the user has permission to grant access to the collections for the new user
|
||||
if (await FlexibleCollectionsIsEnabledAsync(orgId) &&
|
||||
_featureService.IsEnabled(FeatureFlagKeys.FlexibleCollectionsV1) &&
|
||||
model.Collections?.Any() == true)
|
||||
{
|
||||
var collections = await _collectionRepository.GetManyByManyIdsAsync(model.Collections.Select(a => a.Id));
|
||||
var authorized =
|
||||
(await _authorizationService.AuthorizeAsync(User, collections, BulkCollectionOperations.ModifyUserAccess))
|
||||
.Succeeded;
|
||||
if (!authorized)
|
||||
{
|
||||
throw new NotFoundException("You are not authorized to grant access to these collections.");
|
||||
}
|
||||
}
|
||||
|
||||
var userId = _userService.GetProperUserId(User);
|
||||
var result = await _organizationService.InviteUsersAsync(orgGuidId, userId.Value,
|
||||
await _organizationService.InviteUsersAsync(orgId, userId.Value,
|
||||
new (OrganizationUserInvite, string)[] { (new OrganizationUserInvite(model.ToData()), null) });
|
||||
}
|
||||
|
||||
@ -305,43 +330,99 @@ public class OrganizationUsersController : Controller
|
||||
|
||||
[HttpPut("{id}")]
|
||||
[HttpPost("{id}")]
|
||||
public async Task Put(string orgId, string id, [FromBody] OrganizationUserUpdateRequestModel model)
|
||||
public async Task Put(Guid orgId, Guid id, [FromBody] OrganizationUserUpdateRequestModel model)
|
||||
{
|
||||
var orgGuidId = new Guid(orgId);
|
||||
if (!await _currentContext.ManageUsers(orgGuidId))
|
||||
if (await FlexibleCollectionsIsEnabledAsync(orgId) && _featureService.IsEnabled(FeatureFlagKeys.FlexibleCollectionsV1))
|
||||
{
|
||||
// Use new Flexible Collections v1 logic
|
||||
await Put_vNext(orgId, id, model);
|
||||
return;
|
||||
}
|
||||
|
||||
// Pre-Flexible Collections v1 code follows
|
||||
if (!await _currentContext.ManageUsers(orgId))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var organizationUser = await _organizationUserRepository.GetByIdAsync(new Guid(id));
|
||||
if (organizationUser == null || organizationUser.OrganizationId != orgGuidId)
|
||||
var organizationUser = await _organizationUserRepository.GetByIdAsync(id);
|
||||
if (organizationUser == null || organizationUser.OrganizationId != orgId)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var userId = _userService.GetProperUserId(User);
|
||||
await _organizationService.SaveUserAsync(model.ToOrganizationUser(organizationUser), userId.Value,
|
||||
model.Collections?.Select(c => c.ToSelectionReadOnly()).ToList(), model.Groups);
|
||||
await _updateOrganizationUserCommand.UpdateUserAsync(model.ToOrganizationUser(organizationUser), userId.Value,
|
||||
model.Collections.Select(c => c.ToSelectionReadOnly()).ToList(), model.Groups);
|
||||
}
|
||||
|
||||
[HttpPut("{id}/groups")]
|
||||
[HttpPost("{id}/groups")]
|
||||
public async Task PutGroups(string orgId, string id, [FromBody] OrganizationUserUpdateGroupsRequestModel model)
|
||||
/// <summary>
|
||||
/// Put logic for Flexible Collections v1
|
||||
/// </summary>
|
||||
private async Task Put_vNext(Guid orgId, Guid id, [FromBody] OrganizationUserUpdateRequestModel model)
|
||||
{
|
||||
var orgGuidId = new Guid(orgId);
|
||||
if (!await _currentContext.ManageUsers(orgGuidId))
|
||||
if (!await _currentContext.ManageUsers(orgId))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var organizationUser = await _organizationUserRepository.GetByIdAsync(new Guid(id));
|
||||
if (organizationUser == null || organizationUser.OrganizationId != orgGuidId)
|
||||
var (organizationUser, currentAccess) = await _organizationUserRepository.GetByIdWithCollectionsAsync(id);
|
||||
if (organizationUser == null || organizationUser.OrganizationId != orgId)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var loggedInUserId = _userService.GetProperUserId(User);
|
||||
await _updateOrganizationUserGroupsCommand.UpdateUserGroupsAsync(organizationUser, model.GroupIds.Select(g => new Guid(g)), loggedInUserId);
|
||||
var userId = _userService.GetProperUserId(User).Value;
|
||||
var editingSelf = userId == organizationUser.UserId;
|
||||
|
||||
// If admins are not allowed access to all collections, you cannot add yourself to a group.
|
||||
// In this case we just don't update groups.
|
||||
var organizationAbility = await _applicationCacheService.GetOrganizationAbilityAsync(orgId);
|
||||
var groupsToSave = editingSelf && !organizationAbility.AllowAdminAccessToAllCollectionItems
|
||||
? null
|
||||
: model.Groups;
|
||||
|
||||
// If admins are not allowed access to all collections, you cannot add yourself to collections.
|
||||
// This is not caught by the requirement below that you can ModifyUserAccess and must be checked separately
|
||||
var currentAccessIds = currentAccess.Select(c => c.Id).ToHashSet();
|
||||
if (editingSelf &&
|
||||
!organizationAbility.AllowAdminAccessToAllCollectionItems &&
|
||||
model.Collections.Any(c => !currentAccessIds.Contains(c.Id)))
|
||||
{
|
||||
throw new BadRequestException("You cannot add yourself to a collection.");
|
||||
}
|
||||
|
||||
// The client only sends collections that the saving user has permissions to edit.
|
||||
// On the server side, we need to (1) make sure the user has permissions for these collections, and
|
||||
// (2) concat these with the collections that the user can't edit before saving to the database.
|
||||
var currentCollections = await _collectionRepository
|
||||
.GetManyByManyIdsAsync(currentAccess.Select(cas => cas.Id));
|
||||
|
||||
var readonlyCollectionIds = new HashSet<Guid>();
|
||||
foreach (var collection in currentCollections)
|
||||
{
|
||||
if (!(await _authorizationService.AuthorizeAsync(User, collection, BulkCollectionOperations.ModifyUserAccess))
|
||||
.Succeeded)
|
||||
{
|
||||
readonlyCollectionIds.Add(collection.Id);
|
||||
}
|
||||
}
|
||||
|
||||
if (model.Collections.Any(c => readonlyCollectionIds.Contains(c.Id)))
|
||||
{
|
||||
throw new BadRequestException("You must have Can Manage permissions to edit a collection's membership");
|
||||
}
|
||||
|
||||
var editedCollectionAccess = model.Collections
|
||||
.Select(c => c.ToSelectionReadOnly());
|
||||
var readonlyCollectionAccess = currentAccess
|
||||
.Where(ca => readonlyCollectionIds.Contains(ca.Id));
|
||||
var collectionsToSave = editedCollectionAccess
|
||||
.Concat(readonlyCollectionAccess)
|
||||
.ToList();
|
||||
|
||||
await _updateOrganizationUserCommand.UpdateUserAsync(model.ToOrganizationUser(organizationUser), userId,
|
||||
collectionsToSave, groupsToSave);
|
||||
}
|
||||
|
||||
[HttpPut("{userId}/reset-password-enrollment")]
|
||||
@ -557,8 +638,11 @@ public class OrganizationUsersController : Controller
|
||||
orgUser.Type = GetFlexibleCollectionsUserType(orgUser.Type, orgUser.Permissions);
|
||||
|
||||
// Set 'Edit/Delete Assigned Collections' custom permissions to false
|
||||
orgUser.Permissions.EditAssignedCollections = false;
|
||||
orgUser.Permissions.DeleteAssignedCollections = false;
|
||||
if (orgUser.Permissions is not null)
|
||||
{
|
||||
orgUser.Permissions.EditAssignedCollections = false;
|
||||
orgUser.Permissions.DeleteAssignedCollections = false;
|
||||
}
|
||||
|
||||
return orgUser;
|
||||
});
|
||||
@ -570,7 +654,7 @@ public class OrganizationUsersController : Controller
|
||||
private OrganizationUserType GetFlexibleCollectionsUserType(OrganizationUserType type, Permissions permissions)
|
||||
{
|
||||
// Downgrade Custom users with no other permissions than 'Edit/Delete Assigned Collections' to User
|
||||
if (type == OrganizationUserType.Custom)
|
||||
if (type == OrganizationUserType.Custom && permissions is not null)
|
||||
{
|
||||
if ((permissions.EditAssignedCollections || permissions.DeleteAssignedCollections) &&
|
||||
permissions is
|
||||
|
@ -20,6 +20,7 @@ using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Repositories;
|
||||
using Bit.Core.Auth.Services;
|
||||
using Bit.Core.Billing.Commands;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Billing.Queries;
|
||||
using Bit.Core.Context;
|
||||
@ -66,9 +67,11 @@ public class OrganizationsController : Controller
|
||||
private readonly IAddSecretsManagerSubscriptionCommand _addSecretsManagerSubscriptionCommand;
|
||||
private readonly IPushNotificationService _pushNotificationService;
|
||||
private readonly ICancelSubscriptionCommand _cancelSubscriptionCommand;
|
||||
private readonly IGetSubscriptionQuery _getSubscriptionQuery;
|
||||
private readonly ISubscriberQueries _subscriberQueries;
|
||||
private readonly IReferenceEventService _referenceEventService;
|
||||
private readonly IOrganizationEnableCollectionEnhancementsCommand _organizationEnableCollectionEnhancementsCommand;
|
||||
private readonly IProviderRepository _providerRepository;
|
||||
private readonly IScaleSeatsCommand _scaleSeatsCommand;
|
||||
|
||||
public OrganizationsController(
|
||||
IOrganizationRepository organizationRepository,
|
||||
@ -93,9 +96,11 @@ public class OrganizationsController : Controller
|
||||
IAddSecretsManagerSubscriptionCommand addSecretsManagerSubscriptionCommand,
|
||||
IPushNotificationService pushNotificationService,
|
||||
ICancelSubscriptionCommand cancelSubscriptionCommand,
|
||||
IGetSubscriptionQuery getSubscriptionQuery,
|
||||
ISubscriberQueries subscriberQueries,
|
||||
IReferenceEventService referenceEventService,
|
||||
IOrganizationEnableCollectionEnhancementsCommand organizationEnableCollectionEnhancementsCommand)
|
||||
IOrganizationEnableCollectionEnhancementsCommand organizationEnableCollectionEnhancementsCommand,
|
||||
IProviderRepository providerRepository,
|
||||
IScaleSeatsCommand scaleSeatsCommand)
|
||||
{
|
||||
_organizationRepository = organizationRepository;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
@ -119,9 +124,11 @@ public class OrganizationsController : Controller
|
||||
_addSecretsManagerSubscriptionCommand = addSecretsManagerSubscriptionCommand;
|
||||
_pushNotificationService = pushNotificationService;
|
||||
_cancelSubscriptionCommand = cancelSubscriptionCommand;
|
||||
_getSubscriptionQuery = getSubscriptionQuery;
|
||||
_subscriberQueries = subscriberQueries;
|
||||
_referenceEventService = referenceEventService;
|
||||
_organizationEnableCollectionEnhancementsCommand = organizationEnableCollectionEnhancementsCommand;
|
||||
_providerRepository = providerRepository;
|
||||
_scaleSeatsCommand = scaleSeatsCommand;
|
||||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
@ -303,7 +310,7 @@ public class OrganizationsController : Controller
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var updateBilling = !_globalSettings.SelfHosted && (model.BusinessName != organization.BusinessName ||
|
||||
var updateBilling = !_globalSettings.SelfHosted && (model.BusinessName != organization.DisplayBusinessName() ||
|
||||
model.BillingEmail != organization.BillingEmail);
|
||||
|
||||
var hasRequiredPermissions = updateBilling
|
||||
@ -464,8 +471,8 @@ public class OrganizationsController : Controller
|
||||
await _organizationService.VerifyBankAsync(orgIdGuid, model.Amount1.Value, model.Amount2.Value);
|
||||
}
|
||||
|
||||
[HttpPost("{id}/churn")]
|
||||
public async Task PostChurn(Guid id, [FromBody] SubscriptionCancellationRequestModel request)
|
||||
[HttpPost("{id}/cancel")]
|
||||
public async Task PostCancel(Guid id, [FromBody] SubscriptionCancellationRequestModel request)
|
||||
{
|
||||
if (!await _currentContext.EditSubscription(id))
|
||||
{
|
||||
@ -479,7 +486,7 @@ public class OrganizationsController : Controller
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var subscription = await _getSubscriptionQuery.GetSubscription(organization);
|
||||
var subscription = await _subscriberQueries.GetSubscriptionOrThrow(organization);
|
||||
|
||||
await _cancelSubscriptionCommand.CancelSubscription(subscription,
|
||||
new OffboardingSurveyResponse
|
||||
@ -499,19 +506,6 @@ public class OrganizationsController : Controller
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost("{id}/cancel")]
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
public async Task PostCancel(string id)
|
||||
{
|
||||
var orgIdGuid = new Guid(id);
|
||||
if (!await _currentContext.EditSubscription(orgIdGuid))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
await _organizationService.CancelSubscriptionAsync(orgIdGuid);
|
||||
}
|
||||
|
||||
[HttpPost("{id}/reinstate")]
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
public async Task PostReinstate(string id)
|
||||
@ -573,10 +567,23 @@ public class OrganizationsController : Controller
|
||||
await Task.Delay(2000);
|
||||
throw new BadRequestException(string.Empty, "User verification failed.");
|
||||
}
|
||||
else
|
||||
|
||||
var consolidatedBillingEnabled = _featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling);
|
||||
|
||||
if (consolidatedBillingEnabled && organization.IsValidClient())
|
||||
{
|
||||
await _organizationService.DeleteAsync(organization);
|
||||
var provider = await _providerRepository.GetByOrganizationIdAsync(organization.Id);
|
||||
|
||||
if (provider.IsBillable())
|
||||
{
|
||||
await _scaleSeatsCommand.ScalePasswordManagerSeats(
|
||||
provider,
|
||||
organization.PlanType,
|
||||
-organization.Seats ?? 0);
|
||||
}
|
||||
}
|
||||
|
||||
await _organizationService.DeleteAsync(organization);
|
||||
}
|
||||
|
||||
[HttpPost("{id}/import")]
|
||||
@ -737,7 +744,7 @@ public class OrganizationsController : Controller
|
||||
|
||||
[HttpPut("{id}/tax")]
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
public async Task PutTaxInfo(string id, [FromBody] OrganizationTaxInfoUpdateRequestModel model)
|
||||
public async Task PutTaxInfo(string id, [FromBody] ExpandedTaxInfoUpdateRequestModel model)
|
||||
{
|
||||
var orgIdGuid = new Guid(id);
|
||||
if (!await _currentContext.OrganizationOwner(orgIdGuid))
|
||||
|
@ -5,6 +5,7 @@ using Bit.Core.AdminConsole.Providers.Interfaces;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Billing.Commands;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
@ -112,6 +113,9 @@ public class ProviderOrganizationsController : Controller
|
||||
providerOrganization,
|
||||
organization);
|
||||
|
||||
await _removePaymentMethodCommand.RemovePaymentMethod(organization);
|
||||
if (organization.IsStripeEnabled())
|
||||
{
|
||||
await _removePaymentMethodCommand.RemovePaymentMethod(organization);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,12 @@
|
||||
using Bit.Api.AdminConsole.Models.Request.Providers;
|
||||
using Bit.Api.AdminConsole.Models.Response.Providers;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Billing.Commands;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Business;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
@ -20,15 +23,23 @@ public class ProvidersController : Controller
|
||||
private readonly IProviderService _providerService;
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly IStartSubscriptionCommand _startSubscriptionCommand;
|
||||
private readonly ILogger<ProvidersController> _logger;
|
||||
|
||||
public ProvidersController(IUserService userService, IProviderRepository providerRepository,
|
||||
IProviderService providerService, ICurrentContext currentContext, GlobalSettings globalSettings)
|
||||
IProviderService providerService, ICurrentContext currentContext, GlobalSettings globalSettings,
|
||||
IFeatureService featureService, IStartSubscriptionCommand startSubscriptionCommand,
|
||||
ILogger<ProvidersController> logger)
|
||||
{
|
||||
_userService = userService;
|
||||
_providerRepository = providerRepository;
|
||||
_providerService = providerService;
|
||||
_currentContext = currentContext;
|
||||
_globalSettings = globalSettings;
|
||||
_featureService = featureService;
|
||||
_startSubscriptionCommand = startSubscriptionCommand;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
[HttpGet("{id:guid}")]
|
||||
@ -86,6 +97,66 @@ public class ProvidersController : Controller
|
||||
var response =
|
||||
await _providerService.CompleteSetupAsync(model.ToProvider(provider), userId, model.Token, model.Key);
|
||||
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling))
|
||||
{
|
||||
var taxInfo = new TaxInfo
|
||||
{
|
||||
BillingAddressCountry = model.TaxInfo.Country,
|
||||
BillingAddressPostalCode = model.TaxInfo.PostalCode,
|
||||
TaxIdNumber = model.TaxInfo.TaxId,
|
||||
BillingAddressLine1 = model.TaxInfo.Line1,
|
||||
BillingAddressLine2 = model.TaxInfo.Line2,
|
||||
BillingAddressCity = model.TaxInfo.City,
|
||||
BillingAddressState = model.TaxInfo.State
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
await _startSubscriptionCommand.StartSubscription(provider, taxInfo);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// We don't want to trap the user on the setup page, so we'll let this go through but the provider will be in an un-billable state.
|
||||
_logger.LogError("Failed to create subscription for provider with ID {ID} during setup", provider.Id);
|
||||
}
|
||||
}
|
||||
|
||||
return new ProviderResponseModel(response);
|
||||
}
|
||||
|
||||
[HttpPost("{id}/delete-recover-token")]
|
||||
[AllowAnonymous]
|
||||
public async Task PostDeleteRecoverToken(Guid id, [FromBody] ProviderVerifyDeleteRecoverRequestModel model)
|
||||
{
|
||||
var provider = await _providerRepository.GetByIdAsync(id);
|
||||
if (provider == null)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
await _providerService.DeleteAsync(provider, model.Token);
|
||||
}
|
||||
|
||||
[HttpDelete("{id}")]
|
||||
[HttpPost("{id}/delete")]
|
||||
public async Task Delete(Guid id)
|
||||
{
|
||||
if (!_currentContext.ProviderProviderAdmin(id))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var provider = await _providerRepository.GetByIdAsync(id);
|
||||
if (provider == null)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var user = await _userService.GetUserByPrincipalAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
throw new UnauthorizedAccessException();
|
||||
}
|
||||
|
||||
await _providerService.DeleteAsync(provider);
|
||||
}
|
||||
}
|
||||
|
@ -4,9 +4,6 @@ namespace Bit.Api.AdminConsole.Models.Request;
|
||||
|
||||
public class OrganizationDomainRequestModel
|
||||
{
|
||||
[Required]
|
||||
public string Txt { get; set; }
|
||||
|
||||
[Required]
|
||||
public string DomainName { get; set; }
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text.Json.Serialization;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Business;
|
||||
@ -9,9 +10,11 @@ namespace Bit.Api.AdminConsole.Models.Request.Organizations;
|
||||
public class OrganizationCreateRequestModel : IValidatableObject
|
||||
{
|
||||
[Required]
|
||||
[StringLength(50)]
|
||||
[StringLength(50, ErrorMessage = "The field Name exceeds the maximum length.")]
|
||||
[JsonConverter(typeof(HtmlEncodingStringConverter))]
|
||||
public string Name { get; set; }
|
||||
[StringLength(50)]
|
||||
[StringLength(50, ErrorMessage = "The field Business Name exceeds the maximum length.")]
|
||||
[JsonConverter(typeof(HtmlEncodingStringConverter))]
|
||||
public string BusinessName { get; set; }
|
||||
[Required]
|
||||
[StringLength(256)]
|
||||
|
@ -1,16 +1,20 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text.Json.Serialization;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Api.AdminConsole.Models.Request.Organizations;
|
||||
|
||||
public class OrganizationUpdateRequestModel
|
||||
{
|
||||
[Required]
|
||||
[StringLength(50)]
|
||||
[StringLength(50, ErrorMessage = "The field Name exceeds the maximum length.")]
|
||||
[JsonConverter(typeof(HtmlEncodingStringConverter))]
|
||||
public string Name { get; set; }
|
||||
[StringLength(50)]
|
||||
[StringLength(50, ErrorMessage = "The field Business Name exceeds the maximum length.")]
|
||||
[JsonConverter(typeof(HtmlEncodingStringConverter))]
|
||||
public string BusinessName { get; set; }
|
||||
[EmailAddress]
|
||||
[Required]
|
||||
|
@ -102,12 +102,6 @@ public class OrganizationUserUpdateRequestModel
|
||||
}
|
||||
}
|
||||
|
||||
public class OrganizationUserUpdateGroupsRequestModel
|
||||
{
|
||||
[Required]
|
||||
public IEnumerable<string> GroupIds { get; set; }
|
||||
}
|
||||
|
||||
public class OrganizationUserResetPasswordEnrollmentRequestModel
|
||||
{
|
||||
public string ResetPasswordKey { get; set; }
|
||||
|
@ -1,14 +1,19 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text.Json.Serialization;
|
||||
using Bit.Api.Models.Request;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Api.AdminConsole.Models.Request.Providers;
|
||||
|
||||
public class ProviderSetupRequestModel
|
||||
{
|
||||
[Required]
|
||||
[StringLength(50)]
|
||||
[StringLength(50, ErrorMessage = "The field Name exceeds the maximum length.")]
|
||||
[JsonConverter(typeof(HtmlEncodingStringConverter))]
|
||||
public string Name { get; set; }
|
||||
[StringLength(50)]
|
||||
[StringLength(50, ErrorMessage = "The field Business Name exceeds the maximum length.")]
|
||||
[JsonConverter(typeof(HtmlEncodingStringConverter))]
|
||||
public string BusinessName { get; set; }
|
||||
[Required]
|
||||
[StringLength(256)]
|
||||
@ -18,6 +23,7 @@ public class ProviderSetupRequestModel
|
||||
public string Token { get; set; }
|
||||
[Required]
|
||||
public string Key { get; set; }
|
||||
public ExpandedTaxInfoUpdateRequestModel TaxInfo { get; set; }
|
||||
|
||||
public virtual Provider ToProvider(Provider provider)
|
||||
{
|
||||
|
@ -1,15 +1,19 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text.Json.Serialization;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Api.AdminConsole.Models.Request.Providers;
|
||||
|
||||
public class ProviderUpdateRequestModel
|
||||
{
|
||||
[Required]
|
||||
[StringLength(50)]
|
||||
[StringLength(50, ErrorMessage = "The field Name exceeds the maximum length.")]
|
||||
[JsonConverter(typeof(HtmlEncodingStringConverter))]
|
||||
public string Name { get; set; }
|
||||
[StringLength(50)]
|
||||
[StringLength(50, ErrorMessage = "The field Business Name exceeds the maximum length.")]
|
||||
[JsonConverter(typeof(HtmlEncodingStringConverter))]
|
||||
public string BusinessName { get; set; }
|
||||
[EmailAddress]
|
||||
[Required]
|
||||
|
@ -0,0 +1,9 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Bit.Api.AdminConsole.Models.Request.Providers;
|
||||
|
||||
public class ProviderVerifyDeleteRecoverRequestModel
|
||||
{
|
||||
[Required]
|
||||
public string Token { get; set; }
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
using Bit.Api.Models.Response;
|
||||
using System.Text.Json.Serialization;
|
||||
using Bit.Api.Models.Response;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Api;
|
||||
@ -60,7 +61,9 @@ public class OrganizationResponseModel : ResponseModel
|
||||
}
|
||||
|
||||
public Guid Id { get; set; }
|
||||
[JsonConverter(typeof(HtmlEncodingStringConverter))]
|
||||
public string Name { get; set; }
|
||||
[JsonConverter(typeof(HtmlEncodingStringConverter))]
|
||||
public string BusinessName { get; set; }
|
||||
public string BusinessAddress1 { get; set; }
|
||||
public string BusinessAddress2 { get; set; }
|
||||
@ -123,8 +126,14 @@ public class OrganizationSubscriptionResponseModel : OrganizationResponseModel
|
||||
if (hideSensitiveData)
|
||||
{
|
||||
BillingEmail = null;
|
||||
Subscription.Items = null;
|
||||
UpcomingInvoice.Amount = null;
|
||||
if (Subscription != null)
|
||||
{
|
||||
Subscription.Items = null;
|
||||
}
|
||||
if (UpcomingInvoice != null)
|
||||
{
|
||||
UpcomingInvoice.Amount = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -133,12 +142,13 @@ public class OrganizationSubscriptionResponseModel : OrganizationResponseModel
|
||||
{
|
||||
if (license != null)
|
||||
{
|
||||
// License expiration should always include grace period - See OrganizationLicense.cs
|
||||
// License expiration should always include grace period (unless it's in a Trial) - See OrganizationLicense.cs.
|
||||
Expiration = license.Expires;
|
||||
// Use license.ExpirationWithoutGracePeriod if available, otherwise assume license expiration minus grace period
|
||||
ExpirationWithoutGracePeriod = license.ExpirationWithoutGracePeriod ??
|
||||
license.Expires?.AddDays(-Constants
|
||||
.OrganizationSelfHostSubscriptionGracePeriodDays);
|
||||
|
||||
// Use license.ExpirationWithoutGracePeriod if available, otherwise assume license expiration minus grace period unless it's in a Trial.
|
||||
ExpirationWithoutGracePeriod = license.ExpirationWithoutGracePeriod ?? (license.Trial
|
||||
? license.Expires
|
||||
: license.Expires?.AddDays(-Constants.OrganizationSelfHostSubscriptionGracePeriodDays));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using System.Text.Json.Serialization;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Models.Data;
|
||||
using Bit.Core.Enums;
|
||||
@ -73,7 +74,7 @@ public class ProfileOrganizationResponseModel : ResponseModel
|
||||
if (FlexibleCollections)
|
||||
{
|
||||
// Downgrade Custom users with no other permissions than 'Edit/Delete Assigned Collections' to User
|
||||
if (Type == OrganizationUserType.Custom)
|
||||
if (Type == OrganizationUserType.Custom && Permissions is not null)
|
||||
{
|
||||
if ((Permissions.EditAssignedCollections || Permissions.DeleteAssignedCollections) &&
|
||||
Permissions is
|
||||
@ -97,12 +98,16 @@ public class ProfileOrganizationResponseModel : ResponseModel
|
||||
}
|
||||
|
||||
// Set 'Edit/Delete Assigned Collections' custom permissions to false
|
||||
Permissions.EditAssignedCollections = false;
|
||||
Permissions.DeleteAssignedCollections = false;
|
||||
if (Permissions is not null)
|
||||
{
|
||||
Permissions.EditAssignedCollections = false;
|
||||
Permissions.DeleteAssignedCollections = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Guid Id { get; set; }
|
||||
[JsonConverter(typeof(HtmlEncodingStringConverter))]
|
||||
public string Name { get; set; }
|
||||
public bool UsePolicies { get; set; }
|
||||
public bool UseSso { get; set; }
|
||||
@ -135,6 +140,7 @@ public class ProfileOrganizationResponseModel : ResponseModel
|
||||
public Guid? UserId { get; set; }
|
||||
public bool HasPublicAndPrivateKeys { get; set; }
|
||||
public Guid? ProviderId { get; set; }
|
||||
[JsonConverter(typeof(HtmlEncodingStringConverter))]
|
||||
public string ProviderName { get; set; }
|
||||
public ProviderType? ProviderType { get; set; }
|
||||
public string FamilySponsorshipFriendlyName { get; set; }
|
||||
|
@ -1,4 +1,5 @@
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using System.Text.Json.Serialization;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.AdminConsole.Models.Data.Provider;
|
||||
using Bit.Core.Models.Api;
|
||||
using Bit.Core.Models.Data;
|
||||
@ -20,9 +21,11 @@ public class ProfileProviderResponseModel : ResponseModel
|
||||
Permissions = CoreHelpers.LoadClassFromJsonData<Permissions>(provider.Permissions);
|
||||
UserId = provider.UserId;
|
||||
UseEvents = provider.UseEvents;
|
||||
ProviderStatus = provider.ProviderStatus;
|
||||
}
|
||||
|
||||
public Guid Id { get; set; }
|
||||
[JsonConverter(typeof(HtmlEncodingStringConverter))]
|
||||
public string Name { get; set; }
|
||||
public string Key { get; set; }
|
||||
public ProviderUserStatusType Status { get; set; }
|
||||
@ -31,4 +34,5 @@ public class ProfileProviderResponseModel : ResponseModel
|
||||
public Permissions Permissions { get; set; }
|
||||
public Guid? UserId { get; set; }
|
||||
public bool UseEvents { get; set; }
|
||||
public ProviderStatusType ProviderStatus { get; set; }
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user